Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Restake & Redelegate for Validator Staking #622

Merged
merged 23 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Vesting: Added validation to the instantiation process [(#583)](https://github.com/andromedaprotocol/andromeda-core/pull/583)
- Fix precision issue with vestings claim batch method [(#555)](https://github.com/andromedaprotocol/andromeda-core/pull/555)
- (validator-staking) fix: validator staking distribution message for andromeda chain [(#618)](https://github.com/andromedaprotocol/andromeda-core/pull/618)
joemonem marked this conversation as resolved.
Show resolved Hide resolved

### Removed

Expand Down
6 changes: 4 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 @@ -29,7 +29,7 @@ panic = 'abort'
strip = true

[workspace.dependencies]
andromeda-std = { path = "./packages/std", default-features = false, version = "1.0.0", features = [
andromeda-std = { path = "./packages/std", default-features = false, features = [
"deploy",
] }
joemonem marked this conversation as resolved.
Show resolved Hide resolved
andromeda-macros = { path = "./packages/std/macros", default-features = false, version = "1.0.0" }
Expand Down
4 changes: 2 additions & 2 deletions contracts/finance/andromeda-validator-staking/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "andromeda-validator-staking"
version = "0.2.3"
version = "0.3.0-b.1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Version bump requires comprehensive testing

The version increase from 0.2.3-b.2 to 0.3.0-b.1 indicates breaking changes with the addition of Redelegate and Restake features. However, the PR notes that no tests were conducted, which is concerning for a major version change.

Please ensure that:

  1. Comprehensive tests are added for the new features
  2. Existing functionality is verified to work with the changes
  3. Breaking changes are documented in CHANGELOG.md

edition = "2021"
rust-version = "1.75.0"

Expand All @@ -21,7 +21,7 @@ cw-storage-plus = { workspace = true }
cw2 = { workspace = true }
serde = { workspace = true }

andromeda-std = { workspace = true }
andromeda-std = { workspace = true, features = ["distribution"] }
andromeda-finance = { workspace = true }
enum-repr = { workspace = true }
chrono = "0.3"
Expand Down
178 changes: 164 additions & 14 deletions contracts/finance/andromeda-validator-staking/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
state::{DEFAULT_VALIDATOR, UNSTAKING_QUEUE},
state::{DEFAULT_VALIDATOR, RESTAKING_QUEUE, UNSTAKING_QUEUE},
util::decode_unstaking_response_data,
};
use cosmwasm_std::{
Expand All @@ -9,15 +9,17 @@ use cosmwasm_std::{
use cw2::set_contract_version;

use andromeda_finance::validator_staking::{
is_validator, ExecuteMsg, InstantiateMsg, QueryMsg, UnstakingTokens,
is_validator, ExecuteMsg, GetDefaultValidatorResponse, InstantiateMsg, QueryMsg,
UnstakingTokens,
};

use andromeda_std::{
ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg},
ado_contract::ADOContract,
amp::AndrAddr,
common::{context::ExecuteContext, encode_binary},
common::{context::ExecuteContext, distribution::MsgWithdrawDelegatorReward, encode_binary},
error::ContractError,
os::aos_querier::AOSQuerier,
};
use enum_repr::EnumRepr;

Expand All @@ -30,6 +32,7 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
pub enum ReplyId {
ValidatorUnstake = 201,
SetWithdrawAddress = 202,
RestakeReward = 203,
}

#[cfg_attr(not(feature = "library"), entry_point)]
Expand Down Expand Up @@ -81,7 +84,12 @@ pub fn handle_execute(ctx: ExecuteContext, msg: ExecuteMsg) -> Result<Response,
match msg {
ExecuteMsg::Stake { validator } => execute_stake(ctx, validator),
ExecuteMsg::Unstake { validator, amount } => execute_unstake(ctx, validator, amount),
ExecuteMsg::Claim { validator } => execute_claim(ctx, validator),
ExecuteMsg::Claim { validator, restake } => execute_claim(ctx, validator, restake),
ExecuteMsg::Redelegate {
src_validator,
dst_validator,
amount,
} => execute_redelegate(ctx, src_validator, dst_validator, amount),
ExecuteMsg::WithdrawFunds { denom, recipient } => {
execute_withdraw_fund(ctx, denom, recipient)
}
Expand All @@ -106,6 +114,8 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result<Binary, ContractErro
}
QueryMsg::UnstakedTokens {} => encode_binary(&query_unstaked_tokens(deps)?),

QueryMsg::DefaultValidator {} => encode_binary(&query_default_validator(deps)?),

_ => ADOContract::default().query(deps, env, msg),
}
}
Expand Down Expand Up @@ -144,6 +154,73 @@ fn execute_stake(ctx: ExecuteContext, validator: Option<Addr>) -> Result<Respons
Ok(res)
}

fn execute_redelegate(
ctx: ExecuteContext,
src_validator: Option<Addr>,
dst_validator: Addr,
amount: Option<Uint128>,
) -> Result<Response, ContractError> {
let ExecuteContext {
deps, env, info, ..
} = ctx;

// Ensure sender is the contract owner
ensure!(
ADOContract::default().is_contract_owner(deps.storage, info.sender.as_str())?,
ContractError::Unauthorized {}
);

let src_validator = match src_validator {
Some(addr) => {
is_validator(&deps, &addr)?;
addr
}
None => DEFAULT_VALIDATOR.load(deps.storage)?,
};

// Check if the destination validator is valid
is_validator(&deps, &dst_validator)?;

// Get redelegation amount
let Some(full_delegation) = deps
.querier
.query_delegation(env.contract.address.to_string(), src_validator.to_string())?
else {
return Err(ContractError::InvalidValidatorOperation {
operation: "Redelegate".to_string(),
validator: src_validator.to_string(),
});
};
let redelegation_amount = match amount {
Some(amount) => {
if amount > full_delegation.can_redelegate.amount {
return Err(ContractError::InvalidRedelegationAmount {
amount: amount.to_string(),
max: full_delegation.can_redelegate.amount.to_string(),
});
}
amount
}
None => full_delegation.can_redelegate.amount,
};

let res = Response::new()
.add_message(StakingMsg::Redelegate {
src_validator: src_validator.clone().into_string(),
dst_validator: dst_validator.clone().into_string(),
amount: coin(
redelegation_amount.u128(),
full_delegation.can_redelegate.denom,
),
})
.add_attribute("action", "redelegation")
.add_attribute("from", src_validator.to_string())
.add_attribute("to", dst_validator.to_string())
.add_attribute("amount", redelegation_amount.to_string());

Ok(res)
}

fn execute_unstake(
ctx: ExecuteContext,
validator: Option<Addr>,
Expand Down Expand Up @@ -212,7 +289,11 @@ fn execute_unstake(
Ok(res)
}

fn execute_claim(ctx: ExecuteContext, validator: Option<Addr>) -> Result<Response, ContractError> {
fn execute_claim(
ctx: ExecuteContext,
validator: Option<Addr>,
restake: Option<bool>,
) -> Result<Response, ContractError> {
let ExecuteContext {
deps, info, env, ..
} = ctx;
Expand Down Expand Up @@ -246,13 +327,43 @@ fn execute_claim(ctx: ExecuteContext, validator: Option<Addr>) -> Result<Respons
ContractError::InvalidClaim {}
);

let res = Response::new()
.add_message(DistributionMsg::WithdrawDelegatorReward {
validator: validator.to_string(),
})
.add_attribute("action", "validator-claim-reward")
.add_attribute("validator", validator.to_string());
let kernel_addr = ADOContract::default().get_kernel_address(deps.storage)?;
let curr_chain = AOSQuerier::get_current_chain(&deps.querier, &kernel_addr)?;

let withdraw_msg: CosmosMsg = if curr_chain == "andromeda" {
MsgWithdrawDelegatorReward {
delegator_address: delegator.to_string(),
validator_address: validator.to_string(),
}
.into()
} else {
DistributionMsg::WithdrawDelegatorReward {
validator: validator.to_string(),
}
.into()
};
let restake = restake.unwrap_or(false);
// Only one denom is allowed to be restaked at a time
let res = if restake && res.accumulated_rewards.len() == 1 {
// Only the contract owner can decide to restake
ensure!(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make this a permissioned action instead? That way a user can allow someone else to restake?

ADOContract::default().is_contract_owner(deps.storage, info.sender.as_str())?,
ContractError::Unauthorized {}
);
RESTAKING_QUEUE.save(deps.storage, &res)?;
Response::new()
.add_submessage(SubMsg::reply_always(
withdraw_msg,
ReplyId::RestakeReward.repr(),
))
.add_attribute("action", "validator-claim-reward")
.add_attribute("validator", validator.to_string())
} else {
Response::new()
.add_message(withdraw_msg)
.add_attribute("action", "validator-claim-reward")
.add_attribute("validator", validator.to_string())
};
Ok(res)
}

Expand Down Expand Up @@ -354,10 +465,16 @@ fn query_unstaked_tokens(deps: Deps) -> Result<Vec<UnstakingTokens>, ContractErr
Ok(res)
}

fn query_default_validator(deps: Deps) -> Result<GetDefaultValidatorResponse, ContractError> {
let default_validator = DEFAULT_VALIDATOR.load(deps.storage)?;
Ok(GetDefaultValidatorResponse { default_validator })
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> {
match ReplyId::from_repr(msg.id) {
Some(ReplyId::ValidatorUnstake) => on_validator_unstake(deps, msg),
Some(ReplyId::RestakeReward) => on_restake_reward(deps, env, msg),
_ => Ok(Response::default()),
}
}
Expand All @@ -371,7 +488,11 @@ pub fn on_validator_unstake(deps: DepsMut, msg: Reply) -> Result<Response, Contr
let payout_at = Timestamp::from_seconds(seconds);
payout_at.plus_nanos(nanos)
} else {
let attributes = &res.events[0].attributes;
let attributes = &res
.events
.first()
.ok_or(ContractError::EmptyEvents {})?
.attributes;
let mut payout_at = Timestamp::default();
for attr in attributes {
if attr.key == "completion_time" {
Expand All @@ -384,11 +505,40 @@ pub fn on_validator_unstake(deps: DepsMut, msg: Reply) -> Result<Response, Contr
}
payout_at
};
let mut unstake_req = unstaking_queue.pop().unwrap();
let mut unstake_req = unstaking_queue
.pop()
.ok_or(ContractError::EmptyUnstakingQueue {})?;
joemonem marked this conversation as resolved.
Show resolved Hide resolved
unstake_req.payout_at = payout_at;

unstaking_queue.push(unstake_req);
UNSTAKING_QUEUE.save(deps.storage, &unstaking_queue)?;

Ok(Response::default())
}

fn on_restake_reward(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> {
match msg.result {
cosmwasm_std::SubMsgResult::Ok(_) => {
let restaking_queue = RESTAKING_QUEUE.load(deps.storage)?;
RESTAKING_QUEUE.remove(deps.storage);

let res = execute_stake(
ExecuteContext {
deps,
info: MessageInfo {
sender: restaking_queue.delegator,
funds: restaking_queue.accumulated_rewards,
},
env,
amp_ctx: None,
},
Some(Addr::unchecked(restaking_queue.validator)),
)?;
Ok(res.add_attribute("action", "restake-reward"))
}
cosmwasm_std::SubMsgResult::Err(e) => {
RESTAKING_QUEUE.remove(deps.storage);
Err(ContractError::new(e.as_str()))
}
}
}
crnbarr93 marked this conversation as resolved.
Show resolved Hide resolved
31 changes: 28 additions & 3 deletions contracts/finance/andromeda-validator-staking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ impl MockValidatorStaking {
self.execute(app, &msg, sender, &funds)
}

pub fn execute_redelegate(
&self,
app: &mut MockApp,
sender: Addr,
src_validator: Option<Addr>,
dst_validator: Addr,
amount: Option<Uint128>,
) -> ExecuteResult {
let msg = mock_execute_redelegate(src_validator, dst_validator, amount);
self.execute(app, &msg, sender, &[])
}
joemonem marked this conversation as resolved.
Show resolved Hide resolved

pub fn execute_unstake(
&self,
app: &mut MockApp,
Expand All @@ -42,8 +54,9 @@ impl MockValidatorStaking {
app: &mut MockApp,
sender: Addr,
validator: Option<Addr>,
restake: Option<bool>,
) -> ExecuteResult {
let msg = mock_execute_claim_reward(validator);
let msg = mock_execute_claim_reward(validator, restake);
self.execute(app, &msg, sender, &[])
}

Expand Down Expand Up @@ -104,12 +117,24 @@ pub fn mock_execute_stake(validator: Option<Addr>) -> ExecuteMsg {
ExecuteMsg::Stake { validator }
}

pub fn mock_execute_redelegate(
src_validator: Option<Addr>,
dst_validator: Addr,
amount: Option<Uint128>,
) -> ExecuteMsg {
ExecuteMsg::Redelegate {
src_validator,
dst_validator,
amount,
}
}

pub fn mock_execute_unstake(validator: Option<Addr>, amount: Option<Uint128>) -> ExecuteMsg {
ExecuteMsg::Unstake { validator, amount }
}

pub fn mock_execute_claim_reward(validator: Option<Addr>) -> ExecuteMsg {
ExecuteMsg::Claim { validator }
pub fn mock_execute_claim_reward(validator: Option<Addr>, restake: Option<bool>) -> ExecuteMsg {
ExecuteMsg::Claim { validator, restake }
}

pub fn mock_execute_withdraw_fund(
Expand Down
4 changes: 3 additions & 1 deletion contracts/finance/andromeda-validator-staking/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use andromeda_finance::validator_staking::UnstakingTokens;
use cw_storage_plus::Item;

use cosmwasm_std::Addr;
use cosmwasm_std::{Addr, FullDelegation};

pub const DEFAULT_VALIDATOR: Item<Addr> = Item::new("default_validator");

pub const UNSTAKING_QUEUE: Item<Vec<UnstakingTokens>> = Item::new("unstaking_queue");

pub const RESTAKING_QUEUE: Item<FullDelegation> = Item::new("restaking_queue");
Loading
Loading