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/Cargo.lock b/Cargo.lock index 98c44e9..6ecebe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -429,7 +429,7 @@ checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" [[package]] name = "gauge-adapter" -version = "1.2.1" +version = "1.2.3" dependencies = [ "anyhow", "cosmwasm-schema", @@ -865,7 +865,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wynd-lsd-hub" -version = "1.2.1" +version = "1.2.3" dependencies = [ "anyhow", "cosmwasm-schema", diff --git a/Cargo.toml b/Cargo.toml index d4e7f88..4111605 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["packages/*", "contracts/*"] [workspace.package] -version = "1.2.1" +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 b3393b7..7ad4569 100644 --- a/contracts/lsd-hub/src/contract.rs +++ b/contracts/lsd-hub/src/contract.rs @@ -137,14 +137,13 @@ 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), + ExecuteMsg::EmergencyUnbond { validators } => { + execute::emergency_unbond(deps, env, info, validators) } + // Disable all other functions + _ => Err(ContractError::Unauthorized {}), } } @@ -154,13 +153,13 @@ 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, 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 +387,155 @@ mod execute { .add_attribute("action", "update_liquidity_discount") .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 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, + }); + } + + 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 contract state + let mut supply = SUPPLY.load(deps.storage)?; + + // 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)?; + + // 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)] @@ -682,36 +830,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/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..7ba9f01 100644 --- a/contracts/lsd-hub/src/msg.rs +++ b/contracts/lsd-hub/src/msg.rs @@ -61,6 +61,10 @@ 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 {}, + /// Emergency function to unbond validators manually + EmergencyUnbond { validators: Vec<(String, Uint128)> }, } #[cw_serde] diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..72c2e00 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,24 @@ +#/bin/sh +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 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