Skip to content

Commit

Permalink
Merge pull request #157 from osmosis-labs/139/native-burn
Browse files Browse the repository at this point in the history
[native-staking] Burn message
  • Loading branch information
maurolacy authored Oct 27, 2023
2 parents 035587e + 384781c commit d7d6451
Show file tree
Hide file tree
Showing 5 changed files with 335 additions and 21 deletions.
110 changes: 109 additions & 1 deletion contracts/provider/native-staking-proxy/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::cmp::min;

use cosmwasm_std::WasmMsg::Execute;
use cosmwasm_std::{
coin, ensure_eq, to_binary, Coin, DistributionMsg, GovMsg, Response, StakingMsg, VoteOption,
Expand All @@ -20,6 +22,7 @@ pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

pub struct NativeStakingProxyContract<'a> {
config: Item<'a, Config>,
burned: Item<'a, u128>,
}

#[cfg_attr(not(feature = "library"), sylvia::entry_points)]
Expand All @@ -29,6 +32,7 @@ impl NativeStakingProxyContract<'_> {
pub const fn new() -> Self {
Self {
config: Item::new("config"),
burned: Item::new("burned"),
}
}

Expand All @@ -50,6 +54,9 @@ impl NativeStakingProxyContract<'_> {
self.config.save(ctx.deps.storage, &config)?;
set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

// Set burned stake to zero
self.burned.save(ctx.deps.storage, &0)?;

// Stake info.funds on validator
let exec_ctx = ExecCtx {
deps: ctx.deps,
Expand Down Expand Up @@ -83,6 +90,98 @@ impl NativeStakingProxyContract<'_> {
Ok(Response::new().add_message(msg))
}

/// Burn `amount` tokens from the given validator, if set.
/// If `validator` is not set, undelegate evenly from all validators the user has stake in.
/// Can only be called by the parent contract
#[msg(exec)]
fn burn(
&self,
ctx: ExecCtx,
validator: Option<String>,
amount: Coin,
) -> Result<Response, ContractError> {
let cfg = self.config.load(ctx.deps.storage)?;
ensure_eq!(cfg.parent, ctx.info.sender, ContractError::Unauthorized {});

nonpayable(&ctx.info)?;

let validators = match validator {
Some(validator) => {
let validator = ctx
.deps
.querier
.query_delegation(ctx.env.contract.address.clone(), validator)?
.map(|full_delegation| {
(full_delegation.validator, full_delegation.amount.amount)
})
.unwrap();
vec![validator]
}
None => {
let validators = ctx
.deps
.querier
.query_all_delegations(ctx.env.contract.address.clone())?
.iter()
.map(|delegation| (delegation.validator.clone(), delegation.amount.amount))
.collect::<Vec<_>>();
validators
}
};

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

let mut unstake_msgs = vec![];
let mut unstaked = 0;
let proportional_amount = amount.amount.u128() / validators.len() as u128;
for (validator, delegated_amount) in &validators {
// Check validator has `proportional_amount` delegated. Adjust accordingly if not.
let unstake_amount = min(delegated_amount.u128(), proportional_amount);
let unstake_msg = StakingMsg::Undelegate {
validator: validator.to_string(),
amount: coin(unstake_amount, &cfg.denom),
};
unstaked += unstake_amount;
unstake_msgs.push(unstake_msg);
}
// 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 = amount.amount.u128() - unstaked;
for (validator, delegated_amount) in &validators {
if delegated_amount.u128() >= unstake_amount + proportional_amount {
let unstake_msg = StakingMsg::Undelegate {
validator: validator.to_string(),
amount: coin(unstake_amount, &cfg.denom),
};
unstaked += unstake_amount;
unstake_msgs.push(unstake_msg);
break;
}
}
}
// Bail if we still don't have enough stake
if unstaked < amount.amount.u128() {
return Err(ContractError::InsufficientDelegations(
ctx.env.contract.address.to_string(),
amount.amount,
));
}

// Accounting trick to avoid burning stake
self.burned.update(ctx.deps.storage, |old| {
Ok::<_, ContractError>(old + amount.amount.u128())
})?;

Ok(Response::new().add_messages(unstake_msgs))
}

/// Re-stakes the given amount from the one validator to another on behalf of the calling user.
/// Returns an error if the user doesn't have such stake
#[msg(exec)]
Expand Down Expand Up @@ -208,11 +307,20 @@ impl NativeStakingProxyContract<'_> {

nonpayable(&ctx.info)?;

// Simply assume all of our liquid assets are from unbondings
// Simply assumes all of our liquid assets are from unbondings
let balance = ctx
.deps
.querier
.query_balance(ctx.env.contract.address, cfg.denom)?;
// But discount burned stake
// FIXME: this is not accurate, as it doesn't take into account the unbonding period
let burned = self.burned.load(ctx.deps.storage)?;
let balance = coin(balance.amount.u128().saturating_sub(burned), &balance.denom);

// Short circuit if there are no funds to send
if balance.amount.is_zero() {
return Ok(Response::new());
}

// Send them to the parent contract via `release_proxy_stake`
let msg = to_binary(&native_staking_callback::ExecMsg::ReleaseProxyStake {})?;
Expand Down
3 changes: 3 additions & 0 deletions contracts/provider/native-staking-proxy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ pub enum ContractError {

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

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

0 comments on commit d7d6451

Please sign in to comment.