Feature/pledge more (#1679)

* New transactions for increasing amount of pledged tokens

* unit tests

* Added an option to pledge extra tokens through the vesting contract

* Introduced wallet endpoints for new operations

* Using updated pledge cap in the vesting contract

* Changelog update
This commit is contained in:
Jędrzej Stuczyński
2022-12-01 12:44:20 +00:00
committed by GitHub
parent 52d06785fb
commit 033333bb52
24 changed files with 808 additions and 14 deletions
@@ -180,6 +180,35 @@ pub trait MixnetSigningClient {
.await
}
async fn pledge_more(
&self,
additional_pledge: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NymdError> {
self.execute_mixnet_contract(
fee,
MixnetExecuteMsg::PledgeMore {},
vec![additional_pledge],
)
.await
}
async fn pledge_more_on_behalf(
&self,
owner: AccountId,
additional_pledge: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NymdError> {
self.execute_mixnet_contract(
fee,
MixnetExecuteMsg::PledgeMoreOnBehalf {
owner: owner.to_string(),
},
vec![additional_pledge],
)
.await
}
async fn unbond_mixnode(&self, fee: Option<Fee>) -> Result<ExecuteResult, NymdError> {
self.execute_mixnet_contract(fee, MixnetExecuteMsg::UnbondMixnode {}, vec![])
.await
@@ -64,6 +64,21 @@ pub trait VestingSigningClient {
fee: Option<Fee>,
) -> Result<ExecuteResult, NymdError>;
async fn vesting_pledge_more(
&self,
additional_pledge: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NymdError> {
self.execute_vesting_contract(
fee,
VestingExecuteMsg::PledgeMore {
amount: additional_pledge.into(),
},
vec![],
)
.await
}
async fn vesting_unbond_mixnode(&self, fee: Option<Fee>) -> Result<ExecuteResult, NymdError>;
async fn vesting_track_unbond_mixnode(
@@ -12,6 +12,8 @@ pub const EVENT_VERSION_PREFIX: &str = "v2_";
pub enum MixnetEventType {
MixnodeBonding,
PendingPledgeIncrease,
PledgeIncrease,
GatewayBonding,
GatewayUnbonding,
PendingMixnodeUnbonding,
@@ -51,6 +53,8 @@ impl ToString for MixnetEventType {
fn to_string(&self) -> String {
let event_name = match self {
MixnetEventType::MixnodeBonding => "mixnode_bonding",
MixnetEventType::PendingPledgeIncrease => "pending_pledge_increase",
MixnetEventType::PledgeIncrease => "pledge_increase",
MixnetEventType::GatewayBonding => "gateway_bonding",
MixnetEventType::GatewayUnbonding => "gateway_unbonding",
MixnetEventType::PendingMixnodeUnbonding => "pending_mixnode_unbonding",
@@ -330,6 +334,19 @@ pub fn new_mixnode_bonding_event(
.add_attribute(AMOUNT_KEY, amount.to_string())
}
pub fn new_pending_pledge_increase_event(mix_id: MixId, amount: &Coin) -> Event {
Event::new(MixnetEventType::PendingPledgeIncrease)
.add_attribute(MIX_ID_KEY, mix_id.to_string())
.add_attribute(AMOUNT_KEY, amount.to_string())
}
pub fn new_pledge_increase_event(created_at: BlockHeight, mix_id: MixId, amount: &Coin) -> Event {
Event::new(MixnetEventType::PledgeIncrease)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(MIX_ID_KEY, mix_id.to_string())
.add_attribute(AMOUNT_KEY, amount.to_string())
}
pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Event {
Event::new(MixnetEventType::MixnodeUnbonding)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
@@ -325,6 +325,14 @@ impl MixNodeRewarding {
Ok(())
}
pub fn increase_operator_uint128(
&mut self,
amount: Uint128,
) -> Result<(), MixnetContractError> {
self.operator += amount.into_base_decimal()?;
Ok(())
}
pub fn increase_delegates_uint128(
&mut self,
amount: Uint128,
@@ -113,6 +113,10 @@ pub enum ExecuteMsg {
owner_signature: String,
owner: String,
},
PledgeMore {},
PledgeMoreOnBehalf {
owner: String,
},
UnbondMixnode {},
UnbondMixnodeOnBehalf {
owner: String,
@@ -223,6 +227,8 @@ impl ExecuteMsg {
ExecuteMsg::BondMixnodeOnBehalf { mix_node, .. } => {
format!("bonding mixnode {} on behalf", mix_node.identity_key)
}
ExecuteMsg::PledgeMore {} => "pledging additional tokens".into(),
ExecuteMsg::PledgeMoreOnBehalf { .. } => "pledging additional tokens on behalf".into(),
ExecuteMsg::UnbondMixnode { .. } => "unbonding mixnode".into(),
ExecuteMsg::UnbondMixnodeOnBehalf { .. } => "unbonding mixnode on behalf".into(),
ExecuteMsg::UpdateMixnodeCostParams { .. } => "updating mixnode cost parameters".into(),
@@ -34,6 +34,10 @@ pub enum PendingEpochEventKind {
mix_id: MixId,
proxy: Option<Addr>,
},
PledgeMore {
mix_id: MixId,
amount: Coin,
},
UnbondMixnode {
mix_id: MixId,
},
@@ -78,7 +82,6 @@ pub enum PendingIntervalEventKind {
mix_id: MixId,
new_costs: MixNodeCostParams,
},
UpdateRewardingParams {
update: IntervalRewardingParamsUpdate,
},
@@ -14,6 +14,7 @@ pub const VESTING_UNDELEGATION_EVENT_TYPE: &str = "vesting_undelegation";
pub const VESTING_GATEWAY_BONDING_EVENT_TYPE: &str = "vesting_gateway_bonding";
pub const VESTING_GATEWAY_UNBONDING_EVENT_TYPE: &str = "vesting_gateway_unbonding";
pub const VESTING_MIXNODE_BONDING_EVENT_TYPE: &str = "vesting_mixnode_bonding";
pub const VESTING_PLEDGE_MORE_EVENT_TYPE: &str = "vesting_pledge_more";
pub const VESTING_MIXNODE_UNBONDING_EVENT_TYPE: &str = "vesting_mixnode_unbonding";
pub const VESTING_UPDATE_MIXNODE_CONFIG_EVENT_TYPE: &str = "vesting_update_mixnode_config";
pub const VESTING_UPDATE_MIXNODE_COST_PARAMS_EVENT_TYPE: &str =
@@ -112,6 +113,10 @@ pub fn new_vesting_mixnode_bonding_event() -> Event {
Event::new(VESTING_MIXNODE_BONDING_EVENT_TYPE)
}
pub fn new_vesting_pledge_more_event() -> Event {
Event::new(VESTING_PLEDGE_MORE_EVENT_TYPE)
}
pub fn new_vesting_update_mixnode_config_event() -> Event {
Event::new(VESTING_UPDATE_MIXNODE_CONFIG_EVENT_TYPE)
}
@@ -101,6 +101,9 @@ pub enum ExecuteMsg {
owner_signature: String,
amount: Coin,
},
PledgeMore {
amount: Coin,
},
UnbondMixnode {},
TrackUnbondMixnode {
owner: String,
@@ -145,6 +148,7 @@ impl ExecuteMsg {
ExecuteMsg::WithdrawVestedCoins { .. } => "VestingExecuteMsg::WithdrawVestedCoins",
ExecuteMsg::TrackUndelegation { .. } => "VestingExecuteMsg::TrackUndelegation",
ExecuteMsg::BondMixnode { .. } => "VestingExecuteMsg::BondMixnode",
ExecuteMsg::PledgeMore { .. } => "VestingExecuteMsg::PledgeMore",
ExecuteMsg::UnbondMixnode { .. } => "VestingExecuteMsg::UnbondMixnode",
ExecuteMsg::TrackUnbondMixnode { .. } => "VestingExecuteMsg::TrackUnbondMixnode",
ExecuteMsg::BondGateway { .. } => "VestingExecuteMsg::BondGateway",
+10
View File
@@ -57,6 +57,10 @@ pub enum PendingEpochEventData {
mix_id: MixId,
proxy: Option<String>,
},
PledgeMore {
mix_id: MixId,
amount: DecCoin,
},
UnbondMixnode {
mix_id: MixId,
},
@@ -91,6 +95,12 @@ impl PendingEpochEventData {
mix_id,
proxy: proxy.map(|p| p.into_string()),
}),
MixnetContractPendingEpochEventKind::PledgeMore { mix_id, amount } => {
Ok(PendingEpochEventData::PledgeMore {
mix_id,
amount: reg.attempt_convert_to_display_dec_coin(amount.into())?,
})
}
MixnetContractPendingEpochEventKind::UnbondMixnode { mix_id } => {
Ok(PendingEpochEventData::UnbondMixnode { mix_id })
}
+2
View File
@@ -3,7 +3,9 @@
## Added
- Added migration code to the mixnet contract to allow updating stored vesting contract address to make it easier to deploy any future environments ([#1759],[#1769])
- Added an option to pledge additional tokens without the need to rebond minxode ([#1679])
[#1679]: https://github.com/nymtech/nym/pull/1679
[#1759]: https://github.com/nymtech/nym/pull/1759
[#1769]: https://github.com/nymtech/nym/pull/1769
+6
View File
@@ -168,6 +168,12 @@ pub fn execute(
owner,
owner_signature,
),
ExecuteMsg::PledgeMore {} => {
crate::mixnodes::transactions::try_increase_pledge(deps, env, info)
}
ExecuteMsg::PledgeMoreOnBehalf { owner } => {
crate::mixnodes::transactions::try_increase_pledge_on_behalf(deps, env, info, owner)
}
ExecuteMsg::UnbondMixnode {} => {
crate::mixnodes::transactions::try_remove_mixnode(deps, env, info)
}
+263 -1
View File
@@ -7,13 +7,14 @@ use crate::interval::helpers::change_interval_config;
use crate::interval::storage;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::mixnodes::helpers::{cleanup_post_unbond_mixnode_storage, get_mixnode_details_by_id};
use crate::mixnodes::storage as mixnodes_storage;
use crate::rewards::storage as rewards_storage;
use crate::support::helpers::send_to_proxy_or_owner;
use cosmwasm_std::{wasm_execute, Addr, Coin, DepsMut, Env, Response};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::events::{
new_active_set_update_event, new_delegation_event, new_delegation_on_unbonded_node_event,
new_mixnode_cost_params_update_event, new_mixnode_unbonding_event,
new_mixnode_cost_params_update_event, new_mixnode_unbonding_event, new_pledge_increase_event,
new_rewarding_params_update_event, new_undelegation_event,
};
use mixnet_contract_common::mixnode::MixNodeCostParams;
@@ -258,6 +259,42 @@ pub(crate) fn update_active_set_size(
Ok(Response::new().add_event(new_active_set_update_event(created_at, active_set_size)))
}
pub(crate) fn increase_pledge(
deps: DepsMut<'_>,
created_at: BlockHeight,
mix_id: MixId,
increase: Coin,
) -> Result<Response, MixnetContractError> {
// note: we have already validated the amount to know it has the correct denomination
// the target node MUST exist - we have checked it at the time of putting this event onto the queue
// we have also verified there were no preceding unbond events
let mix_details = get_mixnode_details_by_id(deps.storage, mix_id)?.ok_or(
MixnetContractError::InconsistentState {
comment:
"mixnode getting processed to increase its pledge doesn't exist in the storage"
.into(),
},
)?;
let mut updated_bond = mix_details.bond_information.clone();
let mut updated_rewarding = mix_details.rewarding_details;
updated_bond.original_pledge.amount += increase.amount;
updated_rewarding.increase_operator_uint128(increase.amount)?;
// update both, bond information and rewarding details
mixnodes_storage::mixnode_bonds().replace(
deps.storage,
mix_id,
Some(&updated_bond),
Some(&mix_details.bond_information),
)?;
rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &updated_rewarding)?;
Ok(Response::new().add_event(new_pledge_increase_event(created_at, mix_id, &increase)))
}
impl ContractExecutableEvent for PendingEpochEventData {
fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result<Response, MixnetContractError> {
// note that the basic validation on all those events was already performed before
@@ -274,6 +311,9 @@ impl ContractExecutableEvent for PendingEpochEventData {
mix_id,
proxy,
} => undelegate(deps, self.created_at, owner, mix_id, proxy),
PendingEpochEventKind::PledgeMore { mix_id, amount } => {
increase_pledge(deps, self.created_at, mix_id, amount)
}
PendingEpochEventKind::UnbondMixnode { mix_id } => {
unbond_mixnode(deps, env, self.created_at, mix_id)
}
@@ -1135,6 +1175,228 @@ mod tests {
}
}
#[cfg(test)]
mod increasing_pledge {
use super::*;
use cosmwasm_std::Uint128;
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount;
#[test]
fn returns_hard_error_if_mixnode_doesnt_exist() {
// this should have never happened so hard error MUST be thrown here
let mut test = TestSetup::new();
let amount = test.coin(123);
let res = increase_pledge(test.deps_mut(), 123, 1, amount);
assert!(matches!(
res,
Err(MixnetContractError::InconsistentState { .. })
));
}
#[test]
fn updates_stored_bond_information_and_rewarding_details() {
let mut test = TestSetup::new();
let mix_id = test.add_dummy_mixnode("mix-owner", None);
let old_details = get_mixnode_details_by_id(test.deps().storage, mix_id)
.unwrap()
.unwrap();
let amount = test.coin(12345);
increase_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap();
let updated_details = get_mixnode_details_by_id(test.deps().storage, mix_id)
.unwrap()
.unwrap();
assert_eq!(
updated_details.bond_information.original_pledge.amount,
old_details.bond_information.original_pledge.amount + amount.amount
);
assert_eq!(
updated_details.rewarding_details.operator,
old_details.rewarding_details.operator
+ Decimal::from_atomics(amount.amount, 0).unwrap()
);
}
#[test]
fn without_any_events_in_between_is_equivalent_to_pledging_the_same_amount_immediately() {
let mut test = TestSetup::new();
let pledge1 = Uint128::new(150_000_000);
let pledge2 = Uint128::new(50_000_000);
let pledge3 = Uint128::new(200_000_000);
let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1));
let increase = test.coin(pledge2.u128());
increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap();
let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3));
test.add_immediate_delegation("alice", 123_456_789u128, mix_id_repledge);
test.add_immediate_delegation("bob", 500_000_000u128, mix_id_repledge);
test.add_immediate_delegation("carol", 111_111_111u128, mix_id_repledge);
test.add_immediate_delegation("alice", 123_456_789u128, mix_id_full_pledge);
test.add_immediate_delegation("bob", 500_000_000u128, mix_id_full_pledge);
test.add_immediate_delegation("carol", 111_111_111u128, mix_id_full_pledge);
test.skip_to_next_epoch_end();
test.update_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]);
let dist1 =
test.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0));
let dist2 =
test.reward_with_distribution(mix_id_full_pledge, test_helpers::performance(100.0));
assert_eq!(dist1, dist2)
}
#[test]
fn correctly_increases_future_rewards() {
let mut test = TestSetup::new();
let pledge1 = Uint128::new(150_000_000_000);
let pledge2 = Uint128::new(50_000_000_000);
let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1));
test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge);
test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_repledge);
test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge);
test.skip_to_next_epoch_end();
test.update_rewarded_set(vec![mix_id_repledge]);
let dist =
test.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0));
let increase = test.coin(pledge2.u128());
increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap();
let pledge3 = Uint128::new(200_000_000_000) + truncate_reward_amount(dist.operator);
let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3));
test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge);
test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge);
test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_full_pledge);
let lost_operator = dist.operator
- Decimal::from_atomics(truncate_reward_amount(dist.operator), 0).unwrap();
let lost_delegates = dist.delegates
- Decimal::from_atomics(truncate_reward_amount(dist.delegates), 0).unwrap();
// add the tiny bit of lost precision manually
let mut mix_rewarding_full = test.mix_rewarding(mix_id_full_pledge);
mix_rewarding_full.delegates += lost_delegates;
mix_rewarding_full.operator += lost_operator;
rewards_storage::MIXNODE_REWARDING
.save(
test.deps_mut().storage,
mix_id_full_pledge,
&mix_rewarding_full,
)
.unwrap();
test.add_immediate_delegation(
"dave",
truncate_reward_amount(dist.delegates).u128(),
mix_id_full_pledge,
);
test.skip_to_next_epoch_end();
test.update_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]);
// go through few epochs of rewarding
for _ in 0..500 {
test.skip_to_next_epoch_end();
let dist1 = test
.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0));
let dist2 = test
.reward_with_distribution(mix_id_full_pledge, test_helpers::performance(100.0));
assert_eq!(dist1, dist2)
}
}
#[test]
fn correctly_increases_future_rewards_with_more_passed_epochs() {
let mut test = TestSetup::new();
let pledge1 = Uint128::new(150_000_000_000);
let pledge2 = Uint128::new(50_000_000_000);
let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1));
test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge);
test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_repledge);
test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge);
test.skip_to_next_epoch_end();
test.update_rewarded_set(vec![mix_id_repledge]);
let mut cumulative_op_reward = Decimal::zero();
let mut cumulative_del_reward = Decimal::zero();
// go few epochs of rewarding before adding more pledge
for _ in 0..500 {
test.skip_to_next_epoch_end();
let dist = test
.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0));
cumulative_op_reward += dist.operator;
cumulative_del_reward += dist.delegates;
}
let increase = test.coin(pledge2.u128());
increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap();
let pledge3 =
Uint128::new(200_000_000_000) + truncate_reward_amount(cumulative_op_reward);
let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3));
test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge);
test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge);
test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_full_pledge);
let lost_operator = cumulative_op_reward
- Decimal::from_atomics(truncate_reward_amount(cumulative_op_reward), 0).unwrap();
let lost_delegates = cumulative_del_reward
- Decimal::from_atomics(truncate_reward_amount(cumulative_del_reward), 0).unwrap();
// add the tiny bit of lost precision manually
let mut mix_rewarding_full = test.mix_rewarding(mix_id_full_pledge);
mix_rewarding_full.delegates += lost_delegates;
mix_rewarding_full.operator += lost_operator;
rewards_storage::MIXNODE_REWARDING
.save(
test.deps_mut().storage,
mix_id_full_pledge,
&mix_rewarding_full,
)
.unwrap();
test.add_immediate_delegation(
"dave",
truncate_reward_amount(cumulative_del_reward).u128(),
mix_id_full_pledge,
);
test.skip_to_next_epoch_end();
test.update_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]);
// go through few more epochs of rewarding
for _ in 0..500 {
test.skip_to_next_epoch_end();
let dist1 = test
.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0));
let dist2 = test
.reward_with_distribution(mix_id_full_pledge, test_helpers::performance(100.0));
assert_eq!(dist1, dist2)
}
}
}
#[test]
fn updating_active_set_updates_rewarding_params() {
let mut test = TestSetup::new();
@@ -37,7 +37,6 @@ pub(crate) fn minimum_delegation_stake(
.map(|state| state.params.minimum_mixnode_delegation)?)
}
#[allow(unused)]
pub(crate) fn rewarding_denom(storage: &dyn Storage) -> Result<String, MixnetContractError> {
Ok(CONTRACT_STATE
.load(storage)
+6 -6
View File
@@ -147,7 +147,7 @@ pub(crate) fn cleanup_post_unbond_mixnode_storage(
}
#[cfg(test)]
mod tests {
pub(crate) mod tests {
use super::*;
use crate::support::tests::fixtures::{
mix_node_cost_params_fixture, mix_node_fixture, TEST_COIN_DENOM,
@@ -155,13 +155,13 @@ mod tests {
use crate::support::tests::test_helpers::TestSetup;
use cosmwasm_std::coin;
const OWNER_EXISTS: &str = "mix-owner-existing";
const OWNER_UNBONDING: &str = "mix-owner-unbonding";
const OWNER_UNBONDED: &str = "mix-owner-unbonded";
const OWNER_UNBONDED_LEFTOVER: &str = "mix-owner-unbonded-leftover";
pub(crate) const OWNER_EXISTS: &str = "mix-owner-existing";
pub(crate) const OWNER_UNBONDING: &str = "mix-owner-unbonding";
pub(crate) const OWNER_UNBONDED: &str = "mix-owner-unbonded";
pub(crate) const OWNER_UNBONDED_LEFTOVER: &str = "mix-owner-unbonded-leftover";
// create a mixnode that is bonded, unbonded, in the process of unbonding and unbonded with leftover mix rewarding details
fn setup_mix_combinations(test: &mut TestSetup) -> Vec<MixId> {
pub(crate) fn setup_mix_combinations(test: &mut TestSetup) -> Vec<MixId> {
let mix_id_exists = test.add_dummy_mixnode(OWNER_EXISTS, None);
let mix_id_unbonding = test.add_dummy_mixnode(OWNER_UNBONDING, None);
let mix_id_unbonded = test.add_dummy_mixnode(OWNER_UNBONDED, None);
+237 -2
View File
@@ -5,16 +5,20 @@ use super::storage;
use crate::interval::storage as interval_storage;
use crate::interval::storage::push_new_interval_event;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::mixnodes::helpers::{must_get_mixnode_bond_by_owner, save_new_mixnode};
use crate::mixnet_contract_settings::storage::rewarding_denom;
use crate::mixnodes::helpers::{
get_mixnode_details_by_owner, must_get_mixnode_bond_by_owner, save_new_mixnode,
};
use crate::support::helpers::{
ensure_bonded, ensure_no_existing_bond, ensure_proxy_match, validate_node_identity_signature,
validate_pledge,
};
use cosmwasm_std::{Addr, Coin, DepsMut, Env, MessageInfo, Response};
use cosmwasm_std::{coin, Addr, Coin, DepsMut, Env, MessageInfo, Response};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::events::{
new_mixnode_bonding_event, new_mixnode_config_update_event,
new_mixnode_pending_cost_params_update_event, new_pending_mixnode_unbonding_event,
new_pending_pledge_increase_event,
};
use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind};
@@ -117,6 +121,54 @@ fn _try_add_mixnode(
)))
}
pub fn try_increase_pledge(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
) -> Result<Response, MixnetContractError> {
_try_increase_pledge(deps, env, info.funds, info.sender, None)
}
pub fn try_increase_pledge_on_behalf(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
owner: String,
) -> Result<Response, MixnetContractError> {
let proxy = info.sender;
let owner = deps.api.addr_validate(&owner)?;
_try_increase_pledge(deps, env, info.funds, owner, Some(proxy))
}
pub fn _try_increase_pledge(
deps: DepsMut<'_>,
env: Env,
increase: Vec<Coin>,
owner: Addr,
proxy: Option<Addr>,
) -> Result<Response, MixnetContractError> {
let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())?
.ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })?;
let mix_id = mix_details.mix_id();
ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?;
ensure_bonded(&mix_details.bond_information)?;
let rewarding_denom = rewarding_denom(deps.storage)?;
let pledge_increase = validate_pledge(increase, coin(1, rewarding_denom))?;
let cosmos_event = new_pending_pledge_increase_event(mix_id, &pledge_increase);
// push the event to execute it at the end of the epoch
let epoch_event = PendingEpochEventKind::PledgeMore {
mix_id,
amount: pledge_increase,
};
interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?;
Ok(Response::new().add_event(cosmos_event))
}
pub fn try_remove_mixnode_on_behalf(
deps: DepsMut<'_>,
env: Env,
@@ -680,4 +732,187 @@ pub mod tests {
)
.is_err());
}
#[cfg(test)]
mod increasing_mixnode_pledge {
use super::*;
use crate::mixnodes::helpers::tests::{
setup_mix_combinations, OWNER_UNBONDED, OWNER_UNBONDED_LEFTOVER, OWNER_UNBONDING,
};
use crate::support::tests::test_helpers::TestSetup;
#[test]
fn is_not_allowed_if_account_doesnt_own_mixnode() {
let mut test = TestSetup::new();
let env = test.env();
let sender = mock_info("not-mix-owner", &[]);
let res = try_increase_pledge(test.deps_mut(), env, sender);
assert_eq!(
res,
Err(MixnetContractError::NoAssociatedMixNodeBond {
owner: Addr::unchecked("not-mix-owner")
})
)
}
#[test]
fn is_not_allowed_if_theres_proxy_mismatch() {
let mut test = TestSetup::new();
let env = test.env();
let owner_without_proxy = Addr::unchecked("no-proxy");
let owner_with_proxy = Addr::unchecked("with-proxy");
let proxy = Addr::unchecked("proxy");
let wrong_proxy = Addr::unchecked("unrelated-proxy");
test.add_dummy_mixnode(owner_without_proxy.as_str(), None);
test.add_dummy_mixnode_with_proxy(owner_with_proxy.as_str(), None, proxy.clone());
let res = _try_increase_pledge(
test.deps_mut(),
env.clone(),
Vec::new(),
owner_without_proxy.clone(),
Some(proxy),
);
assert_eq!(
res,
Err(MixnetContractError::ProxyMismatch {
existing: "None".to_string(),
incoming: "proxy".to_string()
})
);
let res = _try_increase_pledge(
test.deps_mut(),
env.clone(),
Vec::new(),
owner_with_proxy.clone(),
None,
);
assert_eq!(
res,
Err(MixnetContractError::ProxyMismatch {
existing: "proxy".to_string(),
incoming: "None".to_string()
})
);
let res = _try_increase_pledge(
test.deps_mut(),
env,
Vec::new(),
owner_with_proxy.clone(),
Some(wrong_proxy),
);
assert_eq!(
res,
Err(MixnetContractError::ProxyMismatch {
existing: "proxy".to_string(),
incoming: "unrelated-proxy".to_string()
})
)
}
#[test]
fn is_not_allowed_if_mixnode_has_unbonded_or_is_unbonding() {
let mut test = TestSetup::new();
let env = test.env();
// TODO: I dislike this cross-test access, but it provides us with exactly what we need
// perhaps it should be refactored a bit?
let owner_unbonding = Addr::unchecked(OWNER_UNBONDING);
let owner_unbonded = Addr::unchecked(OWNER_UNBONDED);
let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER);
let ids = setup_mix_combinations(&mut test);
let mix_id_unbonding = ids[1];
let res = try_increase_pledge(
test.deps_mut(),
env.clone(),
mock_info(owner_unbonding.as_str(), &[]),
);
assert_eq!(
res,
Err(MixnetContractError::MixnodeIsUnbonding {
mix_id: mix_id_unbonding
})
);
// if the nodes are gone we treat them as tey never existed in the first place
// (regardless of if there's some leftover data)
let res = try_increase_pledge(
test.deps_mut(),
env.clone(),
mock_info(owner_unbonded_leftover.as_str(), &[]),
);
assert_eq!(
res,
Err(MixnetContractError::NoAssociatedMixNodeBond {
owner: owner_unbonded_leftover
})
);
let res = try_increase_pledge(
test.deps_mut(),
env,
mock_info(owner_unbonded.as_str(), &[]),
);
assert_eq!(
res,
Err(MixnetContractError::NoAssociatedMixNodeBond {
owner: owner_unbonded
})
)
}
#[test]
fn is_not_allowed_if_no_tokens_were_sent() {
let mut test = TestSetup::new();
let env = test.env();
let owner = "mix-owner";
test.add_dummy_mixnode(owner, None);
let sender_empty = mock_info(owner, &[]);
let res = try_increase_pledge(test.deps_mut(), env.clone(), sender_empty);
assert_eq!(res, Err(MixnetContractError::NoBondFound));
let sender_zero = mock_info(owner, &[test.coin(0)]);
let res = try_increase_pledge(test.deps_mut(), env, sender_zero);
assert_eq!(
res,
Err(MixnetContractError::InsufficientPledge {
received: test.coin(0),
minimum: test.coin(1)
})
)
}
#[test]
fn with_valid_information_creates_pending_event() {
let mut test = TestSetup::new();
let env = test.env();
let owner = "mix-owner";
let mix_id = test.add_dummy_mixnode(owner, None);
let events = test.pending_epoch_events();
assert!(events.is_empty());
let sender = mock_info(owner, &[test.coin(1000)]);
try_increase_pledge(test.deps_mut(), env, sender).unwrap();
let events = test.pending_epoch_events();
assert_eq!(
events[0].kind,
PendingEpochEventKind::PledgeMore {
mix_id,
amount: test.coin(1000)
}
);
}
}
}
+36 -2
View File
@@ -28,6 +28,9 @@ pub mod test_helpers {
use crate::mixnodes::transactions::{
try_add_mixnode, try_add_mixnode_on_behalf, try_remove_mixnode,
};
use crate::rewards::queries::{
query_pending_delegator_reward, query_pending_mixnode_operator_reward,
};
use crate::rewards::storage as rewards_storage;
use crate::rewards::transactions::try_reward_mixnode;
use crate::support::tests;
@@ -37,7 +40,7 @@ pub mod test_helpers {
use cosmwasm_std::testing::mock_info;
use cosmwasm_std::testing::MockApi;
use cosmwasm_std::testing::MockQuerier;
use cosmwasm_std::{Addr, BankMsg, CosmosMsg, Storage};
use cosmwasm_std::{coin, Addr, BankMsg, CosmosMsg, Storage};
use cosmwasm_std::{Coin, Order};
use cosmwasm_std::{Decimal, Empty, MemoryStorage};
use cosmwasm_std::{Deps, OwnedDeps};
@@ -138,6 +141,10 @@ pub mod test_helpers {
.vesting_contract_address
}
pub fn coin(&self, amount: u128) -> Coin {
coin(amount, rewarding_denom(self.deps().storage).unwrap())
}
pub fn current_interval(&self) -> Interval {
interval_storage::current_interval(self.deps().storage).unwrap()
}
@@ -308,6 +315,22 @@ pub mod test_helpers {
.unwrap();
}
#[allow(unused)]
pub fn pending_operator_reward(&mut self, mix: MixId) -> Decimal {
query_pending_mixnode_operator_reward(self.deps(), mix)
.unwrap()
.amount_earned_detailed
.expect("no reward!")
}
#[allow(unused)]
pub fn pending_delegator_reward(&mut self, delegator: &str, target: MixId) -> Decimal {
query_pending_delegator_reward(self.deps(), delegator.into(), target, None)
.unwrap()
.amount_earned_detailed
.expect("no reward!")
}
pub fn skip_to_next_epoch_end(&mut self) {
self.skip_to_next_epoch();
self.skip_to_current_epoch_end();
@@ -337,8 +360,19 @@ pub mod test_helpers {
self.env.block.time = Timestamp::from_seconds(epoch_end as u64 + 1);
let advanced = interval.advance_epoch();
if interval.current_epoch_id() != interval.epochs_in_interval() {
assert_eq!(
interval.current_epoch_absolute_id() + 1,
advanced.current_epoch_absolute_id()
);
if interval.current_epoch_id() != interval.epochs_in_interval() - 1 {
assert_eq!(interval.current_epoch_id() + 1, advanced.current_epoch_id())
} else {
assert_eq!(advanced.current_epoch_id(), 0);
assert_eq!(
interval.current_interval_id() + 1,
advanced.current_interval_id()
)
}
interval_storage::save_interval(self.deps_mut().storage, &advanced).unwrap()
+14
View File
@@ -121,6 +121,7 @@ pub fn execute(
env,
deps,
),
ExecuteMsg::PledgeMore { amount } => try_pledge_more(deps, env, info, amount),
ExecuteMsg::UnbondMixnode {} => try_unbond_mixnode(info, deps),
ExecuteMsg::TrackUnbondMixnode { owner, amount } => {
try_track_unbond_mixnode(&owner, amount, info, deps)
@@ -331,6 +332,19 @@ pub fn try_bond_mixnode(
)
}
pub fn try_pledge_more(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
amount: Coin,
) -> Result<Response, ContractError> {
let mix_denom = MIX_DENOM.load(deps.storage)?;
let additional_pledge = validate_funds(&[amount], mix_denom)?;
let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
account.try_pledge_additional_tokens(additional_pledge, &env, deps.storage)
}
/// Unbond a mixnode, sends [mixnet_contract_common::ExecuteMsg::UnbondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].
pub fn try_unbond_mixnode(info: MessageInfo, deps: DepsMut<'_>) -> Result<Response, ContractError> {
let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
@@ -18,6 +18,13 @@ pub trait MixnodeBondingAccount {
storage: &mut dyn Storage,
) -> Result<Response, ContractError>;
fn try_pledge_additional_tokens(
&self,
additional_pledge: Coin,
env: &Env,
storage: &mut dyn Storage,
) -> Result<Response, ContractError>;
fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result<Response, ContractError>;
fn try_track_unbond_mixnode(
@@ -12,7 +12,8 @@ use mixnet_contract_common::mixnode::MixNodeCostParams;
use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, MixNode};
use vesting_contract_common::events::{
new_vesting_mixnode_bonding_event, new_vesting_mixnode_unbonding_event,
new_vesting_update_mixnode_config_event, new_vesting_update_mixnode_cost_params_event,
new_vesting_pledge_more_event, new_vesting_update_mixnode_config_event,
new_vesting_update_mixnode_cost_params_event,
};
use vesting_contract_common::PledgeData;
@@ -84,6 +85,64 @@ impl MixnodeBondingAccount for Account {
.add_event(new_vesting_mixnode_bonding_event()))
}
fn try_pledge_additional_tokens(
&self,
additional_pledge: Coin,
env: &Env,
storage: &mut dyn Storage,
) -> Result<Response, ContractError> {
let current_balance = self.load_balance(storage)?;
let total_pledged_after =
self.total_pledged_locked(storage, env)? + additional_pledge.amount;
let locked_pledge_cap = self.absolute_pledge_cap()?;
if locked_pledge_cap < total_pledged_after {
return Err(ContractError::LockedPledgeCapReached {
current: total_pledged_after,
cap: locked_pledge_cap,
});
}
if current_balance < additional_pledge.amount {
return Err(ContractError::InsufficientBalance(
self.owner_address().as_str().to_string(),
current_balance.u128(),
));
}
let mut pledge_data = if let Some(pledge_data) = self.load_mixnode_pledge(storage)? {
pledge_data
} else {
return Err(ContractError::NoBondFound(
self.owner_address().as_str().to_string(),
));
};
// need a second pair of eyes here to make sure updating existing timestamp on pledge data
// is not going to have some unexpected consequences
pledge_data.amount.amount += additional_pledge.amount;
pledge_data.block_time = env.block.time;
let msg = MixnetExecuteMsg::PledgeMoreOnBehalf {
owner: self.owner_address().into_string(),
};
let new_balance = Uint128::new(current_balance.u128() - additional_pledge.amount.u128());
let pledge_more_mag = wasm_execute(
MIXNET_CONTRACT_ADDRESS.load(storage)?,
&msg,
vec![additional_pledge],
)?;
self.save_balance(new_balance, storage)?;
self.save_mixnode_pledge(pledge_data, storage)?;
Ok(Response::new()
.add_message(pledge_more_mag)
.add_event(new_vesting_pledge_more_event()))
}
fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result<Response, ContractError> {
let msg = MixnetExecuteMsg::UnbondMixnodeOnBehalf {
owner: self.owner_address().into_string(),
+4
View File
@@ -55,6 +55,7 @@ fn main() {
mixnet::admin::update_contract_settings,
mixnet::bond::bond_gateway,
mixnet::bond::bond_mixnode,
mixnet::bond::pledge_more,
mixnet::bond::gateway_bond_details,
mixnet::bond::get_pending_operator_rewards,
mixnet::bond::mixnode_bond_details,
@@ -105,6 +106,7 @@ fn main() {
vesting::rewards::vesting_claim_operator_reward,
vesting::bond::vesting_bond_gateway,
vesting::bond::vesting_bond_mixnode,
vesting::bond::vesting_pledge_more,
vesting::bond::vesting_unbond_gateway,
vesting::bond::vesting_unbond_mixnode,
vesting::bond::vesting_update_mixnode_cost_params,
@@ -130,6 +132,7 @@ fn main() {
simulate::mixnet::simulate_bond_gateway,
simulate::mixnet::simulate_unbond_gateway,
simulate::mixnet::simulate_bond_mixnode,
simulate::mixnet::simulate_pledge_more,
simulate::mixnet::simulate_unbond_mixnode,
simulate::mixnet::simulate_update_mixnode_config,
simulate::mixnet::simulate_update_mixnode_cost_params,
@@ -140,6 +143,7 @@ fn main() {
simulate::vesting::simulate_vesting_bond_gateway,
simulate::vesting::simulate_vesting_unbond_gateway,
simulate::vesting::simulate_vesting_bond_mixnode,
simulate::vesting::simulate_vesting_pledge_more,
simulate::vesting::simulate_vesting_unbond_mixnode,
simulate::vesting::simulate_vesting_update_mixnode_config,
simulate::vesting::simulate_vesting_update_mixnode_cost_params,
@@ -102,6 +102,33 @@ pub async fn bond_mixnode(
)?)
}
#[tauri::command]
pub async fn pledge_more(
fee: Option<Fee>,
additional_pledge: DecCoin,
state: tauri::State<'_, WalletState>,
) -> Result<TransactionExecuteResult, BackendError> {
let guard = state.read().await;
let additional_pledge_base = guard.attempt_convert_to_base_coin(additional_pledge.clone())?;
let fee_amount = guard.convert_tx_fee(fee.as_ref());
log::info!(
">>> Pledge more, additional_pledge_display = {}, additional_pledge_base = {}, fee = {:?}",
additional_pledge,
additional_pledge_base,
fee,
);
let res = guard
.current_client()?
.nymd
.pledge_more(additional_pledge_base, fee)
.await?;
log::info!("<<< tx hash = {}", res.transaction_hash);
log::trace!("<<< {:?}", res);
Ok(TransactionExecuteResult::from_execute_result(
res, fee_amount,
)?)
}
#[tauri::command]
pub async fn unbond_mixnode(
fee: Option<Fee>,
@@ -84,6 +84,14 @@ pub async fn simulate_bond_mixnode(
.await
}
#[tauri::command]
pub async fn simulate_pledge_more(
additional_pledge: DecCoin,
state: tauri::State<'_, WalletState>,
) -> Result<FeeDetails, BackendError> {
simulate_mixnet_operation(ExecuteMsg::PledgeMore {}, Some(additional_pledge), &state).await
}
#[tauri::command]
pub async fn simulate_unbond_mixnode(
state: tauri::State<'_, WalletState>,
@@ -91,6 +91,19 @@ pub async fn simulate_vesting_bond_mixnode(
.await
}
#[tauri::command]
pub async fn simulate_vesting_pledge_more(
additional_pledge: DecCoin,
state: tauri::State<'_, WalletState>,
) -> Result<FeeDetails, BackendError> {
let guard = state.read().await;
let amount = guard
.attempt_convert_to_base_coin(additional_pledge)?
.into();
simulate_vesting_operation(ExecuteMsg::PledgeMore { amount }, None, &state).await
}
#[tauri::command]
pub async fn simulate_vesting_unbond_mixnode(
state: tauri::State<'_, WalletState>,
@@ -93,6 +93,33 @@ pub async fn vesting_bond_mixnode(
)?)
}
#[tauri::command]
pub async fn vesting_pledge_more(
fee: Option<Fee>,
additional_pledge: DecCoin,
state: tauri::State<'_, WalletState>,
) -> Result<TransactionExecuteResult, BackendError> {
let guard = state.read().await;
let additional_pledge_base = guard.attempt_convert_to_base_coin(additional_pledge.clone())?;
let fee_amount = guard.convert_tx_fee(fee.as_ref());
log::info!(
">>> Pledge more with locked tokens, additional_pledge_display = {}, additional_pledge_base = {}, fee = {:?}",
additional_pledge,
additional_pledge_base,
fee,
);
let res = guard
.current_client()?
.nymd
.vesting_pledge_more(additional_pledge_base, fee)
.await?;
log::info!("<<< tx hash = {}", res.transaction_hash);
log::trace!("<<< {:?}", res);
Ok(TransactionExecuteResult::from_execute_result(
res, fee_amount,
)?)
}
#[tauri::command]
pub async fn vesting_unbond_mixnode(
fee: Option<Fee>,