Skip to content

Commit

Permalink
Merge pull request #164 from osmosis-labs/151-support-multiple-slash-…
Browse files Browse the repository at this point in the history
…ratios

support multiple slash ratios
  • Loading branch information
uint authored Nov 14, 2023
2 parents 84d9ff3 + c20eecc commit 76120a4
Show file tree
Hide file tree
Showing 15 changed files with 121 additions and 60 deletions.
46 changes: 31 additions & 15 deletions contracts/provider/external-staking/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::msg::{
StakeInfo, StakesResponse, TxResponse, ValidatorPendingRewards,
};
use crate::stakes::Stakes;
use crate::state::{Config, Distribution, Stake};
use crate::state::{Config, Distribution, SlashRatio, Stake};

pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
Expand Down Expand Up @@ -60,6 +60,11 @@ impl Default for ExternalStakingContract<'_> {
}
}

pub(crate) enum SlashingReason {
Offline,
DoubleSign,
}

#[cfg_attr(not(feature = "library"), sylvia::entry_points)]
#[contract]
#[error(ContractError)]
Expand Down Expand Up @@ -95,21 +100,21 @@ impl ExternalStakingContract<'_> {
vault: String,
unbonding_period: u64,
remote_contact: crate::msg::AuthorizedEndpoint,
max_slashing: Decimal,
slash_ratio: SlashRatio,
) -> Result<Response, ContractError> {
let vault = ctx.deps.api.addr_validate(&vault)?;
let vault = VaultApiHelper(vault);

if max_slashing > Decimal::one() {
return Err(ContractError::InvalidMaxSlashing);
if slash_ratio.double_sign > Decimal::one() || slash_ratio.offline > Decimal::one() {
return Err(ContractError::InvalidSlashRatio);
}

let config = Config {
denom,
rewards_denom,
vault,
unbonding_period,
max_slashing,
slash_ratio,
};

self.config.save(ctx.deps.storage, &config)?;
Expand Down Expand Up @@ -472,7 +477,8 @@ impl ExternalStakingContract<'_> {
.tombstone_validator(deps.storage, valoper, height, time)?;
if active {
// Slash the validator (if bonded)
let slash_msg = self.handle_slashing(&env, deps.storage, valoper)?;
let slash_msg =
self.handle_slashing(&env, deps.storage, valoper, SlashingReason::DoubleSign)?;
if let Some(msg) = slash_msg {
msgs.push(msg)
}
Expand Down Expand Up @@ -506,7 +512,8 @@ impl ExternalStakingContract<'_> {
if active {
// Slash the validator, if bonded
// TODO: Slash with a different slash ratio! (downtime / offline slash ratio)
let slash_msg = self.handle_slashing(&env, deps.storage, valoper)?;
let slash_msg =
self.handle_slashing(&env, deps.storage, valoper, SlashingReason::Offline)?;
if let Some(msg) = slash_msg {
msgs.push(msg)
}
Expand Down Expand Up @@ -845,8 +852,13 @@ impl ExternalStakingContract<'_> {
env: &Env,
storage: &mut dyn Storage,
validator: &str,
reason: SlashingReason,
) -> Result<Option<WasmMsg>, ContractError> {
let config = self.config.load(storage)?;
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
Expand All @@ -872,7 +884,7 @@ impl ExternalStakingContract<'_> {
// Calculating slashing with always the `high` value of the range goes against the user
// in some scenario (pending stakes while slashing); but the scenario is relatively
// unlikely.
let stake_slash = stake_high * config.max_slashing;
let stake_slash = stake_high * slash_ratio;
// Requires proper saturating methods in commit/rollback_stake/unstake
stake.stake = ValueRange::new(
stake_low.saturating_sub(stake_slash),
Expand All @@ -891,7 +903,7 @@ impl ExternalStakingContract<'_> {
self.distribution.save(storage, validator, &distribution)?;

// Slash the unbondings
let pending_slashed = stake.slash_pending(&env.block, config.max_slashing);
let pending_slashed = stake.slash_pending(&env.block, slash_ratio);

self.stakes.stake.save(storage, (&user, validator), stake)?;

Expand Down Expand Up @@ -1161,7 +1173,7 @@ pub mod cross_staking {

use super::*;
use cosmwasm_std::{from_binary, Binary};
use mesh_apis::{cross_staking_api::CrossStakingApi, local_staking_api::MaxSlashResponse};
use mesh_apis::{cross_staking_api::CrossStakingApi, local_staking_api::SlashRatioResponse};

#[contract(module=crate::contract)]
#[messages(mesh_apis::cross_staking_api as CrossStakingApi)]
Expand Down Expand Up @@ -1387,10 +1399,11 @@ pub mod cross_staking {
}

#[msg(query)]
fn max_slash(&self, ctx: QueryCtx) -> Result<MaxSlashResponse, ContractError> {
let Config { max_slashing, .. } = self.config.load(ctx.deps.storage)?;
Ok(MaxSlashResponse {
max_slash: max_slashing,
fn max_slash(&self, ctx: QueryCtx) -> Result<SlashRatioResponse, ContractError> {
let Config { slash_ratio, .. } = self.config.load(ctx.deps.storage)?;
Ok(SlashRatioResponse {
slash_ratio_dsign: slash_ratio.double_sign,
slash_ratio_offline: slash_ratio.offline,
})
}
}
Expand Down Expand Up @@ -1430,7 +1443,10 @@ mod tests {
connection_id: "connection_id_1".to_string(),
port_id: "port_id_1".to_string(),
},
Decimal::percent(10),
SlashRatio {
double_sign: Decimal::percent(10),
offline: Decimal::percent(10),
},
)
.unwrap();
let exec_ctx = ExecCtx {
Expand Down
4 changes: 2 additions & 2 deletions contracts/provider/external-staking/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ pub enum ContractError {
#[error("Invalid denom, {0} expected")]
InvalidDenom(String),

#[error("You cannot use a max slashing rate over 1.0 (100%)")]
InvalidMaxSlashing,
#[error("You cannot specify a slash ratio over 1.0 (100%)")]
InvalidSlashRatio,

#[error("Not enough tokens staked, up to {0} can be unbond")]
NotEnoughStake(Uint128),
Expand Down
18 changes: 13 additions & 5 deletions contracts/provider/external-staking/src/multitest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::contract::cross_staking::test_utils::CrossStakingApi;
use crate::contract::multitest_utils::{CodeId, ExternalStakingContractProxy};
use crate::error::ContractError;
use crate::msg::{AuthorizedEndpoint, ReceiveVirtualStake, StakeInfo, ValidatorPendingRewards};
use crate::state::Stake;
use crate::state::{SlashRatio, Stake};
use crate::test_methods_impl::test_utils::TestMethods;
use utils::{
assert_rewards, get_last_external_staking_pending_tx_id, AppExt as _, ContractExt as _,
Expand All @@ -31,7 +31,8 @@ const STAR: &str = "star";
/// 10% slashing on the remote chain
const SLASHING_PERCENTAGE: u64 = 10;
/// 5% slashing on the local chain (so we can differentiate in future tests)
const LOCAL_SLASHING_PERCENTAGE: u64 = 5;
const LOCAL_SLASHING_PERCENTAGE_DSIGN: u64 = 5;
const LOCAL_SLASHING_PERCENTAGE_OFFLINE: u64 = 5;

// Shortcut setuping all needed contracts
//
Expand All @@ -52,7 +53,8 @@ fn setup<'app>(
let native_staking_instantiate = NativeStakingInstantiateMsg {
denom: OSMO.to_owned(),
proxy_code_id: native_staking_proxy_code.code_id(),
max_slashing: Decimal::percent(LOCAL_SLASHING_PERCENTAGE),
slash_ratio_dsign: Decimal::percent(LOCAL_SLASHING_PERCENTAGE_DSIGN),
slash_ratio_offline: Decimal::percent(LOCAL_SLASHING_PERCENTAGE_OFFLINE),
};

let staking_init = StakingInitInfo {
Expand All @@ -75,7 +77,10 @@ fn setup<'app>(
vault.contract_addr.to_string(),
unbond_period,
remote_contact,
Decimal::percent(SLASHING_PERCENTAGE),
SlashRatio {
double_sign: Decimal::percent(SLASHING_PERCENTAGE),
offline: Decimal::percent(SLASHING_PERCENTAGE),
},
)
.call(owner)?;

Expand All @@ -95,7 +100,10 @@ fn instantiate() {
assert_eq!(stakes.stakes, []);

let max_slash = contract.cross_staking_api_proxy().max_slash().unwrap();
assert_eq!(max_slash.max_slash, Decimal::percent(SLASHING_PERCENTAGE));
assert_eq!(
max_slash.slash_ratio_dsign,
Decimal::percent(SLASHING_PERCENTAGE)
);
}

#[test]
Expand Down
10 changes: 8 additions & 2 deletions contracts/provider/external-staking/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ pub struct Config {
pub vault: VaultApiHelper,
/// Unbonding period for claims in seconds
pub unbonding_period: u64,
/// Max slash percentage (from InstantiateMsg, maybe later from the chain)
pub max_slashing: Decimal,
/// The slash ratio
pub slash_ratio: SlashRatio,
}

#[cw_serde]
pub struct SlashRatio {
pub double_sign: Decimal,
pub offline: Decimal,
}

/// All single stake related information - entry per `(user, validator)` pair, including
Expand Down
7 changes: 6 additions & 1 deletion contracts/provider/external-staking/src/test_methods_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,12 @@ impl TestMethods for ExternalStakingContract<'_> {
) -> Result<Response, ContractError> {
#[cfg(any(test, feature = "mt"))]
{
let slash_msg = self.handle_slashing(&ctx.env, ctx.deps.storage, &validator)?;
let slash_msg = self.handle_slashing(
&ctx.env,
ctx.deps.storage,
&validator,
crate::contract::SlashingReason::DoubleSign,
)?;
match slash_msg {
Some(msg) => Ok(Response::new().add_message(msg)),
None => Ok(Response::new()),
Expand Down
3 changes: 2 additions & 1 deletion contracts/provider/native-staking-proxy/src/multitest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ fn setup<'app>(
msg: to_binary(&mesh_native_staking::contract::InstantiateMsg {
denom: OSMO.to_owned(),
proxy_code_id: staking_proxy_code.code_id(),
max_slashing: Decimal::percent(5),
slash_ratio_dsign: Decimal::percent(5),
slash_ratio_offline: Decimal::percent(5),
})
.unwrap(),
label: None,
Expand Down
10 changes: 6 additions & 4 deletions contracts/provider/native-staking/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,19 @@ impl NativeStakingContract<'_> {
ctx: InstantiateCtx,
denom: String,
proxy_code_id: u64,
max_slashing: Decimal,
slash_ratio_dsign: Decimal,
slash_ratio_offline: Decimal,
) -> Result<Response, ContractError> {
if max_slashing > Decimal::one() {
return Err(ContractError::InvalidMaxSlashing);
if slash_ratio_dsign > Decimal::one() || slash_ratio_offline > Decimal::one() {
return Err(ContractError::InvalidSlashRatio);
}

let config = Config {
denom,
proxy_code_id,
vault: ctx.info.sender,
max_slashing,
slash_ratio_dsign,
slash_ratio_offline,
};
self.config.save(ctx.deps.storage, &config)?;
set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Expand Down
4 changes: 2 additions & 2 deletions contracts/provider/native-staking/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ pub enum ContractError {
#[error("Missing proxy contract for {0}")]
NoProxy(String),

#[error("You cannot use a max slashing rate over 1.0 (100%)")]
InvalidMaxSlashing,
#[error("You cannot specify a slash ratio over 1.0 (100%)")]
InvalidSlashRatio,
}
15 changes: 10 additions & 5 deletions contracts/provider/native-staking/src/local_staking_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use sylvia::types::QueryCtx;
use sylvia::{contract, types::ExecCtx};

#[allow(unused_imports)]
use mesh_apis::local_staking_api::{self, LocalStakingApi, MaxSlashResponse};
use mesh_apis::local_staking_api::{self, LocalStakingApi, SlashRatioResponse};

use crate::contract::{NativeStakingContract, REPLY_ID_INSTANTIATE};
use crate::error::ContractError;
Expand Down Expand Up @@ -121,10 +121,15 @@ impl LocalStakingApi for NativeStakingContract<'_> {

/// Returns the maximum percentage that can be slashed
#[msg(query)]
fn max_slash(&self, ctx: QueryCtx) -> Result<MaxSlashResponse, Self::Error> {
let Config { max_slashing, .. } = self.config.load(ctx.deps.storage)?;
Ok(MaxSlashResponse {
max_slash: max_slashing,
fn max_slash(&self, ctx: QueryCtx) -> Result<SlashRatioResponse, Self::Error> {
let Config {
slash_ratio_dsign,
slash_ratio_offline,
..
} = self.config.load(ctx.deps.storage)?;
Ok(SlashRatioResponse {
slash_ratio_dsign,
slash_ratio_offline,
})
}
}
22 changes: 15 additions & 7 deletions contracts/provider/native-staking/src/multitest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ use crate::msg::{OwnerByProxyResponse, ProxyByOwnerResponse};

const OSMO: &str = "OSMO";

const SLASHING_PERCENTAGE: u64 = 15;
const SLASHING_PERCENTAGE_DSIGN: u64 = 15;
const SLASHING_PERCENTAGE_OFFLINE: u64 = 10;

fn slashing_rate() -> Decimal {
Decimal::percent(SLASHING_PERCENTAGE)
fn slashing_rate_dsign() -> Decimal {
Decimal::percent(SLASHING_PERCENTAGE_DSIGN)
}

fn slashing_rate_offline() -> Decimal {
Decimal::percent(SLASHING_PERCENTAGE_OFFLINE)
}

fn app(balances: &[(&str, (u128, &str))], validators: &[&str]) -> App<MtApp> {
Expand Down Expand Up @@ -108,7 +113,8 @@ fn instantiation() {
.instantiate(
OSMO.to_owned(),
staking_proxy_code.code_id(),
slashing_rate(),
slashing_rate_dsign(),
slashing_rate_offline(),
)
.with_label("Staking")
.call(owner)
Expand All @@ -118,7 +124,7 @@ fn instantiation() {
assert_eq!(config.denom, OSMO);

let res = staking.local_staking_api_proxy().max_slash().unwrap();
assert_eq!(res.max_slash, slashing_rate());
assert_eq!(res.slash_ratio_dsign, slashing_rate_dsign());
}

#[test]
Expand All @@ -141,7 +147,8 @@ fn receiving_stake() {
.instantiate(
OSMO.to_owned(),
staking_proxy_code.code_id(),
slashing_rate(),
slashing_rate_dsign(),
slashing_rate_offline(),
)
.with_label("Staking")
.call(owner)
Expand Down Expand Up @@ -260,7 +267,8 @@ fn releasing_proxy_stake() {
msg: to_binary(&crate::contract::InstantiateMsg {
denom: OSMO.to_owned(),
proxy_code_id: staking_proxy_code.code_id(),
max_slashing: slashing_rate(),
slash_ratio_dsign: slashing_rate_dsign(),
slash_ratio_offline: slashing_rate_offline(),
})
.unwrap(),
label: None,
Expand Down
7 changes: 5 additions & 2 deletions contracts/provider/native-staking/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ pub struct Config {
/// The address of the vault contract (where we get and return stake)
pub vault: Addr,

/// Max slash percentage (from InstantiateMsg, maybe later from the chain)
pub max_slashing: Decimal,
/// The slash ratio for double signing
pub slash_ratio_dsign: Decimal,

/// The slash ratio for being offline
pub slash_ratio_offline: Decimal,
}
Loading

0 comments on commit 76120a4

Please sign in to comment.