From 8175f0adf5007b281a5d4aad0e7e84f98c1b7321 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Sat, 2 Dec 2023 12:11:14 +0100 Subject: [PATCH 01/15] Add slashed to valset update msg --- packages/apis/src/virtual_staking_api.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/apis/src/virtual_staking_api.rs b/packages/apis/src/virtual_staking_api.rs index ff8ed42e..20c6c05d 100644 --- a/packages/apis/src/virtual_staking_api.rs +++ b/packages/apis/src/virtual_staking_api.rs @@ -61,5 +61,25 @@ pub enum SudoMsg { jailed: Option>, unjailed: Option>, tombstoned: Option>, + slashed: Option>, }, } + +#[cw_serde] +pub struct ValidatorSlash { + /// The operator address of the validator (e.g. cosmosvaloper1...). + pub address: String, + /// The height at which the slash is being processed. + pub height: u64, + /// The time at which the slash is being processed, in seconds. + pub time: u64, + /// The height at which the misbehaviour occurred. + pub infraction_height: u64, + /// The time at which the misbehaviour occurred. + pub infraction_time: u64, + /// The validator power when the misbehaviour occurred. + pub power: u64, + /// The (nominal) slash ratio for the validator. + /// Useful in case we don't know if it's a double sign or downtime slash. + pub slash_ratio: String, +} From 64658f369269b5c93748ad7806f6a9eca58db6ae Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Sat, 2 Dec 2023 12:33:21 +0100 Subject: [PATCH 02/15] Add valset update slashed support --- .../consumer/virtual-staking/src/contract.rs | 16 +++++++++++----- .../consumer/virtual-staking/src/multitest.rs | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index 542b83e3..28449eff 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -17,7 +17,7 @@ use mesh_bindings::{ use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx, ReplyCtx}; use sylvia::{contract, schemars}; -use mesh_apis::virtual_staking_api::{self, SudoMsg, VirtualStakingApi}; +use mesh_apis::virtual_staking_api::{self, SudoMsg, ValidatorSlash, VirtualStakingApi}; use crate::error::ContractError; use crate::msg::ConfigResponse; @@ -240,7 +240,10 @@ impl VirtualStakingContract<'_> { jailed: &[String], unjailed: &[String], tombstoned: &[String], + slashed: &[ValidatorSlash], ) -> Result, ContractError> { + // TODO: Process slashed + let _ = slashed; // Account for tombstoned validators. Will be processed in handle_epoch if !tombstoned.is_empty() { self.tombstone_requests.update(deps.storage, |mut old| { @@ -640,6 +643,7 @@ pub fn sudo( jailed, unjailed, tombstoned, + slashed, } => VirtualStakingContract::new().handle_valset_update( deps, &additions.unwrap_or_default(), @@ -648,6 +652,7 @@ pub fn sudo( &jailed.unwrap_or_default(), &unjailed.unwrap_or_default(), &tombstoned.unwrap_or_default(), + &slashed.unwrap_or_default(), ), } } @@ -1433,17 +1438,18 @@ mod tests { &[val.to_string()], &[], &[], + &[], ) .unwrap(); } fn unjail(&self, deps: DepsMut, val: &str) { - self.handle_valset_update(deps, &[], &[], &[], &[], &[val.to_string()], &[]) + self.handle_valset_update(deps, &[], &[], &[], &[], &[val.to_string()], &[], &[]) .unwrap(); } fn tombstone(&self, deps: DepsMut, val: &str) { - self.handle_valset_update(deps, &[], &[], &[], &[], &[], &[val.to_string()]) + self.handle_valset_update(deps, &[], &[], &[], &[], &[], &[val.to_string()], &[]) .unwrap(); } @@ -1454,12 +1460,12 @@ mod tests { max_commission: Default::default(), max_change_rate: Default::default(), }; - self.handle_valset_update(deps, &[val], &[], &[], &[], &[], &[]) + self.handle_valset_update(deps, &[val], &[], &[], &[], &[], &[], &[]) .unwrap(); } fn remove_val(&self, deps: DepsMut, val: &str) { - self.handle_valset_update(deps, &[], &[val.to_string()], &[], &[], &[], &[]) + self.handle_valset_update(deps, &[], &[val.to_string()], &[], &[], &[], &[], &[]) .unwrap(); } } diff --git a/contracts/consumer/virtual-staking/src/multitest.rs b/contracts/consumer/virtual-staking/src/multitest.rs index ee0e3152..fed459b1 100644 --- a/contracts/consumer/virtual-staking/src/multitest.rs +++ b/contracts/consumer/virtual-staking/src/multitest.rs @@ -155,6 +155,7 @@ fn valset_update_sudo() { jailed: None, unjailed: None, tombstoned: Some(tombs), + slashed: None, }; let res = app From cf44e250d468e0a85e2f7ef229cf25ac8ad340ae Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Sun, 3 Dec 2023 12:39:56 +0100 Subject: [PATCH 03/15] Add slashed to valset update --- packages/apis/src/converter_api.rs | 20 +++++++++++++++++++- packages/apis/src/ibc/packet.rs | 7 ++++++- packages/apis/src/virtual_staking_api.rs | 4 ++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/apis/src/converter_api.rs b/packages/apis/src/converter_api.rs index 1f81daff..a10a8b9c 100644 --- a/packages/apis/src/converter_api.rs +++ b/packages/apis/src/converter_api.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Response, StdError, Uint128, Validator}; +use cosmwasm_std::{Coin, Response, StdError, Uint128, Validator}; use sylvia::types::ExecCtx; use sylvia::{interface, schemars}; @@ -41,6 +41,7 @@ pub trait ConverterApi { jailed: Vec, unjailed: Vec, tombstoned: Vec, + slashed: Vec, ) -> Result; } @@ -50,3 +51,20 @@ pub struct RewardInfo { pub validator: String, pub reward: Uint128, } + +#[cw_serde] +pub struct ValidatorSlashInfo { + /// The address of the validator. + pub address: String, + /// The height at which the misbehaviour occurred. + pub infraction_height: u64, + /// The time at which the misbehaviour occurred, in seconds. + pub infraction_time: u64, + /// The validator power when the misbehaviour occurred. + pub power: u64, + /// The (nominal) slash ratio for the validator. + /// Useful in case we don't know if it's a double sign or downtime slash. + pub slash_ratio: String, + /// The slash amount for the validator. + pub slash_amount: Coin, +} diff --git a/packages/apis/src/ibc/packet.rs b/packages/apis/src/ibc/packet.rs index 2af936a5..400dd79f 100644 --- a/packages/apis/src/ibc/packet.rs +++ b/packages/apis/src/ibc/packet.rs @@ -3,7 +3,7 @@ use std::error::Error; use cosmwasm_schema::cw_serde; use cosmwasm_std::{to_binary, Binary, Coin, Decimal, StdResult, Timestamp}; -use crate::converter_api::RewardInfo; +use crate::converter_api::{RewardInfo, ValidatorSlashInfo}; /// These are messages sent from provider -> consumer /// ibc_packet_receive in converter must handle them all. @@ -104,6 +104,11 @@ pub enum ConsumerPacket { /// If the validator doesn't exist or is already tombstoned, this is a no-op for that validator. /// This has precedence over all other events in the same packet tombstoned: Vec, + /// This is sent when a validator is slashed. + /// If the validator doesn't exist or is inactive at the infraction height, this is a no-op + /// for that validator. + /// This has precedence over all other events in the same packet. + slashed: Vec, }, /// This is part of the rewards protocol Distribute { diff --git a/packages/apis/src/virtual_staking_api.rs b/packages/apis/src/virtual_staking_api.rs index 20c6c05d..56446de6 100644 --- a/packages/apis/src/virtual_staking_api.rs +++ b/packages/apis/src/virtual_staking_api.rs @@ -67,7 +67,7 @@ pub enum SudoMsg { #[cw_serde] pub struct ValidatorSlash { - /// The operator address of the validator (e.g. cosmosvaloper1...). + /// The address of the validator. pub address: String, /// The height at which the slash is being processed. pub height: u64, @@ -75,7 +75,7 @@ pub struct ValidatorSlash { pub time: u64, /// The height at which the misbehaviour occurred. pub infraction_height: u64, - /// The time at which the misbehaviour occurred. + /// The time at which the misbehaviour occurred, in seconds. pub infraction_time: u64, /// The validator power when the misbehaviour occurred. pub power: u64, From 907c69f0d785b3db208458adbacd34f4bd3c500f Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Sun, 3 Dec 2023 18:39:51 +0100 Subject: [PATCH 04/15] Implement slashed routing through handle epoch --- .../consumer/virtual-staking/src/contract.rs | 189 ++++++++++-------- 1 file changed, 105 insertions(+), 84 deletions(-) diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index 28449eff..c8b0a610 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -1,16 +1,15 @@ use std::cmp::Ordering; -use std::collections::{BTreeMap, BTreeSet, HashSet}; -use std::str::FromStr; +use std::collections::{BTreeMap, HashMap, HashSet}; use cosmwasm_std::{ - coin, ensure_eq, entry_point, to_binary, Coin, CosmosMsg, CustomQuery, Decimal, DepsMut, + coin, ensure_eq, entry_point, to_binary, Coin, CosmosMsg, CustomQuery, DepsMut, DistributionMsg, Env, Event, Reply, Response, StdResult, Storage, SubMsg, Uint128, Validator, WasmMsg, }; use cw2::set_contract_version; use cw_storage_plus::{Item, Map}; use cw_utils::nonpayable; -use mesh_apis::converter_api::{self, RewardInfo}; +use mesh_apis::converter_api::{self, RewardInfo, ValidatorSlashInfo}; use mesh_bindings::{ TokenQuerier, VirtualStakeCustomMsg, VirtualStakeCustomQuery, VirtualStakeMsg, }; @@ -39,12 +38,9 @@ pub struct VirtualStakingContract<'a> { // `bonded` could be a Map like `bond_requests`, but the only time we use it is to read / write the entire list in bulk (in handle_epoch), // never accessing one element. Reading 100 elements in an Item is much cheaper than ranging over a Map with 100 entries. pub bonded: Item<'a, Vec<(String, Uint128)>>, - /// This is what validators have been requested to be slashed due to tombstoning. + /// This is what validators have been requested to be slashed. // The list will be cleared after processing in `handle_epoch`. - pub tombstone_requests: Item<'a, Vec>, - /// This is what validators have been requested to be slashed due to jailing. - // The list will be cleared after processing in `handle_epoch`. - pub jail_requests: Item<'a, Vec>, + pub slash_requests: Item<'a, Vec>, /// This is what validators are inactive because of tombstoning, jailing or removal (unbonded). // `inactive` could be a Map like `bond_requests`, but the only time we use it is to read / write the entire list in bulk (in handle_epoch), // never accessing one element. Reading 100 elements in an Item is much cheaper than ranging over a Map with 100 entries. @@ -66,8 +62,7 @@ impl VirtualStakingContract<'_> { config: Item::new("config"), bond_requests: Map::new("bond_requests"), bonded: Item::new("bonded"), - tombstone_requests: Item::new("tombstoned"), - jail_requests: Item::new("jailed"), + slash_requests: Item::new("slashed"), inactive: Item::new("inactive"), burned: Map::new("burned"), } @@ -85,8 +80,7 @@ impl VirtualStakingContract<'_> { self.config.save(ctx.deps.storage, &config)?; // initialize these to no one, so no issue when reading for the first time self.bonded.save(ctx.deps.storage, &vec![])?; - self.tombstone_requests.save(ctx.deps.storage, &vec![])?; - self.jail_requests.save(ctx.deps.storage, &vec![])?; + self.slash_requests.save(ctx.deps.storage, &vec![])?; self.inactive.save(ctx.deps.storage, &vec![])?; VALIDATOR_REWARDS_BATCH.init(ctx.deps.storage)?; @@ -123,7 +117,7 @@ impl VirtualStakingContract<'_> { let bonded = self.bonded.load(deps.storage)?; let inactive = self.inactive.load(deps.storage)?; let withdraw = withdraw_reward_msgs(deps.branch(), &bonded, &inactive); - let resp = Response::new().add_submessages(withdraw); + let mut resp = Response::new().add_submessages(withdraw); let bond = TokenQuerier::new(&deps.querier).bond_status(env.contract.address.to_string())?; @@ -136,39 +130,37 @@ impl VirtualStakingContract<'_> { return Ok(resp); } + let config = self.config.load(deps.storage)?; // Make current bonded mutable let mut current = bonded; - // Process tombstoning (unbonded) and jailing (slashed) over bond_requests and current - let tombstone = self.tombstone_requests.load(deps.storage)?; - let jail = self.jail_requests.load(deps.storage)?; - if !tombstone.is_empty() || !jail.is_empty() { - let ratios = TokenQuerier::new(&deps.querier).slash_ratio()?; - let slash_ratio_double_sign = Decimal::from_str(&ratios.slash_fraction_double_sign)?; - let slash_ratio_downtime = Decimal::from_str(&ratios.slash_fraction_downtime)?; - self.adjust_slashings( - deps.storage, - &mut current, - &tombstone, - &jail, - slash_ratio_double_sign, - slash_ratio_downtime, - )?; - // Update inactive list - self.inactive.update(deps.storage, |mut old| { - old.extend_from_slice(&tombstone); - old.extend_from_slice(&jail); + // Process slashes due to tombstoning (unbonded) or jailing, over bond_requests and current + let slash = self.slash_requests.load(deps.storage)?; + if !slash.is_empty() { + let slash_msg = + self.adjust_slashings(deps.branch(), &env, &config, &mut current, &slash)?; + // Update inactive list. Defensive, as it should already been updated in handle_valset_update, due to removals + self.inactive.update(deps.branch().storage, |mut old| { + old.extend_from_slice(&slash.iter().map(|v| v.address.clone()).collect::>()); old.dedup(); Ok::<_, ContractError>(old) })?; - // Clear up both requests - self.tombstone_requests.save(deps.storage, &vec![])?; - self.jail_requests.save(deps.storage, &vec![])?; + // Clear up slash requests + self.slash_requests.save(deps.storage, &vec![])?; + // Add slash message to response + if let Some(msg) = slash_msg { + resp = resp.add_message(msg) + } } // calculate what the delegations should be when we are done let mut requests: Vec<(String, Uint128)> = self .bond_requests - .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .range( + deps.as_ref().storage, + None, + None, + cosmwasm_std::Order::Ascending, + ) .collect::>()?; let total_requested: Uint128 = requests.iter().map(|(_, v)| v).sum(); if total_requested > max_cap { @@ -178,52 +170,90 @@ impl VirtualStakingContract<'_> { } // Save the future values - self.bonded.save(deps.storage, &requests)?; + self.bonded.save(deps.branch().storage, &requests)?; // Compare these two to make bond/unbond calls as needed - let config = self.config.load(deps.storage)?; let rebalance = calculate_rebalance(current, requests, &config.denom); - let resp = resp.add_messages(rebalance); + resp = resp.add_messages(rebalance); Ok(resp) } fn adjust_slashings( &self, - storage: &mut dyn Storage, + deps: DepsMut, + env: &Env, + config: &Config, current: &mut [(String, Uint128)], - tombstones: &[String], - jailing: &[String], - slash_ratio_tombstoning: Decimal, - slash_ratio_jailing: Decimal, - ) -> StdResult<()> { - let tombstones: BTreeSet<_> = tombstones.iter().collect(); - let jailing: BTreeSet<_> = jailing.iter().collect(); + slash: &[ValidatorSlash], + ) -> StdResult> { + let slashes: HashMap = + HashMap::from_iter(slash.iter().map(|s| (s.address.clone(), s.clone()))); + let mut validator_slash_infos = vec![]; // this is linear over current, but better than turn it in to a map for (validator, prev) in current { - let tombstoned = tombstones.contains(validator); - let jailed = jailing.contains(validator); - // Tombstoned has precedence over jailing - let slash_ratio = if tombstoned { - slash_ratio_tombstoning - } else if jailed { - slash_ratio_jailing - } else { - continue; - }; - // Apply slash ratio over current - let slash_amount = *prev * slash_ratio; - *prev -= slash_amount; - // Apply to request as well, to avoid unbonding msg - let mut request = self - .bond_requests - .may_load(storage, validator)? - .unwrap_or_default(); - request = request.saturating_sub(slash_amount); - self.bond_requests.save(storage, validator, &request)?; + match slashes.get(validator) { + None => continue, + Some(s) => { + // We query the chain to get our *current* (post-slashing) delegation amount. + // If it's less than the amount we have in our state, we know we've been slashed over the validator, + // and we need to adjust our state accordingly. + let contract_address = env.contract.address.to_string(); + let delegation = deps + .querier + .query_delegation(contract_address, validator.as_str())?; + let bonded = match delegation { + None => { + // If zero or not found, this has been undelegated fully. + // TODO: Add event + // TODO: Handle full undelegation case. For now, we just ignore it. + continue; + } + Some(delegation) => delegation.amount.amount, + }; + + let slash_amount = *prev - bonded; + *prev -= slash_amount; + // Apply to request as well (to avoid unbonding msg) + let mut request = self + .bond_requests + .may_load(deps.storage, validator)? + .unwrap_or_default(); + request = request.saturating_sub(slash_amount); + self.bond_requests.save(deps.storage, validator, &request)?; + + // Send message to the converter to burn the slashed amount on the Provider side + let validator_slash_info = ValidatorSlashInfo { + address: validator.clone(), + infraction_height: s.infraction_height, + infraction_time: s.infraction_time, + power: s.power, + slash_ratio: s.slash_ratio.clone(), + slash_amount: coin(slash_amount.u128(), config.denom.clone()), + }; + validator_slash_infos.push(validator_slash_info); + } + } } - Ok(()) + if validator_slash_infos.is_empty() { + return Ok(None); + } + let msg = converter_api::ExecMsg::ValsetUpdate { + additions: vec![], + removals: vec![], + updated: vec![], + jailed: vec![], + unjailed: vec![], + tombstoned: vec![], + slashed: validator_slash_infos, + }; + let msg = WasmMsg::Execute { + contract_addr: config.converter.to_string(), + msg: to_binary(&msg)?, + funds: vec![], + }; + Ok(Some(msg)) } /** @@ -242,20 +272,10 @@ impl VirtualStakingContract<'_> { tombstoned: &[String], slashed: &[ValidatorSlash], ) -> Result, ContractError> { - // TODO: Process slashed - let _ = slashed; - // Account for tombstoned validators. Will be processed in handle_epoch - if !tombstoned.is_empty() { - self.tombstone_requests.update(deps.storage, |mut old| { - old.extend_from_slice(tombstoned); - Ok::<_, ContractError>(old) - })?; - } - - // Account for jailed validators. Will be processed in handle_epoch - if !jailed.is_empty() { - self.jail_requests.update(deps.storage, |mut old| { - old.extend_from_slice(jailed); + // Account for slashed validators. Will be processed in handle_epoch + if !slashed.is_empty() { + self.slash_requests.update(deps.storage, |mut old| { + old.extend_from_slice(slashed); Ok::<_, ContractError>(old) })?; } @@ -272,7 +292,7 @@ impl VirtualStakingContract<'_> { Ok::<_, ContractError>(old) })?; } - // Send all updates to the Converter. + // Send all updates to the Converter, except for slashed, which will be sent in `handle_epoch` let cfg = self.config.load(deps.storage)?; let msg = converter_api::ExecMsg::ValsetUpdate { additions: additions.to_vec(), @@ -281,6 +301,7 @@ impl VirtualStakingContract<'_> { jailed: jailed.to_vec(), unjailed: unjailed.to_vec(), tombstoned: tombstoned.to_vec(), + slashed: vec![], }; let msg = WasmMsg::Execute { contract_addr: cfg.converter.to_string(), From bf6de68927865e1fd1082c5ab5470767a0051e92 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Mon, 4 Dec 2023 11:13:46 +0100 Subject: [PATCH 05/15] Support independent slashed list in valset update --- .../provider/external-staking/src/contract.rs | 102 ++++++++++-------- .../provider/external-staking/src/ibc.rs | 2 + .../external-staking/src/test_methods_impl.rs | 2 +- 3 files changed, 62 insertions(+), 44 deletions(-) diff --git a/contracts/provider/external-staking/src/contract.rs b/contracts/provider/external-staking/src/contract.rs index 2e69e0c5..5153f626 100644 --- a/contracts/provider/external-staking/src/contract.rs +++ b/contracts/provider/external-staking/src/contract.rs @@ -8,7 +8,7 @@ use cw_utils::{nonpayable, PaymentError}; use std::cmp::min; use std::collections::HashSet; -use mesh_apis::converter_api::RewardInfo; +use mesh_apis::converter_api::{RewardInfo, ValidatorSlashInfo}; use sylvia::contract; use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; @@ -60,11 +60,6 @@ impl Default for ExternalStakingContract<'_> { } } -pub(crate) enum SlashingReason { - Offline, - DoubleSign, -} - #[cfg_attr(not(feature = "library"), sylvia::entry_points)] #[contract] #[error(ContractError)] @@ -464,27 +459,31 @@ impl ExternalStakingContract<'_> { jailed: &[String], unjailed: &[String], tombstoned: &[String], + slashed: &[ValidatorSlashInfo], ) -> Result<(Event, Vec), ContractError> { let cfg = self.config.load(deps.storage)?; let mut msgs = vec![]; let mut valopers: HashSet = HashSet::new(); - // Process tombstoning events first. Once tombstoned, a validator cannot be changed anymore. - for valoper in tombstoned { - // Check that the validator is active at height and slash it if that is the case - let active = - self.val_set - .is_active_validator_at_height(deps.storage, valoper, height)?; - self.val_set - .tombstone_validator(deps.storage, valoper, height, time)?; + // Process slashing events first. + for valinfo in slashed { + let valoper = &valinfo.address; + // Check that the validator is active at the infraction height, and slash it if that is the case + let active = self.val_set.is_active_validator_at_height( + deps.storage, + valoper, + valinfo.infraction_height, + )?; if active { + // TODO: Compute effective slash ratio + let slash_ratio = match valinfo.slash_ratio.parse::() { + Ok(ratio) => ratio, + Err(_) => { + return Err(ContractError::InvalidSlashRatio); + } + }; // Slash the validator, if bonded - let slash_msg = self.handle_slashing( - &env, - deps.storage, - &cfg, - valoper, - SlashingReason::DoubleSign, - )?; + let slash_msg = + self.handle_slashing(&env, deps.storage, &cfg, valoper, slash_ratio)?; if let Some(msg) = slash_msg { msgs.push(msg) } @@ -492,6 +491,13 @@ impl ExternalStakingContract<'_> { // Maintenance valopers.insert(valoper.clone()); } + // Process tombstoning events second. Once tombstoned, a validator cannot be changed anymore. + for valoper in tombstoned { + self.val_set + .tombstone_validator(deps.storage, valoper, height, time)?; + // Maintenance + valopers.insert(valoper.clone()); + } // Process additions. Already existing validators will be updated and set to active. // If the validator is tombstoned, this will be ignored. for AddValidator { valoper, pub_key } in additions { @@ -502,25 +508,8 @@ impl ExternalStakingContract<'_> { } // Process jailings. Non-existent validators will be ignored. for valoper in jailed { - // Check that the validator is active at height and slash it if that is the case - let active = - self.val_set - .is_active_validator_at_height(deps.storage, valoper, height)?; self.val_set .jail_validator(deps.storage, valoper, height, time)?; - if active { - // Slash the validator, if bonded - let slash_msg = self.handle_slashing( - &env, - deps.storage, - &cfg, - valoper, - SlashingReason::Offline, - )?; - if let Some(msg) = slash_msg { - msgs.push(msg) - } - } // Maintenance valopers.insert(valoper.clone()); } @@ -588,6 +577,16 @@ impl ExternalStakingContract<'_> { if !tombstoned.is_empty() { event = event.add_attribute("tombstoned", tombstoned.join(",")); } + if !slashed.is_empty() { + event = event.add_attribute( + "slashed", + slashed + .iter() + .map(|v| v.address.clone()) + .collect::>() + .join(","), + ); + } Ok((event, msgs)) } @@ -866,12 +865,8 @@ impl ExternalStakingContract<'_> { storage: &mut dyn Storage, config: &Config, validator: &str, - reason: SlashingReason, + slash_ratio: Decimal, ) -> Result, ContractError> { - let slash_ratio = match reason { - SlashingReason::Offline => config.slash_ratio.offline, - SlashingReason::DoubleSign => config.slash_ratio.double_sign, - }; // Get the list of users staking via this validator let users = self .stakes @@ -1516,6 +1511,7 @@ mod tests { &[], &[], &tombs, + &[], ) .unwrap(); @@ -1586,6 +1582,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -1627,6 +1624,7 @@ mod tests { &[], &[], &tombs, + &[], ) .unwrap(); @@ -1702,6 +1700,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -1742,6 +1741,7 @@ mod tests { &[], &[], &tombs, + &[], ) .unwrap(); @@ -1817,6 +1817,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -1871,6 +1872,7 @@ mod tests { &[], &[], &tombs, + &[], ) .unwrap(); @@ -1946,6 +1948,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); // Bob has no cross-delegations (which can be possible) @@ -1965,6 +1968,7 @@ mod tests { &[], &[], &tombs, + &[], ) .unwrap(); @@ -2027,6 +2031,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2045,6 +2050,7 @@ mod tests { &[], &[], &tombs, + &[], ) .unwrap(); @@ -2109,6 +2115,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2127,6 +2134,7 @@ mod tests { &jails, &[], &[], + &[], ) .unwrap(); @@ -2151,6 +2159,7 @@ mod tests { &[], &unjails, &[], + &[], ) .unwrap(); @@ -2209,6 +2218,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2250,6 +2260,7 @@ mod tests { &jails, &[], &[], + &[], ) .unwrap(); @@ -2325,6 +2336,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2343,6 +2355,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2403,6 +2416,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2421,6 +2435,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2445,6 +2460,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); diff --git a/contracts/provider/external-staking/src/ibc.rs b/contracts/provider/external-staking/src/ibc.rs index 892cb033..6e3795fe 100644 --- a/contracts/provider/external-staking/src/ibc.rs +++ b/contracts/provider/external-staking/src/ibc.rs @@ -134,6 +134,7 @@ pub fn ibc_packet_receive( jailed, unjailed, tombstoned, + slashed, } => { let (evt, msgs) = contract.valset_update( deps, @@ -146,6 +147,7 @@ pub fn ibc_packet_receive( &jailed, &unjailed, &tombstoned, + &slashed, )?; let ack = ack_success(&ValsetUpdateAck {})?; IbcReceiveResponse::new() diff --git a/contracts/provider/external-staking/src/test_methods_impl.rs b/contracts/provider/external-staking/src/test_methods_impl.rs index 1cbb3b5a..8eb6b74e 100644 --- a/contracts/provider/external-staking/src/test_methods_impl.rs +++ b/contracts/provider/external-staking/src/test_methods_impl.rs @@ -234,7 +234,7 @@ impl TestMethods for ExternalStakingContract<'_> { ctx.deps.storage, &cfg, &validator, - crate::contract::SlashingReason::DoubleSign, + cfg.slash_ratio.double_sign, )?; match slash_msg { Some(msg) => Ok(Response::new().add_message(msg)), From ed0a2e05c1c337b0476b6494d4735652e2b20b20 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Mon, 4 Dec 2023 11:20:12 +0100 Subject: [PATCH 06/15] Route slashed events --- contracts/consumer/converter/src/contract.rs | 17 ++++++++++++++++- contracts/consumer/converter/src/ibc.rs | 5 ++++- contracts/consumer/converter/src/multitest.rs | 5 +++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/contracts/consumer/converter/src/contract.rs b/contracts/consumer/converter/src/contract.rs index ac64b671..357534a8 100644 --- a/contracts/consumer/converter/src/contract.rs +++ b/contracts/consumer/converter/src/contract.rs @@ -9,7 +9,7 @@ use mesh_apis::ibc::ConsumerPacket; use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx, ReplyCtx}; use sylvia::{contract, schemars}; -use mesh_apis::converter_api::{self, ConverterApi, RewardInfo}; +use mesh_apis::converter_api::{self, ConverterApi, RewardInfo, ValidatorSlashInfo}; use mesh_apis::price_feed_api; use mesh_apis::virtual_staking_api; @@ -395,6 +395,7 @@ impl ConverterApi for ConverterContract<'_> { /// Send validator set additions (entering the active validator set), jailings and tombstonings /// to the external staking contract on the Consumer via IBC. #[msg(exec)] + #[allow(clippy::too_many_arguments)] fn valset_update( &self, ctx: ExecCtx, @@ -404,6 +405,7 @@ impl ConverterApi for ConverterContract<'_> { jailed: Vec, unjailed: Vec, tombstoned: Vec, + slashed: Vec, ) -> Result { self.ensure_authorized(&ctx.deps, &ctx.info)?; @@ -451,6 +453,18 @@ impl ConverterApi for ConverterContract<'_> { event = event.add_attribute("tombstoned", tombstoned.join(",")); is_empty = false; } + if !slashed.is_empty() { + event = event.add_attribute( + "slashed", + slashed + .iter() + .map(|v| v.address.clone()) + .collect::>() + .join(","), + ); + is_empty = false; + // TODO: convert slash amounts to Provider's coin + } let mut resp = Response::new(); if !is_empty { let valset_msg = valset_update_msg( @@ -462,6 +476,7 @@ impl ConverterApi for ConverterContract<'_> { &jailed, &unjailed, &tombstoned, + &slashed, )?; resp = resp.add_message(valset_msg); } diff --git a/contracts/consumer/converter/src/ibc.rs b/contracts/consumer/converter/src/ibc.rs index d2f8186a..4af04b93 100644 --- a/contracts/consumer/converter/src/ibc.rs +++ b/contracts/consumer/converter/src/ibc.rs @@ -9,6 +9,7 @@ use cosmwasm_std::{ }; use cw_storage_plus::Item; +use mesh_apis::converter_api::ValidatorSlashInfo; use mesh_apis::ibc::{ ack_success, validate_channel_order, AckWrapper, AddValidator, ConsumerPacket, ProtocolVersion, ProviderPacket, StakeAck, TransferRewardsAck, UnstakeAck, PROTOCOL_NAME, @@ -115,7 +116,7 @@ pub fn ibc_channel_connect( // Send a validator sync packet to arrive with the newly established channel let validators = deps.querier.query_all_validators()?; - let msg = valset_update_msg(&env, &channel, &validators, &[], &[], &[], &[], &[])?; + let msg = valset_update_msg(&env, &channel, &validators, &[], &[], &[], &[], &[], &[])?; Ok(IbcBasicResponse::new().add_message(msg)) } @@ -130,6 +131,7 @@ pub(crate) fn valset_update_msg( jailed: &[String], unjailed: &[String], tombstoned: &[String], + slashed: &[ValidatorSlashInfo], ) -> Result { let additions = additions .iter() @@ -156,6 +158,7 @@ pub(crate) fn valset_update_msg( jailed: jailed.to_vec(), unjailed: unjailed.to_vec(), tombstoned: tombstoned.to_vec(), + slashed: slashed.to_vec(), }; let msg = IbcMsg::SendPacket { channel_id: channel.endpoint.channel_id.clone(), diff --git a/contracts/consumer/converter/src/multitest.rs b/contracts/consumer/converter/src/multitest.rs index 95c44330..4b9ee564 100644 --- a/contracts/consumer/converter/src/multitest.rs +++ b/contracts/consumer/converter/src/multitest.rs @@ -329,7 +329,7 @@ fn valset_update_works() { // Check that only the virtual staking contract can call this handler let res = converter .converter_api_proxy() - .valset_update(vec![], vec![], vec![], vec![], vec![], vec![]) + .valset_update(vec![], vec![], vec![], vec![], vec![], vec![], vec![]) .call(owner); assert_eq!(res.unwrap_err(), Unauthorized {}); @@ -342,6 +342,7 @@ fn valset_update_works() { vec![], vec![], vec![], + vec![], ) .call(virtual_staking.contract_addr.as_ref()); @@ -397,7 +398,7 @@ fn unauthorized() { let err = converter .converter_api_proxy() - .valset_update(vec![], vec![], vec![], vec![], vec![], vec![]) + .valset_update(vec![], vec![], vec![], vec![], vec![], vec![], vec![]) .call("mallory") .unwrap_err(); From 9d27cc277682c389fd84d53dba8d811272aa1f16 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Mon, 4 Dec 2023 12:24:27 +0100 Subject: [PATCH 07/15] Convert slash amount to provider's denom --- contracts/consumer/converter/src/contract.rs | 80 +++++++++++++++++++- contracts/consumer/converter/src/error.rs | 5 +- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/contracts/consumer/converter/src/contract.rs b/contracts/consumer/converter/src/contract.rs index 357534a8..95678efb 100644 --- a/contracts/consumer/converter/src/contract.rs +++ b/contracts/consumer/converter/src/contract.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{ - ensure_eq, to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Deps, DepsMut, Event, + ensure_eq, to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Deps, DepsMut, Event, Fraction, MessageInfo, Reply, Response, SubMsg, SubMsgResponse, Uint128, Validator, WasmMsg, }; use cw2::set_contract_version; @@ -59,7 +59,7 @@ impl ConverterContract<'_> { ) -> Result { nonpayable(&ctx.info)?; // validate args - if discount > Decimal::one() { + if discount >= Decimal::one() { return Err(ContractError::InvalidDiscount); } if remote_denom.is_empty() { @@ -285,6 +285,34 @@ impl ConverterContract<'_> { }) } + fn invert_price(&self, deps: Deps, amount: Coin) -> Result { + let config = self.config.load(deps.storage)?; + ensure_eq!( + config.local_denom, + amount.denom, + ContractError::WrongDenom { + sent: amount.denom, + expected: config.local_denom + } + ); + + // get the price value (usage is a bit clunky, need to use trait and cannot chain Remote::new() with .querier()) + // also see https://github.com/CosmWasm/sylvia/issues/181 to just store Remote in state + use price_feed_api::Querier; + let remote = price_feed_api::Remote::new(config.price_feed); + let price = remote.querier(&deps.querier).price()?.native_per_foreign; + let converted = (amount.amount * price.inv().ok_or(ContractError::InvalidPrice {})?) + * config + .price_adjustment + .inv() + .ok_or(ContractError::InvalidDiscount {})?; + + Ok(Coin { + denom: config.remote_denom, + amount: converted, + }) + } + pub(crate) fn transfer_rewards( &self, deps: Deps, @@ -405,7 +433,7 @@ impl ConverterApi for ConverterContract<'_> { jailed: Vec, unjailed: Vec, tombstoned: Vec, - slashed: Vec, + mut slashed: Vec, ) -> Result { self.ensure_authorized(&ctx.deps, &ctx.info)?; @@ -462,8 +490,52 @@ impl ConverterApi for ConverterContract<'_> { .collect::>() .join(","), ); + event = event.add_attribute( + "ratios", + slashed + .iter() + .map(|v| v.slash_ratio.clone()) + .collect::>() + .join(","), + ); + event = event.add_attribute( + "amounts", + slashed + .iter() + .map(|v| { + [ + v.slash_amount.amount.to_string(), + v.slash_amount.denom.clone(), + ] + .concat() + }) + .collect::>() + .join(","), + ); + // Convert slash amounts to Provider's coin + slashed + .iter_mut() + .map(|v| { + v.slash_amount = + self.invert_price(ctx.deps.as_ref(), v.slash_amount.clone())?; + Ok(v) + }) + .collect::, ContractError>>()?; + event = event.add_attribute( + "provider_amounts", + slashed + .iter() + .map(|v| { + [ + v.slash_amount.amount.to_string(), + v.slash_amount.denom.clone(), + ] + .concat() + }) + .collect::>() + .join(","), + ); is_empty = false; - // TODO: convert slash amounts to Provider's coin } let mut resp = Response::new(); if !is_empty { diff --git a/contracts/consumer/converter/src/error.rs b/contracts/consumer/converter/src/error.rs index 6ab1f4f0..fb2cba7a 100644 --- a/contracts/consumer/converter/src/error.rs +++ b/contracts/consumer/converter/src/error.rs @@ -32,7 +32,10 @@ pub enum ContractError { #[error("Invalid reply id: {0}")] InvalidReplyId(u64), - #[error("Invalid discount, must be between 0.0 and 1.0")] + #[error("Invalid price, must be greater than 0.0")] + InvalidPrice, + + #[error("Invalid discount, must be greater or equal than 0.0 and less than 1.0")] InvalidDiscount, #[error("Invalid denom: {0}")] From 11eb1c007d3a14b8ae47c7d1fd3c475ccd2eb071 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Mon, 4 Dec 2023 19:56:23 +0100 Subject: [PATCH 08/15] Slash using the efective slash ratio Filter out unbondings by infraction time --- .../provider/external-staking/src/contract.rs | 32 +++++++++++++++---- .../provider/external-staking/src/state.rs | 15 ++++++--- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/contracts/provider/external-staking/src/contract.rs b/contracts/provider/external-staking/src/contract.rs index 5153f626..6c8192ff 100644 --- a/contracts/provider/external-staking/src/contract.rs +++ b/contracts/provider/external-staking/src/contract.rs @@ -474,7 +474,6 @@ impl ExternalStakingContract<'_> { valinfo.infraction_height, )?; if active { - // TODO: Compute effective slash ratio let slash_ratio = match valinfo.slash_ratio.parse::() { Ok(ratio) => ratio, Err(_) => { @@ -482,8 +481,15 @@ impl ExternalStakingContract<'_> { } }; // Slash the validator, if bonded - let slash_msg = - self.handle_slashing(&env, deps.storage, &cfg, valoper, slash_ratio)?; + let slash_msg = self.handle_slashing( + &env, + deps.storage, + &cfg, + valoper, + slash_ratio, + valinfo.slash_amount.amount, + valinfo.infraction_time, + )?; if let Some(msg) = slash_msg { msgs.push(msg) } @@ -866,8 +872,11 @@ impl ExternalStakingContract<'_> { config: &Config, validator: &str, slash_ratio: Decimal, + slash_amount: Uint128, + infraction_time: u64, ) -> Result, ContractError> { // Get the list of users staking via this validator + // FIXME: It should be over the *historical* (at infraction height) stake. Not over the *current* stake let users = self .stakes .stake @@ -883,6 +892,12 @@ impl ExternalStakingContract<'_> { if users.is_empty() { return Ok(None); } + // Compute effective slash ratio + let total_amount = users + .iter() + .map(|(_, stake)| stake.stake.high()) + .sum::(); + let effective_slash_ratio = Decimal::from_ratio(slash_amount, total_amount); // Slash their stake in passing let mut slash_infos = vec![]; @@ -895,7 +910,7 @@ impl ExternalStakingContract<'_> { if stake_high.is_zero() { continue; } - let stake_slash = stake_high * slash_ratio; + let stake_slash = stake_high * effective_slash_ratio; // Requires proper saturating methods in commit/rollback_stake/unstake stake.stake = ValueRange::new( stake_low.saturating_sub(stake_slash), @@ -913,8 +928,13 @@ impl ExternalStakingContract<'_> { distribution.total_stake = distribution.total_stake.saturating_sub(stake_slash); // Don't fail if pending bond tx self.distribution.save(storage, validator, &distribution)?; - // Slash the unbondings - let pending_slashed = stake.slash_pending(&env.block, slash_ratio); + // Slash the unbondings. We use the nominal slash ratio here, like in the blockchain + let pending_slashed = stake.slash_pending( + &env.block, + slash_ratio, + config.unbonding_period, + infraction_time, + ); self.stakes.stake.save(storage, (&user, validator), stake)?; diff --git a/contracts/provider/external-staking/src/state.rs b/contracts/provider/external-staking/src/state.rs index 03229e17..9898099d 100644 --- a/contracts/provider/external-staking/src/state.rs +++ b/contracts/provider/external-staking/src/state.rs @@ -93,12 +93,19 @@ impl Stake { } /// Slashes all the entries in `pending_unbonds`, returning total slashed amount. - pub fn slash_pending(&mut self, info: &BlockInfo, slash_ratio: Decimal) -> Uint128 { - // TODO: Only slash undelegations that started after the misbehaviour's time. This is not - // possible right now, because we don't have access to the misbehaviour's time. (#177) + pub fn slash_pending( + &mut self, + info: &BlockInfo, + slash_ratio: Decimal, + unbonding_period: u64, + infraction_time: u64, + ) -> Uint128 { self.pending_unbonds .iter_mut() - .filter(|pending| pending.release_at > info.time) + .filter(|pending| { + pending.release_at.seconds() - unbonding_period > infraction_time + && pending.release_at > info.time + }) .map(|pending| { let slash = pending.amount * slash_ratio; // Slash it From bf62d7bc553f144b745c6fa090cfd688fcfbe0e7 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Mon, 4 Dec 2023 19:57:28 +0100 Subject: [PATCH 09/15] Adapt slash test method helper --- contracts/provider/external-staking/src/multitest.rs | 12 ++++++------ .../provider/external-staking/src/test_methods.rs | 3 ++- .../external-staking/src/test_methods_impl.rs | 7 +++++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/contracts/provider/external-staking/src/multitest.rs b/contracts/provider/external-staking/src/multitest.rs index 254c35e1..010dac67 100644 --- a/contracts/provider/external-staking/src/multitest.rs +++ b/contracts/provider/external-staking/src/multitest.rs @@ -1359,7 +1359,7 @@ fn slashing() { // But now validators[0] slashing happens contract .test_methods_proxy() - .test_handle_slashing(validators[0].to_string()) + .test_handle_slashing(validators[0].to_string(), Uint128::new(8)) .call("test") .unwrap(); @@ -1444,7 +1444,7 @@ fn slashing_pending_tx_partial_unbond() { // Now validators[0] slashing happens contract .test_methods_proxy() - .test_handle_slashing(validators[0].to_string()) + .test_handle_slashing(validators[0].to_string(), Uint128::new(20)) .call("test") .unwrap(); @@ -1527,7 +1527,7 @@ fn slashing_pending_tx_full_unbond() { // Now validators[0] slashing happens contract .test_methods_proxy() - .test_handle_slashing(validators[0].to_string()) + .test_handle_slashing(validators[0].to_string(), Uint128::new(20)) .call("test") .unwrap(); @@ -1612,7 +1612,7 @@ fn slashing_pending_tx_full_unbond_rolled_back() { // Now validators[0] slashing happens contract .test_methods_proxy() - .test_handle_slashing(validators[0].to_string()) + .test_handle_slashing(validators[0].to_string(), Uint128::new(20)) .call("test") .unwrap(); @@ -1717,7 +1717,7 @@ fn slashing_pending_tx_bond() { // Now validators[0] slashing happens contract .test_methods_proxy() - .test_handle_slashing(validators[0].to_string()) + .test_handle_slashing(validators[0].to_string(), Uint128::new(20)) .call("test") .unwrap(); @@ -1804,7 +1804,7 @@ fn slashing_pending_tx_bond_rolled_back() { // Now validators[0] slashing happens contract .test_methods_proxy() - .test_handle_slashing(validators[0].to_string()) + .test_handle_slashing(validators[0].to_string(), Uint128::new(20)) .call("test") .unwrap(); diff --git a/contracts/provider/external-staking/src/test_methods.rs b/contracts/provider/external-staking/src/test_methods.rs index 7049e798..d6b7e638 100644 --- a/contracts/provider/external-staking/src/test_methods.rs +++ b/contracts/provider/external-staking/src/test_methods.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, Response, StdError}; +use cosmwasm_std::{Coin, Response, StdError, Uint128}; use mesh_apis::converter_api::RewardInfo; use mesh_apis::ibc::AddValidator; use sylvia::interface; @@ -97,5 +97,6 @@ pub trait TestMethods { &self, ctx: ExecCtx, validator: String, + slash_amount: Uint128, ) -> Result; } diff --git a/contracts/provider/external-staking/src/test_methods_impl.rs b/contracts/provider/external-staking/src/test_methods_impl.rs index 8eb6b74e..2df3a3b6 100644 --- a/contracts/provider/external-staking/src/test_methods_impl.rs +++ b/contracts/provider/external-staking/src/test_methods_impl.rs @@ -2,7 +2,7 @@ use crate::contract::ExternalStakingContract; use crate::error::ContractError; use crate::test_methods::TestMethods; -use cosmwasm_std::{Coin, Response}; +use cosmwasm_std::{Coin, Response, Uint128}; use mesh_apis::converter_api::RewardInfo; use mesh_apis::ibc::AddValidator; use sylvia::contract; @@ -225,6 +225,7 @@ impl TestMethods for ExternalStakingContract<'_> { &self, ctx: ExecCtx, validator: String, + slash_amount: Uint128, ) -> Result { #[cfg(any(test, feature = "mt"))] { @@ -235,6 +236,8 @@ impl TestMethods for ExternalStakingContract<'_> { &cfg, &validator, cfg.slash_ratio.double_sign, + slash_amount, + ctx.env.block.time.seconds(), )?; match slash_msg { Some(msg) => Ok(Response::new().add_message(msg)), @@ -243,7 +246,7 @@ impl TestMethods for ExternalStakingContract<'_> { } #[cfg(not(any(test, feature = "mt")))] { - let _ = (ctx, validator); + let _ = (ctx, validator, slash_amount); Err(ContractError::Unauthorized {}) } } From 9c480c76f0928e230b8b5a683dabab1a657fe599 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Tue, 5 Dec 2023 14:56:02 +0100 Subject: [PATCH 10/15] Add slash amount to validator slash --- packages/apis/src/virtual_staking_api.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/apis/src/virtual_staking_api.rs b/packages/apis/src/virtual_staking_api.rs index 56446de6..63329f0f 100644 --- a/packages/apis/src/virtual_staking_api.rs +++ b/packages/apis/src/virtual_staking_api.rs @@ -79,6 +79,8 @@ pub struct ValidatorSlash { pub infraction_time: u64, /// The validator power when the misbehaviour occurred. pub power: u64, + /// The slashed amount over the virtual-staking contract. + pub slash_amount: String, /// The (nominal) slash ratio for the validator. /// Useful in case we don't know if it's a double sign or downtime slash. pub slash_ratio: String, From 29e2f179ce4f42187334d7b16fb470bd67cf102d Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Tue, 5 Dec 2023 17:17:15 +0100 Subject: [PATCH 11/15] Pass slash info including amount directly in valset_update --- .../consumer/virtual-staking/src/contract.rs | 79 +++++-------------- packages/apis/src/converter_api.rs | 4 +- packages/apis/src/virtual_staking_api.rs | 4 +- 3 files changed, 22 insertions(+), 65 deletions(-) diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index c8b0a610..51ab483f 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -136,8 +136,7 @@ impl VirtualStakingContract<'_> { // Process slashes due to tombstoning (unbonded) or jailing, over bond_requests and current let slash = self.slash_requests.load(deps.storage)?; if !slash.is_empty() { - let slash_msg = - self.adjust_slashings(deps.branch(), &env, &config, &mut current, &slash)?; + self.adjust_slashings(deps.branch(), &mut current, &slash)?; // Update inactive list. Defensive, as it should already been updated in handle_valset_update, due to removals self.inactive.update(deps.branch().storage, |mut old| { old.extend_from_slice(&slash.iter().map(|v| v.address.clone()).collect::>()); @@ -146,10 +145,6 @@ impl VirtualStakingContract<'_> { })?; // Clear up slash requests self.slash_requests.save(deps.storage, &vec![])?; - // Add slash message to response - if let Some(msg) = slash_msg { - resp = resp.add_message(msg) - } } // calculate what the delegations should be when we are done @@ -182,78 +177,30 @@ impl VirtualStakingContract<'_> { fn adjust_slashings( &self, deps: DepsMut, - env: &Env, - config: &Config, current: &mut [(String, Uint128)], slash: &[ValidatorSlash], - ) -> StdResult> { + ) -> StdResult<()> { let slashes: HashMap = HashMap::from_iter(slash.iter().map(|s| (s.address.clone(), s.clone()))); - let mut validator_slash_infos = vec![]; // this is linear over current, but better than turn it in to a map for (validator, prev) in current { match slashes.get(validator) { None => continue, Some(s) => { - // We query the chain to get our *current* (post-slashing) delegation amount. - // If it's less than the amount we have in our state, we know we've been slashed over the validator, - // and we need to adjust our state accordingly. - let contract_address = env.contract.address.to_string(); - let delegation = deps - .querier - .query_delegation(contract_address, validator.as_str())?; - let bonded = match delegation { - None => { - // If zero or not found, this has been undelegated fully. - // TODO: Add event - // TODO: Handle full undelegation case. For now, we just ignore it. - continue; - } - Some(delegation) => delegation.amount.amount, - }; - - let slash_amount = *prev - bonded; - *prev -= slash_amount; + // Just deduct the slash amount passed by the chain + *prev -= s.slash_amount; // Apply to request as well (to avoid unbonding msg) let mut request = self .bond_requests .may_load(deps.storage, validator)? .unwrap_or_default(); - request = request.saturating_sub(slash_amount); + request = request.saturating_sub(s.slash_amount); self.bond_requests.save(deps.storage, validator, &request)?; - - // Send message to the converter to burn the slashed amount on the Provider side - let validator_slash_info = ValidatorSlashInfo { - address: validator.clone(), - infraction_height: s.infraction_height, - infraction_time: s.infraction_time, - power: s.power, - slash_ratio: s.slash_ratio.clone(), - slash_amount: coin(slash_amount.u128(), config.denom.clone()), - }; - validator_slash_infos.push(validator_slash_info); } } } - if validator_slash_infos.is_empty() { - return Ok(None); - } - let msg = converter_api::ExecMsg::ValsetUpdate { - additions: vec![], - removals: vec![], - updated: vec![], - jailed: vec![], - unjailed: vec![], - tombstoned: vec![], - slashed: validator_slash_infos, - }; - let msg = WasmMsg::Execute { - contract_addr: config.converter.to_string(), - msg: to_binary(&msg)?, - funds: vec![], - }; - Ok(Some(msg)) + Ok(()) } /** @@ -292,7 +239,7 @@ impl VirtualStakingContract<'_> { Ok::<_, ContractError>(old) })?; } - // Send all updates to the Converter, except for slashed, which will be sent in `handle_epoch` + // Send all updates to the converter. let cfg = self.config.load(deps.storage)?; let msg = converter_api::ExecMsg::ValsetUpdate { additions: additions.to_vec(), @@ -301,7 +248,17 @@ impl VirtualStakingContract<'_> { jailed: jailed.to_vec(), unjailed: unjailed.to_vec(), tombstoned: tombstoned.to_vec(), - slashed: vec![], + slashed: slashed + .iter() + .map(|s| ValidatorSlashInfo { + address: s.address.clone(), + infraction_height: s.infraction_height, + infraction_time: s.infraction_time, + power: s.power, + slash_amount: coin(s.slash_amount.u128(), cfg.denom.clone()), + slash_ratio: s.slash_ratio.clone(), + }) + .collect(), }; let msg = WasmMsg::Execute { contract_addr: cfg.converter.to_string(), diff --git a/packages/apis/src/converter_api.rs b/packages/apis/src/converter_api.rs index a10a8b9c..77d0fb67 100644 --- a/packages/apis/src/converter_api.rs +++ b/packages/apis/src/converter_api.rs @@ -62,9 +62,9 @@ pub struct ValidatorSlashInfo { pub infraction_time: u64, /// The validator power when the misbehaviour occurred. pub power: u64, + /// The slash amount over the amount delegated by virtual-staking for the validator. + pub slash_amount: Coin, /// The (nominal) slash ratio for the validator. /// Useful in case we don't know if it's a double sign or downtime slash. pub slash_ratio: String, - /// The slash amount for the validator. - pub slash_amount: Coin, } diff --git a/packages/apis/src/virtual_staking_api.rs b/packages/apis/src/virtual_staking_api.rs index 63329f0f..e997b86a 100644 --- a/packages/apis/src/virtual_staking_api.rs +++ b/packages/apis/src/virtual_staking_api.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Coin, Response, StdError, Validator}; +use cosmwasm_std::{Coin, Response, StdError, Uint128, Validator}; use sylvia::types::ExecCtx; use sylvia::{interface, schemars}; @@ -80,7 +80,7 @@ pub struct ValidatorSlash { /// The validator power when the misbehaviour occurred. pub power: u64, /// The slashed amount over the virtual-staking contract. - pub slash_amount: String, + pub slash_amount: Uint128, /// The (nominal) slash ratio for the validator. /// Useful in case we don't know if it's a double sign or downtime slash. pub slash_ratio: String, From 9b14677cbbea12084a4b1dc26120f03c0e10c185 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 8 Dec 2023 11:05:52 +0100 Subject: [PATCH 12/15] Adapt jail / tombstone tests --- .../consumer/virtual-staking/src/contract.rs | 83 +++++++++++++++---- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index 51ab483f..3fe73893 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -646,6 +646,7 @@ mod tests { use cosmwasm_std::{ coins, from_binary, testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + Decimal, }; use mesh_bindings::{BondStatusResponse, SlashRatioResponse}; use serde::de::DeserializeOwned; @@ -856,8 +857,8 @@ mod tests { .assert_bond(&[("val1", (10u128, &denom)), ("val2", (20u128, &denom))]) .assert_rewards(&[]); - // val1 is being jailed - contract.jail(deps.as_mut(), "val1"); + // val1 is being jailed and slashed for being offline + contract.jail(deps.as_mut(), "val1", Decimal::percent(10), Uint128::one()); contract .hit_epoch(deps.as_mut()) @@ -923,7 +924,7 @@ mod tests { contract.quick_bond(deps.as_mut(), "val1", 20); // And it's being jailed at the same time - contract.jail(deps.as_mut(), "val1"); + contract.jail(deps.as_mut(), "val1", Decimal::percent(10), Uint128::one()); contract .hit_epoch(deps.as_mut()) @@ -955,7 +956,7 @@ mod tests { contract.quick_unbond(deps.as_mut(), "val1", 10); // And it's being jailed at the same time - contract.jail(deps.as_mut(), "val1"); + contract.jail(deps.as_mut(), "val1", Decimal::percent(10), Uint128::one()); contract .hit_epoch(deps.as_mut()) @@ -1028,7 +1029,7 @@ mod tests { .assert_rewards(&[]); // Val1 is being tombstoned - contract.tombstone(deps.as_mut(), "val1"); + contract.tombstone(deps.as_mut(), "val1", Decimal::percent(25), Uint128::new(5)); contract .hit_epoch(deps.as_mut()) .assert_bond(&[]) // No bond msgs after tombstoning @@ -1070,7 +1071,7 @@ mod tests { contract.quick_bond(deps.as_mut(), "val1", 20); // And it's being tombstoned at the same time - contract.tombstone(deps.as_mut(), "val1"); + contract.tombstone(deps.as_mut(), "val1", Decimal::percent(25), Uint128::new(2)); contract .hit_epoch(deps.as_mut()) @@ -1110,7 +1111,7 @@ mod tests { contract.quick_unbond(deps.as_mut(), "val1", 10); // And it's being tombstoned at the same time - contract.tombstone(deps.as_mut(), "val1"); + contract.tombstone(deps.as_mut(), "val1", Decimal::percent(25), Uint128::new(2)); contract .hit_epoch(deps.as_mut()) @@ -1303,9 +1304,21 @@ mod tests { validator: &[&str], amount: u128, ) -> Result; - fn jail(&self, deps: DepsMut, val: &str); + fn jail( + &self, + deps: DepsMut, + val: &str, + nominal_slash_ratio: Decimal, + slash_amount: Uint128, + ); fn unjail(&self, deps: DepsMut, val: &str); - fn tombstone(&self, deps: DepsMut, val: &str); + fn tombstone( + &self, + deps: DepsMut, + val: &str, + nominal_slash_ratio: Decimal, + slash_amount: Uint128, + ); fn add_val(&self, deps: DepsMut, val: &str); fn remove_val(&self, deps: DepsMut, val: &str); } @@ -1406,8 +1419,14 @@ mod tests { ) } - fn jail(&self, deps: DepsMut, val: &str) { - // We sent a removal along with the jail, as this is what the blockchain does + fn jail( + &self, + deps: DepsMut, + val: &str, + nominal_slash_ratio: Decimal, + slash_amount: Uint128, + ) { + // We sent a removal and a slash along with the jail, as this is what the blockchain does self.handle_valset_update( deps, &[], @@ -1416,7 +1435,16 @@ mod tests { &[val.to_string()], &[], &[], - &[], + &[ValidatorSlash { + address: val.to_string(), + height: 0, + time: 0, + infraction_height: 0, + infraction_time: 0, + power: 0, + slash_amount, + slash_ratio: nominal_slash_ratio.to_string(), + }], ) .unwrap(); } @@ -1426,9 +1454,34 @@ mod tests { .unwrap(); } - fn tombstone(&self, deps: DepsMut, val: &str) { - self.handle_valset_update(deps, &[], &[], &[], &[], &[], &[val.to_string()], &[]) - .unwrap(); + fn tombstone( + &self, + deps: DepsMut, + val: &str, + nominal_slash_ratio: Decimal, + slash_amount: Uint128, + ) { + // We sent a slash along with the tombstone, as this is what the blockchain does + self.handle_valset_update( + deps, + &[], + &[], + &[], + &[], + &[], + &[val.to_string()], + &[ValidatorSlash { + address: val.to_string(), + height: 0, + time: 0, + infraction_height: 0, + infraction_time: 0, + power: 0, + slash_amount, + slash_ratio: nominal_slash_ratio.to_string(), + }], + ) + .unwrap(); } fn add_val(&self, deps: DepsMut, val: &str) { From 38382f190b42901a8c3a6516eb4ce00a79aa6e8e Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 8 Dec 2023 11:12:30 +0100 Subject: [PATCH 13/15] Allow too many arguments --- .github/workflows/basic.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index 7b5d3ef4..7e59f2f3 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -84,4 +84,4 @@ jobs: with: toolchain: 1.70.0 command: clippy - args: --all-targets -- -D warnings + args: --all-targets -- -D warnings -A clippy::too-many-arguments From e4cd1e78e7ae50a649847790d25e532ac4dd5927 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 8 Dec 2023 15:44:12 +0100 Subject: [PATCH 14/15] Adapt slashing tests --- .../provider/external-staking/src/contract.rs | 88 +++++++++++++++---- .../external-staking/src/multitest.rs | 12 +-- .../external-staking/src/test_methods_impl.rs | 4 +- 3 files changed, 78 insertions(+), 26 deletions(-) diff --git a/contracts/provider/external-staking/src/contract.rs b/contracts/provider/external-staking/src/contract.rs index 6c8192ff..02a773b7 100644 --- a/contracts/provider/external-staking/src/contract.rs +++ b/contracts/provider/external-staking/src/contract.rs @@ -1574,7 +1574,7 @@ mod tests { } #[test] - fn valset_update_tombstoning_slashes() { + fn valset_update_tombstoning_and_slashing() { let mut deps = mock_dependencies(); let (mut ctx, contract) = do_instantiate(deps.as_mut()); @@ -1629,7 +1629,7 @@ mod tests { // Commit stake contract.commit_stake(stake_deps, 1).unwrap(); - // Bob is tombstoned next + // Bob is slashed and tombstoned next let update_ctx = ctx.branch(); let tombs = vec!["bob".to_string()]; let (evt, msgs) = contract @@ -1644,12 +1644,25 @@ mod tests { &[], &[], &tombs, - &[], + &[ValidatorSlashInfo { + address: "bob".to_string(), + infraction_height: 200, + infraction_time: 2345, + power: 100, + slash_amount: coin(10, "uosmo"), + slash_ratio: Decimal::percent(10).to_string(), + }], ) .unwrap(); // Check the event - assert_eq!(evt.attributes, vec![Attribute::new("tombstoned", "bob"),]); + assert_eq!( + evt.attributes, + vec![ + Attribute::new("tombstoned", "bob"), + Attribute::new("slashed", "bob") + ] + ); // Check the slashing message assert_eq!(msgs.len(), 1); @@ -1692,7 +1705,7 @@ mod tests { } #[test] - fn valset_update_tombstoning_slashes_pending_bond() { + fn valset_update_tombstoning_and_slashing_pending_bond() { let mut deps = mock_dependencies(); let (mut ctx, contract) = do_instantiate(deps.as_mut()); @@ -1746,7 +1759,7 @@ mod tests { .unwrap(); // Stake tx is pending - // Bob is tombstoned next + // Bob is slashed and tombstoned next let update_ctx = ctx.branch(); let tombs = vec!["bob".to_string()]; let (evt, msgs) = contract @@ -1761,12 +1774,25 @@ mod tests { &[], &[], &tombs, - &[], + &[ValidatorSlashInfo { + address: "bob".to_string(), + infraction_height: 200, + infraction_time: 2345, + power: 100, + slash_amount: coin(10, "uosmo"), + slash_ratio: Decimal::percent(10).to_string(), + }], ) .unwrap(); // Check the event - assert_eq!(evt.attributes, vec![Attribute::new("tombstoned", "bob"),]); + assert_eq!( + evt.attributes, + vec![ + Attribute::new("tombstoned", "bob"), + Attribute::new("slashed", "bob") + ] + ); // Check the slashing message assert_eq!(msgs.len(), 1); @@ -1809,7 +1835,7 @@ mod tests { } #[test] - fn valset_update_tombstoning_slashes_pending_unbond() { + fn valset_update_tombstoning_and_slashing_pending_unbond() { let mut deps = mock_dependencies(); let (mut ctx, contract) = do_instantiate(deps.as_mut()); @@ -1877,7 +1903,7 @@ mod tests { .unwrap(); // Unstake tx is pending - // Bob is tombstoned next + // Bob is slashed and tombstoned next let update_ctx = ctx.branch(); let tombs = vec!["bob".to_string()]; let (evt, msgs) = contract @@ -1892,12 +1918,25 @@ mod tests { &[], &[], &tombs, - &[], + &[ValidatorSlashInfo { + address: "bob".to_string(), + infraction_height: 200, + infraction_time: 2345, + power: 100, + slash_amount: coin(10, "uosmo"), + slash_ratio: Decimal::percent(10).to_string(), + }], ) .unwrap(); // Check the event - assert_eq!(evt.attributes, vec![Attribute::new("tombstoned", "bob"),]); + assert_eq!( + evt.attributes, + vec![ + Attribute::new("tombstoned", "bob"), + Attribute::new("slashed", "bob") + ] + ); // Check the slashing message assert_eq!(msgs.len(), 1); @@ -1940,7 +1979,7 @@ mod tests { } #[test] - fn valset_update_tombstoning_slashes_no_stake() { + fn valset_update_tombstoning_and_slashing_no_stake() { let mut deps = mock_dependencies(); let (mut ctx, contract) = do_instantiate(deps.as_mut()); @@ -1973,7 +2012,7 @@ mod tests { .unwrap(); // Bob has no cross-delegations (which can be possible) - // Bob is tombstoned next + // Bob is slashed and tombstoned next let update_ctx = ctx.branch(); let tombs = vec!["bob".to_string()]; let (evt, msgs) = contract @@ -2210,7 +2249,7 @@ mod tests { } #[test] - fn valset_update_jailing_slashes() { + fn valset_update_jailing_and_slashing() { let mut deps = mock_dependencies(); let (mut ctx, contract) = do_instantiate(deps.as_mut()); @@ -2265,7 +2304,7 @@ mod tests { // Commit stake contract.commit_stake(stake_deps, 1).unwrap(); - // Bob is jailed next + // Bob is slashed and jailed next let update_ctx = ctx.branch(); let jails = vec!["bob".to_string()]; let (evt, msgs) = contract @@ -2280,12 +2319,25 @@ mod tests { &jails, &[], &[], - &[], + &[ValidatorSlashInfo { + address: "bob".to_string(), + infraction_height: 200, + infraction_time: 2345, + power: 100, + slash_amount: coin(10, "uosmo"), + slash_ratio: Decimal::percent(10).to_string(), + }], ) .unwrap(); // Check the event - assert_eq!(evt.attributes, vec![Attribute::new("jailed", "bob"),]); + assert_eq!( + evt.attributes, + vec![ + Attribute::new("jailed", "bob"), + Attribute::new("slashed", "bob") + ] + ); // Check the slashing message assert_eq!(msgs.len(), 1); diff --git a/contracts/provider/external-staking/src/multitest.rs b/contracts/provider/external-staking/src/multitest.rs index 010dac67..2d8041a3 100644 --- a/contracts/provider/external-staking/src/multitest.rs +++ b/contracts/provider/external-staking/src/multitest.rs @@ -1714,10 +1714,10 @@ fn slashing_pending_tx_bond() { ValueRange::new(Uint128::new(250), Uint128::new(300)) ); - // Now validators[0] slashing happens + // Now validators[0] slashing happens, over the amount included the pending bond contract .test_methods_proxy() - .test_handle_slashing(validators[0].to_string(), Uint128::new(20)) + .test_handle_slashing(validators[0].to_string(), Uint128::new(25)) .call("test") .unwrap(); @@ -1801,20 +1801,20 @@ fn slashing_pending_tx_bond_rolled_back() { ValueRange::new(Uint128::new(250), Uint128::new(300)) ); - // Now validators[0] slashing happens + // Now validators[0] slashing happens, but over the amount without the pending bond contract .test_methods_proxy() .test_handle_slashing(validators[0].to_string(), Uint128::new(20)) .call("test") .unwrap(); - // Claims on vault got reduced, for high end of pending slashed bond + // Claims on vault got reduced, for *low* end of pending slashed bond let claim = vault .claim(user.to_owned(), contract.contract_addr.to_string()) .unwrap(); assert_eq!( claim.amount, - ValueRange::new(Uint128::new(225), Uint128::new(275)) + ValueRange::new(Uint128::new(230), Uint128::new(280)) ); // Now the extra bond gets rolled back (i.e. failed) @@ -1828,5 +1828,5 @@ fn slashing_pending_tx_bond_rolled_back() { let claim = vault .claim(user.to_owned(), contract.contract_addr.to_string()) .unwrap(); - assert_eq!(claim.amount.val().unwrap().u128(), 225); + assert_eq!(claim.amount.val().unwrap().u128(), 230); } diff --git a/contracts/provider/external-staking/src/test_methods_impl.rs b/contracts/provider/external-staking/src/test_methods_impl.rs index 2df3a3b6..a9687631 100644 --- a/contracts/provider/external-staking/src/test_methods_impl.rs +++ b/contracts/provider/external-staking/src/test_methods_impl.rs @@ -235,9 +235,9 @@ impl TestMethods for ExternalStakingContract<'_> { ctx.deps.storage, &cfg, &validator, - cfg.slash_ratio.double_sign, + cfg.slash_ratio.double_sign, // TODO: Add slash ratio parameter slash_amount, - ctx.env.block.time.seconds(), + 0, // TODO: Add infraction time parameter )?; match slash_msg { Some(msg) => Ok(Response::new().add_message(msg)), From 1080a83831665e36dea60ec31e051e8bb2933abe Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Sat, 9 Dec 2023 08:24:40 +0100 Subject: [PATCH 15/15] Fix / adapt vault slashing tests --- contracts/provider/vault/src/multitest.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/contracts/provider/vault/src/multitest.rs b/contracts/provider/vault/src/multitest.rs index 90749ad3..7c17f123 100644 --- a/contracts/provider/vault/src/multitest.rs +++ b/contracts/provider/vault/src/multitest.rs @@ -1749,7 +1749,7 @@ fn cross_slash_scenario_1() { // Validator 1 is slashed cross_staking .test_methods_proxy() - .test_handle_slashing(validator1.to_string()) + .test_handle_slashing(validator1.to_string(), Uint128::new(10)) .call("test") .unwrap(); @@ -1861,7 +1861,7 @@ fn cross_slash_scenario_2() { // Validator 1 is slashed cross_staking .test_methods_proxy() - .test_handle_slashing(validator1.to_string()) + .test_handle_slashing(validator1.to_string(), Uint128::new(20)) .call("test") .unwrap(); @@ -1969,7 +1969,7 @@ fn cross_slash_scenario_3() { // Validator 1 is slashed cross_staking .test_methods_proxy() - .test_handle_slashing(validator1.to_string()) + .test_handle_slashing(validator1.to_string(), Uint128::new(15)) .call("test") .unwrap(); @@ -2102,7 +2102,7 @@ fn cross_slash_scenario_4() { // Validator 1 is slashed cross_staking_1 .test_methods_proxy() - .test_handle_slashing(validator1.to_string()) + .test_handle_slashing(validator1.to_string(), Uint128::new(14)) .call("test") .unwrap(); @@ -2279,7 +2279,10 @@ fn cross_slash_scenario_5() { // Validator 1 is slashed cross_staking_1 .test_methods_proxy() - .test_handle_slashing(validator1.to_string()) + .test_handle_slashing( + validator1.to_string(), + Uint128::new(180) * Decimal::percent(slashing_percentage), + ) .call("test") .unwrap(); @@ -2419,7 +2422,7 @@ fn cross_slash_no_native_staking() { // Validator 1 is slashed cross_staking_1 .test_methods_proxy() - .test_handle_slashing(validator1.to_string()) + .test_handle_slashing(validator1.to_string(), Uint128::new(14)) .call("test") .unwrap(); @@ -2548,10 +2551,10 @@ fn cross_slash_pending_unbonding() { assert_eq!(cross_stake1.stake, ValueRange::new_val(Uint128::new(50))); assert_eq!(cross_stake1.pending_unbonds[0].amount, Uint128::new(50)); - // Validator 1 is slashed + // Validator 1 is slashed, over the current bond cross_staking .test_methods_proxy() - .test_handle_slashing(validator1.to_string()) + .test_handle_slashing(validator1.to_string(), Uint128::new(5)) .call("test") .unwrap();