From a277791ff8152bc71bd006c394c0a538000bfd47 Mon Sep 17 00:00:00 2001 From: dimiandre Date: Thu, 20 Feb 2025 15:55:42 +0100 Subject: [PATCH 1/3] wip unbond all --- .gitignore | 2 + contracts/lsd-hub/src/contract.rs | 65 ++++++++++++++++++++++++++----- contracts/lsd-hub/src/error.rs | 9 +++++ contracts/lsd-hub/src/msg.rs | 2 + deploy.sh | 4 ++ 5 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 deploy.sh diff --git a/.gitignore b/.gitignore index e1fc8df..965357a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ node_ci_test/build node_ci_test/node_modules node_ci_test/package-lock.json + +.DS_Store \ No newline at end of file diff --git a/contracts/lsd-hub/src/contract.rs b/contracts/lsd-hub/src/contract.rs index b3393b7..9e83ec3 100644 --- a/contracts/lsd-hub/src/contract.rs +++ b/contracts/lsd-hub/src/contract.rs @@ -137,14 +137,10 @@ pub fn execute( match msg { ExecuteMsg::Receive(msg) => execute::handle_receive(deps, env, info, msg), ExecuteMsg::Claim {} => execute::claim(deps, env, info), - ExecuteMsg::Bond {} => execute::bond(deps, env, info), - ExecuteMsg::Reinvest {} => execute::reinvest(deps, env), - ExecuteMsg::SetValidators { new_validators } => { - execute::set_validators(deps, info, env, new_validators) - } - ExecuteMsg::UpdateLiquidityDiscount { new_discount } => { - execute::update_liquidity_discount(deps, info, new_discount) - } + ExecuteMsg::Bond {} => Err(ContractError::BondingDisabled {}), + ExecuteMsg::EmergencyUnbondAll {} => execute::emergency_unbond_all(deps, env, info), + // Disable all other functions + _ => Err(ContractError::Unauthorized {}), } } @@ -159,8 +155,8 @@ mod execute { use super::*; use crate::state::CleanedSupply; use cosmwasm_std::{ - from_binary, to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, Timestamp, Uint128, - WasmMsg, + from_binary, to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, StakingMsg, Timestamp, + Uint128, WasmMsg, }; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; use cw_utils::{must_pay, Expiration}; @@ -388,6 +384,55 @@ mod execute { .add_attribute("action", "update_liquidity_discount") .add_attribute("liquidity_discount", new_discount.to_string())) } + + pub fn emergency_unbond_all( + deps: DepsMut, + env: Env, + info: MessageInfo, + ) -> Result { + // Only owner can call this + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner { + return Err(ContractError::NotOwner {}); + } + + let mut messages = vec![]; + + // Query all delegations from chain state + let delegations = deps.querier.query_all_delegations(&env.contract.address)?; + + for delegation in delegations { + messages.push(StakingMsg::Undelegate { + validator: delegation.validator, + amount: delegation.amount, + }); + } + + if messages.is_empty() { + return Err(ContractError::NoDelegationsFound {}); + } + + // Update contract state + let mut supply = SUPPLY.load(deps.storage)?; + supply.total_unbonding += messages + .iter() + .map(|msg| match msg { + StakingMsg::Undelegate { amount, .. } => amount.amount, + _ => Uint128::zero(), + }) + .sum::(); + + supply.total_bonded = Uint128::zero(); + SUPPLY.save(deps.storage, &supply)?; + + // Clear bonded state + BONDED.save(deps.storage, &vec![])?; + + Ok(Response::new() + .add_messages(messages) + .add_attribute("action", "emergency_unbond_all") + .add_attribute("amount_unbonded", supply.total_unbonding)) + } } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/lsd-hub/src/error.rs b/contracts/lsd-hub/src/error.rs index 2c107ce..fc1d09c 100644 --- a/contracts/lsd-hub/src/error.rs +++ b/contracts/lsd-hub/src/error.rs @@ -48,6 +48,15 @@ pub enum ContractError { #[error("Migration failed - unbondings vector is not empty")] MigrationFailed {}, + + #[error("Bonding is currently disabled")] + BondingDisabled {}, + + #[error("You are not the owner")] + NotOwner {}, + + #[error("No delegations found")] + NoDelegationsFound {}, } impl From for ContractError { diff --git a/contracts/lsd-hub/src/msg.rs b/contracts/lsd-hub/src/msg.rs index 7632003..d5068be 100644 --- a/contracts/lsd-hub/src/msg.rs +++ b/contracts/lsd-hub/src/msg.rs @@ -61,6 +61,8 @@ pub enum ExecuteMsg { }, /// Updates the liquidity discount used for the [`QueryMsg::TargetValue`] query UpdateLiquidityDiscount { new_discount: Decimal }, + /// Emergency function to unbond all staked tokens + EmergencyUnbondAll {}, } #[cw_serde] diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..403ffae --- /dev/null +++ b/deploy.sh @@ -0,0 +1,4 @@ +#/bin/sh +junod tx wasm instantiate 2 '{"treasury": "juno1dcugtegqgswzjvdrl4cr88vv9v75wjrssucaf2", "commission": "0.09", "owner": "juno1dcugtegqgswzjvdrl4cr88vv9v75wjrssucaf2", "validators": [["junovaloper1q5860u6ducms20qu5emqwvju63ztrl5d8t055g", "0.3"],["junovaloper12szygttgeu2u0ffvfuz0ejmnx4lqwydnztwwvy", "0.3"], ["junovaloper1h3zfnp2lcdd6ddmpyft40zegsddqtvsqyz3249", "0.4"]], "cw20_init": {"cw20_code_id": 1, "decimals": 6, "label": "dimi lsd", "name": "wyJUNO", "symbol": "wyJUNO", "initial_balances": [] }, "epoch_period": 3600, "unbond_period": 3600, "max_concurrent_unbondings": 7, "liquidity_discount": "0.03"}' --from drip --label "culo" --admin juno1dcugtegqgswzjvdrl4cr88vv9v75wjrssucaf2 --gas 10000000 + +junod tx wasm execute juno1wug8sewp6cedgkmrmvhl3lf3tulagm9hnvy8p0rppz9yjw0g4wtqwrw37d '{"bond": {}}' --from drip --amount 1000000000ujuno --gas 1000000 From f4b23deb5e6506d88d18c42445b4599f6d43f40f Mon Sep 17 00:00:00 2001 From: dimiandre Date: Thu, 20 Feb 2025 19:29:44 +0100 Subject: [PATCH 2/3] implement unbond all --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- contracts/lsd-hub/src/contract.rs | 33 +------------------------------ deploy.sh | 24 ++++++++++++++++++++-- 4 files changed, 26 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98c44e9..ca6a7d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -429,7 +429,7 @@ checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" [[package]] name = "gauge-adapter" -version = "1.2.1" +version = "1.2.2" dependencies = [ "anyhow", "cosmwasm-schema", @@ -865,7 +865,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wynd-lsd-hub" -version = "1.2.1" +version = "1.2.2" dependencies = [ "anyhow", "cosmwasm-schema", diff --git a/Cargo.toml b/Cargo.toml index d4e7f88..397481b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["packages/*", "contracts/*"] [workspace.package] -version = "1.2.1" +version = "1.2.2" edition = "2021" license = "GPL 3.0" repository = "https://github.com/cosmorama/wynd-lsd" diff --git a/contracts/lsd-hub/src/contract.rs b/contracts/lsd-hub/src/contract.rs index 9e83ec3..4fde03c 100644 --- a/contracts/lsd-hub/src/contract.rs +++ b/contracts/lsd-hub/src/contract.rs @@ -391,8 +391,7 @@ mod execute { info: MessageInfo, ) -> Result { // Only owner can call this - let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner { + if info.sender != Addr::unchecked("juno1s33zct2zhhaf60x4a90cpe9yquw99jj0zen8pt") { return Err(ContractError::NotOwner {}); } @@ -727,36 +726,6 @@ pub mod migration { pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { let version = ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - if version < "1.1.0".parse::().unwrap() { - use cw_storage_plus::Item; - let old_storage: Item = Item::new("supply"); - let old_supply = old_storage.load(deps.storage)?; - - let new_supply = Supply { - bond_denom: old_supply.bond_denom, - issued: old_supply.issued, - total_bonded: old_supply.total_bonded, - claims: old_supply.claims, - total_unbonding: old_supply.total_unbonding, - }; - SUPPLY.save(deps.storage, &new_supply)?; - - BONDED.save(deps.storage, &old_supply.bonded)?; - - // UNBONDING doesn't need to be saved; This Map with current state it should be empty - ensure!( - old_supply.unbonding.is_empty(), - ContractError::MigrationFailed {} - ); - } - - if let Some(new_owner) = msg.new_owner { - CONFIG.update::<_, StdError>(deps.storage, |mut config| { - config.owner = deps.api.addr_validate(&new_owner)?; - Ok(config) - })?; - } - Ok(Response::new()) } diff --git a/deploy.sh b/deploy.sh index 403ffae..72c2e00 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,4 +1,24 @@ #/bin/sh -junod tx wasm instantiate 2 '{"treasury": "juno1dcugtegqgswzjvdrl4cr88vv9v75wjrssucaf2", "commission": "0.09", "owner": "juno1dcugtegqgswzjvdrl4cr88vv9v75wjrssucaf2", "validators": [["junovaloper1q5860u6ducms20qu5emqwvju63ztrl5d8t055g", "0.3"],["junovaloper12szygttgeu2u0ffvfuz0ejmnx4lqwydnztwwvy", "0.3"], ["junovaloper1h3zfnp2lcdd6ddmpyft40zegsddqtvsqyz3249", "0.4"]], "cw20_init": {"cw20_code_id": 1, "decimals": 6, "label": "dimi lsd", "name": "wyJUNO", "symbol": "wyJUNO", "initial_balances": [] }, "epoch_period": 3600, "unbond_period": 3600, "max_concurrent_unbondings": 7, "liquidity_discount": "0.03"}' --from drip --label "culo" --admin juno1dcugtegqgswzjvdrl4cr88vv9v75wjrssucaf2 --gas 10000000 +junod tx wasm instantiate 3 '{"treasury": "juno1dcugtegqgswzjvdrl4cr88vv9v75wjrssucaf2", "commission": "0.09", "owner": "juno1dcugtegqgswzjvdrl4cr88vv9v75wjrssucaf2", "validators": [["junovaloper1q5860u6ducms20qu5emqwvju63ztrl5d8t055g", "0.3"],["junovaloper12szygttgeu2u0ffvfuz0ejmnx4lqwydnztwwvy", "0.3"], ["junovaloper1h3zfnp2lcdd6ddmpyft40zegsddqtvsqyz3249", "0.4"]], "cw20_init": {"cw20_code_id": 1, "decimals": 6, "label": "dimi lsd", "name": "wyJUNO", "symbol": "wyJUNO", "initial_balances": [] }, "epoch_period": 3600, "unbond_period": 3600, "max_concurrent_unbondings": 7, "liquidity_discount": "0.03"}' --from drip --label "culo" --admin juno1dcugtegqgswzjvdrl4cr88vv9v75wjrssucaf2 --gas 10000000 -junod tx wasm execute juno1wug8sewp6cedgkmrmvhl3lf3tulagm9hnvy8p0rppz9yjw0g4wtqwrw37d '{"bond": {}}' --from drip --amount 1000000000ujuno --gas 1000000 +junod tx wasm execute juno1hrpna9v7vs3stzyd4z3xf00676kf78zpe2u5ksvljswn2vnjp3ys7tlgu0 '{"bond": {}}' --from drip --amount 2000000000ujuno --gas 1000000 +junod q wasm contract-state smart juno1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsf8smqw '{"balance": {"address": "juno1hj5fveer5cjtn4wd6wstzugjfdxzl0xps73ftl"}}' + +junod q wasm contract-state smart juno1hrpna9v7vs3stzyd4z3xf00676kf78zpe2u5ksvljswn2vnjp3ys7tlgu0 '{"validator_set":{}}' + +junod tx wasm execute juno1hrpna9v7vs3stzyd4z3xf00676kf78zpe2u5ksvljswn2vnjp3ys7tlgu0 '{"reinvest": {}}' --from drip --gas 1000000 + + +junod tx wasm execute juno1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsf8smqw '{"send":{"amount": "1900000000" , "contract": "juno1hrpna9v7vs3stzyd4z3xf00676kf78zpe2u5ksvljswn2vnjp3ys7tlgu0", "msg": "eyJ1bmJvbmQiOiB7fX0="}}' --from drip --gas 1000000 + +junod q wasm contract-state smart juno1hrpna9v7vs3stzyd4z3xf00676kf78zpe2u5ksvljswn2vnjp3ys7tlgu0 '{"claims":{"address": "juno1hj5fveer5cjtn4wd6wstzugjfdxzl0xps73ftl"}}' + +junod tx wasm execute juno1hrpna9v7vs3stzyd4z3xf00676kf78zpe2u5ksvljswn2vnjp3ys7tlgu0 '{"claim":{}}' --from drip --gas 1000000 + +--- docker + +junod tx staking unbond junovaloper1g8ejmp8yjjp99t5grgc3mvan5mlya8fnmn84s0 4999990000000ujuno --from validator --home /var/cosmos-chain/juno --keyring-backend test + + +// bonded 626F6E646564 +// stake info 7374616B655F696E666F \ No newline at end of file From 7ca1cc5b08420940a1f474401665fbe317899774 Mon Sep 17 00:00:00 2001 From: dimiandre Date: Thu, 6 Mar 2025 11:47:57 +0100 Subject: [PATCH 3/3] add function to unbond from validators manually - fix unbonding of all validators together --- Cargo.lock | 4 +- Cargo.toml | 2 +- contracts/lsd-hub/src/contract.rs | 126 +++++++++++++++++++++++++++--- contracts/lsd-hub/src/msg.rs | 2 + 4 files changed, 120 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca6a7d0..6ecebe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -429,7 +429,7 @@ checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" [[package]] name = "gauge-adapter" -version = "1.2.2" +version = "1.2.3" dependencies = [ "anyhow", "cosmwasm-schema", @@ -865,7 +865,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wynd-lsd-hub" -version = "1.2.2" +version = "1.2.3" dependencies = [ "anyhow", "cosmwasm-schema", diff --git a/Cargo.toml b/Cargo.toml index 397481b..4111605 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["packages/*", "contracts/*"] [workspace.package] -version = "1.2.2" +version = "1.2.3" edition = "2021" license = "GPL 3.0" repository = "https://github.com/cosmorama/wynd-lsd" diff --git a/contracts/lsd-hub/src/contract.rs b/contracts/lsd-hub/src/contract.rs index 4fde03c..7ad4569 100644 --- a/contracts/lsd-hub/src/contract.rs +++ b/contracts/lsd-hub/src/contract.rs @@ -139,6 +139,9 @@ pub fn execute( ExecuteMsg::Claim {} => execute::claim(deps, env, info), ExecuteMsg::Bond {} => Err(ContractError::BondingDisabled {}), ExecuteMsg::EmergencyUnbondAll {} => execute::emergency_unbond_all(deps, env, info), + ExecuteMsg::EmergencyUnbond { validators } => { + execute::emergency_unbond(deps, env, info, validators) + } // Disable all other functions _ => Err(ContractError::Unauthorized {}), } @@ -150,10 +153,10 @@ mod execute { state::{TmpState, CLAIMS}, valset::ValsetChange, }; - use std::cmp::max; + use std::{cmp::max, collections::BTreeMap}; use super::*; - use crate::state::CleanedSupply; + use crate::state::{CleanedSupply, Unbonding, UNBONDING}; use cosmwasm_std::{ from_binary, to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, StakingMsg, Timestamp, Uint128, WasmMsg, @@ -385,25 +388,125 @@ mod execute { .add_attribute("liquidity_discount", new_discount.to_string())) } + pub fn emergency_unbond( + deps: DepsMut, + env: Env, + info: MessageInfo, + validators: Vec<(String, Uint128)>, + ) -> Result { + // Only dimi can call this + if info.sender != Addr::unchecked("juno1s33zct2zhhaf60x4a90cpe9yquw99jj0zen8pt") { + return Err(ContractError::NotOwner {}); + } + + if validators.is_empty() { + return Err(ContractError::NoDelegationsFound {}); + } + + let config = CONFIG.load(deps.storage)?; + let mut messages = vec![]; + let mut unbondings = vec![]; + let mut supply = SUPPLY.load(deps.storage)?; + + let mut bonded = BONDED + .load(deps.storage)? + .into_iter() + .collect::>(); + + // for each specified validator unbond delegations + for (address, amount) in validators { + let mut unbond_amount = Coin { + denom: supply.bond_denom.clone(), + amount: Uint128::zero(), + }; + + // if amount is 0, we query the current delegation, otherwise we use specified amount + if (amount.is_zero()) { + let delegation = deps + .querier + .query_delegation(&env.contract.address, address.clone())? + .unwrap(); + + unbond_amount.amount = delegation.amount.amount; + } else { + unbond_amount.amount = amount; + } + + // skip delegations = 0 + if unbond_amount.amount.is_zero() { + continue; + } + + messages.push(StakingMsg::Undelegate { + validator: address.clone(), + amount: unbond_amount.clone(), + }); + + // Create corresponding Unbonding entry + unbondings.push(Unbonding { + validator: address.clone(), + amount: unbond_amount.amount, + }); + + // remove bond from validator + *bonded + .get_mut(&address) + .expect("tried to undelegate non-existent stake") -= unbond_amount.amount; + } + + if messages.is_empty() { + return Err(ContractError::NoDelegationsFound {}); + } + + // Save unbondings with unbond time as key + let unbond_time = env.block.time.plus_seconds(config.unbond_period); + UNBONDING.save(deps.storage, unbond_time.seconds(), &unbondings)?; + + // update total_unbonding + let total_unbonded: Uint128 = unbondings.iter().map(|u| u.amount).sum(); + supply.total_unbonding = total_unbonded; + + let new_balances = bonded.into_iter().filter(|(_, b)| !b.is_zero()).collect(); + BONDED.save(deps.storage, &new_balances)?; + supply.total_bonded = new_balances.iter().map(|(_, v)| *v).sum(); + + SUPPLY.save(deps.storage, &supply)?; + + return Err(ContractError::NoDelegationsFound {}); + } + pub fn emergency_unbond_all( deps: DepsMut, env: Env, info: MessageInfo, ) -> Result { - // Only owner can call this + // Only dimi can call this if info.sender != Addr::unchecked("juno1s33zct2zhhaf60x4a90cpe9yquw99jj0zen8pt") { return Err(ContractError::NotOwner {}); } + let config = CONFIG.load(deps.storage)?; let mut messages = vec![]; + let mut unbondings = vec![]; // Query all delegations from chain state let delegations = deps.querier.query_all_delegations(&env.contract.address)?; for delegation in delegations { + // skip delegations = 0 + if delegation.amount.amount.is_zero() { + continue; + } + messages.push(StakingMsg::Undelegate { + validator: delegation.validator.clone(), + amount: delegation.amount.clone(), + }); + + // Create corresponding Unbonding entry + unbondings.push(Unbonding { validator: delegation.validator, - amount: delegation.amount, + amount: delegation.amount.amount, }); } @@ -411,15 +514,16 @@ mod execute { return Err(ContractError::NoDelegationsFound {}); } + // Save unbondings with unbond time as key + let unbond_time = env.block.time.plus_seconds(config.unbond_period); + UNBONDING.save(deps.storage, unbond_time.seconds(), &unbondings)?; + // Update contract state let mut supply = SUPPLY.load(deps.storage)?; - supply.total_unbonding += messages - .iter() - .map(|msg| match msg { - StakingMsg::Undelegate { amount, .. } => amount.amount, - _ => Uint128::zero(), - }) - .sum::(); + + // update total_unbonding + let total_unbonded: Uint128 = unbondings.iter().map(|u| u.amount).sum(); + supply.total_unbonding = total_unbonded; supply.total_bonded = Uint128::zero(); SUPPLY.save(deps.storage, &supply)?; diff --git a/contracts/lsd-hub/src/msg.rs b/contracts/lsd-hub/src/msg.rs index d5068be..7ba9f01 100644 --- a/contracts/lsd-hub/src/msg.rs +++ b/contracts/lsd-hub/src/msg.rs @@ -63,6 +63,8 @@ pub enum ExecuteMsg { UpdateLiquidityDiscount { new_discount: Decimal }, /// Emergency function to unbond all staked tokens EmergencyUnbondAll {}, + /// Emergency function to unbond validators manually + EmergencyUnbond { validators: Vec<(String, Uint128)> }, } #[cw_serde]