Skip to content

Commit

Permalink
Merge pull request #156 from osmosis-labs/139/slashing-propagation-acct
Browse files Browse the repository at this point in the history
Slashing propagation acct
  • Loading branch information
maurolacy committed Nov 6, 2023
2 parents 02cfa91 + 3b06ac5 commit 9743347
Show file tree
Hide file tree
Showing 31 changed files with 1,315 additions and 66 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
48 changes: 48 additions & 0 deletions contracts/consumer/converter/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,27 @@ impl ConverterContract<'_> {
}
}

/// This is only used for tests.
/// Ideally we want conditional compilation of these whole methods and the enum variants
#[msg(exec)]
fn test_burn(
&self,
ctx: ExecCtx,
validators: Vec<String>,
burn: Coin,
) -> Result<Response, ContractError> {
#[cfg(any(test, feature = "mt"))]
{
// This can only ever be called in tests
self.burn(ctx.deps, &validators, burn)
}
#[cfg(not(any(test, feature = "mt")))]
{
let _ = (ctx, validators, burn);
Err(ContractError::Unauthorized)
}
}

#[msg(query)]
fn config(&self, ctx: QueryCtx) -> Result<ConfigResponse, ContractError> {
let config = self.config.load(ctx.deps.storage)?;
Expand Down Expand Up @@ -213,6 +234,33 @@ impl ConverterContract<'_> {
Ok(Response::new().add_message(msg).add_event(event))
}

/// This is called by ibc_packet_receive.
/// It is pulled out into a method, so it can also be called by test_burn for testing
pub(crate) fn burn(
&self,
deps: DepsMut,
validators: &[String],
burn: Coin,
) -> Result<Response, ContractError> {
let amount = self.normalize_price(deps.as_ref(), burn)?;

let event = Event::new("mesh-burn")
.add_attribute("validators", validators.join(","))
.add_attribute("amount", amount.amount.to_string());

let msg = virtual_staking_api::ExecMsg::Burn {
validators: validators.to_vec(),
amount,
};
let msg = WasmMsg::Execute {
contract_addr: self.virtual_stake.load(deps.storage)?.into(),
msg: to_binary(&msg)?,
funds: vec![],
};

Ok(Response::new().add_message(msg).add_event(event))
}

fn normalize_price(&self, deps: Deps, amount: Coin) -> Result<Coin, ContractError> {
let config = self.config.load(deps.storage)?;
ensure_eq!(
Expand Down
9 changes: 9 additions & 0 deletions contracts/consumer/converter/src/ibc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,15 @@ pub fn ibc_packet_receive(
.add_events(response.events)
.add_attributes(response.attributes)
}
ProviderPacket::Burn { validators, burn } => {
let response = contract.burn(deps, &validators, burn)?;
let ack = ack_success(&UnstakeAck {})?;
IbcReceiveResponse::new()
.set_ack(ack)
.add_submessages(response.messages)
.add_events(response.events)
.add_attributes(response.attributes)
}
ProviderPacket::TransferRewards {
rewards, recipient, ..
} => {
Expand Down
86 changes: 86 additions & 0 deletions contracts/consumer/converter/src/multitest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,92 @@ fn ibc_stake_and_unstake() {
);
}

#[test]
fn ibc_stake_and_burn() {
let app = App::default();

let owner = "sunny"; // Owner of the staking contract (i. e. the vault contract)
let admin = "theman";
let discount = Decimal::percent(40); // 1 OSMO worth of JUNO should give 0.6 OSMO of stake
let native_per_foreign = Decimal::percent(50); // 1 JUNO is worth 0.5 OSMO

let SetupResponse {
price_feed: _,
converter,
virtual_staking,
} = setup(
&app,
SetupArgs {
owner,
admin,
discount,
native_per_foreign,
},
);

// no one is staked
let val1 = "Val Kilmer";
let val2 = "Valley Girl";
assert!(virtual_staking.all_stake().unwrap().stakes.is_empty());
assert_eq!(
virtual_staking
.stake(val1.to_string())
.unwrap()
.stake
.u128(),
0
);
assert_eq!(
virtual_staking
.stake(val2.to_string())
.unwrap()
.stake
.u128(),
0
);

// let's stake some
converter
.test_stake(val1.to_string(), coin(1000, JUNO))
.call(owner)
.unwrap();
converter
.test_stake(val2.to_string(), coin(4000, JUNO))
.call(owner)
.unwrap();

// and burn some
converter
.test_burn(vec![val2.to_string()], coin(2000, JUNO))
.call(owner)
.unwrap();

// and check the stakes (1000 * 0.6 * 0.5 = 300) (2000 * 0.6 * 0.5 = 600)
assert_eq!(
virtual_staking
.stake(val1.to_string())
.unwrap()
.stake
.u128(),
300
);
assert_eq!(
virtual_staking
.stake(val2.to_string())
.unwrap()
.stake
.u128(),
600
);
assert_eq!(
virtual_staking.all_stake().unwrap().stakes,
vec![
(val1.to_string(), Uint128::new(300)),
(val2.to_string(), Uint128::new(600)),
]
);
}

#[test]
fn valset_update_works() {
let app = App::default();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ pub enum ContractError {

#[error("Wrong denom. Cannot stake {0}")]
WrongDenom(String),

#[error("Virtual staking {0} has not enough delegated funds: {1}")]
InsufficientDelegations(String, Uint128),
}

/// This is a stub implementation of the virtual staking contract, for test purposes only.
Expand Down Expand Up @@ -133,7 +136,7 @@ impl VirtualStakingApi for VirtualStakingMock<'_> {

/// Requests to unbond tokens from a validator. This will be actually handled at the next epoch.
/// If the virtual staking module is over the max cap, it will trigger a rebalance in addition to unbond.
/// If the virtual staking contract doesn't have at least amont tokens staked to the given validator, this will return an error.
/// If the virtual staking contract doesn't have at least amount tokens staked to the given validator, this will return an error.
#[msg(exec)]
fn unbond(
&self,
Expand All @@ -158,4 +161,65 @@ impl VirtualStakingApi for VirtualStakingMock<'_> {

Ok(Response::new())
}

/// Requests to unbond and burn tokens from a lists of validators (one or more). This will be actually handled at the next epoch.
/// If the virtual staking module is over the max cap, it will trigger a rebalance in addition to unbond.
/// If the virtual staking contract doesn't have at least amount tokens staked over the given validators, this will return an error.
#[msg(exec)]
fn burn(
&self,
ctx: ExecCtx,
validators: Vec<String>,
amount: Coin,
) -> Result<Response, Self::Error> {
nonpayable(&ctx.info)?;
let cfg = self.config.load(ctx.deps.storage)?;
// only the converter can call this
ensure_eq!(ctx.info.sender, cfg.converter, ContractError::Unauthorized);
ensure_eq!(
amount.denom,
cfg.denom,
ContractError::WrongDenom(cfg.denom)
);

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));
}
}

// 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 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
Loading

0 comments on commit 9743347

Please sign in to comment.