Skip to content

Commit

Permalink
Merge pull request #159 from osmosis-labs/139/slashing-proportional-burn
Browse files Browse the repository at this point in the history
139/slashing proportional burn
  • Loading branch information
maurolacy committed Nov 4, 2023
2 parents e6325f5 + 3510b50 commit 3b06ac5
Show file tree
Hide file tree
Showing 14 changed files with 312 additions and 139 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ repository = "https://github.com/osmosis-labs/mesh-security"

[workspace.dependencies]
mesh-apis = { path = "./packages/apis" }
mesh-bindings = { path = "./packages/bindings" }
mesh-bindings = { path = "./packages/bindings" }
mesh-burn = { path = "./packages/burn" }
mesh-sync = { path = "./packages/sync" }
mesh-virtual-staking-mock = { path = "./packages/virtual-staking-mock" }

Expand Down
1 change: 1 addition & 0 deletions contracts/consumer/converter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ serde = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
mesh-burn = { workspace = true }
mesh-simple-price-feed = { workspace = true, features = ["mt"] }

cw-multi-test = { workspace = true }
Expand Down
67 changes: 28 additions & 39 deletions contracts/consumer/converter/src/multitest/virtual_staking_mock.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{ensure_eq, Addr, Coin, Response, StdError, StdResult, Uint128};
use std::cmp::min;

use cw_storage_plus::{Item, Map};
use cw_utils::{nonpayable, PaymentError};
Expand Down Expand Up @@ -30,9 +29,6 @@ pub enum ContractError {
#[error("Wrong denom. Cannot stake {0}")]
WrongDenom(String),

#[error("Empty validators list")]
NoValidators {},

#[error("Virtual staking {0} has not enough delegated funds: {1}")]
InsufficientDelegations(String, Uint128),
}
Expand Down Expand Up @@ -186,51 +182,44 @@ impl VirtualStakingApi for VirtualStakingMock<'_> {
ContractError::WrongDenom(cfg.denom)
);

// Error if no validators
if validators.is_empty() {
return Err(ContractError::NoValidators {});
let mut stakes = vec![];
for validator in validators {
let stake = self
.stake
.may_load(ctx.deps.storage, &validator)?
.unwrap_or_default()
.u128();
if stake != 0 {
stakes.push((validator, stake));
}
}

let mut unstaked = 0;
let proportional_amount = Uint128::new(amount.amount.u128() / validators.len() as u128);
for validator in &validators {
// Checks that validator has `proportional_amount` delegated. Adjust accordingly if not.
self.stake
.update::<_, ContractError>(ctx.deps.storage, validator, |old| {
let delegated_amount = old.unwrap_or_default();
let unstake_amount = min(delegated_amount, proportional_amount);
unstaked += unstake_amount.u128();
Ok(delegated_amount - unstake_amount)
})?;
}
// Adjust possible rounding issues
if unstaked < amount.amount.u128() {
// Look for the first validator that has enough stake, and unstake it from there
let unstake_amount = Uint128::new(amount.amount.u128() - unstaked);
for validator in &validators {
let delegated_amount = self
.stake
.may_load(ctx.deps.storage, validator)?
.unwrap_or_default();
if delegated_amount >= unstake_amount {
self.stake.save(
ctx.deps.storage,
validator,
&(delegated_amount - unstake_amount),
)?;
unstaked += unstake_amount.u128();
break;
}
}
// Error if no delegations
if stakes.is_empty() {
return Err(ContractError::InsufficientDelegations(
ctx.env.contract.address.to_string(),
amount.amount,
));
}

let (burned, burns) = mesh_burn::distribute_burn(stakes.as_slice(), amount.amount.u128());

// Bail if we still don't have enough stake
if unstaked < amount.amount.u128() {
if burned < amount.amount.u128() {
return Err(ContractError::InsufficientDelegations(
ctx.env.contract.address.to_string(),
amount.amount,
));
}

// Update stake
for (validator, burn_amount) in burns {
self.stake
.update::<_, ContractError>(ctx.deps.storage, validator, |old| {
Ok(old.unwrap_or_default() - Uint128::new(burn_amount))
})?;
}

Ok(Response::new())
}
}
1 change: 1 addition & 0 deletions contracts/consumer/virtual-staking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mt = ["library", "sylvia/mt"]

[dependencies]
mesh-apis = { workspace = true }
mesh-burn = { workspace = true }
mesh-bindings = { workspace = true }

sylvia = { workspace = true }
Expand Down
64 changes: 26 additions & 38 deletions contracts/consumer/virtual-staking/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::cmp::{min, Ordering};
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::str::FromStr;

Expand Down Expand Up @@ -579,54 +579,42 @@ impl VirtualStakingApi for VirtualStakingContract<'_> {
cfg.denom,
ContractError::WrongDenom(cfg.denom)
);
let mut bonds = vec![];
for validator in validators {
let stake = self
.bond_requests
.may_load(ctx.deps.storage, &validator)?
.unwrap_or_default()
.u128();
if stake != 0 {
bonds.push((validator, stake));
}
}

