diff --git a/contracts/provider/native-staking-proxy/src/contract.rs b/contracts/provider/native-staking-proxy/src/contract.rs index ed780578..0e788a5e 100644 --- a/contracts/provider/native-staking-proxy/src/contract.rs +++ b/contracts/provider/native-staking-proxy/src/contract.rs @@ -1,3 +1,4 @@ +use cosmwasm_std::BankMsg::Burn; use cosmwasm_std::WasmMsg::Execute; use cosmwasm_std::{ coin, ensure_eq, to_binary, Coin, DistributionMsg, GovMsg, Response, StakingMsg, VoteOption, @@ -5,6 +6,7 @@ use cosmwasm_std::{ }; use cw2::set_contract_version; use cw_storage_plus::Item; +use std::cmp::min; use cw_utils::{must_pay, nonpayable}; use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; @@ -83,6 +85,93 @@ 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, + amount: Coin, + ) -> Result { + 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::>(); + validators + } + }; + + 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, + )); + } + + // Burn stake + // FIXME? Accounting trick to avoid burning + let burn_msg = Burn { + amount: vec![amount], + }; + + Ok(Response::new() + .add_messages(unstake_msgs) + .add_message(burn_msg)) + } + /// 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)] diff --git a/contracts/provider/native-staking-proxy/src/error.rs b/contracts/provider/native-staking-proxy/src/error.rs index d2c416a7..094fa45e 100644 --- a/contracts/provider/native-staking-proxy/src/error.rs +++ b/contracts/provider/native-staking-proxy/src/error.rs @@ -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), } diff --git a/contracts/provider/native-staking/src/error.rs b/contracts/provider/native-staking/src/error.rs index ffb4a4e7..30e9a7d2 100644 --- a/contracts/provider/native-staking/src/error.rs +++ b/contracts/provider/native-staking/src/error.rs @@ -22,6 +22,9 @@ pub enum ContractError { #[error("Missing instantiate reply data")] NoInstantiateData {}, + #[error("Missing proxy contract for {0}")] + NoProxy(String), + #[error("You cannot use a max slashing rate over 1.0 (100%)")] InvalidMaxSlashing, } diff --git a/contracts/provider/native-staking/src/local_staking_api.rs b/contracts/provider/native-staking/src/local_staking_api.rs index 06ff57a0..69a8355a 100644 --- a/contracts/provider/native-staking/src/local_staking_api.rs +++ b/contracts/provider/native-staking/src/local_staking_api.rs @@ -95,9 +95,28 @@ impl LocalStakingApi for NativeStakingContract<'_> { // Assert no funds are passed in nonpayable(&ctx.info)?; - let _ = (owner, amount, validator); + let owner_addr = ctx.deps.api.addr_validate(&owner)?; - todo!() + // Look up if there is a proxy to match. Fail or call burn on existing + match self + .proxy_by_owner + .may_load(ctx.deps.storage, &owner_addr)? + { + None => Err(ContractError::NoProxy(owner)), + Some(proxy_addr) => { + // Send burn message to the proxy contract + let msg = to_binary(&mesh_native_staking_proxy::contract::ExecMsg::Burn { + validator, + amount, + })?; + let wasm_msg = WasmMsg::Execute { + contract_addr: proxy_addr.into(), + msg, + funds: ctx.info.funds, + }; + Ok(Response::new().add_message(wasm_msg)) + } + } } /// Returns the maximum percentage that can be slashed