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 diff --git a/contracts/consumer/converter/src/contract.rs b/contracts/consumer/converter/src/contract.rs index ac64b671..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; @@ -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; @@ -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, @@ -395,6 +423,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 +433,7 @@ impl ConverterApi for ConverterContract<'_> { jailed: Vec, unjailed: Vec, tombstoned: Vec, + mut slashed: Vec, ) -> Result { self.ensure_authorized(&ctx.deps, &ctx.info)?; @@ -451,6 +481,62 @@ 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(","), + ); + 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; + } let mut resp = Response::new(); if !is_empty { let valset_msg = valset_update_msg( @@ -462,6 +548,7 @@ impl ConverterApi for ConverterContract<'_> { &jailed, &unjailed, &tombstoned, + &slashed, )?; resp = resp.add_message(valset_msg); } 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}")] 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(); diff --git a/contracts/consumer/virtual-staking/src/contract.rs b/contracts/consumer/virtual-staking/src/contract.rs index 542b83e3..3fe73893 100644 --- a/contracts/consumer/virtual-staking/src/contract.rs +++ b/contracts/consumer/virtual-staking/src/contract.rs @@ -1,23 +1,22 @@ 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, }; 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; @@ -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,32 @@ 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() { + 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::>()); 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![])?; } // 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,50 +165,40 @@ 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, current: &mut [(String, Uint128)], - tombstones: &[String], - jailing: &[String], - slash_ratio_tombstoning: Decimal, - slash_ratio_jailing: Decimal, + slash: &[ValidatorSlash], ) -> StdResult<()> { - let tombstones: BTreeSet<_> = tombstones.iter().collect(); - let jailing: BTreeSet<_> = jailing.iter().collect(); + let slashes: HashMap = + HashMap::from_iter(slash.iter().map(|s| (s.address.clone(), s.clone()))); // 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) => { + // 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(s.slash_amount); + self.bond_requests.save(deps.storage, validator, &request)?; + } + } } Ok(()) } @@ -240,19 +217,12 @@ impl VirtualStakingContract<'_> { jailed: &[String], unjailed: &[String], tombstoned: &[String], + slashed: &[ValidatorSlash], ) -> Result, ContractError> { - // 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) })?; } @@ -269,7 +239,7 @@ impl VirtualStakingContract<'_> { Ok::<_, ContractError>(old) })?; } - // Send all updates to the Converter. + // Send all updates to the converter. let cfg = self.config.load(deps.storage)?; let msg = converter_api::ExecMsg::ValsetUpdate { additions: additions.to_vec(), @@ -278,6 +248,17 @@ impl VirtualStakingContract<'_> { jailed: jailed.to_vec(), unjailed: unjailed.to_vec(), tombstoned: tombstoned.to_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(), @@ -640,6 +621,7 @@ pub fn sudo( jailed, unjailed, tombstoned, + slashed, } => VirtualStakingContract::new().handle_valset_update( deps, &additions.unwrap_or_default(), @@ -648,6 +630,7 @@ pub fn sudo( &jailed.unwrap_or_default(), &unjailed.unwrap_or_default(), &tombstoned.unwrap_or_default(), + &slashed.unwrap_or_default(), ), } } @@ -663,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; @@ -873,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()) @@ -940,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()) @@ -972,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()) @@ -1045,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 @@ -1087,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()) @@ -1127,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()) @@ -1320,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); } @@ -1423,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, &[], @@ -1433,18 +1435,53 @@ 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(); } 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()]) - .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) { @@ -1454,12 +1491,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 diff --git a/contracts/provider/external-staking/src/contract.rs b/contracts/provider/external-staking/src/contract.rs index 2e69e0c5..02a773b7 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,26 +459,36 @@ 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 { + 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, + slash_ratio, + valinfo.slash_amount.amount, + valinfo.infraction_time, )?; if let Some(msg) = slash_msg { msgs.push(msg) @@ -492,6 +497,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 +514,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 +583,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,13 +871,12 @@ impl ExternalStakingContract<'_> { storage: &mut dyn Storage, config: &Config, validator: &str, - reason: SlashingReason, + slash_ratio: Decimal, + slash_amount: Uint128, + infraction_time: u64, ) -> 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 + // FIXME: It should be over the *historical* (at infraction height) stake. Not over the *current* stake let users = self .stakes .stake @@ -888,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![]; @@ -900,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), @@ -918,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)?; @@ -1516,6 +1531,7 @@ mod tests { &[], &[], &tombs, + &[], ) .unwrap(); @@ -1558,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()); @@ -1586,6 +1602,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -1612,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 @@ -1627,11 +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); @@ -1674,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()); @@ -1702,6 +1733,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -1727,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 @@ -1742,11 +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); @@ -1789,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()); @@ -1817,6 +1863,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -1856,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 @@ -1871,11 +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); @@ -1918,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()); @@ -1946,11 +2007,12 @@ 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 @@ -1965,6 +2027,7 @@ mod tests { &[], &[], &tombs, + &[], ) .unwrap(); @@ -2027,6 +2090,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2045,6 +2109,7 @@ mod tests { &[], &[], &tombs, + &[], ) .unwrap(); @@ -2109,6 +2174,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2127,6 +2193,7 @@ mod tests { &jails, &[], &[], + &[], ) .unwrap(); @@ -2151,6 +2218,7 @@ mod tests { &[], &unjails, &[], + &[], ) .unwrap(); @@ -2181,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()); @@ -2209,6 +2277,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2235,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 @@ -2250,11 +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); @@ -2325,6 +2408,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2343,6 +2427,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2403,6 +2488,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2421,6 +2507,7 @@ mod tests { &[], &[], &[], + &[], ) .unwrap(); @@ -2445,6 +2532,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/multitest.rs b/contracts/provider/external-staking/src/multitest.rs index 254c35e1..2d8041a3 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(); @@ -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()) + .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()) + .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/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 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 1cbb3b5a..a9687631 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"))] { @@ -234,7 +235,9 @@ impl TestMethods for ExternalStakingContract<'_> { ctx.deps.storage, &cfg, &validator, - crate::contract::SlashingReason::DoubleSign, + cfg.slash_ratio.double_sign, // TODO: Add slash ratio parameter + slash_amount, + 0, // TODO: Add infraction time parameter )?; 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 {}) } } 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(); diff --git a/packages/apis/src/converter_api.rs b/packages/apis/src/converter_api.rs index 1f81daff..77d0fb67 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 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, +} 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 ff8ed42e..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}; @@ -61,5 +61,27 @@ pub enum SudoMsg { jailed: Option>, unjailed: Option>, tombstoned: Option>, + slashed: Option>, }, } + +#[cw_serde] +pub struct ValidatorSlash { + /// The address of the validator. + 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, in seconds. + 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: 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, +}