// Error if no validators
if validators.is_empty() {
return Err(ContractError::NoValidators {});
// Error if no delegations
if bonds.is_empty() {
return Err(ContractError::InsufficientDelegations(
ctx.env.contract.address.to_string(),
amount.amount,
));
}

let mut unstaked = 0;
let proportional_amount = Uint128::new(amount.amount.u128() / validators.len() as u128);
for validator in &validators {
// Checks that validator has `proportional_amount` bonded. Adjust accordingly if not.
let (burned, burns) = mesh_burn::distribute_burn(bonds.as_slice(), amount.amount.u128());

for (validator, burn_amount) in burns {
// Update bond requests
self.bond_requests
.update::<_, ContractError>(ctx.deps.storage, validator, |old| {
let bonded_amount = old.unwrap_or_default();
let unstake_amount = min(bonded_amount, proportional_amount);
unstaked += unstake_amount.u128();
Ok(bonded_amount - unstake_amount)
Ok(old.unwrap_or_default() - Uint128::new(burn_amount))
})?;
// Accounting trick to avoid burning stake
self.burned.update(ctx.deps.storage, validator, |old| {
Ok::<_, ContractError>(old.unwrap_or_default() + proportional_amount.u128())
Ok::<_, ContractError>(old.unwrap_or_default() + burn_amount)
})?;
}
// Adjust possible rounding issues
if unstaked < amount.amount.u128() {
// Look for the first validator that has enough stake, and unstake it from there
let unstake_amount = Uint128::new(amount.amount.u128() - unstaked);
for validator in &validators {
let bonded_amount = self
.bond_requests
.may_load(ctx.deps.storage, validator)?
.unwrap_or_default();
if bonded_amount >= unstake_amount {
self.bond_requests.save(
ctx.deps.storage,
validator,
&(bonded_amount - unstake_amount),
)?;
// Accounting trick to avoid burning stake
self.burned.update(ctx.deps.storage, validator, |old| {
Ok::<_, ContractError>(old.unwrap_or_default() + unstake_amount.u128())
})?;
unstaked += unstake_amount.u128();
break;
}
}
}

// Bail if we still don't have enough stake
if unstaked < amount.amount.u128() {
if burned < amount.amount.u128() {
return Err(ContractError::InsufficientDelegations(
ctx.env.contract.address.to_string(),
amount.amount,
Expand Down
1 change: 1 addition & 0 deletions contracts/provider/external-staking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mt = ["library", "sylvia/mt"]

[dependencies]
mesh-apis = { workspace = true }
mesh-burn = { workspace = true }
mesh-sync = { workspace = true }

sylvia = { workspace = true }
Expand Down
63 changes: 45 additions & 18 deletions contracts/provider/external-staking/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1272,11 +1272,16 @@ pub mod cross_staking {

let owner = ctx.deps.api.addr_validate(&owner)?;

let validators: Vec<_> = match validator {
let stakes: Vec<_> = match validator {
Some(validator) => {
// Burn from validator
// TODO: Preferentially, i.e. burn remaining amount, if any, from other validators
vec![validator]
let stake = self
.stakes
.stake
.may_load(ctx.deps.storage, (&owner, &validator))?
.unwrap_or_default();
vec![(validator, stake.stake.high().u128())] // Burn takes precedence over any pending txs
}
None => {
// Burn proportionally from all validators associated to the user
Expand All @@ -1285,27 +1290,42 @@ pub mod cross_staking {
.prefix(&owner)
.range(ctx.deps.storage, None, None, Order::Ascending)
.map(|item| {
let (validator, _) = item?;
Ok::<_, Self::Error>(validator)
let (validator, stake) = item?;
Ok::<_, Self::Error>((validator, stake.stake.high().u128()))
// Burn takes precedence over any pending txs
})
.collect::<Result<_, _>>()?
}
};
let num_validators = Uint128::new(validators.len() as u128);
// FIXME? Check for zero len validators
// TODO: Deal with rounding / unbonded validators
let proportional_amount = amount.amount / num_validators;
for validator in &validators {

// Error if no stakes
if stakes.is_empty() {
return Err(ContractError::InsufficientDelegations(
owner.to_string(),
amount.amount,
));
}

let (burned, burns) = mesh_burn::distribute_burn(&stakes, amount.amount.u128());

// Bail if we don't have enough stake
if burned < amount.amount.u128() {
return Err(ContractError::InsufficientDelegations(
owner.to_string(),
amount.amount,
));
}

for (validator, burn_amount) in &burns {
let burn_amount = Uint128::new(*burn_amount);
// Perform stake subtraction
let mut stake = self
.stakes
.stake
.may_load(ctx.deps.storage, (&owner, validator))?
.unwrap_or_default();

// Perform stake subtraction.
.load(ctx.deps.storage, (&owner, validator))?;
// We don't check for min here, as this call can only come from the `vault` contract, which already
// performed the proper check.
stake.stake.sub(proportional_amount, None)?;
stake.stake.sub(burn_amount, None)?;

// Load distribution
let mut distribution = self
Expand All @@ -1316,8 +1336,8 @@ pub mod cross_staking {
// Distribution alignment
stake
.points_alignment
.stake_decreased(proportional_amount, distribution.points_per_stake);
distribution.total_stake -= proportional_amount;
.stake_decreased(burn_amount, distribution.points_per_stake);
distribution.total_stake -= burn_amount;

// Save stake
self.stakes
Expand All @@ -1331,7 +1351,7 @@ pub mod cross_staking {

let channel = IBC_CHANNEL.load(ctx.deps.storage)?;
let packet = ProviderPacket::Burn {
validators: validators.clone(),
validators: burns.iter().map(|v| v.0.to_string()).collect(),
burn: amount.clone(),
};
let msg = IbcMsg::SendPacket {
Expand All @@ -1353,7 +1373,14 @@ pub mod cross_staking {
resp = resp
.add_attribute("action", "burn_virtual_stake")
.add_attribute("owner", owner)
.add_attribute("validators", validators.join(", "))
.add_attribute(
"validators",
stakes
.into_iter()
.map(|s| s.0)
.collect::<Vec<_>>()
.join(", "),
)
.add_attribute("amount", amount.to_string());

Ok(resp)
Expand Down
3 changes: 3 additions & 0 deletions contracts/provider/external-staking/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,7 @@ pub enum ContractError {

#[error("{0}")]
Range(#[from] RangeError),

#[error("User {0} has not enough delegated funds: {1}")]
InsufficientDelegations(String, Uint128),
}
1 change: 1 addition & 0 deletions contracts/provider/native-staking-proxy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mt = ["library", "sylvia/mt"]

[dependencies]
mesh-apis = { workspace = true }
mesh-burn = { workspace = true }

sylvia = { workspace = true }
cosmwasm-schema = { workspace = true }
Expand Down
Loading

0 comments on commit 3b06ac5

Please sign in to comment.