From 8cf2ea4da0abaf5e5cae20041be15f28b7b8eaa9 Mon Sep 17 00:00:00 2001 From: Trinity Date: Tue, 2 Jul 2024 15:00:57 +0700 Subject: [PATCH 01/21] Custom contract for zero max cap --- Cargo.lock | 1 + contracts/consumer/converter/Cargo.toml | 1 + contracts/consumer/converter/src/contract.rs | 60 ++++++- contracts/consumer/converter/src/ibc.rs | 43 ++++- contracts/consumer/converter/src/multitest.rs | 10 +- .../src/multitest/virtual_staking_mock.rs | 12 ++ contracts/consumer/virtual-staking/Cargo.toml | 1 + .../consumer/virtual-staking/src/contract.rs | 160 +++++++++++++----- .../provider/external-staking/src/contract.rs | 66 +++++++- .../provider/external-staking/src/ibc.rs | 5 + packages/apis/src/converter_api.rs | 10 ++ packages/apis/src/ibc/packet.rs | 10 ++ packages/apis/src/virtual_staking_api.rs | 13 ++ packages/bindings/src/query.rs | 23 ++- packages/virtual-staking-mock/src/lib.rs | 10 +- 15 files changed, 363 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0df67f14..ae9071cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,6 +610,7 @@ dependencies = [ "mesh-bindings", "mesh-burn", "mesh-simple-price-feed", + "mesh-sync", "schemars", "serde", "sylvia", diff --git a/contracts/consumer/converter/Cargo.toml b/contracts/consumer/converter/Cargo.toml index a152fbeb..0a03ffe6 100644 --- a/contracts/consumer/converter/Cargo.toml +++ b/contracts/consumer/converter/Cargo.toml @@ -23,6 +23,7 @@ fake-custom = [ "mesh-simple-price-feed/fake-custom" ] [dependencies] mesh-apis = { workspace = true } mesh-bindings = { workspace = true } +mesh-sync = { workspace = true } sylvia = { workspace = true } cosmwasm-schema = { workspace = true } diff --git a/contracts/consumer/converter/src/contract.rs b/contracts/consumer/converter/src/contract.rs index caedb92d..ba62e558 100644 --- a/contracts/consumer/converter/src/contract.rs +++ b/contracts/consumer/converter/src/contract.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{ ensure_eq, to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Deps, DepsMut, Event, - Fraction, MessageInfo, Reply, Response, StdError, SubMsg, SubMsgResponse, Uint128, Validator, - WasmMsg, + Fraction, IbcMsg, MessageInfo, Reply, Response, StdError, SubMsg, SubMsgResponse, Uint128, + Validator, WasmMsg, }; use cw2::set_contract_version; use cw_storage_plus::Item; @@ -15,7 +15,9 @@ use mesh_apis::price_feed_api; use mesh_apis::virtual_staking_api; use crate::error::ContractError; -use crate::ibc::{make_ibc_packet, valset_update_msg, IBC_CHANNEL}; +use crate::ibc::{ + make_ibc_packet, packet_timeout_internal_unstake, valset_update_msg, IBC_CHANNEL, +}; use crate::msg::ConfigResponse; use crate::state::Config; @@ -138,13 +140,14 @@ impl ConverterContract<'_> { fn test_stake( &self, ctx: ExecCtx, + delegator: String, validator: String, stake: Coin, ) -> Result { #[cfg(any(test, feature = "mt"))] { // This can only ever be called in tests - self.stake(ctx.deps, validator, stake) + self.stake(ctx.deps, delegator, validator, stake) } #[cfg(not(any(test, feature = "mt")))] { @@ -159,13 +162,14 @@ impl ConverterContract<'_> { fn test_unstake( &self, ctx: ExecCtx, + delegator: String, validator: String, unstake: Coin, ) -> Result { #[cfg(any(test, feature = "mt"))] { // This can only ever be called in tests - self.unstake(ctx.deps, validator, unstake) + self.unstake(ctx.deps, delegator, validator, unstake) } #[cfg(not(any(test, feature = "mt")))] { @@ -214,6 +218,7 @@ impl ConverterContract<'_> { pub(crate) fn stake( &self, deps: DepsMut, + delegator: String, validator: String, stake: Coin, ) -> Result { @@ -223,7 +228,11 @@ impl ConverterContract<'_> { .add_attribute("validator", &validator) .add_attribute("amount", amount.amount.to_string()); - let msg = virtual_staking_api::sv::ExecMsg::Bond { validator, amount }; + let msg = virtual_staking_api::sv::ExecMsg::Bond { + delegator, + validator, + amount, + }; let msg = WasmMsg::Execute { contract_addr: self.virtual_stake.load(deps.storage)?.into(), msg: to_json_binary(&msg)?, @@ -238,6 +247,7 @@ impl ConverterContract<'_> { pub(crate) fn unstake( &self, deps: DepsMut, + delegator: String, validator: String, unstake: Coin, ) -> Result { @@ -247,7 +257,11 @@ impl ConverterContract<'_> { .add_attribute("validator", &validator) .add_attribute("amount", amount.amount.to_string()); - let msg = virtual_staking_api::sv::ExecMsg::Unbond { validator, amount }; + let msg = virtual_staking_api::sv::ExecMsg::Unbond { + delegator, + validator, + amount, + }; let msg = WasmMsg::Execute { contract_addr: self.virtual_stake.load(deps.storage)?.into(), msg: to_json_binary(&msg)?, @@ -603,4 +617,36 @@ impl ConverterApi for ConverterContract<'_> { resp = resp.add_event(event); Ok(resp) } + + fn internal_unstake( + &self, + ctx: ExecCtx, + delegator: String, + validator: String, + amount: Coin, + ) -> Result { + let virtual_stake = self.virtual_stake.load(ctx.deps.storage)?; + ensure_eq!(ctx.info.sender, virtual_stake, ContractError::Unauthorized); + + #[allow(unused_mut)] + let mut resp = Response::new() + .add_attribute("action", "internal_unstake") + .add_attribute("amount", amount.amount.to_string()) + .add_attribute("owner", delegator.clone()); + + let channel = IBC_CHANNEL.load(ctx.deps.storage)?; + let packet = ConsumerPacket::InternalUnstake { + delegator: delegator, + validator, + amount, + }; + let msg = IbcMsg::SendPacket { + channel_id: channel.endpoint.channel_id, + data: to_json_binary(&packet)?, + timeout: packet_timeout_internal_unstake(&ctx.env), + }; + // send packet if we are ibc enabled + resp = resp.add_message(msg); + Ok(resp) + } } diff --git a/contracts/consumer/converter/src/ibc.rs b/contracts/consumer/converter/src/ibc.rs index c54a19dd..f719fee8 100644 --- a/contracts/consumer/converter/src/ibc.rs +++ b/contracts/consumer/converter/src/ibc.rs @@ -5,7 +5,7 @@ use cosmwasm_std::{ from_json, to_json_binary, DepsMut, Env, Event, Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcChannelOpenResponse, IbcMsg, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, - IbcReceiveResponse, IbcTimeout, Validator, + IbcReceiveResponse, IbcTimeout, Validator, WasmMsg, }; use cw_storage_plus::Item; @@ -14,6 +14,7 @@ use mesh_apis::ibc::{ ack_success, validate_channel_order, AckWrapper, AddValidator, ConsumerPacket, ProtocolVersion, ProviderPacket, StakeAck, TransferRewardsAck, UnstakeAck, PROTOCOL_NAME, }; +use mesh_apis::virtual_staking_api; use sylvia::types::ExecCtx; use crate::{ @@ -34,6 +35,8 @@ const DEFAULT_VALIDATOR_TIMEOUT: u64 = 24 * 60 * 60; // But reward messages should go faster or timeout const DEFAULT_REWARD_TIMEOUT: u64 = 60 * 60; +const DEFAULT_INTERNAL_UNSTAKE_TIMEOUT: u64 = 60 * 60; + pub fn packet_timeout_validator(env: &Env) -> IbcTimeout { // No idea about their block time, but 24 hours ahead of our view of the clock // should be decently in the future. @@ -48,6 +51,13 @@ pub fn packet_timeout_rewards(env: &Env) -> IbcTimeout { IbcTimeout::with_timestamp(timeout) } +pub fn packet_timeout_internal_unstake(env: &Env) -> IbcTimeout { + // No idea about their block time, but 24 hours ahead of our view of the clock + // should be decently in the future. + let timeout = env.block.time.plus_seconds(DEFAULT_INTERNAL_UNSTAKE_TIMEOUT); + IbcTimeout::with_timestamp(timeout) +} + #[cfg_attr(not(feature = "library"), entry_point)] /// enforces ordering and versioning constraints pub fn ibc_channel_open( @@ -195,11 +205,12 @@ pub fn ibc_packet_receive( let contract = ConverterContract::new(); let res = match packet { ProviderPacket::Stake { + delegator, validator, stake, tx_id: _, } => { - let response = contract.stake(deps, validator, stake)?; + let response = contract.stake(deps, delegator, validator, stake)?; let ack = ack_success(&StakeAck {})?; IbcReceiveResponse::new() .set_ack(ack) @@ -208,11 +219,12 @@ pub fn ibc_packet_receive( .add_attributes(response.attributes) } ProviderPacket::Unstake { + delegator, validator, unstake, tx_id: _, } => { - let response = contract.unstake(deps, validator, unstake)?; + let response = contract.unstake(deps, delegator, validator, unstake)?; let ack = ack_success(&UnstakeAck {})?; IbcReceiveResponse::new() .set_ack(ack) @@ -245,14 +257,35 @@ pub fn ibc_packet_receive( /// If it succeeded, take no action. If it errored, we can't do anything else and let it go. /// We just log the error cases so they can be detected. pub fn ibc_packet_ack( - _deps: DepsMut, + deps: DepsMut, _env: Env, msg: IbcPacketAckMsg, ) -> Result { let ack: AckWrapper = from_json(&msg.acknowledgement.data)?; + let contract = ConverterContract::new(); let mut res = IbcBasicResponse::new(); match ack { - AckWrapper::Result(_) => {} + AckWrapper::Result(_) => { + let packet: ConsumerPacket = from_json(&msg.original_packet.data)?; + if let ConsumerPacket::InternalUnstake { + delegator, + validator, + amount, + } = packet { + // execute virtual contract's internal unbond + let msg = virtual_staking_api::sv::ExecMsg::InternalUnbond { + delegator, + validator, + amount, + }; + let msg = WasmMsg::Execute { + contract_addr: contract.virtual_stake.load(deps.storage)?.into(), + msg: to_json_binary(&msg)?, + funds: vec![], + }; + res = res.add_message(msg); + } + } AckWrapper::Error(e) => { // The wasmd framework will label this with the contract_addr, which helps us find the port and issue. // Provide info to find the actual packet. diff --git a/contracts/consumer/converter/src/multitest.rs b/contracts/consumer/converter/src/multitest.rs index 22169d99..2ee2b19e 100644 --- a/contracts/consumer/converter/src/multitest.rs +++ b/contracts/consumer/converter/src/multitest.rs @@ -172,17 +172,17 @@ fn ibc_stake_and_unstake() { // let's stake some converter - .test_stake(val1.to_string(), coin(1000, JUNO)) + .test_stake(owner.to_string(), val1.to_string(), coin(1000, JUNO)) .call(owner) .unwrap(); converter - .test_stake(val2.to_string(), coin(4000, JUNO)) + .test_stake(owner.to_string(), val2.to_string(), coin(4000, JUNO)) .call(owner) .unwrap(); // and unstake some converter - .test_unstake(val2.to_string(), coin(2000, JUNO)) + .test_unstake(owner.to_string(), val2.to_string(), coin(2000, JUNO)) .call(owner) .unwrap(); @@ -258,11 +258,11 @@ fn ibc_stake_and_burn() { // let's stake some converter - .test_stake(val1.to_string(), coin(1000, JUNO)) + .test_stake(owner.to_string(),val1.to_string(), coin(1000, JUNO)) .call(owner) .unwrap(); converter - .test_stake(val2.to_string(), coin(4000, JUNO)) + .test_stake(owner.to_string(), val2.to_string(), coin(4000, JUNO)) .call(owner) .unwrap(); diff --git a/contracts/consumer/converter/src/multitest/virtual_staking_mock.rs b/contracts/consumer/converter/src/multitest/virtual_staking_mock.rs index 3faaedf8..d4422445 100644 --- a/contracts/consumer/converter/src/multitest/virtual_staking_mock.rs +++ b/contracts/consumer/converter/src/multitest/virtual_staking_mock.rs @@ -133,6 +133,7 @@ impl VirtualStakingApi for VirtualStakingMock<'_> { fn bond( &self, ctx: ExecCtx, + _delegator: String, validator: String, amount: Coin, ) -> Result, Self::Error> { @@ -160,6 +161,7 @@ impl VirtualStakingApi for VirtualStakingMock<'_> { fn unbond( &self, ctx: ExecCtx, + _delegator: String, validator: String, amount: Coin, ) -> Result, Self::Error> { @@ -241,6 +243,16 @@ impl VirtualStakingApi for VirtualStakingMock<'_> { Ok(Response::new()) } + fn internal_unbond( + &self, + _ctx:ExecCtx, + _delegator:String, + _validator:String, + _amount:Coin + ) -> Result ,Self::Error> { + unimplemented!() + } + /// SudoMsg::HandleEpoch{} should be called once per epoch by the sdk (in EndBlock). /// It allows the virtual staking contract to bond or unbond any pending requests, as well /// as to perform a rebalance if needed (over the max cap). diff --git a/contracts/consumer/virtual-staking/Cargo.toml b/contracts/consumer/virtual-staking/Cargo.toml index 2952a33b..000abb3c 100644 --- a/contracts/consumer/virtual-staking/Cargo.toml +++ b/contracts/consumer/virtual-staking/Cargo.toml @@ -35,6 +35,7 @@ serde = { workspace = true } thiserror = { workspace = true } [dev-dependencies] +sylvia = { workspace = true, features = ["mt"] } mesh-simple-price-feed = { workspace = true, features = ["mt", "fake-custom"] } mesh-converter = { workspace = true, features = ["mt", "fake-custom"] } cw-multi-test = { workspace = true } diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index 52af7d8b..01f23edc 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -24,6 +24,9 @@ use crate::state::Config; pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +// TODO: lack test for the appropriate max retrieve. +pub const MAX_RETRIEVE: u16 = 50; + pub struct VirtualStakingContract<'a> { pub config: Item<'a, Config>, /// Amount of tokens that have been requested to bond to a validator @@ -360,6 +363,7 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { fn bond( &self, ctx: ExecCtx, + _delegator: String, validator: String, amount: Coin, ) -> Result, Self::Error> { @@ -390,6 +394,7 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { fn unbond( &self, ctx: ExecCtx, + _delegator: String, validator: String, amount: Coin, ) -> Result, Self::Error> { @@ -475,6 +480,44 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { Ok(Response::new()) } + /// Immediately unbond the given amount due to zero max cap + fn internal_unbond( + &self, + ctx: ExecCtx, + _delegator: String, + validator: String, + amount: Coin, + ) -> Result, Self::Error> { + nonpayable(&ctx.info)?; + let cfg = self.config.load(ctx.deps.storage)?; + ensure_eq!(ctx.info.sender, cfg.converter, ContractError::Unauthorized); // only the converter can call this + ensure_eq!( + amount.denom, + cfg.denom, + ContractError::WrongDenom(cfg.denom) + ); + + // Immediately unbond + let bonded = self.bond_requests.load(ctx.deps.storage, &validator)?; + let bonded = bonded + .checked_sub(amount.amount) + .map_err(|_| ContractError::InsufficientBond(validator.clone(), amount.amount))?; + self.bond_requests + .save(ctx.deps.storage, &validator, &bonded)?; + + let requests: Vec<(String, Uint128)> = self + .bond_requests + .range( + ctx.deps.as_ref().storage, + None, + None, + cosmwasm_std::Order::Ascending, + ) + .collect::>()?; + self.bonded.save(ctx.deps.storage, &requests)?; + return Ok(Response::new()) + } + // FIXME: need to handle custom message types and queries /** * This is called once per epoch to withdraw all rewards and rebalance the bonded tokens. @@ -506,15 +549,35 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { let bond = TokenQuerier::new(&deps.querier).bond_status(env.contract.address.to_string())?; let max_cap = bond.cap.amount; + + let config = self.config.load(deps.storage)?; // If 0 max cap, then we assume all tokens were force unbonded already, and just return the withdraw rewards // call and set bonded to empty // TODO: verify this behavior with SDK module (otherwise we send unbond message) if max_cap.is_zero() { - self.bonded.save(deps.storage, &vec![])?; - return Ok(resp); + let all_delegations = + TokenQuerier::new(&deps.querier).all_delegations( + env.contract.address.to_string(), MAX_RETRIEVE + )?; + let mut msgs = vec![]; + for delegation in all_delegations.delegations { + let validator = delegation.validator.clone(); + // Send unstake request to converter contract + let msg = converter_api::sv::ExecMsg::InternalUnstake { + delegator: delegation.delegator, + validator: validator, + amount: Coin { denom: config.denom.clone(), amount: delegation.amount } + }; + let msg = WasmMsg::Execute { + contract_addr: config.converter.to_string(), + msg: to_json_binary(&msg)?, + funds: vec![], + }; + msgs.push(msg); + } + return Ok(resp.add_messages(msgs)); } - let config = self.config.load(deps.storage)?; // Make current bonded mutable let mut current = bonded; // Process slashes due to tombstoning (unbonded) or jailing, over bond_requests and current @@ -645,9 +708,7 @@ mod tests { }; use cosmwasm_std::{ - coins, from_json, - testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - Decimal, + coins, from_json, testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, AllDelegationsResponse, Decimal }; use mesh_bindings::{BondStatusResponse, SlashRatioResponse}; @@ -665,7 +726,7 @@ mod tests { contract.quick_inst(deps.as_mut()); knobs.bond_status.update_cap(0u128); - contract.quick_bond(deps.as_mut(), "val1", 5); + contract.quick_bond(deps.as_mut(), "owner", "val1", 5); contract .hit_epoch(deps.as_mut()) .assert_no_bonding() @@ -681,7 +742,7 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(10u128); - contract.quick_bond(deps.as_mut(), "val1", 5); + contract.quick_bond(deps.as_mut(), "owner", "val1", 5); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (5u128, &denom))]) @@ -697,8 +758,8 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(10u128); - contract.quick_bond(deps.as_mut(), "val1", 6); - contract.quick_bond(deps.as_mut(), "val2", 4); + contract.quick_bond(deps.as_mut(), "owner", "val1", 6); + contract.quick_bond(deps.as_mut(), "owner", "val2", 4); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (6u128, &denom)), ("val2", (4u128, &denom))]) @@ -716,8 +777,8 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(5u128); - contract.quick_bond(deps.as_mut(), "val1", 10); - contract.quick_bond(deps.as_mut(), "val2", 40); + contract.quick_bond(deps.as_mut(), "owner", "val1", 10); + contract.quick_bond(deps.as_mut(), "owner", "val2", 40); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (1u128, &denom)), ("val2", (4u128, &denom))]) @@ -733,13 +794,13 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(10u128); - contract.quick_bond(deps.as_mut(), "val1", 5); + contract.quick_bond(deps.as_mut(), "owner", "val1", 5); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (5u128, &denom))]) .assert_rewards(&[]); - contract.quick_unbond(deps.as_mut(), "val1", 5); + contract.quick_unbond(deps.as_mut(), "owner", "val1", 5); contract .hit_epoch(deps.as_mut()) .assert_unbond(&[("val1", (5u128, &denom))]) @@ -755,7 +816,7 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(10u128); - contract.quick_bond(deps.as_mut(), "val1", 5); + contract.quick_bond(deps.as_mut(), "owner", "val1", 5); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (5u128, &denom))]) @@ -777,8 +838,8 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(100u128); - contract.quick_bond(deps.as_mut(), "val1", 5); - contract.quick_bond(deps.as_mut(), "val2", 20); + contract.quick_bond(deps.as_mut(), "owner", "val1", 5); + contract.quick_bond(deps.as_mut(), "owner", "val2", 20); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (5u128, &denom)), ("val2", (20u128, &denom))]) @@ -802,8 +863,8 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(100u128); - contract.quick_bond(deps.as_mut(), "val1", 5); - contract.quick_bond(deps.as_mut(), "val2", 10); + contract.quick_bond(deps.as_mut(), "owner", "val1", 5); + contract.quick_bond(deps.as_mut(), "owner", "val2", 10); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (5u128, &denom)), ("val2", (10u128, &denom))]) @@ -827,8 +888,8 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(100u128); - contract.quick_bond(deps.as_mut(), "val1", 5); - contract.quick_bond(deps.as_mut(), "val2", 10); + contract.quick_bond(deps.as_mut(), "owner", "val1", 5); + contract.quick_bond(deps.as_mut(), "owner", "val2", 10); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (5u128, &denom)), ("val2", (10u128, &denom))]) @@ -850,8 +911,8 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(100u128); - contract.quick_bond(deps.as_mut(), "val1", 10); - contract.quick_bond(deps.as_mut(), "val2", 20); + contract.quick_bond(deps.as_mut(), "owner", "val1", 10); + contract.quick_bond(deps.as_mut(), "owner", "val2", 20); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (10u128, &denom)), ("val2", (20u128, &denom))]) @@ -914,14 +975,14 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(100u128); - contract.quick_bond(deps.as_mut(), "val1", 10); + contract.quick_bond(deps.as_mut(), "owner", "val1", 10); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (10u128, &denom))]) .assert_rewards(&[]); // Val1 is bonding some more - contract.quick_bond(deps.as_mut(), "val1", 20); + contract.quick_bond(deps.as_mut(), "owner", "val1", 20); // And it's being jailed at the same time contract.jail(deps.as_mut(), "val1", Decimal::percent(10), Uint128::one()); @@ -946,14 +1007,14 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(100u128); - contract.quick_bond(deps.as_mut(), "val1", 10); + contract.quick_bond(deps.as_mut(), "owner", "val1", 10); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (10u128, &denom))]) .assert_rewards(&[]); // Val1 is unbonding - contract.quick_unbond(deps.as_mut(), "val1", 10); + contract.quick_unbond(deps.as_mut(), "owner", "val1", 10); // And it's being jailed at the same time contract.jail(deps.as_mut(), "val1", Decimal::percent(10), Uint128::one()); @@ -993,7 +1054,7 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(10u128); - contract.quick_bond(deps.as_mut(), "val1", 5); + contract.quick_bond(deps.as_mut(), "owner", "val1", 5); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (5u128, &denom))]) @@ -1021,8 +1082,8 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(100u128); - contract.quick_bond(deps.as_mut(), "val1", 20); - contract.quick_bond(deps.as_mut(), "val2", 20); + contract.quick_bond(deps.as_mut(), "owner", "val1", 20); + contract.quick_bond(deps.as_mut(), "owner", "val2", 20); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (20u128, &denom)), ("val2", (20u128, &denom))]) @@ -1061,14 +1122,14 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(100u128); - contract.quick_bond(deps.as_mut(), "val1", 10); + contract.quick_bond(deps.as_mut(), "owner", "val1", 10); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (10u128, &denom))]) .assert_rewards(&[]); // Val1 is bonding some more - contract.quick_bond(deps.as_mut(), "val1", 20); + contract.quick_bond(deps.as_mut(), "owner", "val1", 20); // And it's being tombstoned at the same time contract.tombstone(deps.as_mut(), "val1", Decimal::percent(25), Uint128::new(2)); @@ -1101,14 +1162,14 @@ mod tests { let denom = contract.config.load(&deps.storage).unwrap().denom; knobs.bond_status.update_cap(100u128); - contract.quick_bond(deps.as_mut(), "val1", 10); + contract.quick_bond(deps.as_mut(), "owner", "val1", 10); contract .hit_epoch(deps.as_mut()) .assert_bond(&[("val1", (10u128, &denom))]) .assert_rewards(&[]); // Val1 is unbonding - contract.quick_unbond(deps.as_mut(), "val1", 10); + contract.quick_unbond(deps.as_mut(), "owner", "val1", 10); // And it's being tombstoned at the same time contract.tombstone(deps.as_mut(), "val1", Decimal::percent(25), Uint128::new(2)); @@ -1213,6 +1274,9 @@ mod tests { slash_fraction_downtime: "0.1".to_string(), slash_fraction_double_sign: "0.25".to_string(), }); + let all_delegations = MockAllDelegations::new(AllDelegationsResponse { + delegations: vec![] + }); let handler = { let bs_copy = bond_status.clone(); @@ -1229,6 +1293,11 @@ mod tests { to_json_binary(&*slash_ratio.borrow()).unwrap(), )) } + mesh_bindings::VirtualStakeQuery::AllDelegations { .. } => { + cosmwasm_std::SystemResult::Ok(cosmwasm_std::ContractResult::Ok( + to_json_binary(&*all_delegations.borrow()).unwrap(), + )) + } } } }; @@ -1279,6 +1348,19 @@ mod tests { } } + #[derive(Clone)] + struct MockAllDelegations(Rc>); + + impl MockAllDelegations { + fn new(res: AllDelegationsResponse) -> Self { + Self(Rc::new(RefCell::new(res))) + } + + fn borrow(&self) -> Ref<'_, AllDelegationsResponse> { + self.0.borrow() + } + } + fn set_reward_targets(storage: &mut dyn Storage, targets: &[&str]) { REWARD_TARGETS .save( @@ -1292,8 +1374,8 @@ mod tests { fn quick_inst(&self, deps: DepsMut); fn push_rewards(&self, deps: &mut OwnedDeps, amount: u128) -> PushRewardsResult; fn hit_epoch(&self, deps: DepsMut) -> HitEpochResult; - fn quick_bond(&self, deps: DepsMut, validator: &str, amount: u128); - fn quick_unbond(&self, deps: DepsMut, validator: &str, amount: u128); + fn quick_bond(&self, deps: DepsMut, delegator: &str, validator: &str, amount: u128); + fn quick_unbond(&self, deps: DepsMut, delegator: &str, validator: &str, amount: u128); fn quick_burn( &self, deps: DepsMut, @@ -1366,7 +1448,7 @@ mod tests { HitEpochResult::new(self.handle_epoch(deps).unwrap()) } - fn quick_bond(&self, deps: DepsMut, validator: &str, amount: u128) { + fn quick_bond(&self, deps: DepsMut, delegator: &str, validator: &str, amount: u128) { let denom = self.config.load(deps.storage).unwrap().denom; self.bond( @@ -1375,13 +1457,14 @@ mod tests { env: mock_env(), info: mock_info("me", &[]), }, + delegator.to_string(), validator.to_string(), coin(amount, denom), ) .unwrap(); } - fn quick_unbond(&self, deps: DepsMut, validator: &str, amount: u128) { + fn quick_unbond(&self, deps: DepsMut, delegator: &str, validator: &str, amount: u128) { let denom = self.config.load(deps.storage).unwrap().denom; self.unbond( @@ -1390,6 +1473,7 @@ mod tests { env: mock_env(), info: mock_info("me", &[]), }, + delegator.to_string(), validator.to_string(), coin(amount, denom), ) diff --git a/contracts/provider/external-staking/src/contract.rs b/contracts/provider/external-staking/src/contract.rs index 25293bc3..2b996ea4 100644 --- a/contracts/provider/external-staking/src/contract.rs +++ b/contracts/provider/external-staking/src/contract.rs @@ -26,7 +26,7 @@ use crate::msg::{ StakeInfo, StakesResponse, TxResponse, ValidatorPendingRewards, }; use crate::stakes::Stakes; -use crate::state::{Config, Distribution, SlashRatio, Stake}; +use crate::state::{Config, Distribution, PendingUnbond, SlashRatio, Stake}; pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -296,10 +296,11 @@ impl ExternalStakingContract<'_> { let mut resp = Response::new() .add_attribute("action", "unstake") .add_attribute("amount", amount.amount.to_string()) - .add_attribute("owner", info.sender); + .add_attribute("owner", info.sender.clone()); let channel = IBC_CHANNEL.load(deps.storage)?; let packet = ProviderPacket::Unstake { + delegator: info.sender.to_string(), validator, unstake: amount, tx_id, @@ -445,6 +446,66 @@ impl ExternalStakingContract<'_> { Ok(()) } + pub(crate) fn internal_unstake( + &self, + deps: DepsMut, + env: Env, + delegator: String, + validator: String, + amount: Coin, + ) -> Result { + let config = self.config.load(deps.storage)?; + let user = deps.api.addr_validate(&delegator)?; + // Load stake + let mut stake = self.stakes.stake.load(deps.storage, (&user, &validator))?; + + // Load distribution + let mut distribution = self + .distribution + .may_load(deps.storage, &validator)? + .unwrap_or_default(); + + // Commit sub amount, saturating if slashed + let amount = min(amount.amount, stake.stake.high()); + stake.stake.commit_sub(amount); + + let immediate_release = matches!( + self.val_set.validator_state(deps.storage, &validator)?, + State::Unbonded {} | State::Tombstoned {} + ); + + // FIXME? Release period being computed after successful IBC tx + // (Note: this is good for now, but can be revisited in v1 design) + let release_at = if immediate_release { + env.block.time + } else { + env.block.time.plus_seconds(config.unbonding_period) + }; + let unbond = PendingUnbond { amount, release_at }; + stake.pending_unbonds.push(unbond); + + // Distribution alignment + stake + .points_alignment + .stake_decreased(amount, distribution.points_per_stake); + distribution.total_stake -= amount; + + // Save stake + self.stakes + .stake + .save(deps.storage, (&user, &validator), &stake)?; + + // Save distribution + self.distribution + .save(deps.storage, &validator, &distribution)?; + let event = Event::new("internal_unstake") + .add_attribute("delegator", delegator) + .add_attribute("validator", validator) + .add_attribute("amount", amount.to_string()); + + Ok(event) + } + /// In non-test code, this is called from `ibc_packet_ack` #[allow(clippy::too_many_arguments)] pub(crate) fn valset_update( @@ -1271,6 +1332,7 @@ pub mod cross_staking { let channel = IBC_CHANNEL.load(ctx.deps.storage)?; let packet = ProviderPacket::Stake { + delegator: owner.to_string(), validator: msg.validator, stake: amount.clone(), tx_id, diff --git a/contracts/provider/external-staking/src/ibc.rs b/contracts/provider/external-staking/src/ibc.rs index 69127f2d..fc24ea14 100644 --- a/contracts/provider/external-staking/src/ibc.rs +++ b/contracts/provider/external-staking/src/ibc.rs @@ -155,6 +155,11 @@ pub fn ibc_packet_receive( .add_event(evt) .add_messages(msgs) } + ConsumerPacket::InternalUnstake { delegator, validator, amount} => { + let evt = contract.internal_unstake(deps, env, delegator, validator, amount)?; + let ack = ack_success(&DistributeAck {})?; + IbcReceiveResponse::new().set_ack(ack).add_event(evt) + } ConsumerPacket::Distribute { validator, rewards } => { let evt = contract.distribute_rewards(deps, &validator, rewards)?; let ack = ack_success(&DistributeAck {})?; diff --git a/packages/apis/src/converter_api.rs b/packages/apis/src/converter_api.rs index 8fc26412..d9d900e9 100644 --- a/packages/apis/src/converter_api.rs +++ b/packages/apis/src/converter_api.rs @@ -51,6 +51,16 @@ pub trait ConverterApi { tombstoned: Vec, slashed: Vec, ) -> Result, Self::Error>; + + /// Send ibc packet, request the external staking contract to unstake + #[sv::msg(exec)] + fn internal_unstake( + &self, + ctx: ExecCtx, + delegator: String, + validator: String, + amount: Coin, + ) -> Result, Self::Error>; } #[cw_serde] diff --git a/packages/apis/src/ibc/packet.rs b/packages/apis/src/ibc/packet.rs index 0978b9e8..7d02b76b 100644 --- a/packages/apis/src/ibc/packet.rs +++ b/packages/apis/src/ibc/packet.rs @@ -12,6 +12,7 @@ use crate::converter_api::{RewardInfo, ValidatorSlashInfo}; pub enum ProviderPacket { /// This should be called when we lock more tokens to virtually stake on a given validator Stake { + delegator: String, validator: String, /// This is the local (provider-side) denom that is held in the vault. /// It will be converted to the consumer-side staking token in the converter with help @@ -22,6 +23,7 @@ pub enum ProviderPacket { }, /// This should be called when we begin the unbonding period of some more tokens previously virtually staked Unstake { + delegator: String, validator: String, /// This is the local (provider-side) denom that is held in the vault. /// It will be converted to the consumer-side staking token in the converter with help @@ -110,6 +112,14 @@ pub enum ConsumerPacket { /// This has precedence over all other events in the same packet. slashed: Vec, }, + InternalUnstake { + delegator: String, + validator: String, + /// This is the local (provider-side) denom that is held in the vault. + /// It will be converted to the consumer-side staking token in the converter with help + /// of the price feed. + amount: Coin, + }, /// This is part of the rewards protocol Distribute { /// The validator whose stakers should receive these rewards diff --git a/packages/apis/src/virtual_staking_api.rs b/packages/apis/src/virtual_staking_api.rs index 982855ce..5c6df27e 100644 --- a/packages/apis/src/virtual_staking_api.rs +++ b/packages/apis/src/virtual_staking_api.rs @@ -25,6 +25,7 @@ pub trait VirtualStakingApi { fn bond( &self, ctx: ExecCtx, + delegator: String, validator: String, amount: Coin, ) -> Result, Self::Error>; @@ -36,6 +37,7 @@ pub trait VirtualStakingApi { fn unbond( &self, ctx: ExecCtx, + delegator: String, validator: String, amount: Coin, ) -> Result, Self::Error>; @@ -51,6 +53,17 @@ pub trait VirtualStakingApi { amount: Coin, ) -> Result, Self::Error>; + /// TODO: docs for this function + /// + #[sv::msg(exec)] + fn internal_unbond( + &self, + ctx: ExecCtx, + delegator: String, + validator: String, + amount: Coin, + ) -> Result, Self::Error>; + /// SudoMsg::HandleEpoch{} should be called once per epoch by the sdk (in EndBlock). /// It allows the virtual staking contract to bond or unbond any pending requests, as well /// as to perform a rebalance if needed (over the max cap). diff --git a/packages/bindings/src/query.rs b/packages/bindings/src/query.rs index 07af5ac5..c561ec17 100644 --- a/packages/bindings/src/query.rs +++ b/packages/bindings/src/query.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Coin, CustomQuery, QuerierWrapper, QueryRequest, StdResult}; +use cosmwasm_std::{Coin, Uint128, CustomQuery, QuerierWrapper, QueryRequest, StdResult}; #[cw_serde] #[derive(QueryResponses)] @@ -21,6 +21,10 @@ pub enum VirtualStakeQuery { /// Returns the blockchain's slashing ratios. #[returns(SlashRatioResponse)] SlashRatio {}, + + /// Returns a max retrieve amount of delegations for the given contract + #[returns(AllDelegationsResponse)] + AllDelegations { contract: String, max_retrieve: u16 }, } /// Bookkeeping info in the virtual staking sdk module @@ -42,6 +46,18 @@ pub struct SlashRatioResponse { pub slash_fraction_double_sign: String, } +#[cw_serde] +pub struct AllDelegationsResponse { + pub delegations: Vec, +} + +#[cw_serde] +pub struct Delegation { + pub delegator: String, + pub validator: String, + pub amount: Uint128, +} + impl CustomQuery for VirtualStakeCustomQuery {} impl From for QueryRequest { @@ -69,4 +85,9 @@ impl<'a> TokenQuerier<'a> { let slash_ratio_query = VirtualStakeQuery::SlashRatio {}; self.querier.query(&slash_ratio_query.into()) } + + pub fn all_delegations(&self, contract: String, max_retrieve: u16) -> StdResult { + let all_delegations_query = VirtualStakeQuery::AllDelegations { contract, max_retrieve }; + self.querier.query(&all_delegations_query.into()) + } } diff --git a/packages/virtual-staking-mock/src/lib.rs b/packages/virtual-staking-mock/src/lib.rs index 09abfeb6..b9a8b7b7 100644 --- a/packages/virtual-staking-mock/src/lib.rs +++ b/packages/virtual-staking-mock/src/lib.rs @@ -1,9 +1,6 @@ use anyhow::Result as AnyResult; use cosmwasm_std::{ - coin, - testing::{MockApi, MockStorage}, - to_json_binary, Addr, Api, Binary, BlockInfo, CustomQuery, Empty, Querier, QuerierWrapper, - Storage, Uint128, + coin, testing::{MockApi, MockStorage}, to_json_binary, Addr, AllDelegationsResponse, Api, Binary, BlockInfo, CustomQuery, Empty, Querier, QuerierWrapper, Storage, Uint128 }; use cw_multi_test::{AppResponse, BankKeeper, Module, WasmKeeper}; use cw_storage_plus::{Item, Map}; @@ -181,6 +178,11 @@ impl Module for VirtualStakingModule { mesh_bindings::VirtualStakeQuery::SlashRatio {} => { to_json_binary(&self.slash_ratio.load(storage)?)? } + mesh_bindings::VirtualStakeQuery::AllDelegations { .. } => { + to_json_binary(&AllDelegationsResponse { + delegations: vec![] + })? + } }; Ok(to_json_binary(&result)?) From 766fe9816e48eec42820ef52a290840602b2ae9d Mon Sep 17 00:00:00 2001 From: Trinity Date: Tue, 2 Jul 2024 16:24:31 +0700 Subject: [PATCH 02/21] change internal_unstake logic to immediate --- .../provider/external-staking/src/contract.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/contracts/provider/external-staking/src/contract.rs b/contracts/provider/external-staking/src/contract.rs index 2b996ea4..542ece4f 100644 --- a/contracts/provider/external-staking/src/contract.rs +++ b/contracts/provider/external-staking/src/contract.rs @@ -446,6 +446,7 @@ impl ExternalStakingContract<'_> { Ok(()) } + // immediate unstake assets pub(crate) fn internal_unstake( &self, deps: DepsMut, @@ -454,7 +455,6 @@ impl ExternalStakingContract<'_> { validator: String, amount: Coin, ) -> Result { - let config = self.config.load(deps.storage)?; let user = deps.api.addr_validate(&delegator)?; // Load stake let mut stake = self.stakes.stake.load(deps.storage, (&user, &validator))?; @@ -469,19 +469,7 @@ impl ExternalStakingContract<'_> { let amount = min(amount.amount, stake.stake.high()); stake.stake.commit_sub(amount); - let immediate_release = matches!( - self.val_set.validator_state(deps.storage, &validator)?, - State::Unbonded {} | State::Tombstoned {} - ); - - // FIXME? Release period being computed after successful IBC tx - // (Note: this is good for now, but can be revisited in v1 design) - let release_at = if immediate_release { - env.block.time - } else { - env.block.time.plus_seconds(config.unbonding_period) - }; - let unbond = PendingUnbond { amount, release_at }; + let unbond = PendingUnbond { amount, release_at: env.block.time }; stake.pending_unbonds.push(unbond); // Distribution alignment From 4cc0bab7b742178506a5ee92fab98723bfc62939 Mon Sep 17 00:00:00 2001 From: Trinity Date: Wed, 3 Jul 2024 16:17:55 +0700 Subject: [PATCH 03/21] move update delegation to separate msg --- .../consumer/virtual-staking/src/contract.rs | 20 +++++++++++++++---- packages/bindings/src/msg.rs | 16 +++++++++++++++ packages/virtual-staking-mock/src/lib.rs | 3 +++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index 01f23edc..0697374f 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -363,7 +363,7 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { fn bond( &self, ctx: ExecCtx, - _delegator: String, + delegator: String, validator: String, amount: Coin, ) -> Result, Self::Error> { @@ -385,7 +385,13 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { self.bond_requests .save(ctx.deps.storage, &validator, &bonded)?; - Ok(Response::new()) + let msg = VirtualStakeMsg::UpdateDelegation { + amount, + is_deduct: false, + delegator, + validator + }; + Ok(Response::new().add_message(msg)) } /// Requests to unbond tokens from a validator. This will be actually handled at the next epoch. @@ -394,7 +400,7 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { fn unbond( &self, ctx: ExecCtx, - _delegator: String, + delegator: String, validator: String, amount: Coin, ) -> Result, Self::Error> { @@ -415,7 +421,13 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { self.bond_requests .save(ctx.deps.storage, &validator, &bonded)?; - Ok(Response::new()) + let msg = VirtualStakeMsg::UpdateDelegation { + amount, + is_deduct: true, + delegator, + validator + }; + Ok(Response::new().add_message(msg)) } /// Requests to unbond and burn tokens from a list of validators. Unbonding will be actually handled at the next epoch. diff --git a/packages/bindings/src/msg.rs b/packages/bindings/src/msg.rs index 4ecce123..850fcf0b 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -30,6 +30,9 @@ pub enum VirtualStakeMsg { /// It will then burn those tokens from the caller's account, /// and update the currently minted amount. Unbond { amount: Coin, validator: String }, + /// TODO: Add docs for this msg + /// + UpdateDelegation { amount: Coin, is_deduct: bool, delegator: String, validator: String}, } impl VirtualStakeMsg { @@ -54,6 +57,19 @@ impl VirtualStakeMsg { validator: validator.to_string(), } } + + pub fn update_delegation(denom: &str, is_deduct: bool, amount: impl Into, delgator: &str, validator: &str) -> VirtualStakeMsg { + let coin = Coin { + amount: amount.into(), + denom: denom.into(), + }; + VirtualStakeMsg::UpdateDelegation { + amount: coin, + is_deduct, + delegator: delgator.to_string(), + validator: validator.to_string(), + } + } } impl From for CosmosMsg { diff --git a/packages/virtual-staking-mock/src/lib.rs b/packages/virtual-staking-mock/src/lib.rs index b9a8b7b7..31e9a0b1 100644 --- a/packages/virtual-staking-mock/src/lib.rs +++ b/packages/virtual-staking-mock/src/lib.rs @@ -132,6 +132,9 @@ impl Module for VirtualStakingModule { Err(anyhow::anyhow!("bonded amount exceeded")) } } + mesh_bindings::VirtualStakeMsg::UpdateDelegation { .. } => { + Ok(AppResponse::default()) + } } } From 3d0c6025cdcc67f5dd1260cedf7db3e6211c134f Mon Sep 17 00:00:00 2001 From: Trinity Date: Fri, 5 Jul 2024 20:40:54 +0700 Subject: [PATCH 04/21] move max_retrieve to config --- contracts/consumer/virtual-staking/src/contract.rs | 11 ++++++----- contracts/consumer/virtual-staking/src/state.rs | 4 ++++ contracts/provider/vault/src/contract.rs | 3 +-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index 0697374f..af20e3ac 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -24,9 +24,6 @@ use crate::state::Config; pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -// TODO: lack test for the appropriate max retrieve. -pub const MAX_RETRIEVE: u16 = 50; - pub struct VirtualStakingContract<'a> { pub config: Item<'a, Config>, /// Amount of tokens that have been requested to bond to a validator @@ -77,12 +74,14 @@ impl VirtualStakingContract<'_> { pub fn instantiate( &self, ctx: InstantiateCtx, + max_retrieve: u16 ) -> Result, ContractError> { nonpayable(&ctx.info)?; let denom = ctx.deps.querier.query_bonded_denom()?; let config = Config { denom, converter: ctx.info.sender, + max_retrieve: max_retrieve, }; self.config.save(ctx.deps.storage, &config)?; // initialize these to no one, so no issue when reading for the first time @@ -569,7 +568,7 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { if max_cap.is_zero() { let all_delegations = TokenQuerier::new(&deps.querier).all_delegations( - env.contract.address.to_string(), MAX_RETRIEVE + env.contract.address.to_string(), config.max_retrieve )?; let mut msgs = vec![]; for delegation in all_delegations.delegations { @@ -1419,7 +1418,9 @@ mod tests { deps, env: mock_env(), info: mock_info("me", &[]), - }) + }, + 50 + ) .unwrap(); } diff --git a/contracts/consumer/virtual-staking/src/state.rs b/contracts/consumer/virtual-staking/src/state.rs index 705e61a7..931bb7db 100644 --- a/contracts/consumer/virtual-staking/src/state.rs +++ b/contracts/consumer/virtual-staking/src/state.rs @@ -8,4 +8,8 @@ pub struct Config { /// The address of the converter contract (that is authorized to bond/unbond and will receive rewards) pub converter: Addr, + + /// Maximum + /// + pub max_retrieve: u16, } diff --git a/contracts/provider/vault/src/contract.rs b/contracts/provider/vault/src/contract.rs index 04a0d0e6..4538295e 100644 --- a/contracts/provider/vault/src/contract.rs +++ b/contracts/provider/vault/src/contract.rs @@ -1,6 +1,5 @@ use cosmwasm_std::{ - coin, ensure, Addr, BankMsg, Binary, Coin, Decimal, DepsMut, Fraction, Order, Reply, Response, - StdResult, Storage, SubMsg, SubMsgResponse, Uint128, WasmMsg, + coin, ensure, Addr, BankMsg, Binary, Coin, Decimal, DepsMut, Fraction, Order, Reply, Response, StdError, StdResult, Storage, SubMsg, SubMsgResponse, Uint128, WasmMsg }; use cw2::set_contract_version; use cw_storage_plus::{Bounder, Item, Map}; From 1f9b907390e7a8754f32ae2b53f49c3d0458ef55 Mon Sep 17 00:00:00 2001 From: Trinity Date: Tue, 9 Jul 2024 14:47:57 +0700 Subject: [PATCH 05/21] make max_retrieve work on converter contract --- Cargo.lock | 1 + contracts/consumer/converter/Cargo.toml | 3 ++- contracts/consumer/converter/src/contract.rs | 10 +++++++--- contracts/consumer/converter/src/multitest.rs | 1 + contracts/consumer/virtual-staking/src/multitest.rs | 1 + contracts/provider/vault/src/contract.rs | 3 ++- 6 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae9071cf..e73e7d98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -611,6 +611,7 @@ dependencies = [ "mesh-burn", "mesh-simple-price-feed", "mesh-sync", + "mesh-virtual-staking", "schemars", "serde", "sylvia", diff --git a/contracts/consumer/converter/Cargo.toml b/contracts/consumer/converter/Cargo.toml index 0a03ffe6..aefd9b14 100644 --- a/contracts/consumer/converter/Cargo.toml +++ b/contracts/consumer/converter/Cargo.toml @@ -24,6 +24,7 @@ fake-custom = [ "mesh-simple-price-feed/fake-custom" ] mesh-apis = { workspace = true } mesh-bindings = { workspace = true } mesh-sync = { workspace = true } +mesh-virtual-staking = { workspace = true } sylvia = { workspace = true } cosmwasm-schema = { workspace = true } @@ -39,7 +40,7 @@ thiserror = { workspace = true } [dev-dependencies] mesh-burn = { workspace = true } mesh-simple-price-feed = { workspace = true, features = ["mt"] } - +mesh-virtual-staking = { workspace = true, features = ["mt"] } cw-multi-test = { workspace = true } test-case = { workspace = true } derivative = { workspace = true } diff --git a/contracts/consumer/converter/src/contract.rs b/contracts/consumer/converter/src/contract.rs index ba62e558..674606e4 100644 --- a/contracts/consumer/converter/src/contract.rs +++ b/contracts/consumer/converter/src/contract.rs @@ -74,6 +74,7 @@ impl ConverterContract<'_> { remote_denom: String, virtual_staking_code_id: u64, admin: Option, + max_retrieve: u16, ) -> Result { nonpayable(&ctx.info)?; // validate args @@ -97,11 +98,14 @@ impl ConverterContract<'_> { ctx.deps.api.addr_validate(admin)?; } + let msg = to_json_binary(&mesh_virtual_staking::contract::sv::InstantiateMsg{ + max_retrieve + })?; // Instantiate virtual staking contract let init_msg = WasmMsg::Instantiate { admin, code_id: virtual_staking_code_id, - msg: b"{}".into(), + msg, funds: vec![], label: format!("Virtual Staking: {}", &config.remote_denom), }; @@ -151,7 +155,7 @@ impl ConverterContract<'_> { } #[cfg(not(any(test, feature = "mt")))] { - let _ = (ctx, validator, stake); + let _ = (ctx, delegator, validator, stake); Err(ContractError::Unauthorized) } } @@ -173,7 +177,7 @@ impl ConverterContract<'_> { } #[cfg(not(any(test, feature = "mt")))] { - let _ = (ctx, validator, unstake); + let _ = (ctx, delegator, validator, unstake); Err(ContractError::Unauthorized) } } diff --git a/contracts/consumer/converter/src/multitest.rs b/contracts/consumer/converter/src/multitest.rs index 2ee2b19e..56ac9181 100644 --- a/contracts/consumer/converter/src/multitest.rs +++ b/contracts/consumer/converter/src/multitest.rs @@ -63,6 +63,7 @@ fn setup<'a>(app: &'a App, args: SetupArgs<'a>) -> SetupResponse<'a> { JUNO.to_owned(), virtual_staking_code.code_id(), Some(admin.to_owned()), + 50 ) .with_label("Juno Converter") .with_admin(admin) diff --git a/contracts/consumer/virtual-staking/src/multitest.rs b/contracts/consumer/virtual-staking/src/multitest.rs index 54be3760..76f6ff1e 100644 --- a/contracts/consumer/virtual-staking/src/multitest.rs +++ b/contracts/consumer/virtual-staking/src/multitest.rs @@ -60,6 +60,7 @@ fn setup<'a>(app: &'a App, args: SetupArgs<'a>) -> SetupResponse<'a> { JUNO.to_owned(), virtual_staking_code.code_id(), Some(admin.to_owned()), + 50, ) .with_label("Juno Converter") .with_admin(admin) diff --git a/contracts/provider/vault/src/contract.rs b/contracts/provider/vault/src/contract.rs index 4538295e..7a7d0d5a 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, StdError, StdResult, Storage, SubMsg, SubMsgResponse, Uint128, WasmMsg + coin, ensure, Addr, BankMsg, 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}; @@ -13,6 +13,7 @@ use mesh_apis::local_staking_api::{ 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}; From d83cab757a2f051674c41360df0210dad8bf398f Mon Sep 17 00:00:00 2001 From: Dzung Do Date: Thu, 11 Jul 2024 22:09:52 +0700 Subject: [PATCH 06/21] 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 07/21] 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 08/21] 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 09/21] 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 360a8f469a198ca948862424dc9e76448feceec8 Mon Sep 17 00:00:00 2001 From: Trinity Date: Mon, 5 Aug 2024 11:20:09 +0700 Subject: [PATCH 10/21] lint --- contracts/consumer/converter/src/contract.rs | 5 +- contracts/consumer/converter/src/ibc.rs | 22 ++++--- contracts/consumer/converter/src/multitest.rs | 4 +- .../src/multitest/virtual_staking_mock.rs | 10 +-- .../consumer/virtual-staking/src/contract.rs | 64 ++++++++++--------- .../consumer/virtual-staking/src/state.rs | 4 +- .../provider/external-staking/src/contract.rs | 5 +- .../provider/external-staking/src/ibc.rs | 6 +- contracts/provider/vault/src/contract.rs | 3 +- packages/apis/src/virtual_staking_api.rs | 2 +- packages/bindings/src/msg.rs | 19 ++++-- packages/bindings/src/query.rs | 15 +++-- packages/virtual-staking-mock/src/lib.rs | 11 ++-- 13 files changed, 102 insertions(+), 68 deletions(-) diff --git a/contracts/consumer/converter/src/contract.rs b/contracts/consumer/converter/src/contract.rs index 674606e4..900401ec 100644 --- a/contracts/consumer/converter/src/contract.rs +++ b/contracts/consumer/converter/src/contract.rs @@ -98,9 +98,8 @@ impl ConverterContract<'_> { ctx.deps.api.addr_validate(admin)?; } - let msg = to_json_binary(&mesh_virtual_staking::contract::sv::InstantiateMsg{ - max_retrieve - })?; + let msg = + to_json_binary(&mesh_virtual_staking::contract::sv::InstantiateMsg { max_retrieve })?; // Instantiate virtual staking contract let init_msg = WasmMsg::Instantiate { admin, diff --git a/contracts/consumer/converter/src/ibc.rs b/contracts/consumer/converter/src/ibc.rs index f719fee8..875809b3 100644 --- a/contracts/consumer/converter/src/ibc.rs +++ b/contracts/consumer/converter/src/ibc.rs @@ -54,7 +54,10 @@ pub fn packet_timeout_rewards(env: &Env) -> IbcTimeout { pub fn packet_timeout_internal_unstake(env: &Env) -> IbcTimeout { // No idea about their block time, but 24 hours ahead of our view of the clock // should be decently in the future. - let timeout = env.block.time.plus_seconds(DEFAULT_INTERNAL_UNSTAKE_TIMEOUT); + let timeout = env + .block + .time + .plus_seconds(DEFAULT_INTERNAL_UNSTAKE_TIMEOUT); IbcTimeout::with_timestamp(timeout) } @@ -267,15 +270,16 @@ pub fn ibc_packet_ack( match ack { AckWrapper::Result(_) => { let packet: ConsumerPacket = from_json(&msg.original_packet.data)?; - if let ConsumerPacket::InternalUnstake { - delegator, - validator, - amount, - } = packet { - // execute virtual contract's internal unbond + if let ConsumerPacket::InternalUnstake { + delegator, + validator, + amount, + } = packet + { + // execute virtual contract's internal unbond let msg = virtual_staking_api::sv::ExecMsg::InternalUnbond { - delegator, - validator, + delegator, + validator, amount, }; let msg = WasmMsg::Execute { diff --git a/contracts/consumer/converter/src/multitest.rs b/contracts/consumer/converter/src/multitest.rs index 56ac9181..6d3a8ab7 100644 --- a/contracts/consumer/converter/src/multitest.rs +++ b/contracts/consumer/converter/src/multitest.rs @@ -63,7 +63,7 @@ fn setup<'a>(app: &'a App, args: SetupArgs<'a>) -> SetupResponse<'a> { JUNO.to_owned(), virtual_staking_code.code_id(), Some(admin.to_owned()), - 50 + 50, ) .with_label("Juno Converter") .with_admin(admin) @@ -259,7 +259,7 @@ fn ibc_stake_and_burn() { // let's stake some converter - .test_stake(owner.to_string(),val1.to_string(), coin(1000, JUNO)) + .test_stake(owner.to_string(), val1.to_string(), coin(1000, JUNO)) .call(owner) .unwrap(); converter diff --git a/contracts/consumer/converter/src/multitest/virtual_staking_mock.rs b/contracts/consumer/converter/src/multitest/virtual_staking_mock.rs index d4422445..96048e17 100644 --- a/contracts/consumer/converter/src/multitest/virtual_staking_mock.rs +++ b/contracts/consumer/converter/src/multitest/virtual_staking_mock.rs @@ -245,11 +245,11 @@ impl VirtualStakingApi for VirtualStakingMock<'_> { fn internal_unbond( &self, - _ctx:ExecCtx, - _delegator:String, - _validator:String, - _amount:Coin - ) -> Result ,Self::Error> { + _ctx: ExecCtx, + _delegator: String, + _validator: String, + _amount: Coin, + ) -> Result, Self::Error> { unimplemented!() } diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index af20e3ac..96425c3e 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -74,7 +74,7 @@ impl VirtualStakingContract<'_> { pub fn instantiate( &self, ctx: InstantiateCtx, - max_retrieve: u16 + max_retrieve: u16, ) -> Result, ContractError> { nonpayable(&ctx.info)?; let denom = ctx.deps.querier.query_bonded_denom()?; @@ -384,11 +384,11 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { self.bond_requests .save(ctx.deps.storage, &validator, &bonded)?; - let msg = VirtualStakeMsg::UpdateDelegation { - amount, + let msg = VirtualStakeMsg::UpdateDelegation { + amount, is_deduct: false, - delegator, - validator + delegator, + validator, }; Ok(Response::new().add_message(msg)) } @@ -420,11 +420,11 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { self.bond_requests .save(ctx.deps.storage, &validator, &bonded)?; - let msg = VirtualStakeMsg::UpdateDelegation { - amount, + let msg = VirtualStakeMsg::UpdateDelegation { + amount, is_deduct: true, - delegator, - validator + delegator, + validator, }; Ok(Response::new().add_message(msg)) } @@ -498,7 +498,7 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { _delegator: String, validator: String, amount: Coin, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { nonpayable(&ctx.info)?; let cfg = self.config.load(ctx.deps.storage)?; ensure_eq!(ctx.info.sender, cfg.converter, ContractError::Unauthorized); // only the converter can call this @@ -526,7 +526,7 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { ) .collect::>()?; self.bonded.save(ctx.deps.storage, &requests)?; - return Ok(Response::new()) + return Ok(Response::new()); } // FIXME: need to handle custom message types and queries @@ -566,18 +566,19 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { // call and set bonded to empty // TODO: verify this behavior with SDK module (otherwise we send unbond message) if max_cap.is_zero() { - let all_delegations = - TokenQuerier::new(&deps.querier).all_delegations( - env.contract.address.to_string(), config.max_retrieve - )?; + let all_delegations = TokenQuerier::new(&deps.querier) + .all_delegations(env.contract.address.to_string(), config.max_retrieve)?; let mut msgs = vec![]; for delegation in all_delegations.delegations { let validator = delegation.validator.clone(); // Send unstake request to converter contract - let msg = converter_api::sv::ExecMsg::InternalUnstake { - delegator: delegation.delegator, - validator: validator, - amount: Coin { denom: config.denom.clone(), amount: delegation.amount } + let msg = converter_api::sv::ExecMsg::InternalUnstake { + delegator: delegation.delegator, + validator: validator, + amount: Coin { + denom: config.denom.clone(), + amount: delegation.amount, + }, }; let msg = WasmMsg::Execute { contract_addr: config.converter.to_string(), @@ -719,7 +720,9 @@ mod tests { }; use cosmwasm_std::{ - coins, from_json, testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, AllDelegationsResponse, Decimal + coins, from_json, + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + AllDelegationsResponse, Decimal, }; use mesh_bindings::{BondStatusResponse, SlashRatioResponse}; @@ -1285,8 +1288,8 @@ mod tests { slash_fraction_downtime: "0.1".to_string(), slash_fraction_double_sign: "0.25".to_string(), }); - let all_delegations = MockAllDelegations::new(AllDelegationsResponse { - delegations: vec![] + let all_delegations = MockAllDelegations::new(AllDelegationsResponse { + delegations: vec![], }); let handler = { @@ -1371,7 +1374,7 @@ mod tests { self.0.borrow() } } - + fn set_reward_targets(storage: &mut dyn Storage, targets: &[&str]) { REWARD_TARGETS .save( @@ -1414,13 +1417,14 @@ mod tests { impl VirtualStakingExt for VirtualStakingContract<'_> { fn quick_inst(&self, deps: DepsMut) { - self.instantiate(InstantiateCtx { - deps, - env: mock_env(), - info: mock_info("me", &[]), - }, - 50 - ) + self.instantiate( + InstantiateCtx { + deps, + env: mock_env(), + info: mock_info("me", &[]), + }, + 50, + ) .unwrap(); } diff --git a/contracts/consumer/virtual-staking/src/state.rs b/contracts/consumer/virtual-staking/src/state.rs index 931bb7db..76a6cb25 100644 --- a/contracts/consumer/virtual-staking/src/state.rs +++ b/contracts/consumer/virtual-staking/src/state.rs @@ -9,7 +9,7 @@ pub struct Config { /// The address of the converter contract (that is authorized to bond/unbond and will receive rewards) pub converter: Addr, - /// Maximum - /// + /// Maximum + /// pub max_retrieve: u16, } diff --git a/contracts/provider/external-staking/src/contract.rs b/contracts/provider/external-staking/src/contract.rs index 542ece4f..d2684505 100644 --- a/contracts/provider/external-staking/src/contract.rs +++ b/contracts/provider/external-staking/src/contract.rs @@ -469,7 +469,10 @@ impl ExternalStakingContract<'_> { let amount = min(amount.amount, stake.stake.high()); stake.stake.commit_sub(amount); - let unbond = PendingUnbond { amount, release_at: env.block.time }; + let unbond = PendingUnbond { + amount, + release_at: env.block.time, + }; stake.pending_unbonds.push(unbond); // Distribution alignment diff --git a/contracts/provider/external-staking/src/ibc.rs b/contracts/provider/external-staking/src/ibc.rs index fc24ea14..42cf9328 100644 --- a/contracts/provider/external-staking/src/ibc.rs +++ b/contracts/provider/external-staking/src/ibc.rs @@ -155,7 +155,11 @@ pub fn ibc_packet_receive( .add_event(evt) .add_messages(msgs) } - ConsumerPacket::InternalUnstake { delegator, validator, amount} => { + ConsumerPacket::InternalUnstake { + delegator, + validator, + amount, + } => { let evt = contract.internal_unstake(deps, env, delegator, validator, amount)?; let ack = ack_success(&DistributeAck {})?; IbcReceiveResponse::new().set_ack(ack).add_event(evt) diff --git a/contracts/provider/vault/src/contract.rs b/contracts/provider/vault/src/contract.rs index 7a7d0d5a..33ad3987 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, BankMsg, Binary, Coin, Decimal, DepsMut, Fraction, Order, Reply, Response, StdResult, Storage, SubMsg, SubMsgResponse, Uint128, WasmMsg + coin, ensure, Addr, BankMsg, 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}; diff --git a/packages/apis/src/virtual_staking_api.rs b/packages/apis/src/virtual_staking_api.rs index 5c6df27e..ed02e47b 100644 --- a/packages/apis/src/virtual_staking_api.rs +++ b/packages/apis/src/virtual_staking_api.rs @@ -54,7 +54,7 @@ pub trait VirtualStakingApi { ) -> Result, Self::Error>; /// TODO: docs for this function - /// + /// #[sv::msg(exec)] fn internal_unbond( &self, diff --git a/packages/bindings/src/msg.rs b/packages/bindings/src/msg.rs index 850fcf0b..49e3b36a 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -31,8 +31,13 @@ pub enum VirtualStakeMsg { /// and update the currently minted amount. Unbond { amount: Coin, validator: String }, /// TODO: Add docs for this msg - /// - UpdateDelegation { amount: Coin, is_deduct: bool, delegator: String, validator: String}, + /// + UpdateDelegation { + amount: Coin, + is_deduct: bool, + delegator: String, + validator: String, + }, } impl VirtualStakeMsg { @@ -57,8 +62,14 @@ impl VirtualStakeMsg { validator: validator.to_string(), } } - - pub fn update_delegation(denom: &str, is_deduct: bool, amount: impl Into, delgator: &str, validator: &str) -> VirtualStakeMsg { + + pub fn update_delegation( + denom: &str, + is_deduct: bool, + amount: impl Into, + delgator: &str, + validator: &str, + ) -> VirtualStakeMsg { let coin = Coin { amount: amount.into(), denom: denom.into(), diff --git a/packages/bindings/src/query.rs b/packages/bindings/src/query.rs index c561ec17..72b3bf14 100644 --- a/packages/bindings/src/query.rs +++ b/packages/bindings/src/query.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Coin, Uint128, CustomQuery, QuerierWrapper, QueryRequest, StdResult}; +use cosmwasm_std::{Coin, CustomQuery, QuerierWrapper, QueryRequest, StdResult, Uint128}; #[cw_serde] #[derive(QueryResponses)] @@ -22,7 +22,7 @@ pub enum VirtualStakeQuery { #[returns(SlashRatioResponse)] SlashRatio {}, - /// Returns a max retrieve amount of delegations for the given contract + /// Returns a max retrieve amount of delegations for the given contract #[returns(AllDelegationsResponse)] AllDelegations { contract: String, max_retrieve: u16 }, } @@ -86,8 +86,15 @@ impl<'a> TokenQuerier<'a> { self.querier.query(&slash_ratio_query.into()) } - pub fn all_delegations(&self, contract: String, max_retrieve: u16) -> StdResult { - let all_delegations_query = VirtualStakeQuery::AllDelegations { contract, max_retrieve }; + pub fn all_delegations( + &self, + contract: String, + max_retrieve: u16, + ) -> StdResult { + let all_delegations_query = VirtualStakeQuery::AllDelegations { + contract, + max_retrieve, + }; self.querier.query(&all_delegations_query.into()) } } diff --git a/packages/virtual-staking-mock/src/lib.rs b/packages/virtual-staking-mock/src/lib.rs index 31e9a0b1..44dd07af 100644 --- a/packages/virtual-staking-mock/src/lib.rs +++ b/packages/virtual-staking-mock/src/lib.rs @@ -1,6 +1,9 @@ use anyhow::Result as AnyResult; use cosmwasm_std::{ - coin, testing::{MockApi, MockStorage}, to_json_binary, Addr, AllDelegationsResponse, Api, Binary, BlockInfo, CustomQuery, Empty, Querier, QuerierWrapper, Storage, Uint128 + coin, + testing::{MockApi, MockStorage}, + to_json_binary, Addr, AllDelegationsResponse, Api, Binary, BlockInfo, CustomQuery, Empty, + Querier, QuerierWrapper, Storage, Uint128, }; use cw_multi_test::{AppResponse, BankKeeper, Module, WasmKeeper}; use cw_storage_plus::{Item, Map}; @@ -132,9 +135,7 @@ impl Module for VirtualStakingModule { Err(anyhow::anyhow!("bonded amount exceeded")) } } - mesh_bindings::VirtualStakeMsg::UpdateDelegation { .. } => { - Ok(AppResponse::default()) - } + mesh_bindings::VirtualStakeMsg::UpdateDelegation { .. } => Ok(AppResponse::default()), } } @@ -183,7 +184,7 @@ impl Module for VirtualStakingModule { } mesh_bindings::VirtualStakeQuery::AllDelegations { .. } => { to_json_binary(&AllDelegationsResponse { - delegations: vec![] + delegations: vec![], })? } }; From d7fa3964502e7b71516ea37c37321c980fbbffaa Mon Sep 17 00:00:00 2001 From: Trinity Date: Mon, 5 Aug 2024 11:28:40 +0700 Subject: [PATCH 11/21] lint --- contracts/consumer/virtual-staking/src/contract.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index 96425c3e..b087cd47 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -81,7 +81,7 @@ impl VirtualStakingContract<'_> { let config = Config { denom, converter: ctx.info.sender, - max_retrieve: max_retrieve, + max_retrieve, }; self.config.save(ctx.deps.storage, &config)?; // initialize these to no one, so no issue when reading for the first time @@ -526,7 +526,7 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { ) .collect::>()?; self.bonded.save(ctx.deps.storage, &requests)?; - return Ok(Response::new()); + Ok(Response::new()) } // FIXME: need to handle custom message types and queries @@ -574,7 +574,7 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { // Send unstake request to converter contract let msg = converter_api::sv::ExecMsg::InternalUnstake { delegator: delegation.delegator, - validator: validator, + validator, amount: Coin { denom: config.denom.clone(), amount: delegation.amount, From 0894cda3e4bed0e6e28ad200a099f433a8dd2c9d Mon Sep 17 00:00:00 2001 From: Trinity Date: Mon, 5 Aug 2024 11:40:06 +0700 Subject: [PATCH 12/21] add doc --- packages/apis/src/ibc/packet.rs | 2 ++ packages/apis/src/virtual_staking_api.rs | 5 +++-- packages/bindings/src/msg.rs | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/apis/src/ibc/packet.rs b/packages/apis/src/ibc/packet.rs index 7d02b76b..c873f697 100644 --- a/packages/apis/src/ibc/packet.rs +++ b/packages/apis/src/ibc/packet.rs @@ -112,6 +112,8 @@ pub enum ConsumerPacket { /// This has precedence over all other events in the same packet. slashed: Vec, }, + /// This is a part of zero max cap process + /// The consumer chain will send this packet to provider, force user to unbond token InternalUnstake { delegator: String, validator: String, diff --git a/packages/apis/src/virtual_staking_api.rs b/packages/apis/src/virtual_staking_api.rs index ed02e47b..9377eee1 100644 --- a/packages/apis/src/virtual_staking_api.rs +++ b/packages/apis/src/virtual_staking_api.rs @@ -53,8 +53,9 @@ pub trait VirtualStakingApi { amount: Coin, ) -> Result, Self::Error>; - /// TODO: docs for this function - /// + /// Immediately unbond the given amount due to zero max cap + /// When the consumer chain receives ack packet from provider - which means the unbond process from provider is success, + /// consumer chain will trigger this function to unbond this contract actor staking base on the delegate amount. #[sv::msg(exec)] fn internal_unbond( &self, diff --git a/packages/bindings/src/msg.rs b/packages/bindings/src/msg.rs index 49e3b36a..74fecafe 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -30,8 +30,8 @@ pub enum VirtualStakeMsg { /// It will then burn those tokens from the caller's account, /// and update the currently minted amount. Unbond { amount: Coin, validator: String }, - /// TODO: Add docs for this msg - /// + /// After each bonding or unbond process, a msg will be sent to the chain + /// Consumer chain will save the data - represent each delegator's stake amount UpdateDelegation { amount: Coin, is_deduct: bool, From b6a0bfbe2dd1fdc5558fdd17595aa4782cf6be70 Mon Sep 17 00:00:00 2001 From: Trinity Date: Mon, 5 Aug 2024 11:41:03 +0700 Subject: [PATCH 13/21] lint --- contracts/consumer/converter/src/contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/consumer/converter/src/contract.rs b/contracts/consumer/converter/src/contract.rs index 900401ec..d24f94ac 100644 --- a/contracts/consumer/converter/src/contract.rs +++ b/contracts/consumer/converter/src/contract.rs @@ -639,7 +639,7 @@ impl ConverterApi for ConverterContract<'_> { let channel = IBC_CHANNEL.load(ctx.deps.storage)?; let packet = ConsumerPacket::InternalUnstake { - delegator: delegator, + delegator, validator, amount, }; From aba6c2af282155baf185fc60db248365a00761e3 Mon Sep 17 00:00:00 2001 From: Trinity Date: Fri, 23 Aug 2024 13:49:31 +0700 Subject: [PATCH 14/21] Recalculate the price when unbond --- contracts/consumer/converter/src/contract.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/consumer/converter/src/contract.rs b/contracts/consumer/converter/src/contract.rs index d24f94ac..5f2b0da3 100644 --- a/contracts/consumer/converter/src/contract.rs +++ b/contracts/consumer/converter/src/contract.rs @@ -638,6 +638,9 @@ impl ConverterApi for ConverterContract<'_> { .add_attribute("owner", delegator.clone()); let channel = IBC_CHANNEL.load(ctx.deps.storage)?; + + // Recalculate the price when unbond + let amount = self.invert_price(ctx.deps.as_ref(), amount)?; let packet = ConsumerPacket::InternalUnstake { delegator, validator, From 90c34a420889ec9836d6f238207fcf8cf84be0ff Mon Sep 17 00:00:00 2001 From: Trinity Date: Fri, 23 Aug 2024 15:15:19 +0700 Subject: [PATCH 15/21] fix logic for zero max cap --- contracts/consumer/converter/src/contract.rs | 5 +++-- contracts/consumer/converter/src/ibc.rs | 6 ++++-- .../consumer/virtual-staking/src/contract.rs | 16 ++++++++++++++-- .../provider/external-staking/src/contract.rs | 4 ++-- contracts/provider/external-staking/src/ibc.rs | 5 +++-- packages/apis/src/ibc/packet.rs | 3 ++- 6 files changed, 28 insertions(+), 11 deletions(-) diff --git a/contracts/consumer/converter/src/contract.rs b/contracts/consumer/converter/src/contract.rs index 5f2b0da3..894761b0 100644 --- a/contracts/consumer/converter/src/contract.rs +++ b/contracts/consumer/converter/src/contract.rs @@ -640,11 +640,12 @@ impl ConverterApi for ConverterContract<'_> { let channel = IBC_CHANNEL.load(ctx.deps.storage)?; // Recalculate the price when unbond - let amount = self.invert_price(ctx.deps.as_ref(), amount)?; + let inverted_amount = self.invert_price(ctx.deps.as_ref(), amount.clone())?; let packet = ConsumerPacket::InternalUnstake { delegator, validator, - amount, + normalize_amount: amount, + inverted_amount, }; let msg = IbcMsg::SendPacket { channel_id: channel.endpoint.channel_id, diff --git a/contracts/consumer/converter/src/ibc.rs b/contracts/consumer/converter/src/ibc.rs index 875809b3..0c8fb107 100644 --- a/contracts/consumer/converter/src/ibc.rs +++ b/contracts/consumer/converter/src/ibc.rs @@ -273,14 +273,16 @@ pub fn ibc_packet_ack( if let ConsumerPacket::InternalUnstake { delegator, validator, - amount, + normalize_amount, + inverted_amount: _, } = packet { + // execute virtual contract's internal unbond let msg = virtual_staking_api::sv::ExecMsg::InternalUnbond { delegator, validator, - amount, + amount: normalize_amount, }; let msg = WasmMsg::Execute { contract_addr: contract.virtual_stake.load(deps.storage)?.into(), diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index b087cd47..a6a302bc 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -495,7 +495,7 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { fn internal_unbond( &self, ctx: ExecCtx, - _delegator: String, + delegator: String, validator: String, amount: Coin, ) -> Result, Self::Error> { @@ -526,7 +526,19 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { ) .collect::>()?; self.bonded.save(ctx.deps.storage, &requests)?; - Ok(Response::new()) + + let mut msgs: Vec<_> = vec![]; + msgs.push(VirtualStakeMsg::UpdateDelegation { + amount: amount.clone(), + is_deduct: true, + delegator, + validator: validator.clone(), + }); + msgs.push(VirtualStakeMsg::Unbond { + amount, + validator, + }); + Ok(Response::new().add_messages(msgs)) } // FIXME: need to handle custom message types and queries diff --git a/contracts/provider/external-staking/src/contract.rs b/contracts/provider/external-staking/src/contract.rs index d2684505..cd3a93c3 100644 --- a/contracts/provider/external-staking/src/contract.rs +++ b/contracts/provider/external-staking/src/contract.rs @@ -467,8 +467,8 @@ impl ExternalStakingContract<'_> { // Commit sub amount, saturating if slashed let amount = min(amount.amount, stake.stake.high()); - stake.stake.commit_sub(amount); - + stake.stake.sub(amount, Uint128::zero())?; + let unbond = PendingUnbond { amount, release_at: env.block.time, diff --git a/contracts/provider/external-staking/src/ibc.rs b/contracts/provider/external-staking/src/ibc.rs index 42cf9328..b054d7ec 100644 --- a/contracts/provider/external-staking/src/ibc.rs +++ b/contracts/provider/external-staking/src/ibc.rs @@ -158,9 +158,10 @@ pub fn ibc_packet_receive( ConsumerPacket::InternalUnstake { delegator, validator, - amount, + normalize_amount: _, + inverted_amount, } => { - let evt = contract.internal_unstake(deps, env, delegator, validator, amount)?; + let evt = contract.internal_unstake(deps, env, delegator, validator, inverted_amount)?; let ack = ack_success(&DistributeAck {})?; IbcReceiveResponse::new().set_ack(ack).add_event(evt) } diff --git a/packages/apis/src/ibc/packet.rs b/packages/apis/src/ibc/packet.rs index c873f697..86bdac64 100644 --- a/packages/apis/src/ibc/packet.rs +++ b/packages/apis/src/ibc/packet.rs @@ -120,7 +120,8 @@ pub enum ConsumerPacket { /// This is the local (provider-side) denom that is held in the vault. /// It will be converted to the consumer-side staking token in the converter with help /// of the price feed. - amount: Coin, + normalize_amount: Coin, + inverted_amount: Coin, }, /// This is part of the rewards protocol Distribute { From a0bba9b5f7eed6768d7a84ac766afb9d89dea4c2 Mon Sep 17 00:00:00 2001 From: Trinity Date: Fri, 23 Aug 2024 15:16:21 +0700 Subject: [PATCH 16/21] lint --- contracts/consumer/converter/src/contract.rs | 2 +- contracts/consumer/converter/src/ibc.rs | 1 - contracts/consumer/virtual-staking/src/contract.rs | 5 +---- contracts/provider/external-staking/src/contract.rs | 2 +- contracts/provider/external-staking/src/ibc.rs | 3 ++- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/contracts/consumer/converter/src/contract.rs b/contracts/consumer/converter/src/contract.rs index 894761b0..98eb878c 100644 --- a/contracts/consumer/converter/src/contract.rs +++ b/contracts/consumer/converter/src/contract.rs @@ -640,7 +640,7 @@ impl ConverterApi for ConverterContract<'_> { let channel = IBC_CHANNEL.load(ctx.deps.storage)?; // Recalculate the price when unbond - let inverted_amount = self.invert_price(ctx.deps.as_ref(), amount.clone())?; + let inverted_amount = self.invert_price(ctx.deps.as_ref(), amount.clone())?; let packet = ConsumerPacket::InternalUnstake { delegator, validator, diff --git a/contracts/consumer/converter/src/ibc.rs b/contracts/consumer/converter/src/ibc.rs index 0c8fb107..90865843 100644 --- a/contracts/consumer/converter/src/ibc.rs +++ b/contracts/consumer/converter/src/ibc.rs @@ -277,7 +277,6 @@ pub fn ibc_packet_ack( inverted_amount: _, } = packet { - // execute virtual contract's internal unbond let msg = virtual_staking_api::sv::ExecMsg::InternalUnbond { delegator, diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index a6a302bc..a9cd06e0 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -534,10 +534,7 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { delegator, validator: validator.clone(), }); - msgs.push(VirtualStakeMsg::Unbond { - amount, - validator, - }); + msgs.push(VirtualStakeMsg::Unbond { amount, validator }); Ok(Response::new().add_messages(msgs)) } diff --git a/contracts/provider/external-staking/src/contract.rs b/contracts/provider/external-staking/src/contract.rs index cd3a93c3..115d1bef 100644 --- a/contracts/provider/external-staking/src/contract.rs +++ b/contracts/provider/external-staking/src/contract.rs @@ -468,7 +468,7 @@ impl ExternalStakingContract<'_> { // Commit sub amount, saturating if slashed let amount = min(amount.amount, stake.stake.high()); stake.stake.sub(amount, Uint128::zero())?; - + let unbond = PendingUnbond { amount, release_at: env.block.time, diff --git a/contracts/provider/external-staking/src/ibc.rs b/contracts/provider/external-staking/src/ibc.rs index b054d7ec..c8080f70 100644 --- a/contracts/provider/external-staking/src/ibc.rs +++ b/contracts/provider/external-staking/src/ibc.rs @@ -161,7 +161,8 @@ pub fn ibc_packet_receive( normalize_amount: _, inverted_amount, } => { - let evt = contract.internal_unstake(deps, env, delegator, validator, inverted_amount)?; + let evt = + contract.internal_unstake(deps, env, delegator, validator, inverted_amount)?; let ack = ack_success(&DistributeAck {})?; IbcReceiveResponse::new().set_ack(ack).add_event(evt) } From 2dd8d509702ce6c2cbbea250fe88ea2b6c185254 Mon Sep 17 00:00:00 2001 From: Trinity Date: Fri, 23 Aug 2024 16:33:26 +0700 Subject: [PATCH 17/21] lint --- .../consumer/virtual-staking/src/contract.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index a9cd06e0..a025575e 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -527,14 +527,15 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { .collect::>()?; self.bonded.save(ctx.deps.storage, &requests)?; - let mut msgs: Vec<_> = vec![]; - msgs.push(VirtualStakeMsg::UpdateDelegation { - amount: amount.clone(), - is_deduct: true, - delegator, - validator: validator.clone(), - }); - msgs.push(VirtualStakeMsg::Unbond { amount, validator }); + let msgs = vec![ + VirtualStakeMsg::UpdateDelegation { + amount: amount.clone(), + is_deduct: true, + delegator, + validator: validator.clone(), + }, + VirtualStakeMsg::Unbond { amount, validator } + ]; Ok(Response::new().add_messages(msgs)) } From 5d9d7f9811d83dad26080dc10d42de3315826b7e Mon Sep 17 00:00:00 2001 From: Trinity Date: Tue, 27 Aug 2024 12:49:14 +0700 Subject: [PATCH 18/21] 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 19/21] 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(), From 9d82cc7bf766a4ccab08212dea78bf4a42ef2e1a Mon Sep 17 00:00:00 2001 From: Trinity Date: Thu, 5 Sep 2024 11:50:07 +0700 Subject: [PATCH 20/21] Update schedule logic for zero max cap --- contracts/consumer/virtual-staking/src/contract.rs | 3 +++ packages/bindings/src/msg.rs | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index a025575e..fee5bd71 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -578,6 +578,9 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { if max_cap.is_zero() { let all_delegations = TokenQuerier::new(&deps.querier) .all_delegations(env.contract.address.to_string(), config.max_retrieve)?; + if all_delegations.delegations.len() == 0 { + return Ok(resp.add_message(VirtualStakeMsg::DeleteAllScheduledTasks{})); + } let mut msgs = vec![]; for delegation in all_delegations.delegations { let validator = delegation.validator.clone(); diff --git a/packages/bindings/src/msg.rs b/packages/bindings/src/msg.rs index d732f39a..e145ed48 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -38,6 +38,8 @@ pub enum VirtualStakeMsg { delegator: String, validator: String, }, + /// Delete all scheduled tasks after zero max cap and unbond all delegations + DeleteAllScheduledTasks {} } impl VirtualStakeMsg { @@ -81,6 +83,10 @@ impl VirtualStakeMsg { validator: validator.to_string(), } } + + pub fn delete_all_scheduled_tasks() -> VirtualStakeMsg { + VirtualStakeMsg::DeleteAllScheduledTasks {} + } } impl From for CosmosMsg { From 8b6f4ee0002e8bd285d790f52d204b848afbce7e Mon Sep 17 00:00:00 2001 From: Trinity Date: Thu, 5 Sep 2024 12:20:49 +0700 Subject: [PATCH 21/21] Fix tests --- contracts/consumer/virtual-staking/src/contract.rs | 8 ++++---- packages/bindings/src/msg.rs | 2 +- packages/virtual-staking-mock/src/lib.rs | 3 +++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index fee5bd71..037e28d4 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -534,7 +534,7 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { delegator, validator: validator.clone(), }, - VirtualStakeMsg::Unbond { amount, validator } + VirtualStakeMsg::Unbond { amount, validator }, ]; Ok(Response::new().add_messages(msgs)) } @@ -579,7 +579,7 @@ impl VirtualStakingApi for VirtualStakingContract<'_> { let all_delegations = TokenQuerier::new(&deps.querier) .all_delegations(env.contract.address.to_string(), config.max_retrieve)?; if all_delegations.delegations.len() == 0 { - return Ok(resp.add_message(VirtualStakeMsg::DeleteAllScheduledTasks{})); + return Ok(resp.add_message(VirtualStakeMsg::DeleteAllScheduledTasks {})); } let mut msgs = vec![]; for delegation in all_delegations.delegations { @@ -1737,9 +1737,9 @@ mod tests { #[track_caller] fn assert_no_bonding(&self) -> &Self { - if !self.virtual_stake_msgs.is_empty() { + if self.virtual_stake_msgs.len() > 1 { panic!( - "hit_epoch result was expected to be a noop, but has these: {:?}", + "hit_epoch result was expected to only contain DeleteAllScheduledTasks, but has these: {:?}", self.virtual_stake_msgs ); } diff --git a/packages/bindings/src/msg.rs b/packages/bindings/src/msg.rs index e145ed48..b897c850 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -39,7 +39,7 @@ pub enum VirtualStakeMsg { validator: String, }, /// Delete all scheduled tasks after zero max cap and unbond all delegations - DeleteAllScheduledTasks {} + DeleteAllScheduledTasks {}, } impl VirtualStakeMsg { diff --git a/packages/virtual-staking-mock/src/lib.rs b/packages/virtual-staking-mock/src/lib.rs index 44dd07af..0796f5e5 100644 --- a/packages/virtual-staking-mock/src/lib.rs +++ b/packages/virtual-staking-mock/src/lib.rs @@ -136,6 +136,9 @@ impl Module for VirtualStakingModule { } } mesh_bindings::VirtualStakeMsg::UpdateDelegation { .. } => Ok(AppResponse::default()), + mesh_bindings::VirtualStakeMsg::DeleteAllScheduledTasks { .. } => { + Ok(AppResponse::default()) + } } }