Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@
node_ci_test/build
node_ci_test/node_modules
node_ci_test/package-lock.json

.DS_Store
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
200 changes: 159 additions & 41 deletions contracts/lsd-hub/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}),
}
}

Expand All @@ -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};
Expand Down Expand Up @@ -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<Response, ContractError> {
// 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::<BTreeMap<_, _>>();

// 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<Response, ContractError> {
// 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)]
Expand Down Expand Up @@ -682,36 +830,6 @@ pub mod migration {
pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
let version = ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

if version < "1.1.0".parse::<Version>().unwrap() {
use cw_storage_plus::Item;
let old_storage: Item<migration::OldSupply> = 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())
}

Expand Down
9 changes: 9 additions & 0 deletions contracts/lsd-hub/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OverflowError> for ContractError {
Expand Down
4 changes: 4 additions & 0 deletions contracts/lsd-hub/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
24 changes: 24 additions & 0 deletions deploy.sh
Original file line number Diff line number Diff line change
@@ -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