Compare commits

...

14 Commits

Author SHA1 Message Date
mx 7e7200a7c8 added note that operators can decrease self bond via wallet 2023-04-24 15:07:41 +02:00
Jon Häggblad 3f0d4846df Fix a few clippy warnings in contract test code (#3340)
* ci: don't fail fast for contracts

* contracts: fix clippy in tests
2023-04-24 10:30:22 +02:00
Jon Häggblad 9bfcdbe8e2 Add --all-targets to clippy for contracts (#3337) 2023-04-21 11:47:43 +02:00
Tommy Verrall 8c4885ce2c Merge pull request #3294 from nymtech/feature/fix-clippy-warnings
A branch with all clippy warnings dealt with in contracts
2023-04-21 10:37:33 +01:00
Tommy Verrall 926389df89 Merge pull request #3300 from nymtech/bugfix/empty-ack-stream-map
make sure to clear inner 'ack_map' in 'GatewaysReader'
2023-04-20 08:31:31 +01:00
Tommy Verrall b55db00408 Merge pull request #3324 from nymtech/bugfix/nym-cli-gateway-commands
exposed missing gateway commands in nym-cli
2023-04-20 07:55:56 +01:00
Jędrzej Stuczyński cfcb64f7e5 Feature/reduce pledge (#3254)
* basic contract work for 'decrease_pledge' functionality

note: it doesn't yet return tokens back to the operator

* returning extra tokens after decreasing pledge

* added vesting message to track pledge decrease

* attaching the track message when processing delegation decrease

* checking for zero value request

* fixed event test

* allowing to decrease pledge from the vesting contract

* integration test for the feature

* reorganised the integration tests

* updated nyxd client traits

* wallet support

* typescript helpers

* moved 'pledge more' functionality to operator commands

* cli commands for decreasing pledge

* changed error variant to make clippy happier

* removed unused import

* eslint

* fixed post-rebase imports

* added cargo config

* added PendingMixNodeChanges to MixNodeDetails

* returning event id after creating it

* Streamlined getting mixnode details by identity key

* setting pending pledge changes on increase/decrease

* clearing the value on resolving the event

* checking for correct invariants when clearing events

* further pending events unit tests fixes

* new unit tests for tx endpoints

* queries for pending events (by id)

* migration code

* using default value for pending changes if unavailable

* improved integration test assertions
2023-04-20 07:52:10 +01:00
Jon Häggblad 9c6c5f5170 Add --all-targets to nym-wallet CI clippy (#3326) 2023-04-19 10:44:46 +02:00
Jon Häggblad f28888e3e7 Update Cargo.lock files after bumping internal versions during 1.1.15 release 2023-04-19 09:41:03 +02:00
Jędrzej Stuczyński 9549bed8bb exposed missing gateway commands in nym-cli 2023-04-18 16:28:13 +01:00
Jędrzej Stuczyński 7a50f0c3b2 make sure to clear inner 'ack_map' in 'GatewaysReader' 2023-04-13 10:51:03 +01:00
Dave Hrycyszyn 2da6a2fbfa Adding a clippy.toml so we can see correct warnings in mixnet contract 2023-04-11 14:57:04 +01:00
Dave Hrycyszyn d910a4e0ee The make test target seems to be wrapping differently than local 2023-04-11 14:53:51 +01:00
Dave Hrycyszyn 672ab79421 A branch with all clippy warnings dealt with in contracts 2023-04-11 14:46:45 +01:00
91 changed files with 3196 additions and 498 deletions
+2 -1
View File
@@ -30,6 +30,7 @@ jobs:
continue-on-error: ${{ matrix.rust == 'nightly' }} continue-on-error: ${{ matrix.rust == 'nightly' }}
needs: matrix_prep needs: matrix_prep
strategy: strategy:
fail-fast: false
matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}} matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -63,4 +64,4 @@ jobs:
if: ${{ matrix.rust != 'nightly' }} if: ${{ matrix.rust != 'nightly' }}
with: with:
command: clippy command: clippy
args: --manifest-path contracts/Cargo.toml --workspace -- -D warnings args: --manifest-path contracts/Cargo.toml --workspace --all-targets -- -D warnings
+1 -1
View File
@@ -64,4 +64,4 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: clippy command: clippy
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features -- -D warnings args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features --all-targets -- -D warnings
Generated
+2 -2
View File
@@ -4202,7 +4202,7 @@ dependencies = [
[[package]] [[package]]
name = "nym-vesting-contract" name = "nym-vesting-contract"
version = "1.3.0" version = "1.3.1"
dependencies = [ dependencies = [
"cosmwasm-derive", "cosmwasm-derive",
"cosmwasm-std", "cosmwasm-std",
@@ -4220,7 +4220,7 @@ dependencies = [
[[package]] [[package]]
name = "nym-vesting-contract-common" name = "nym-vesting-contract-common"
version = "0.4.0" version = "0.5.0"
dependencies = [ dependencies = [
"cosmwasm-std", "cosmwasm-std",
"nym-contracts-common", "nym-contracts-common",
@@ -24,8 +24,9 @@ use nym_mixnet_contract_common::{
MixOwnershipResponse, MixnodeDetailsResponse, NumberOfPendingEventsResponse, MixOwnershipResponse, MixnodeDetailsResponse, NumberOfPendingEventsResponse,
PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedFamiliesResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedFamiliesResponse,
PagedGatewayResponse, PagedMembersResponse, PagedMixNodeDelegationsResponse, PagedGatewayResponse, PagedMembersResponse, PagedMixNodeDelegationsResponse,
PagedMixnodeBondsResponse, PagedRewardedSetResponse, PendingEpochEventsResponse, PagedMixnodeBondsResponse, PagedRewardedSetResponse, PendingEpochEventResponse,
PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg, PendingEpochEventsResponse, PendingIntervalEventResponse, PendingIntervalEventsResponse,
QueryMsg as MixnetQueryMsg,
}; };
use serde::Deserialize; use serde::Deserialize;
@@ -174,6 +175,16 @@ pub trait MixnetQueryClient {
.await .await
} }
async fn get_mixnode_details_by_identity(
&self,
mix_identity: IdentityKey,
) -> Result<Option<MixNodeDetails>, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetBondedMixnodeDetailsByIdentity {
mix_identity,
})
.await
}
async fn get_mixnode_rewarding_details( async fn get_mixnode_rewarding_details(
&self, &self,
mix_id: MixId, mix_id: MixId,
@@ -374,14 +385,20 @@ pub trait MixnetQueryClient {
.await .await
} }
async fn get_mixnode_details_by_identity( async fn get_pending_epoch_event(
&self, &self,
mix_identity: IdentityKey, event_id: EpochEventId,
) -> Result<Option<MixNodeDetails>, NyxdError> { ) -> Result<PendingEpochEventResponse, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetBondedMixnodeDetailsByIdentity { self.query_mixnet_contract(MixnetQueryMsg::GetPendingEpochEvent { event_id })
mix_identity, .await
}) }
.await
async fn get_pending_interval_event(
&self,
event_id: IntervalEventId,
) -> Result<PendingIntervalEventResponse, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetPendingIntervalEvent { event_id })
.await
} }
async fn get_number_of_pending_events( async fn get_number_of_pending_events(
@@ -331,6 +331,38 @@ pub trait MixnetSigningClient {
.await .await
} }
async fn decrease_pledge(
&self,
decrease_by: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_mixnet_contract(
fee,
MixnetExecuteMsg::DecreasePledge {
decrease_by: decrease_by.into(),
},
vec![],
)
.await
}
async fn decrease_pledge_on_behalf(
&self,
owner: AccountId,
decrease_by: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_mixnet_contract(
fee,
MixnetExecuteMsg::DecreasePledgeOnBehalf {
owner: owner.to_string(),
decrease_by: decrease_by.into(),
},
vec![],
)
.await
}
async fn unbond_mixnode(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError> { async fn unbond_mixnode(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError> {
self.execute_mixnet_contract(fee, MixnetExecuteMsg::UnbondMixnode {}, vec![]) self.execute_mixnet_contract(fee, MixnetExecuteMsg::UnbondMixnode {}, vec![])
.await .await
@@ -91,6 +91,21 @@ pub trait VestingSigningClient {
.await .await
} }
async fn vesting_decrease_pledge(
&self,
decrease_by: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_vesting_contract(
fee,
VestingExecuteMsg::DecreasePledge {
amount: decrease_by.into(),
},
vec![],
)
.await
}
async fn vesting_unbond_mixnode(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError>; async fn vesting_unbond_mixnode(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError>;
async fn vesting_track_unbond_mixnode( async fn vesting_track_unbond_mixnode(
@@ -6,11 +6,9 @@ use clap::{Args, Subcommand};
pub mod rewards; pub mod rewards;
pub mod delegate_to_mixnode; pub mod delegate_to_mixnode;
pub mod pledge_more;
pub mod query_for_delegations; pub mod query_for_delegations;
pub mod undelegate_from_mixnode; pub mod undelegate_from_mixnode;
pub mod vesting_delegate_to_mixnode; pub mod vesting_delegate_to_mixnode;
pub mod vesting_pledge_more;
pub mod vesting_undelegate_from_mixnode; pub mod vesting_undelegate_from_mixnode;
#[derive(Debug, Args)] #[derive(Debug, Args)]
@@ -34,8 +32,4 @@ pub enum MixnetDelegatorsCommands {
DelegateVesting(vesting_delegate_to_mixnode::Args), DelegateVesting(vesting_delegate_to_mixnode::Args),
/// Undelegate from a mixnode (when originally using locked tokens) /// Undelegate from a mixnode (when originally using locked tokens)
UndelegateVesting(vesting_undelegate_from_mixnode::Args), UndelegateVesting(vesting_undelegate_from_mixnode::Args),
/// Pledge more
PledgeMore(pledge_more::Args),
/// Pledge more with locked tokens
PledgeMoreVesting(vesting_pledge_more::Args),
} }
@@ -26,7 +26,7 @@ pub struct Args {
pub version: Option<String>, pub version: Option<String>,
} }
pub async fn vesting_update_config(client: SigningClient, args: Args) { pub async fn vesting_update_config(args: Args, client: SigningClient) {
info!("Update vesting gateway config!"); info!("Update vesting gateway config!");
let current_details = match client let current_details = match client
@@ -45,7 +45,9 @@ pub struct Args {
pub force: bool, pub force: bool,
} }
pub async fn vesting_bond_gateway(client: SigningClient, args: Args, denom: &str) { pub async fn vesting_bond_gateway(args: Args, client: SigningClient) {
let denom = client.current_chain_details().mix_denom.base.as_str();
info!("Starting vesting gateway bonding!"); info!("Starting vesting gateway bonding!");
// if we're trying to bond less than 1 token // if we're trying to bond less than 1 token
@@ -0,0 +1,29 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::context::SigningClient;
use clap::Parser;
use log::info;
use nym_mixnet_contract_common::Coin;
use nym_validator_client::nyxd::traits::MixnetSigningClient;
#[derive(Debug, Parser)]
pub struct Args {
#[clap(long)]
pub decrease_by: u128,
}
pub async fn decrease_pledge(args: Args, client: SigningClient) {
let denom = client.current_chain_details().mix_denom.base.as_str();
info!("Starting to decrease pledge");
let coin = Coin::new(args.decrease_by, denom);
let res = client
.pledge_more(coin.into(), None)
.await
.expect("failed to decrease pledge!");
info!("decreasing pledge: {:?}", res);
}
@@ -4,13 +4,17 @@
use clap::{Args, Subcommand}; use clap::{Args, Subcommand};
pub mod bond_mixnode; pub mod bond_mixnode;
pub mod decrease_pledge;
pub mod families; pub mod families;
pub mod keys; pub mod keys;
pub mod mixnode_bonding_sign_payload; pub mod mixnode_bonding_sign_payload;
pub mod pledge_more;
pub mod rewards; pub mod rewards;
pub mod settings; pub mod settings;
pub mod unbond_mixnode; pub mod unbond_mixnode;
pub mod vesting_bond_mixnode; pub mod vesting_bond_mixnode;
pub mod vesting_decrease_pledge;
pub mod vesting_pledge_more;
pub mod vesting_unbond_mixnode; pub mod vesting_unbond_mixnode;
#[derive(Debug, Args)] #[derive(Debug, Args)]
@@ -40,4 +44,12 @@ pub enum MixnetOperatorsMixnodeCommands {
UnbondVesting(vesting_unbond_mixnode::Args), UnbondVesting(vesting_unbond_mixnode::Args),
/// Create base58-encoded payload required for producing valid bonding signature. /// Create base58-encoded payload required for producing valid bonding signature.
CreateMixnodeBondingSignPayload(mixnode_bonding_sign_payload::Args), CreateMixnodeBondingSignPayload(mixnode_bonding_sign_payload::Args),
/// Pledge more
PledgeMore(pledge_more::Args),
/// Pledge more with locked tokens
PledgeMoreVesting(vesting_pledge_more::Args),
/// Decrease pledge
DecreasePledge(decrease_pledge::Args),
/// Decrease pledge with locked tokens
DecreasePledgeVesting(vesting_decrease_pledge::Args),
} }
@@ -0,0 +1,29 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::context::SigningClient;
use clap::Parser;
use log::info;
use nym_mixnet_contract_common::Coin;
use nym_validator_client::nyxd::VestingSigningClient;
#[derive(Debug, Parser)]
pub struct Args {
#[clap(long)]
pub decrease_by: u128,
}
pub async fn vesting_decrease_pledge(args: Args, client: SigningClient) {
let denom = client.current_chain_details().mix_denom.base.as_str();
info!("Starting vesting to decrease pledge");
let coin = Coin::new(args.decrease_by, denom);
let res = client
.vesting_decrease_pledge(coin.into(), None)
.await
.expect("failed to vesting decrease pledge!");
info!("vesting decreasing pledge: {:?}", res);
}
@@ -1,13 +1,16 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net> // Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use crate::{EpochState, IdentityKey, MixId}; use crate::{EpochEventId, EpochState, IdentityKey, MixId};
use contracts_common::signing::verifier::ApiVerifierError; use contracts_common::signing::verifier::ApiVerifierError;
use cosmwasm_std::{Addr, Coin, Decimal}; use cosmwasm_std::{Addr, Coin, Decimal, Uint128};
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug, PartialEq)] #[derive(Error, Debug, PartialEq)]
pub enum MixnetContractError { pub enum MixnetContractError {
#[error("could not perform contract migration: {comment}")]
FailedMigration { comment: String },
#[error("{source}")] #[error("{source}")]
StdErr { StdErr {
#[from] #[from]
@@ -26,6 +29,17 @@ pub enum MixnetContractError {
#[error("Not enough funds sent for node pledge. (received {received}, minimum {minimum})")] #[error("Not enough funds sent for node pledge. (received {received}, minimum {minimum})")]
InsufficientPledge { received: Coin, minimum: Coin }, InsufficientPledge { received: Coin, minimum: Coin },
#[error("Attempted to reduce node pledge ({current}{denom} - {decrease_by}{denom}) below the minimum amount: {minimum}{denom}")]
InvalidPledgeReduction {
current: Uint128,
decrease_by: Uint128,
minimum: Uint128,
denom: String,
},
#[error("A pledge change is already pending in this epoch. The event id: {pending_event_id}")]
PendingPledgeChange { pending_event_id: EpochEventId },
#[error("Not enough funds sent for node delegation. (received {received}, minimum {minimum})")] #[error("Not enough funds sent for node delegation. (received {received}, minimum {minimum})")]
InsufficientDelegation { received: Coin, minimum: Coin }, InsufficientDelegation { received: Coin, minimum: Coin },
@@ -190,6 +204,9 @@ pub enum MixnetContractError {
#[error("epoch duration must be > 0")] #[error("epoch duration must be > 0")]
EpochDurationZero, EpochDurationZero,
#[error("attempted to perform the operation with 0 coins. This is not allowed")]
ZeroCoinAmount,
#[error("this validator ({current_validator}) is not the one responsible for advancing this epoch. It's responsibility of {chosen_validator}.")] #[error("this validator ({current_validator}) is not the one responsible for advancing this epoch. It's responsibility of {chosen_validator}.")]
RewardingValidatorMismatch { RewardingValidatorMismatch {
current_validator: Addr, current_validator: Addr,
@@ -226,3 +243,11 @@ pub enum MixnetContractError {
source: ApiVerifierError, source: ApiVerifierError,
}, },
} }
impl MixnetContractError {
pub fn inconsistent_state<S: Into<String>>(comment: S) -> Self {
MixnetContractError::InconsistentState {
comment: comment.into(),
}
}
}
@@ -15,6 +15,8 @@ pub enum MixnetEventType {
MixnodeBonding, MixnodeBonding,
PendingPledgeIncrease, PendingPledgeIncrease,
PledgeIncrease, PledgeIncrease,
PendingPledgeDecrease,
PledgeDecrease,
GatewayBonding, GatewayBonding,
GatewayUnbonding, GatewayUnbonding,
PendingMixnodeUnbonding, PendingMixnodeUnbonding,
@@ -58,6 +60,8 @@ impl ToString for MixnetEventType {
MixnetEventType::MixnodeBonding => "mixnode_bonding", MixnetEventType::MixnodeBonding => "mixnode_bonding",
MixnetEventType::PendingPledgeIncrease => "pending_pledge_increase", MixnetEventType::PendingPledgeIncrease => "pending_pledge_increase",
MixnetEventType::PledgeIncrease => "pledge_increase", MixnetEventType::PledgeIncrease => "pledge_increase",
MixnetEventType::PendingPledgeDecrease => "pending_pledge_decrease",
MixnetEventType::PledgeDecrease => "pledge_decrease",
MixnetEventType::GatewayBonding => "gateway_bonding", MixnetEventType::GatewayBonding => "gateway_bonding",
MixnetEventType::GatewayUnbonding => "gateway_unbonding", MixnetEventType::GatewayUnbonding => "gateway_unbonding",
MixnetEventType::PendingMixnodeUnbonding => "pending_mixnode_unbonding", MixnetEventType::PendingMixnodeUnbonding => "pending_mixnode_unbonding",
@@ -354,6 +358,19 @@ pub fn new_pledge_increase_event(created_at: BlockHeight, mix_id: MixId, amount:
.add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(AMOUNT_KEY, amount.to_string())
} }
pub fn new_pending_pledge_decrease_event(mix_id: MixId, amount: &Coin) -> Event {
Event::new(MixnetEventType::PendingPledgeDecrease)
.add_attribute(MIX_ID_KEY, mix_id.to_string())
.add_attribute(AMOUNT_KEY, amount.to_string())
}
pub fn new_pledge_decrease_event(created_at: BlockHeight, mix_id: MixId, amount: &Coin) -> Event {
Event::new(MixnetEventType::PledgeDecrease)
.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 { pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Event {
Event::new(MixnetEventType::MixnodeUnbonding) Event::new(MixnetEventType::MixnodeUnbonding)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
@@ -3,7 +3,10 @@
use crate::error::MixnetContractError; use crate::error::MixnetContractError;
use crate::pending_events::{PendingEpochEvent, PendingIntervalEvent}; use crate::pending_events::{PendingEpochEvent, PendingIntervalEvent};
use crate::{EpochId, IntervalId, MixId}; use crate::{
EpochEventId, EpochId, IntervalEventId, IntervalId, MixId, PendingEpochEventData,
PendingIntervalEventData,
};
use cosmwasm_std::{Addr, Env}; use cosmwasm_std::{Addr, Env};
use schemars::gen::SchemaGenerator; use schemars::gen::SchemaGenerator;
use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::schema::{InstanceType, Schema, SchemaObject};
@@ -528,6 +531,30 @@ impl PendingIntervalEventsResponse {
} }
} }
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct PendingEpochEventResponse {
pub event_id: EpochEventId,
pub event: Option<PendingEpochEventData>,
}
impl PendingEpochEventResponse {
pub fn new(event_id: EpochEventId, event: Option<PendingEpochEventData>) -> Self {
PendingEpochEventResponse { event_id, event }
}
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct PendingIntervalEventResponse {
pub event_id: IntervalEventId,
pub event: Option<PendingIntervalEventData>,
}
impl PendingIntervalEventResponse {
pub fn new(event_id: IntervalEventId, event: Option<PendingIntervalEventData>) -> Self {
PendingIntervalEventResponse { event_id, event }
}
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct NumberOfPendingEventsResponse { pub struct NumberOfPendingEventsResponse {
pub epoch_events: u32, pub epoch_events: u32,
@@ -32,7 +32,8 @@ pub use gateway::{
}; };
pub use interval::{ pub use interval::{
CurrentIntervalResponse, EpochState, EpochStatus, Interval, NumberOfPendingEventsResponse, CurrentIntervalResponse, EpochState, EpochStatus, Interval, NumberOfPendingEventsResponse,
PendingEpochEventsResponse, PendingIntervalEventsResponse, PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEventResponse,
PendingIntervalEventsResponse,
}; };
pub use mixnode::{ pub use mixnode::{
Layer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeCostParams, MixNodeDetails, Layer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeCostParams, MixNodeDetails,
@@ -10,7 +10,7 @@ use crate::helpers::IntoBaseDecimal;
use crate::reward_params::{NodeRewardParams, RewardingParams}; use crate::reward_params::{NodeRewardParams, RewardingParams};
use crate::rewarding::helpers::truncate_reward; use crate::rewarding::helpers::truncate_reward;
use crate::rewarding::RewardDistribution; use crate::rewarding::RewardDistribution;
use crate::{Delegation, EpochId, IdentityKey, MixId, Percent, SphinxKey}; use crate::{Delegation, EpochEventId, EpochId, IdentityKey, MixId, Percent, SphinxKey};
use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128}; use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -37,13 +37,20 @@ impl RewardedSetNodeStatus {
pub struct MixNodeDetails { pub struct MixNodeDetails {
pub bond_information: MixNodeBond, pub bond_information: MixNodeBond,
pub rewarding_details: MixNodeRewarding, pub rewarding_details: MixNodeRewarding,
#[serde(default)]
pub pending_changes: PendingMixNodeChanges,
} }
impl MixNodeDetails { impl MixNodeDetails {
pub fn new(bond_information: MixNodeBond, rewarding_details: MixNodeRewarding) -> Self { pub fn new(
bond_information: MixNodeBond,
rewarding_details: MixNodeRewarding,
pending_changes: PendingMixNodeChanges,
) -> Self {
MixNodeDetails { MixNodeDetails {
bond_information, bond_information,
rewarding_details, rewarding_details,
pending_changes,
} }
} }
@@ -73,6 +80,10 @@ impl MixNodeDetails {
pub fn total_stake(&self) -> Decimal { pub fn total_stake(&self) -> Decimal {
self.rewarding_details.node_bond() self.rewarding_details.node_bond()
} }
pub fn pending_pledge_change(&self) -> Option<EpochEventId> {
self.pending_changes.pledge_change
}
} }
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
@@ -332,6 +343,22 @@ impl MixNodeRewarding {
Ok(()) Ok(())
} }
/// Decreases total pledge of operator by the specified amount.
pub fn decrease_operator_uint128(
&mut self,
amount: Uint128,
) -> Result<(), MixnetContractError> {
let amount_decimal = amount.into_base_decimal()?;
if self.operator < amount_decimal {
return Err(MixnetContractError::OverflowDecimalSubtraction {
minuend: self.operator,
subtrahend: amount_decimal,
});
}
self.operator -= amount_decimal;
Ok(())
}
pub fn increase_delegates_uint128( pub fn increase_delegates_uint128(
&mut self, &mut self,
amount: Uint128, amount: Uint128,
@@ -601,6 +628,25 @@ impl From<Layer> for u8 {
} }
} }
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(export_to = "ts-packages/types/src/types/rust/PendingMixnodeChanges.ts")
)]
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
pub struct PendingMixNodeChanges {
pub pledge_change: Option<EpochEventId>,
// pub cost_params_change: Option<IntervalEventId>,
}
impl PendingMixNodeChanges {
pub fn new_empty() -> PendingMixNodeChanges {
PendingMixNodeChanges {
pledge_change: None,
}
}
}
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr( #[cfg_attr(
feature = "generate-ts", feature = "generate-ts",
@@ -10,10 +10,13 @@ use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
use crate::reward_params::{ use crate::reward_params::{
IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams, IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams,
}; };
use crate::{delegation, ContractStateParams, Layer, LayerAssignment, MixId, Percent}; use crate::{
delegation, ContractStateParams, EpochEventId, IntervalEventId, Layer, LayerAssignment, MixId,
Percent,
};
use crate::{Gateway, IdentityKey, MixNode}; use crate::{Gateway, IdentityKey, MixNode};
use contracts_common::signing::MessageSignature; use contracts_common::signing::MessageSignature;
use cosmwasm_std::Decimal; use cosmwasm_std::{Coin, Decimal};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration; use std::time::Duration;
@@ -161,6 +164,13 @@ pub enum ExecuteMsg {
PledgeMoreOnBehalf { PledgeMoreOnBehalf {
owner: String, owner: String,
}, },
DecreasePledge {
decrease_by: Coin,
},
DecreasePledgeOnBehalf {
owner: String,
decrease_by: Coin,
},
UnbondMixnode {}, UnbondMixnode {},
UnbondMixnodeOnBehalf { UnbondMixnodeOnBehalf {
owner: String, owner: String,
@@ -297,6 +307,10 @@ impl ExecuteMsg {
} }
ExecuteMsg::PledgeMore {} => "pledging additional tokens".into(), ExecuteMsg::PledgeMore {} => "pledging additional tokens".into(),
ExecuteMsg::PledgeMoreOnBehalf { .. } => "pledging additional tokens on behalf".into(), ExecuteMsg::PledgeMoreOnBehalf { .. } => "pledging additional tokens on behalf".into(),
ExecuteMsg::DecreasePledge { .. } => "decreasing mixnode pledge".into(),
ExecuteMsg::DecreasePledgeOnBehalf { .. } => {
"decreasing mixnode pledge on behalf".into()
}
ExecuteMsg::UnbondMixnode { .. } => "unbonding mixnode".into(), ExecuteMsg::UnbondMixnode { .. } => "unbonding mixnode".into(),
ExecuteMsg::UnbondMixnodeOnBehalf { .. } => "unbonding mixnode on behalf".into(), ExecuteMsg::UnbondMixnodeOnBehalf { .. } => "unbonding mixnode on behalf".into(),
ExecuteMsg::UpdateMixnodeCostParams { .. } => "updating mixnode cost parameters".into(), ExecuteMsg::UpdateMixnodeCostParams { .. } => "updating mixnode cost parameters".into(),
@@ -506,6 +520,12 @@ pub enum QueryMsg {
limit: Option<u32>, limit: Option<u32>,
start_after: Option<u32>, start_after: Option<u32>,
}, },
GetPendingEpochEvent {
event_id: EpochEventId,
},
GetPendingIntervalEvent {
event_id: IntervalEventId,
},
GetNumberOfPendingEvents {}, GetNumberOfPendingEvents {},
// signing-related // signing-related
@@ -38,6 +38,10 @@ pub enum PendingEpochEventKind {
mix_id: MixId, mix_id: MixId,
amount: Coin, amount: Coin,
}, },
DecreasePledge {
mix_id: MixId,
decrease_by: Coin,
},
UnbondMixnode { UnbondMixnode {
mix_id: MixId, mix_id: MixId,
}, },
@@ -66,7 +70,7 @@ impl From<(EpochEventId, PendingEpochEventData)> for PendingEpochEvent {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct PendingIntervalEvent { pub struct PendingIntervalEvent {
pub id: EpochEventId, pub id: IntervalEventId,
pub event: PendingIntervalEventData, pub event: PendingIntervalEventData,
} }
@@ -15,6 +15,7 @@ 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_GATEWAY_UNBONDING_EVENT_TYPE: &str = "vesting_gateway_unbonding";
pub const VESTING_MIXNODE_BONDING_EVENT_TYPE: &str = "vesting_mixnode_bonding"; 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_PLEDGE_MORE_EVENT_TYPE: &str = "vesting_pledge_more";
pub const VESTING_DECREASE_PLEDGE_EVENT_TYPE: &str = "vesting_pledge_decrease";
pub const VESTING_MIXNODE_UNBONDING_EVENT_TYPE: &str = "vesting_mixnode_unbonding"; 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_CONFIG_EVENT_TYPE: &str = "vesting_update_mixnode_config";
pub const VESTING_UPDATE_GATEWAY_CONFIG_EVENT_TYPE: &str = "vesting_update_gateway_config"; pub const VESTING_UPDATE_GATEWAY_CONFIG_EVENT_TYPE: &str = "vesting_update_gateway_config";
@@ -22,6 +23,7 @@ pub const VESTING_UPDATE_MIXNODE_COST_PARAMS_EVENT_TYPE: &str =
"vesting_update_mixnode_cost_params"; "vesting_update_mixnode_cost_params";
pub const TRACK_MIXNODE_UNBOND_EVENT_TYPE: &str = "track_mixnode_unbond"; pub const TRACK_MIXNODE_UNBOND_EVENT_TYPE: &str = "track_mixnode_unbond";
pub const TRACK_MIXNODE_PLEDGE_DECREASE_EVENT_TYPE: &str = "track_mixnode_pledge_decrease";
pub const TRACK_GATEWAY_UNBOND_EVENT_TYPE: &str = "track_gateway_unbond"; pub const TRACK_GATEWAY_UNBOND_EVENT_TYPE: &str = "track_gateway_unbond";
pub const TRACK_UNDELEGATION_EVENT_TYPE: &str = "track_undelegation"; pub const TRACK_UNDELEGATION_EVENT_TYPE: &str = "track_undelegation";
pub const TRACK_REWARD_EVENT_TYPE: &str = "track_reaward"; pub const TRACK_REWARD_EVENT_TYPE: &str = "track_reaward";
@@ -118,6 +120,10 @@ pub fn new_vesting_pledge_more_event() -> Event {
Event::new(VESTING_PLEDGE_MORE_EVENT_TYPE) Event::new(VESTING_PLEDGE_MORE_EVENT_TYPE)
} }
pub fn new_vesting_decrease_pledge_event() -> Event {
Event::new(VESTING_DECREASE_PLEDGE_EVENT_TYPE)
}
pub fn new_vesting_update_mixnode_config_event() -> Event { pub fn new_vesting_update_mixnode_config_event() -> Event {
Event::new(VESTING_UPDATE_MIXNODE_CONFIG_EVENT_TYPE) Event::new(VESTING_UPDATE_MIXNODE_CONFIG_EVENT_TYPE)
} }
@@ -146,6 +152,10 @@ pub fn new_track_mixnode_unbond_event() -> Event {
Event::new(TRACK_MIXNODE_UNBOND_EVENT_TYPE) Event::new(TRACK_MIXNODE_UNBOND_EVENT_TYPE)
} }
pub fn new_track_mixnode_pledge_decrease_event() -> Event {
Event::new(TRACK_MIXNODE_PLEDGE_DECREASE_EVENT_TYPE)
}
pub fn new_track_gateway_unbond_event() -> Event { pub fn new_track_gateway_unbond_event() -> Event {
Event::new(TRACK_GATEWAY_UNBOND_EVENT_TYPE) Event::new(TRACK_GATEWAY_UNBOND_EVENT_TYPE)
} }
@@ -123,11 +123,18 @@ pub enum ExecuteMsg {
PledgeMore { PledgeMore {
amount: Coin, amount: Coin,
}, },
DecreasePledge {
amount: Coin,
},
UnbondMixnode {}, UnbondMixnode {},
TrackUnbondMixnode { TrackUnbondMixnode {
owner: String, owner: String,
amount: Coin, amount: Coin,
}, },
TrackDecreasePledge {
owner: String,
amount: Coin,
},
BondGateway { BondGateway {
gateway: Gateway, gateway: Gateway,
owner_signature: MessageSignature, owner_signature: MessageSignature,
@@ -175,8 +182,10 @@ impl ExecuteMsg {
ExecuteMsg::TrackUndelegation { .. } => "VestingExecuteMsg::TrackUndelegation", ExecuteMsg::TrackUndelegation { .. } => "VestingExecuteMsg::TrackUndelegation",
ExecuteMsg::BondMixnode { .. } => "VestingExecuteMsg::BondMixnode", ExecuteMsg::BondMixnode { .. } => "VestingExecuteMsg::BondMixnode",
ExecuteMsg::PledgeMore { .. } => "VestingExecuteMsg::PledgeMore", ExecuteMsg::PledgeMore { .. } => "VestingExecuteMsg::PledgeMore",
ExecuteMsg::DecreasePledge { .. } => "VestingExecuteMsg::DecreasePledge",
ExecuteMsg::UnbondMixnode { .. } => "VestingExecuteMsg::UnbondMixnode", ExecuteMsg::UnbondMixnode { .. } => "VestingExecuteMsg::UnbondMixnode",
ExecuteMsg::TrackUnbondMixnode { .. } => "VestingExecuteMsg::TrackUnbondMixnode", ExecuteMsg::TrackUnbondMixnode { .. } => "VestingExecuteMsg::TrackUnbondMixnode",
ExecuteMsg::TrackDecreasePledge { .. } => "VestingExecuteMsg::TrackDecreasePledge",
ExecuteMsg::BondGateway { .. } => "VestingExecuteMsg::BondGateway", ExecuteMsg::BondGateway { .. } => "VestingExecuteMsg::BondGateway",
ExecuteMsg::UnbondGateway { .. } => "VestingExecuteMsg::UnbondGateway", ExecuteMsg::UnbondGateway { .. } => "VestingExecuteMsg::UnbondGateway",
ExecuteMsg::TrackUnbondGateway { .. } => "VestingExecuteMsg::TrackUnbondGateway", ExecuteMsg::TrackUnbondGateway { .. } => "VestingExecuteMsg::TrackUnbondGateway",
+11
View File
@@ -61,6 +61,10 @@ pub enum PendingEpochEventData {
mix_id: MixId, mix_id: MixId,
amount: DecCoin, amount: DecCoin,
}, },
DecreasePledge {
mix_id: MixId,
decrease_by: DecCoin,
},
UnbondMixnode { UnbondMixnode {
mix_id: MixId, mix_id: MixId,
}, },
@@ -101,6 +105,13 @@ impl PendingEpochEventData {
amount: reg.attempt_convert_to_display_dec_coin(amount.into())?, amount: reg.attempt_convert_to_display_dec_coin(amount.into())?,
}) })
} }
MixnetContractPendingEpochEventKind::DecreasePledge {
mix_id,
decrease_by,
} => Ok(PendingEpochEventData::DecreasePledge {
mix_id,
decrease_by: reg.attempt_convert_to_display_dec_coin(decrease_by.into())?,
}),
MixnetContractPendingEpochEventKind::UnbondMixnode { mix_id } => { MixnetContractPendingEpochEventKind::UnbondMixnode { mix_id } => {
Ok(PendingEpochEventData::UnbondMixnode { mix_id }) Ok(PendingEpochEventData::UnbondMixnode { mix_id })
} }
+6
View File
@@ -0,0 +1,6 @@
[alias]
wasm = "build --target wasm32-unknown-unknown"
[build]
rustflags = ["-C", "link-arg=-s"]
#target = "wasm32-unknown-unknown"
+16
View File
@@ -949,6 +949,22 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "mixnet-vesting-integration-tests"
version = "0.1.0"
dependencies = [
"cosmwasm-std",
"cosmwasm-storage",
"cw-multi-test",
"nym-contracts-common",
"nym-crypto",
"nym-mixnet-contract",
"nym-mixnet-contract-common",
"nym-vesting-contract",
"nym-vesting-contract-common",
"rand_chacha 0.2.2",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.15" version = "0.2.15"
+1
View File
@@ -4,6 +4,7 @@ members = [
"coconut-dkg", "coconut-dkg",
"coconut-test", "coconut-test",
"mixnet", "mixnet",
"mixnet-vesting-integration-tests",
"multisig/cw3-flex-multisig", "multisig/cw3-flex-multisig",
"multisig/cw4-group", "multisig/cw4-group",
"service-provider-directory", "service-provider-directory",
+2 -2
View File
@@ -86,9 +86,9 @@ mod tests {
assert!(res.is_none()); assert!(res.is_none());
let mut spend_credential = SpendCredential::new( let mut spend_credential = SpendCredential::new(
funds.clone(), funds,
blind_serial_number.to_string(), blind_serial_number.to_string(),
gateway_cosmos_address.clone(), gateway_cosmos_address,
); );
spend_credential.mark_as_spent(); spend_credential.mark_as_spent();
@@ -20,6 +20,6 @@ pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>>
}; };
let env = mock_env(); let env = mock_env();
let info = mock_info("creator", &[]); let info = mock_info("creator", &[]);
instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); instantiate(deps.as_mut(), env, info, msg).unwrap();
deps deps
} }
@@ -173,7 +173,7 @@ mod tests {
); );
let info = mock_info("requester", &[coin]); let info = mock_info("requester", &[coin]);
let tx = deposit_funds(deps.as_mut(), env.clone(), info, data).unwrap(); let tx = deposit_funds(deps.as_mut(), env, info, data).unwrap();
let events: Vec<_> = tx let events: Vec<_> = tx
.events .events
@@ -246,13 +246,8 @@ mod tests {
deps.querier deps.querier
.update_balance(env.contract.address.clone(), vec![funds.clone()]); .update_balance(env.contract.address.clone(), vec![funds.clone()]);
let err = release_funds( let err =
deps.as_mut(), release_funds(deps.as_mut(), env, mock_info(invalid_admin, &[]), funds).unwrap_err();
env.clone(),
mock_info(invalid_admin, &[]),
funds.clone(),
)
.unwrap_err();
assert_eq!(err, ContractError::Admin(AdminError::NotAdmin {})); assert_eq!(err, ContractError::Admin(AdminError::NotAdmin {}));
} }
@@ -294,7 +289,7 @@ mod tests {
{ {
assert_eq!(contract_addr, MULTISIG_CONTRACT); assert_eq!(contract_addr, MULTISIG_CONTRACT);
assert!(funds.is_empty()); assert!(funds.is_empty());
let multisig_msg: MultisigExecuteMsg = from_binary(&msg).unwrap(); let multisig_msg: MultisigExecuteMsg = from_binary(msg).unwrap();
if let MultisigExecuteMsg::Propose { if let MultisigExecuteMsg::Propose {
title: _, title: _,
description, description,
@@ -312,7 +307,7 @@ mod tests {
{ {
assert_eq!(*contract_addr, env.contract.address.into_string()); assert_eq!(*contract_addr, env.contract.address.into_string());
assert!(funds.is_empty()); assert!(funds.is_empty());
let release_funds_req: ExecuteMsg = from_binary(&msg).unwrap(); let release_funds_req: ExecuteMsg = from_binary(msg).unwrap();
if let ExecuteMsg::ReleaseFunds { funds } = release_funds_req { if let ExecuteMsg::ReleaseFunds { funds } = release_funds_req {
assert_eq!(funds, *data.funds()); assert_eq!(funds, *data.funds());
} else { } else {
+5 -5
View File
@@ -216,7 +216,7 @@ mod tests {
}; };
let info = mock_info("creator", &[]); let info = mock_info("creator", &[]);
let res = instantiate(deps.as_mut(), env.clone(), info, msg); let res = instantiate(deps.as_mut(), env, info, msg);
assert!(res.is_ok()) assert!(res.is_ok())
} }
@@ -245,7 +245,7 @@ mod tests {
announce_address: "127.0.0.1:8000".to_string(), announce_address: "127.0.0.1:8000".to_string(),
resharing: false, resharing: false,
}, },
&vec![], &[],
) )
.unwrap(); .unwrap();
assert_eq!(parse_node_index(res), (idx + 1) as u64); assert_eq!(parse_node_index(res), (idx + 1) as u64);
@@ -259,7 +259,7 @@ mod tests {
announce_address: "127.0.0.1:8000".to_string(), announce_address: "127.0.0.1:8000".to_string(),
resharing: false, resharing: false,
}, },
&vec![], &[],
) )
.unwrap_err(); .unwrap_err();
assert_eq!(ContractError::AlreadyADealer, err.downcast().unwrap()); assert_eq!(ContractError::AlreadyADealer, err.downcast().unwrap());
@@ -269,13 +269,13 @@ mod tests {
let err = app let err = app
.execute_contract( .execute_contract(
unauthorized_member, unauthorized_member,
coconut_dkg_contract_addr.clone(), coconut_dkg_contract_addr,
&RegisterDealer { &RegisterDealer {
bte_key_with_proof: "bte_key_with_proof".to_string(), bte_key_with_proof: "bte_key_with_proof".to_string(),
announce_address: "127.0.0.1:8000".to_string(), announce_address: "127.0.0.1:8000".to_string(),
resharing: false, resharing: false,
}, },
&vec![], &[],
) )
.unwrap_err(); .unwrap_err();
assert_eq!(ContractError::Unauthorized, err.downcast().unwrap()); assert_eq!(ContractError::Unauthorized, err.downcast().unwrap());
@@ -153,9 +153,9 @@ pub(crate) mod tests {
let ret = try_add_dealer( let ret = try_add_dealer(
deps.as_mut(), deps.as_mut(),
info.clone(), info,
bte_key_with_proof.clone(), bte_key_with_proof,
announce_address.clone(), announce_address,
false, false,
) )
.unwrap_err(); .unwrap_err();
@@ -56,11 +56,11 @@ pub(crate) mod tests {
for n in 0..size { for n in 0..size {
let dealing_share = dealing_bytes_fixture(); let dealing_share = dealing_bytes_fixture();
let sender = Addr::unchecked(format!("owner{}", n)); let sender = Addr::unchecked(format!("owner{}", n));
for idx in 0..TOTAL_DEALINGS { (0..TOTAL_DEALINGS).for_each(|idx| {
DEALINGS_BYTES[idx] DEALINGS_BYTES[idx]
.save(deps.storage, &sender, &dealing_share) .save(deps.storage, &sender, &dealing_share)
.unwrap(); .unwrap();
} });
} }
} }
@@ -131,8 +131,7 @@ pub(crate) mod tests {
assert!(ret.is_ok()); assert!(ret.is_ok());
assert!(dealings.has(deps.as_mut().storage, &owner)); assert!(dealings.has(deps.as_mut().storage, &owner));
} }
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), true) let ret = try_commit_dealings(deps.as_mut(), info, dealing_bytes, true).unwrap_err();
.unwrap_err();
assert_eq!( assert_eq!(
ret, ret,
ContractError::AlreadyCommitted { ContractError::AlreadyCommitted {
@@ -236,10 +236,10 @@ pub(crate) mod tests {
let limit = *limits.next().unwrap(); let limit = *limits.next().unwrap();
{ {
let mut group_members = GROUP_MEMBERS.lock().unwrap(); let mut group_members = GROUP_MEMBERS.lock().unwrap();
for i in 0..n as usize { for dealer in dealers.iter() {
group_members.push(( group_members.push((
Member { Member {
addr: dealers[i].address.to_string(), addr: dealer.address.to_string(),
weight: 10, weight: 10,
}, },
1, 1,
@@ -339,7 +339,7 @@ pub(crate) mod tests {
) )
.unwrap() .unwrap()
); );
for i in 0..3 as u64 { for i in 0..3_u64 {
let details = dealer_details_fixture(i + 1); let details = dealer_details_fixture(i + 1);
current_dealers() current_dealers()
.save(deps.as_mut().storage, &details.address, &details) .save(deps.as_mut().storage, &details.address, &details)
@@ -90,6 +90,6 @@ pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>>
}; };
let env = mock_env(); let env = mock_env();
let info = mock_info(ADMIN_ADDRESS, &[]); let info = mock_info(ADMIN_ADDRESS, &[]);
instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); instantiate(deps.as_mut(), env, info, msg).unwrap();
deps deps
} }
@@ -129,14 +129,8 @@ mod tests {
.save(deps.as_mut().storage, &dealer, &dealer_details) .save(deps.as_mut().storage, &dealer, &dealer_details)
.unwrap(); .unwrap();
try_commit_verification_key_share( try_commit_verification_key_share(deps.as_mut(), env, info.clone(), share.clone(), false)
deps.as_mut(), .unwrap();
env.clone(),
info.clone(),
share.clone(),
false,
)
.unwrap();
let vk_share = vk_shares().load(&deps.storage, (&info.sender, 0)).unwrap(); let vk_share = vk_shares().load(&deps.storage, (&info.sender, 0)).unwrap();
assert_eq!( assert_eq!(
vk_share, vk_share,
@@ -215,14 +209,8 @@ mod tests {
) )
.unwrap(); .unwrap();
let ret = try_commit_verification_key_share( let ret =
deps.as_mut(), try_commit_verification_key_share(deps.as_mut(), env, info, share, false).unwrap_err();
env.clone(),
info.clone(),
share.clone(),
false,
)
.unwrap_err();
assert_eq!( assert_eq!(
ret, ret,
ContractError::AlreadyCommitted { ContractError::AlreadyCommitted {
@@ -318,14 +306,7 @@ mod tests {
dealers_storage::current_dealers() dealers_storage::current_dealers()
.save(deps.as_mut().storage, &owner, &dealer_details) .save(deps.as_mut().storage, &owner, &dealer_details)
.unwrap(); .unwrap();
try_commit_verification_key_share( try_commit_verification_key_share(deps.as_mut(), env.clone(), info, share, false).unwrap();
deps.as_mut(),
env.clone(),
info.clone(),
share.clone(),
false,
)
.unwrap();
env.block.time = env env.block.time = env
.block .block
@@ -338,7 +319,6 @@ mod tests {
.plus_seconds(TimeConfiguration::default().verification_key_validation_time_secs); .plus_seconds(TimeConfiguration::default().verification_key_validation_time_secs);
advance_epoch_state(deps.as_mut(), env).unwrap(); advance_epoch_state(deps.as_mut(), env).unwrap();
try_verify_verification_key_share(deps.as_mut(), multisig_info, owner.clone(), false) try_verify_verification_key_share(deps.as_mut(), multisig_info, owner, false).unwrap();
.unwrap();
} }
} }
@@ -89,13 +89,8 @@ fn deposit_and_release() {
let msg = ExecuteMsg::ReleaseFunds { let msg = ExecuteMsg::ReleaseFunds {
funds: deposit_funds[0].clone(), funds: deposit_funds[0].clone(),
}; };
app.execute_contract( app.execute_contract(Addr::unchecked(multisig_addr), contract_addr, &msg, &[])
Addr::unchecked(multisig_addr), .unwrap();
contract_addr.clone(),
&msg,
&[],
)
.unwrap();
let pool_bal = app.wrap().query_balance(pool_addr, TEST_MIX_DENOM).unwrap(); let pool_bal = app.wrap().query_balance(pool_addr, TEST_MIX_DENOM).unwrap();
assert_eq!(pool_bal, deposit_funds[0]); assert_eq!(pool_bal, deposit_funds[0]);
} }
@@ -101,7 +101,7 @@ fn spend_credential_creates_proposal() {
Addr::unchecked(OWNER), Addr::unchecked(OWNER),
coconut_bandwidth_contract_addr.clone(), coconut_bandwidth_contract_addr.clone(),
&msg, &msg,
&vec![], &[],
) )
.unwrap(); .unwrap();
let proposal_id = res let proposal_id = res
@@ -124,7 +124,7 @@ fn spend_credential_creates_proposal() {
Addr::unchecked(OWNER), Addr::unchecked(OWNER),
coconut_bandwidth_contract_addr.clone(), coconut_bandwidth_contract_addr.clone(),
&msg, &msg,
&vec![], &[],
) )
.unwrap_err(); .unwrap_err();
assert_eq!( assert_eq!(
@@ -142,9 +142,9 @@ fn spend_credential_creates_proposal() {
let res = app let res = app
.execute_contract( .execute_contract(
Addr::unchecked(OWNER), Addr::unchecked(OWNER),
coconut_bandwidth_contract_addr.clone(), coconut_bandwidth_contract_addr,
&msg, &msg,
&vec![], &[],
) )
.unwrap(); .unwrap();
let proposal_id = res let proposal_id = res
@@ -105,7 +105,7 @@ fn dkg_proposal() {
announce_address: "127.0.0.1:8000".to_string(), announce_address: "127.0.0.1:8000".to_string(),
resharing: false, resharing: false,
}, },
&vec![], &[],
) )
.unwrap(); .unwrap();
@@ -115,7 +115,7 @@ fn dkg_proposal() {
Addr::unchecked(OWNER), Addr::unchecked(OWNER),
coconut_dkg_contract_addr.clone(), coconut_dkg_contract_addr.clone(),
&AdvanceEpochState {}, &AdvanceEpochState {},
&vec![], &[],
) )
.unwrap(); .unwrap();
} }
@@ -132,7 +132,7 @@ fn dkg_proposal() {
Addr::unchecked(MEMBER1), Addr::unchecked(MEMBER1),
coconut_dkg_contract_addr.clone(), coconut_dkg_contract_addr.clone(),
&msg, &msg,
&vec![], &[],
) )
.unwrap(); .unwrap();
@@ -174,7 +174,7 @@ fn dkg_proposal() {
proposal_id, proposal_id,
vote: cw3::Vote::Yes, vote: cw3::Vote::Yes,
}, },
&vec![], &[],
) )
.unwrap(); .unwrap();
@@ -184,16 +184,16 @@ fn dkg_proposal() {
Addr::unchecked(OWNER), Addr::unchecked(OWNER),
coconut_dkg_contract_addr.clone(), coconut_dkg_contract_addr.clone(),
&AdvanceEpochState {}, &AdvanceEpochState {},
&vec![], &[],
) )
.unwrap(); .unwrap();
} }
app.execute_contract( app.execute_contract(
Addr::unchecked(MEMBER1), Addr::unchecked(MEMBER1),
multisig_contract_addr.clone(), multisig_contract_addr,
&Execute { proposal_id }, &Execute { proposal_id },
&vec![], &[],
) )
.unwrap(); .unwrap();
@@ -0,0 +1,32 @@
[package]
name = "mixnet-vesting-integration-tests"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# cosmwasm dependencies
cosmwasm-std = { workspace = true }
cosmwasm-storage = { workspace = true }
cw-multi-test = { workspace = true }
# contracts dependencies
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" }
nym-vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
nym-mixnet-contract = { path = "../mixnet" }
nym-vesting-contract = { path = "../vesting" }
# other local dependencies
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
# external dependencies
rand_chacha = "0.2"
[[test]]
name = "mixnet-vesting-test"
path = "src/tests.rs"
@@ -0,0 +1,240 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::support::helpers::{mix_coin, mix_coins, vesting_owner};
use crate::support::setup::{TestSetup, MIX_DENOM};
use cosmwasm_std::Addr;
use cw_multi_test::Executor;
use nym_contracts_common::Percent;
use nym_mixnet_contract_common::error::MixnetContractError;
use nym_mixnet_contract_common::{ContractStateParams, MixNodeCostParams};
use nym_mixnet_contract_common::{MixOwnershipResponse, QueryMsg as MixnetQueryMsg};
use nym_vesting_contract_common::ExecuteMsg as VestingExecuteMsg;
use vesting_contract::errors::ContractError as VestingContractError;
#[test]
fn decrease_mixnode_pledge_from_vesting_account_with_minimum_pledge() {
let mut test = TestSetup::new_simple();
let vesting_account = "vesting-account";
// 0. get the minimum pledge amount
let state_params: ContractStateParams = test
.app
.wrap()
.query_wasm_smart(test.mixnet_contract(), &MixnetQueryMsg::GetStateParams {})
.unwrap();
let minimum_pledge = state_params.minimum_mixnode_pledge;
// 1. create vesting account
let create_msg = VestingExecuteMsg::CreateAccount {
owner_address: vesting_account.to_string(),
staking_address: None,
vesting_spec: None,
cap: None,
};
test.app
.execute_contract(
vesting_owner(),
test.vesting_contract(),
&create_msg,
&mix_coins(1_000_000_000),
)
.unwrap();
// 2. bond mixnode with the vesting account
let pledge = minimum_pledge.clone();
let cost_params = MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: mix_coin(40_000_000),
};
let (mix_node, owner_signature) = test.valid_mixnode_with_sig(
vesting_account,
Some(test.vesting_contract()),
cost_params.clone(),
pledge.clone(),
);
let bond_msg = VestingExecuteMsg::BondMixnode {
mix_node,
cost_params,
owner_signature,
amount: pledge.clone(),
};
test.app
.execute_contract(
Addr::unchecked(vesting_account),
test.vesting_contract(),
&bond_msg,
&[],
)
.unwrap();
// 3. try to decrease the pledge
// trying to decrease by a zero amount - not valid
let decrease_pledge_msg = VestingExecuteMsg::DecreasePledge {
amount: mix_coin(0),
};
let res_zero = test
.app
.execute_contract(
Addr::unchecked(vesting_account),
test.vesting_contract(),
&decrease_pledge_msg,
&[],
)
.unwrap_err();
assert_eq!(
VestingContractError::EmptyFunds,
res_zero.downcast().unwrap()
);
// trying to go below the cap - also not valid
let amount = mix_coin(50_000);
let decrease_pledge_msg = VestingExecuteMsg::DecreasePledge {
amount: amount.clone(),
};
let res_below = test
.app
.execute_contract(
Addr::unchecked(vesting_account),
test.vesting_contract(),
&decrease_pledge_msg,
&[],
)
.unwrap_err();
assert_eq!(
MixnetContractError::InvalidPledgeReduction {
current: pledge.amount,
decrease_by: amount.amount,
minimum: minimum_pledge.amount,
denom: minimum_pledge.denom
},
res_below.downcast().unwrap()
)
}
#[test]
fn decrease_mixnode_pledge_from_vesting_account_with_sufficient_pledge() {
let mut test = TestSetup::new_simple();
let vesting_account = "vesting-account";
// 1. create vesting account
let create_msg = VestingExecuteMsg::CreateAccount {
owner_address: vesting_account.to_string(),
staking_address: None,
vesting_spec: None,
cap: None,
};
test.app
.execute_contract(
vesting_owner(),
test.vesting_contract(),
&create_msg,
&mix_coins(10_000_000_000),
)
.unwrap();
// 2. bond mixnode with the vesting account
let pledge = mix_coin(150_000_000);
let cost_params = MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: mix_coin(40_000_000),
};
let (mix_node, owner_signature) = test.valid_mixnode_with_sig(
vesting_account,
Some(test.vesting_contract()),
cost_params.clone(),
pledge.clone(),
);
let bond_msg = VestingExecuteMsg::BondMixnode {
mix_node,
cost_params,
owner_signature,
amount: pledge,
};
test.app
.execute_contract(
Addr::unchecked(vesting_account),
test.vesting_contract(),
&bond_msg,
&[],
)
.unwrap();
// 3. try to decrease the pledge
let before: MixOwnershipResponse = test
.app
.wrap()
.query_wasm_smart(
test.mixnet_contract(),
&MixnetQueryMsg::GetOwnedMixnode {
address: vesting_account.to_string(),
},
)
.unwrap();
let balance_before = test
.app
.wrap()
.query_balance(test.vesting_contract(), MIX_DENOM)
.unwrap();
assert_eq!(balance_before.amount.u128(), 9_850_000_000);
let decrease_pledge_msg = VestingExecuteMsg::DecreasePledge {
amount: mix_coin(50_000_000),
};
test.app
.execute_contract(
Addr::unchecked(vesting_account),
test.vesting_contract(),
&decrease_pledge_msg,
&[],
)
.unwrap();
let after_decrease: MixOwnershipResponse = test
.app
.wrap()
.query_wasm_smart(
test.mixnet_contract(),
&MixnetQueryMsg::GetOwnedMixnode {
address: vesting_account.to_string(),
},
)
.unwrap();
// note: nothing has changed with the pledge because the event hasn't been resolved yet!
assert_eq!(before.address, after_decrease.address);
let before_details = before.mixnode_details.unwrap();
let after_details = after_decrease.mixnode_details.unwrap();
assert_eq!(
before_details.rewarding_details,
after_details.rewarding_details
);
assert_eq!(
before_details.bond_information,
after_details.bond_information
);
// but we have the pending change saved now!
assert!(before_details.pending_changes.pledge_change.is_none());
assert_eq!(Some(1), after_details.pending_changes.pledge_change);
// 4. resolve events
test.advance_mixnet_epoch();
let balance_after = test
.app
.wrap()
.query_balance(test.vesting_contract(), MIX_DENOM)
.unwrap();
assert_eq!(balance_after.amount.u128(), 9_900_000_000);
}
@@ -0,0 +1,28 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::support::setup::{MIX_DENOM, REWARDING_VALIDATOR};
use cosmwasm_std::Decimal;
use nym_contracts_common::Percent;
use nym_mixnet_contract_common::InitialRewardingParams;
use std::time::Duration;
pub fn default_mixnet_init_msg() -> nym_mixnet_contract_common::InstantiateMsg {
nym_mixnet_contract_common::InstantiateMsg {
rewarding_validator_address: REWARDING_VALIDATOR.to_string(),
vesting_contract_address: "placeholder".to_string(),
rewarding_denom: MIX_DENOM.to_string(),
epochs_in_interval: 720,
epoch_duration: Duration::from_secs(60 * 60),
initial_rewarding_params: InitialRewardingParams {
initial_reward_pool: Decimal::from_atomics(250_000_000_000_000u128, 0).unwrap(),
initial_staking_supply: Decimal::from_atomics(223_000_000_000_000u128, 0).unwrap(),
staking_supply_scale_factor: Percent::hundred(),
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
rewarded_set_size: 240,
active_set_size: 100,
},
}
}
@@ -0,0 +1,56 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::support::setup::{MIXNET_OWNER, MIX_DENOM, REWARDING_VALIDATOR, VESTING_OWNER};
use cosmwasm_std::{coin, coins, Addr, Coin, Empty};
use cw_multi_test::{Contract, ContractWrapper};
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
#[allow(unused)]
pub fn mixnet_owner() -> Addr {
Addr::unchecked(MIXNET_OWNER)
}
pub fn vesting_owner() -> Addr {
Addr::unchecked(VESTING_OWNER)
}
pub fn rewarding_validator() -> Addr {
Addr::unchecked(REWARDING_VALIDATOR)
}
pub fn mix_coins(amount: u128) -> Vec<Coin> {
coins(amount, MIX_DENOM)
}
pub fn mix_coin(amount: u128) -> Coin {
coin(amount, MIX_DENOM)
}
pub fn test_rng() -> ChaCha20Rng {
let dummy_seed = [42u8; 32];
ChaCha20Rng::from_seed(dummy_seed)
}
pub fn mixnet_contract_wrapper() -> Box<dyn Contract<Empty>> {
Box::new(
ContractWrapper::new(
mixnet_contract::contract::execute,
mixnet_contract::contract::instantiate,
mixnet_contract::contract::query,
)
.with_migrate(mixnet_contract::contract::migrate),
)
}
pub fn vesting_contract_wrapper() -> Box<dyn Contract<Empty>> {
Box::new(
ContractWrapper::new(
vesting_contract::contract::execute,
vesting_contract::contract::instantiate,
vesting_contract::contract::query,
)
.with_migrate(vesting_contract::contract::migrate),
)
}
@@ -0,0 +1,6 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod fixtures;
pub mod helpers;
pub mod setup;
@@ -0,0 +1,328 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::support::fixtures;
use crate::support::helpers::{
mixnet_contract_wrapper, rewarding_validator, test_rng, vesting_contract_wrapper,
};
use cosmwasm_std::{coins, Addr, Coin, Timestamp};
use cw_multi_test::{App, AppBuilder, Executor};
use nym_contracts_common::signing::{ContractMessageContent, MessageSignature, Nonce};
use nym_crypto::asymmetric::identity;
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::{
CurrentIntervalResponse, LayerAssignment, MixNodeCostParams, MixnodeBondingPayload,
PagedRewardedSetResponse, RewardingParams, SignableMixNodeBondingMsg,
};
use nym_mixnet_contract_common::{
ExecuteMsg as MixnetExecuteMsg, MixNode, QueryMsg as MixnetQueryMsg,
};
use rand_chacha::ChaCha20Rng;
use std::collections::HashMap;
// our global accounts that should always get some coins at the start
pub const MIXNET_OWNER: &str = "mixnet-owner";
pub const VESTING_OWNER: &str = "vesting-owner";
pub const REWARDING_VALIDATOR: &str = "rewarding-validator";
pub const MIX_DENOM: &str = "unym";
pub struct ContractInstantiationResult {
mixnet_contract_address: Addr,
vesting_contract_address: Addr,
}
#[allow(unused)]
pub struct TestSetupBuilder {
mixnet_init_msg: nym_mixnet_contract_common::InstantiateMsg,
initial_balances: HashMap<Addr, Vec<Coin>>,
}
#[allow(unused)]
impl TestSetupBuilder {
pub fn new() -> Self {
TestSetupBuilder {
mixnet_init_msg: fixtures::default_mixnet_init_msg(),
initial_balances: Default::default(),
}
}
pub fn with_mixnet_init_msg(
mut self,
mixnet_init_msg: nym_mixnet_contract_common::InstantiateMsg,
) -> Self {
self.mixnet_init_msg = mixnet_init_msg;
self
}
pub fn with_initial_balances(mut self, initial_balances: HashMap<Addr, Vec<Coin>>) -> Self {
self.initial_balances = initial_balances;
self
}
pub fn with_initial_balance(mut self, addr: impl Into<String>, balance: Vec<Coin>) -> Self {
self.initial_balances.insert(Addr::unchecked(addr), balance);
self
}
pub fn build(self) -> TestSetup {
TestSetup::new(self.initial_balances, self.mixnet_init_msg)
}
}
pub struct TestSetup {
pub app: App,
pub rng: ChaCha20Rng,
pub mixnet_contract: Addr,
pub vesting_contract: Addr,
}
impl TestSetup {
pub fn new_simple() -> Self {
TestSetup::new(Default::default(), fixtures::default_mixnet_init_msg())
}
pub fn new(
initial_balances: HashMap<Addr, Vec<Coin>>,
custom_mixnet_init: nym_mixnet_contract_common::InstantiateMsg,
) -> Self {
let (app, contracts) = instantiate_contracts(initial_balances, Some(custom_mixnet_init));
TestSetup {
app,
rng: test_rng(),
mixnet_contract: contracts.mixnet_contract_address,
vesting_contract: contracts.vesting_contract_address,
}
}
pub fn mixnet_contract(&self) -> Addr {
self.mixnet_contract.clone()
}
pub fn vesting_contract(&self) -> Addr {
self.vesting_contract.clone()
}
pub fn skip_to_current_epoch_end(&mut self) {
let current_interval: CurrentIntervalResponse = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetCurrentIntervalDetails {},
)
.unwrap();
let epoch_end = current_interval.interval.current_epoch_end_unix_timestamp();
self.app.update_block(|current_block| {
// skip few blocks just in case
current_block.height += 10;
current_block.time = Timestamp::from_seconds(epoch_end as u64)
})
}
pub fn full_mixnet_epoch_operations(&mut self) {
let current_rewarded_set: PagedRewardedSetResponse = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetRewardedSet {
limit: Some(9999),
start_after: None,
},
)
.unwrap();
let current_params: RewardingParams = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetRewardingParams {},
)
.unwrap();
// TODO: handle paging
// begin epoch transition
self.app
.execute_contract(
rewarding_validator(),
self.mixnet_contract(),
&MixnetExecuteMsg::BeginEpochTransition {},
&[],
)
.unwrap();
// reward
for (mix_id, _status) in &current_rewarded_set.nodes {
self.app
.execute_contract(
rewarding_validator(),
self.mixnet_contract(),
&MixnetExecuteMsg::RewardMixnode {
mix_id: *mix_id,
performance: Performance::hundred(),
},
&[],
)
.unwrap();
}
// events
self.app
.execute_contract(
rewarding_validator(),
self.mixnet_contract(),
&MixnetExecuteMsg::ReconcileEpochEvents { limit: None },
&[],
)
.unwrap();
// don't bother changing the active set, use the same node for update and advance
let new_rewarded_set = current_rewarded_set
.nodes
.into_iter()
.enumerate()
.map(|(i, (node, _))| {
LayerAssignment::new(node, ((i as u8 % 3) + 1).try_into().unwrap())
})
.collect();
self.app
.execute_contract(
rewarding_validator(),
self.mixnet_contract(),
&MixnetExecuteMsg::AdvanceCurrentEpoch {
new_rewarded_set,
expected_active_set_size: current_params.active_set_size,
},
&[],
)
.unwrap();
}
pub fn advance_mixnet_epoch(&mut self) {
self.skip_to_current_epoch_end();
self.full_mixnet_epoch_operations();
}
pub fn valid_mixnode_with_sig(
&mut self,
owner: &str,
proxy: Option<Addr>,
cost_params: MixNodeCostParams,
stake: Coin,
) -> (MixNode, MessageSignature) {
let signing_nonce: Nonce = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetSigningNonce {
address: owner.to_string(),
},
)
.unwrap();
let keypair = identity::KeyPair::new(&mut self.rng);
let identity_key = keypair.public_key().to_base58_string();
let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng);
let mixnode = MixNode {
identity_key,
sphinx_key: legit_sphinx_keys.public_key().to_base58_string(),
host: "mix.node.org".to_string(),
mix_port: 1789,
verloc_port: 1790,
http_api_port: 8000,
version: "1.1.14".to_string(),
};
let payload = MixnodeBondingPayload::new(mixnode.clone(), cost_params);
let content =
ContractMessageContent::new(Addr::unchecked(owner), proxy, vec![stake], payload);
let sign_payload = SignableMixNodeBondingMsg::new(signing_nonce, content);
let plaintext = sign_payload.to_plaintext().unwrap();
let signature = keypair.private_key().sign(&plaintext);
let msg_signature = MessageSignature::from(signature.to_bytes().as_ref());
(mixnode, msg_signature)
}
}
pub fn instantiate_contracts(
mut initial_funds: HashMap<Addr, Vec<Coin>>,
custom_mixnet_init: Option<nym_mixnet_contract_common::InstantiateMsg>,
) -> (App, ContractInstantiationResult) {
// add our global addresses to the map
initial_funds.insert(
Addr::unchecked(MIXNET_OWNER),
coins(100_000_000_000, MIX_DENOM),
);
initial_funds.insert(
Addr::unchecked(VESTING_OWNER),
coins(100_000_000_000, MIX_DENOM),
);
initial_funds.insert(
Addr::unchecked(REWARDING_VALIDATOR),
coins(1_000_000_000_000, MIX_DENOM),
);
let mut app = AppBuilder::new().build(|router, _api, storage| {
for (addr, funds) in initial_funds {
router
.bank
.init_balance(storage, &addr, funds.clone())
.unwrap()
}
});
let mixnet_code_id = app.store_code(mixnet_contract_wrapper());
let vesting_code_id = app.store_code(vesting_contract_wrapper());
let mixnet_contract_address = app
.instantiate_contract(
mixnet_code_id,
Addr::unchecked(MIXNET_OWNER),
&custom_mixnet_init.unwrap_or(fixtures::default_mixnet_init_msg()),
&[],
"mixnet-contract",
Some(MIXNET_OWNER.to_string()),
)
.unwrap();
let vesting_contract_address = app
.instantiate_contract(
vesting_code_id,
Addr::unchecked(VESTING_OWNER),
&nym_vesting_contract_common::InitMsg {
mixnet_contract_address: mixnet_contract_address.to_string(),
mix_denom: MIX_DENOM.to_string(),
},
&[],
"vesting-contract",
Some(VESTING_OWNER.to_string()),
)
.unwrap();
// now fix up vesting contract address...
app.migrate_contract(
Addr::unchecked(MIXNET_OWNER),
mixnet_contract_address.clone(),
&nym_mixnet_contract_common::MigrateMsg {
vesting_contract_address: Some(vesting_contract_address.to_string()),
},
mixnet_code_id,
)
.unwrap();
(
app,
ContractInstantiationResult {
mixnet_contract_address,
vesting_contract_address,
},
)
}
@@ -0,0 +1,5 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
mod decrease_mixnode_pledge;
mod support;
+2
View File
@@ -0,0 +1,2 @@
allow-unwrap-in-tests = true
allow-expect-in-tests = true
+2 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net> // Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Uint128; use cosmwasm_std::Uint128;
@@ -61,6 +61,7 @@ pub const CONTRACT_STATE_KEY: &str = "state";
pub const LAYER_DISTRIBUTION_KEY: &str = "layers"; pub const LAYER_DISTRIBUTION_KEY: &str = "layers";
pub const NODE_ID_COUNTER_KEY: &str = "nic"; pub const NODE_ID_COUNTER_KEY: &str = "nic";
pub const PENDING_MIXNODE_CHANGES_NAMESPACE: &str = "pmc";
pub const MIXNODES_PK_NAMESPACE: &str = "mnn"; pub const MIXNODES_PK_NAMESPACE: &str = "mnn";
pub const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno"; pub const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno";
pub const MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "mni"; pub const MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "mni";
+20 -1
View File
@@ -251,6 +251,18 @@ pub fn execute(
ExecuteMsg::PledgeMoreOnBehalf { owner } => { ExecuteMsg::PledgeMoreOnBehalf { owner } => {
crate::mixnodes::transactions::try_increase_pledge_on_behalf(deps, env, info, owner) crate::mixnodes::transactions::try_increase_pledge_on_behalf(deps, env, info, owner)
} }
ExecuteMsg::DecreasePledge { decrease_by } => {
crate::mixnodes::transactions::try_decrease_pledge(deps, env, info, decrease_by)
}
ExecuteMsg::DecreasePledgeOnBehalf { owner, decrease_by } => {
crate::mixnodes::transactions::try_decrease_pledge_on_behalf(
deps,
env,
info,
decrease_by,
owner,
)
}
ExecuteMsg::UnbondMixnode {} => { ExecuteMsg::UnbondMixnode {} => {
crate::mixnodes::transactions::try_remove_mixnode(deps, env, info) crate::mixnodes::transactions::try_remove_mixnode(deps, env, info)
} }
@@ -573,6 +585,12 @@ pub fn query(
limit, limit,
)?, )?,
), ),
QueryMsg::GetPendingEpochEvent { event_id } => to_binary(
&crate::interval::queries::query_pending_epoch_event(deps, event_id)?,
),
QueryMsg::GetPendingIntervalEvent { event_id } => to_binary(
&crate::interval::queries::query_pending_interval_event(deps, event_id)?,
),
QueryMsg::GetNumberOfPendingEvents {} => to_binary( QueryMsg::GetNumberOfPendingEvents {} => to_binary(
&crate::interval::queries::query_number_of_pending_events(deps)?, &crate::interval::queries::query_number_of_pending_events(deps)?,
), ),
@@ -586,7 +604,7 @@ pub fn query(
#[entry_point] #[entry_point]
pub fn migrate( pub fn migrate(
deps: DepsMut<'_>, mut deps: DepsMut<'_>,
_env: Env, _env: Env,
msg: MigrateMsg, msg: MigrateMsg,
) -> Result<Response, MixnetContractError> { ) -> Result<Response, MixnetContractError> {
@@ -612,6 +630,7 @@ pub fn migrate(
// If state structure changed in any contract version in the way migration is needed, it // If state structure changed in any contract version in the way migration is needed, it
// should occur here, for example anything from `crate::queued_migrations::` // should occur here, for example anything from `crate::queued_migrations::`
crate::queued_migrations::insert_pending_pledge_changes(deps.branch())?;
} }
// due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address // due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address
@@ -489,7 +489,7 @@ mod test {
test.deps_mut(), test.deps_mut(),
mock_info(illegal_proxy.as_ref(), &[]), mock_info(illegal_proxy.as_ref(), &[]),
new_member.to_string(), new_member.to_string(),
family_head.clone(), family_head,
) )
.unwrap_err(); .unwrap_err();
@@ -362,7 +362,7 @@ pub mod tests {
let res = try_add_gateway( let res = try_add_gateway(
test.deps_mut(), test.deps_mut(),
env.clone(), env.clone(),
info.clone(), info,
gateway.clone(), gateway.clone(),
signature.clone(), signature.clone(),
); );
+598 -51
View File
@@ -1,6 +1,22 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net> // Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{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_pledge_decrease_event,
new_pledge_increase_event, new_rewarding_params_update_event, new_undelegation_event,
};
use mixnet_contract_common::mixnode::MixNodeCostParams;
use mixnet_contract_common::pending_events::{
PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData,
PendingIntervalEventKind,
};
use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate;
use mixnet_contract_common::{BlockHeight, Delegation, MixId};
use crate::delegations; use crate::delegations;
use crate::delegations::storage as delegations_storage; use crate::delegations::storage as delegations_storage;
use crate::interval::helpers::change_interval_config; use crate::interval::helpers::change_interval_config;
@@ -9,20 +25,6 @@ use crate::mixnodes::helpers::{cleanup_post_unbond_mixnode_storage, get_mixnode_
use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::storage as mixnodes_storage;
use crate::rewards::storage as rewards_storage; use crate::rewards::storage as rewards_storage;
use crate::support::helpers::{send_to_proxy_or_owner, VestingTracking}; use crate::support::helpers::{send_to_proxy_or_owner, VestingTracking};
use cosmwasm_std::{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_pledge_increase_event,
new_rewarding_params_update_event, new_undelegation_event,
};
use mixnet_contract_common::mixnode::MixNodeCostParams;
use mixnet_contract_common::pending_events::{
PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData,
PendingIntervalEventKind,
};
use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate;
use mixnet_contract_common::{BlockHeight, Delegation, MixId};
pub(crate) trait ContractExecutableEvent { pub(crate) trait ContractExecutableEvent {
// note: the error only means a HARD error like we failed to read from storage. // note: the error only means a HARD error like we failed to read from storage.
@@ -146,10 +148,9 @@ pub(crate) fn undelegate(
Some(delegation) => delegation, Some(delegation) => delegation,
}; };
let mix_rewarding = let mix_rewarding =
rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::InconsistentState { rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::inconsistent_state(
comment: "mixnode rewarding got removed from the storage whilst there's still an existing delegation" "mixnode rewarding got removed from the storage whilst there's still an existing delegation",
.into(), ))?;
})?;
// this also appropriately adjusts the storage // this also appropriately adjusts the storage
let tokens_to_return = let tokens_to_return =
delegations::helpers::undelegate(deps.storage, delegation, mix_rewarding)?; delegations::helpers::undelegate(deps.storage, delegation, mix_rewarding)?;
@@ -180,11 +181,15 @@ pub(crate) fn unbond_mixnode(
// in unbonding state and thus nothing could have been done to it (such as attempting to double unbond it) // in unbonding state and thus nothing could have been done to it (such as attempting to double unbond it)
// thus the node with all its associated information MUST exist in the storage. // thus the node with all its associated information MUST exist in the storage.
let node_details = get_mixnode_details_by_id(deps.storage, mix_id)?.ok_or( let node_details = get_mixnode_details_by_id(deps.storage, mix_id)?.ok_or(
MixnetContractError::InconsistentState { MixnetContractError::inconsistent_state(
comment: "mixnode getting processed to get unbonded doesn't exist in the storage" "mixnode getting processed to get unbonded doesn't exist in the storage",
.into(), ),
},
)?; )?;
if node_details.pending_changes.pledge_change.is_some() {
return Err(MixnetContractError::inconsistent_state(
"attempted to unbond mixnode while there are associated pending pledge changes",
));
}
// the denom on the original pledge was validated at the time of bonding so we can safely reuse it here // the denom on the original pledge was validated at the time of bonding so we can safely reuse it here
let rewarding_denom = &node_details.bond_information.original_pledge.denom; let rewarding_denom = &node_details.bond_information.original_pledge.denom;
@@ -244,12 +249,15 @@ pub(crate) fn increase_pledge(
// the target node MUST exist - we have checked it at the time of putting this event onto the queue // 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 // we have also verified there were no preceding unbond events
let mix_details = get_mixnode_details_by_id(deps.storage, mix_id)?.ok_or( let mix_details = get_mixnode_details_by_id(deps.storage, mix_id)?.ok_or(
MixnetContractError::InconsistentState { MixnetContractError::inconsistent_state(
comment: "mixnode getting processed to increase its pledge doesn't exist in the storage",
"mixnode getting processed to increase its pledge doesn't exist in the storage" ),
.into(),
},
)?; )?;
if mix_details.pending_changes.pledge_change.is_none() {
return Err(MixnetContractError::inconsistent_state(
"attempted to increase mixnode pledge while there are no associated pending changes",
));
}
let mut updated_bond = mix_details.bond_information.clone(); let mut updated_bond = mix_details.bond_information.clone();
let mut updated_rewarding = mix_details.rewarding_details; let mut updated_rewarding = mix_details.rewarding_details;
@@ -257,7 +265,10 @@ pub(crate) fn increase_pledge(
updated_bond.original_pledge.amount += increase.amount; updated_bond.original_pledge.amount += increase.amount;
updated_rewarding.increase_operator_uint128(increase.amount)?; updated_rewarding.increase_operator_uint128(increase.amount)?;
// update both, bond information and rewarding details let mut pending_changes = mix_details.pending_changes;
pending_changes.pledge_change = None;
// update all: bond information, rewarding details and pending pledge changes
mixnodes_storage::mixnode_bonds().replace( mixnodes_storage::mixnode_bonds().replace(
deps.storage, deps.storage,
mix_id, mix_id,
@@ -265,10 +276,70 @@ pub(crate) fn increase_pledge(
Some(&mix_details.bond_information), Some(&mix_details.bond_information),
)?; )?;
rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &updated_rewarding)?; rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &updated_rewarding)?;
mixnodes_storage::PENDING_MIXNODE_CHANGES.save(deps.storage, mix_id, &pending_changes)?;
Ok(Response::new().add_event(new_pledge_increase_event(created_at, mix_id, &increase))) Ok(Response::new().add_event(new_pledge_increase_event(created_at, mix_id, &increase)))
} }
pub(crate) fn decrease_pledge(
deps: DepsMut<'_>,
created_at: BlockHeight,
mix_id: MixId,
decrease_by: Coin,
) -> Result<Response, MixnetContractError> {
// 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::inconsistent_state(
"mixnode getting processed to increase its pledge doesn't exist in the storage",
),
)?;
if mix_details.pending_changes.pledge_change.is_none() {
return Err(MixnetContractError::inconsistent_state(
"attempted to decrease mixnode pledge while there are no associated pending changes",
));
}
let mut updated_bond = mix_details.bond_information.clone();
let mut updated_rewarding = mix_details.rewarding_details;
let mut pending_changes = mix_details.pending_changes;
pending_changes.pledge_change = None;
// SAFETY: the subtraction here can't overflow as before the event was pushed into the queue,
// we checked that the new value will be higher than minimum pledge (which is also strictly positive)
updated_bond.original_pledge.amount -= decrease_by.amount;
updated_rewarding.decrease_operator_uint128(decrease_by.amount)?;
let proxy = &mix_details.bond_information.proxy;
let owner = &mix_details.bond_information.owner;
// send the removed tokens back to the operator
let return_tokens = send_to_proxy_or_owner(proxy, owner, vec![decrease_by.clone()]);
// update all: bond information, rewarding details and pending pledge changes
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)?;
mixnodes_storage::PENDING_MIXNODE_CHANGES.save(deps.storage, mix_id, &pending_changes)?;
let response = Response::new()
.add_message(return_tokens)
.add_event(new_pledge_decrease_event(created_at, mix_id, &decrease_by))
.maybe_add_track_vesting_decrease_mixnode_pledge(
deps.storage,
proxy.clone(),
owner.clone().to_string(),
decrease_by,
)?;
Ok(response)
}
impl ContractExecutableEvent for PendingEpochEventData { impl ContractExecutableEvent for PendingEpochEventData {
fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result<Response, MixnetContractError> { fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result<Response, MixnetContractError> {
// note that the basic validation on all those events was already performed before // note that the basic validation on all those events was already performed before
@@ -288,6 +359,10 @@ impl ContractExecutableEvent for PendingEpochEventData {
PendingEpochEventKind::PledgeMore { mix_id, amount } => { PendingEpochEventKind::PledgeMore { mix_id, amount } => {
increase_pledge(deps, self.created_at, mix_id, amount) increase_pledge(deps, self.created_at, mix_id, amount)
} }
PendingEpochEventKind::DecreasePledge {
mix_id,
decrease_by,
} => decrease_pledge(deps, self.created_at, mix_id, decrease_by),
PendingEpochEventKind::UnbondMixnode { mix_id } => { PendingEpochEventKind::UnbondMixnode { mix_id } => {
unbond_mixnode(deps, env, self.created_at, mix_id) unbond_mixnode(deps, env, self.created_at, mix_id)
} }
@@ -397,26 +472,33 @@ impl ContractExecutableEvent for PendingIntervalEventData {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use std::time::Duration;
use cosmwasm_std::Decimal;
use mixnet_contract_common::Percent;
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
use crate::support::tests::test_helpers; use crate::support::tests::test_helpers;
use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; use crate::support::tests::test_helpers::{assert_decimals, TestSetup};
use cosmwasm_std::Decimal;
use mixnet_contract_common::Percent; use super::*;
use std::time::Duration;
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
// note that authorization and basic validation has already been performed for all of those // note that authorization and basic validation has already been performed for all of those
// before being pushed onto the event queues // before being pushed onto the event queues
#[cfg(test)] #[cfg(test)]
mod delegating { mod delegating {
use super::*; use cosmwasm_std::testing::mock_info;
use cosmwasm_std::{coin, to_binary, CosmosMsg, Decimal, WasmMsg};
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount;
use crate::mixnodes::transactions::try_remove_mixnode; use crate::mixnodes::transactions::try_remove_mixnode;
use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::fixtures::TEST_COIN_DENOM;
use crate::support::tests::test_helpers::get_bank_send_msg; use crate::support::tests::test_helpers::get_bank_send_msg;
use cosmwasm_std::testing::mock_info;
use cosmwasm_std::{coin, to_binary, CosmosMsg, Decimal, WasmMsg}; use super::*;
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount;
#[test] #[test]
fn returns_the_tokens_if_mixnode_has_unbonded() { fn returns_the_tokens_if_mixnode_has_unbonded() {
@@ -829,7 +911,7 @@ mod tests {
res_other_proxy, res_other_proxy,
MixnetContractError::ProxyIsNotVestingContract { MixnetContractError::ProxyIsNotVestingContract {
received: dummy_proxy, received: dummy_proxy,
vesting_contract vesting_contract,
} }
); );
} }
@@ -837,11 +919,14 @@ mod tests {
#[cfg(test)] #[cfg(test)]
mod undelegating { mod undelegating {
use super::*; use cosmwasm_std::{coin, to_binary, CosmosMsg, WasmMsg};
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount;
use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::fixtures::TEST_COIN_DENOM;
use crate::support::tests::test_helpers::get_bank_send_msg; use crate::support::tests::test_helpers::get_bank_send_msg;
use cosmwasm_std::{coin, to_binary, CosmosMsg, WasmMsg};
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; use super::*;
#[test] #[test]
fn doesnt_return_any_tokens_if_it_doesnt_exist() { fn doesnt_return_any_tokens_if_it_doesnt_exist() {
@@ -1021,7 +1106,7 @@ mod tests {
res_other_proxy, res_other_proxy,
MixnetContractError::ProxyIsNotVestingContract { MixnetContractError::ProxyIsNotVestingContract {
received: dummy_proxy, received: dummy_proxy,
vesting_contract vesting_contract,
} }
); );
} }
@@ -1029,13 +1114,17 @@ mod tests {
#[cfg(test)] #[cfg(test)]
mod mixnode_unbonding { mod mixnode_unbonding {
use super::*; use cosmwasm_std::{coin, to_binary, CosmosMsg, Uint128, WasmMsg};
use mixnet_contract_common::mixnode::{PendingMixNodeChanges, UnbondedMixnode};
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount;
use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::storage as mixnodes_storage;
use crate::mixnodes::transactions::{_try_decrease_pledge, _try_increase_pledge};
use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::fixtures::TEST_COIN_DENOM;
use crate::support::tests::test_helpers::get_bank_send_msg; use crate::support::tests::test_helpers::get_bank_send_msg;
use cosmwasm_std::{coin, to_binary, CosmosMsg, Uint128, WasmMsg};
use mixnet_contract_common::mixnode::UnbondedMixnode; use super::*;
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount;
#[test] #[test]
fn returns_hard_error_if_mixnode_doesnt_exist() { fn returns_hard_error_if_mixnode_doesnt_exist() {
@@ -1050,6 +1139,71 @@ mod tests {
)); ));
} }
#[test]
fn returns_hard_error_if_there_are_pending_pledge_changes() {
let mut test = TestSetup::new();
let env = test.env();
let change = test.coins(1234);
// increase
let owner = "mix-owner1";
let pledge = Uint128::new(250_000_000);
let mix_id = test.add_dummy_mixnode(owner, Some(pledge));
_try_increase_pledge(
test.deps_mut(),
env.clone(),
change.clone(),
Addr::unchecked(owner),
None,
)
.unwrap();
let res = unbond_mixnode(test.deps_mut(), &env, 123, mix_id);
assert!(matches!(
res,
Err(MixnetContractError::InconsistentState { .. })
));
// decrease
let owner = "mix-owner2";
let pledge = Uint128::new(250_000_000);
let mix_id = test.add_dummy_mixnode(owner, Some(pledge));
_try_decrease_pledge(
test.deps_mut(),
env.clone(),
change[0].clone(),
Addr::unchecked(owner),
None,
)
.unwrap();
let res = unbond_mixnode(test.deps_mut(), &env, 123, mix_id);
assert!(matches!(
res,
Err(MixnetContractError::InconsistentState { .. })
));
// artificial
let owner = "mix-owner3";
let pledge = Uint128::new(250_000_000);
let mix_id = test.add_dummy_mixnode(owner, Some(pledge));
let changes = PendingMixNodeChanges {
pledge_change: Some(1234),
};
mixnodes_storage::PENDING_MIXNODE_CHANGES
.save(test.deps_mut().storage, mix_id, &changes)
.unwrap();
let res = unbond_mixnode(test.deps_mut(), &env, 123, mix_id);
assert!(matches!(
res,
Err(MixnetContractError::InconsistentState { .. })
));
}
#[test] #[test]
fn returns_original_pledge_alongside_any_earned_rewards() { fn returns_original_pledge_alongside_any_earned_rewards() {
let mut test = TestSetup::new(); let mut test = TestSetup::new();
@@ -1178,7 +1332,7 @@ mod tests {
res_other_proxy, res_other_proxy,
MixnetContractError::ProxyIsNotVestingContract { MixnetContractError::ProxyIsNotVestingContract {
received: dummy_proxy, received: dummy_proxy,
vesting_contract vesting_contract,
} }
); );
} }
@@ -1186,10 +1340,12 @@ mod tests {
#[cfg(test)] #[cfg(test)]
mod increasing_pledge { mod increasing_pledge {
use super::*;
use cosmwasm_std::Uint128; use cosmwasm_std::Uint128;
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; use mixnet_contract_common::rewarding::helpers::truncate_reward_amount;
use super::*;
#[test] #[test]
fn returns_hard_error_if_mixnode_doesnt_exist() { fn returns_hard_error_if_mixnode_doesnt_exist() {
// this should have never happened so hard error MUST be thrown here // this should have never happened so hard error MUST be thrown here
@@ -1203,10 +1359,27 @@ mod tests {
)); ));
} }
#[test]
fn returns_hard_error_if_there_are_no_pending_pledge_changes() {
let mut test = TestSetup::new();
let change = test.coin(1234);
let owner = "mix-owner";
let pledge = Uint128::new(250_000_000);
let mix_id = test.add_dummy_mixnode(owner, Some(pledge));
let res = increase_pledge(test.deps_mut(), 123, mix_id, change);
assert!(matches!(
res,
Err(MixnetContractError::InconsistentState { .. })
));
}
#[test] #[test]
fn updates_stored_bond_information_and_rewarding_details() { fn updates_stored_bond_information_and_rewarding_details() {
let mut test = TestSetup::new(); let mut test = TestSetup::new();
let mix_id = test.add_dummy_mixnode("mix-owner", None); let mix_id = test.add_dummy_mixnode("mix-owner", None);
test.set_pending_pledge_change(mix_id, None);
let old_details = get_mixnode_details_by_id(test.deps().storage, mix_id) let old_details = get_mixnode_details_by_id(test.deps().storage, mix_id)
.unwrap() .unwrap()
@@ -1239,6 +1412,8 @@ mod tests {
let pledge3 = Uint128::new(200_000_000); let pledge3 = Uint128::new(200_000_000);
let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1));
test.set_pending_pledge_change(mix_id_repledge, None);
let increase = test.coin(pledge2.u128()); let increase = test.coin(pledge2.u128());
increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap();
@@ -1274,6 +1449,7 @@ mod tests {
let pledge2 = Uint128::new(50_000_000_000); let pledge2 = Uint128::new(50_000_000_000);
let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1));
test.set_pending_pledge_change(mix_id_repledge, None);
test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); 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("bob", 500_000_000_000u128, mix_id_repledge);
@@ -1346,6 +1522,7 @@ mod tests {
let pledge2 = Uint128::new(50_000_000_000); let pledge2 = Uint128::new(50_000_000_000);
let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1));
test.set_pending_pledge_change(mix_id_repledge, None);
test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); 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("bob", 500_000_000_000u128, mix_id_repledge);
@@ -1420,6 +1597,373 @@ mod tests {
assert_eq!(dist1, dist2) assert_eq!(dist1, dist2)
} }
} }
#[test]
fn updates_the_pending_pledge_changes_field() {
let mut test = TestSetup::new();
let mix_id = test.add_dummy_mixnode("mix-owner", None);
test.set_pending_pledge_change(mix_id, None);
let amount = test.coin(12345);
increase_pledge(test.deps_mut(), 123, mix_id, amount).unwrap();
let pending = mixnodes_storage::PENDING_MIXNODE_CHANGES
.load(test.deps().storage, mix_id)
.unwrap();
assert!(pending.pledge_change.is_none())
}
}
#[cfg(test)]
mod decreasing_pledge {
use cosmwasm_std::{to_binary, BankMsg, CosmosMsg, Uint128, WasmMsg};
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount;
use super::*;
#[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 = decrease_pledge(test.deps_mut(), 123, 1, amount);
assert!(matches!(
res,
Err(MixnetContractError::InconsistentState { .. })
));
}
#[test]
fn returns_hard_error_if_there_are_no_pending_pledge_changes() {
let mut test = TestSetup::new();
let change = test.coin(1234);
let owner = "mix-owner";
let pledge = Uint128::new(250_000_000);
let mix_id = test.add_dummy_mixnode(owner, Some(pledge));
let res = decrease_pledge(test.deps_mut(), 123, mix_id, change);
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);
test.set_pending_pledge_change(mix_id, None);
let old_details = get_mixnode_details_by_id(test.deps().storage, mix_id)
.unwrap()
.unwrap();
let amount = test.coin(12345);
decrease_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 returns_tokens_back_to_the_owner() {
let mut test = TestSetup::new();
let owner = "mix-owner";
let mix_id = test.add_dummy_mixnode(owner, None);
test.set_pending_pledge_change(mix_id, None);
let amount = test.coin(12345);
let res = decrease_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap();
assert_eq!(res.messages.len(), 1);
assert_eq!(
res.messages[0].msg,
CosmosMsg::Bank(BankMsg::Send {
to_address: owner.to_string(),
amount: vec![amount],
})
)
}
#[test]
fn returns_tokens_back_to_the_proxy_if_bonded_with_vesting() {
let mut test = TestSetup::new();
let owner = "mix-owner";
let mix_id = test.add_dummy_mixnode_with_legal_proxy(owner, None);
test.set_pending_pledge_change(mix_id, None);
let vesting_contract = test.vesting_contract();
let amount = test.coin(12345);
let res = decrease_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap();
assert_eq!(res.messages.len(), 2);
assert_eq!(
res.messages[0].msg,
CosmosMsg::Bank(BankMsg::Send {
to_address: vesting_contract.to_string(),
amount: vec![amount],
})
)
}
#[test]
fn attaches_vesting_track_message() {
let mut test = TestSetup::new();
let mix_id_no_proxy = test.add_dummy_mixnode("mix-owner1", None);
test.set_pending_pledge_change(mix_id_no_proxy, None);
let mix_id_proxy = test.add_dummy_mixnode_with_legal_proxy("mix-owner2", None);
test.set_pending_pledge_change(mix_id_proxy, None);
let vesting_contract = test.vesting_contract();
let amount = test.coin(12345);
let res_no_proxy =
decrease_pledge(test.deps_mut(), 123, mix_id_no_proxy, amount.clone()).unwrap();
// nothing was attached (apart from bank message tested in `returns_tokens_back_to_the_owner`)
// because it wasn't done with proxy!
assert_eq!(res_no_proxy.messages.len(), 1);
let res_proxy =
decrease_pledge(test.deps_mut(), 123, mix_id_proxy, amount.clone()).unwrap();
assert_eq!(res_proxy.messages.len(), 2);
assert_eq!(
res_proxy.messages[1].msg,
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: vesting_contract.to_string(),
msg: to_binary(&VestingContractExecuteMsg::TrackDecreasePledge {
owner: "mix-owner2".to_string(),
amount,
})
.unwrap(),
funds: vec![],
})
);
}
#[test]
fn without_any_events_in_between_is_equivalent_to_pledging_the_same_amount_immediately() {
let mut test = TestSetup::new();
let pledge1 = Uint128::new(200_000_000);
let pledge_change = Uint128::new(50_000_000);
let pledge3 = Uint128::new(150_000_000);
let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1));
test.set_pending_pledge_change(mix_id_repledge, None);
let decrease = test.coin(pledge_change.u128());
decrease_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).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.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]);
let dist1 = test.reward_with_distribution_with_state_bypass(
mix_id_repledge,
test_helpers::performance(100.0),
);
let dist2 = test.reward_with_distribution_with_state_bypass(
mix_id_full_pledge,
test_helpers::performance(100.0),
);
assert_eq!(dist1, dist2)
}
#[test]
fn correctly_decreases_future_rewards() {
let mut test = TestSetup::new();
let pledge1 = Uint128::new(200_000_000_000);
let pledge_change = Uint128::new(50_000_000_000);
let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1));
test.set_pending_pledge_change(mix_id_repledge, None);
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.force_change_rewarded_set(vec![mix_id_repledge]);
let dist = test.reward_with_distribution_with_state_bypass(
mix_id_repledge,
test_helpers::performance(100.0),
);
let decrease = test.coin(pledge_change.u128());
decrease_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap();
let pledge3 = Uint128::new(150_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.force_change_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_with_state_bypass(
mix_id_repledge,
test_helpers::performance(100.0),
);
let dist2 = test.reward_with_distribution_with_state_bypass(
mix_id_full_pledge,
test_helpers::performance(100.0),
);
assert_eq!(dist1, dist2)
}
}
#[test]
fn correctly_decreases_future_rewards_with_more_passed_epochs() {
let mut test = TestSetup::new();
let pledge1 = Uint128::new(200_000_000_000);
let pledge_change = Uint128::new(50_000_000_000);
let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1));
test.set_pending_pledge_change(mix_id_repledge, None);
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.force_change_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 decreasing pledge
for _ in 0..500 {
test.skip_to_next_epoch_end();
let dist = test.reward_with_distribution_with_state_bypass(
mix_id_repledge,
test_helpers::performance(100.0),
);
cumulative_op_reward += dist.operator;
cumulative_del_reward += dist.delegates;
}
let decrease = test.coin(pledge_change.u128());
decrease_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap();
let pledge3 =
Uint128::new(150_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.force_change_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_with_state_bypass(
mix_id_repledge,
test_helpers::performance(100.0),
);
let dist2 = test.reward_with_distribution_with_state_bypass(
mix_id_full_pledge,
test_helpers::performance(100.0),
);
assert_eq!(dist1, dist2)
}
}
#[test]
fn updates_the_pending_pledge_changes_field() {
let mut test = TestSetup::new();
let mix_id = test.add_dummy_mixnode("mix-owner", None);
test.set_pending_pledge_change(mix_id, None);
let amount = test.coin(12345);
decrease_pledge(test.deps_mut(), 123, mix_id, amount).unwrap();
let pending = mixnodes_storage::PENDING_MIXNODE_CHANGES
.load(test.deps().storage, mix_id)
.unwrap();
assert!(pending.pledge_change.is_none())
}
} }
#[test] #[test]
@@ -1439,11 +1983,14 @@ mod tests {
#[cfg(test)] #[cfg(test)]
mod changing_mix_cost_params { mod changing_mix_cost_params {
use super::*;
use crate::support::tests::fixtures::TEST_COIN_DENOM;
use cosmwasm_std::coin; use cosmwasm_std::coin;
use mixnet_contract_common::Percent; use mixnet_contract_common::Percent;
use crate::support::tests::fixtures::TEST_COIN_DENOM;
use super::*;
#[test] #[test]
fn doesnt_do_anything_if_mixnode_has_unbonded() { fn doesnt_do_anything_if_mixnode_has_unbonded() {
let mut test = TestSetup::new(); let mut test = TestSetup::new();
@@ -1480,7 +2027,7 @@ mod tests {
Response::new().add_event(new_mixnode_cost_params_update_event( Response::new().add_event(new_mixnode_cost_params_update_event(
123, 123,
mix_id, mix_id,
&new_params &new_params,
)) ))
) )
); );
+100 -2
View File
@@ -13,8 +13,8 @@ use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::pending_events::{PendingEpochEvent, PendingIntervalEvent}; use mixnet_contract_common::pending_events::{PendingEpochEvent, PendingIntervalEvent};
use mixnet_contract_common::{ use mixnet_contract_common::{
CurrentIntervalResponse, EpochEventId, EpochStatus, IntervalEventId, MixId, CurrentIntervalResponse, EpochEventId, EpochStatus, IntervalEventId, MixId,
NumberOfPendingEventsResponse, PagedRewardedSetResponse, PendingEpochEventsResponse, NumberOfPendingEventsResponse, PagedRewardedSetResponse, PendingEpochEventResponse,
PendingIntervalEventsResponse, PendingEpochEventsResponse, PendingIntervalEventResponse, PendingIntervalEventsResponse,
}; };
pub fn query_epoch_status(deps: Deps<'_>) -> StdResult<EpochStatus> { pub fn query_epoch_status(deps: Deps<'_>) -> StdResult<EpochStatus> {
@@ -112,6 +112,22 @@ pub fn query_pending_interval_events_paged(
}) })
} }
pub fn query_pending_epoch_event(
deps: Deps<'_>,
event_id: EpochEventId,
) -> Result<PendingEpochEventResponse, MixnetContractError> {
let event = storage::PENDING_EPOCH_EVENTS.may_load(deps.storage, event_id)?;
Ok(PendingEpochEventResponse { event_id, event })
}
pub fn query_pending_interval_event(
deps: Deps<'_>,
event_id: IntervalEventId,
) -> Result<PendingIntervalEventResponse, MixnetContractError> {
let event = storage::PENDING_INTERVAL_EVENTS.may_load(deps.storage, event_id)?;
Ok(PendingIntervalEventResponse { event_id, event })
}
pub fn query_number_of_pending_events( pub fn query_number_of_pending_events(
deps: Deps<'_>, deps: Deps<'_>,
) -> Result<NumberOfPendingEventsResponse, MixnetContractError> { ) -> Result<NumberOfPendingEventsResponse, MixnetContractError> {
@@ -540,6 +556,88 @@ mod tests {
} }
} }
#[test]
fn query_for_pending_epoch_event() {
let mut test = TestSetup::new();
// it doesn't exist
let expected = PendingEpochEventResponse {
event_id: 123,
event: None,
};
assert_eq!(
expected,
query_pending_epoch_event(test.deps(), 123).unwrap()
);
// it exists
let dummy_action = PendingEpochEventKind::Undelegate {
owner: Addr::unchecked("foomp"),
mix_id: test.rng.next_u32(),
proxy: None,
};
let env = test.env();
storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action.clone()).unwrap();
let expected = PendingEpochEventResponse {
event_id: 1,
event: Some(dummy_action.attach_source_height(env.block.height)),
};
assert_eq!(expected, query_pending_epoch_event(test.deps(), 1).unwrap());
// it no longer exist (but used to)
test.execute_all_pending_events();
let expected = PendingEpochEventResponse {
event_id: 1,
event: None,
};
assert_eq!(expected, query_pending_epoch_event(test.deps(), 1).unwrap());
}
#[test]
fn query_for_pending_interval_event() {
let mut test = TestSetup::new();
// it doesn't exist
let expected = PendingIntervalEventResponse {
event_id: 123,
event: None,
};
assert_eq!(
expected,
query_pending_interval_event(test.deps(), 123).unwrap()
);
// it exists
let dummy_action = PendingIntervalEventKind::ChangeMixCostParams {
mix_id: test.rng.next_u32(),
new_costs: fixtures::mix_node_cost_params_fixture(),
};
let env = test.env();
storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action.clone())
.unwrap();
let expected = PendingIntervalEventResponse {
event_id: 1,
event: Some(dummy_action.attach_source_height(env.block.height)),
};
assert_eq!(
expected,
query_pending_interval_event(test.deps(), 1).unwrap()
);
// it no longer exist (but used to)
test.execute_all_pending_events();
let expected = PendingIntervalEventResponse {
event_id: 1,
event: None,
};
assert_eq!(
expected,
query_pending_interval_event(test.deps(), 1).unwrap()
);
}
#[test] #[test]
fn querying_for_number_of_pending_events() { fn querying_for_number_of_pending_events() {
let mut test = TestSetup::new(); let mut test = TestSetup::new();
+67 -4
View File
@@ -81,7 +81,7 @@ pub(crate) fn push_new_epoch_event(
storage: &mut dyn Storage, storage: &mut dyn Storage,
env: &Env, env: &Env,
event: PendingEpochEventKind, event: PendingEpochEventKind,
) -> StdResult<()> { ) -> StdResult<EpochEventId> {
// not included in non-test code as it messes with our return types as we expected `StdResult` // not included in non-test code as it messes with our return types as we expected `StdResult`
// from all storage-related operations. // from all storage-related operations.
// However, the callers MUST HAVE ensured the below invariant // However, the callers MUST HAVE ensured the below invariant
@@ -90,14 +90,15 @@ pub(crate) fn push_new_epoch_event(
let event_id = next_epoch_event_id_counter(storage)?; let event_id = next_epoch_event_id_counter(storage)?;
let event_data = event.attach_source_height(env.block.height); let event_data = event.attach_source_height(env.block.height);
PENDING_EPOCH_EVENTS.save(storage, event_id, &event_data) PENDING_EPOCH_EVENTS.save(storage, event_id, &event_data)?;
Ok(event_id)
} }
pub(crate) fn push_new_interval_event( pub(crate) fn push_new_interval_event(
storage: &mut dyn Storage, storage: &mut dyn Storage,
env: &Env, env: &Env,
event: PendingIntervalEventKind, event: PendingIntervalEventKind,
) -> StdResult<()> { ) -> StdResult<IntervalEventId> {
// not included in non-test code as it messes with our return types as we expected `StdResult` // not included in non-test code as it messes with our return types as we expected `StdResult`
// from all storage-related operations. // from all storage-related operations.
// However, the callers MUST HAVE ensured the below invariant // However, the callers MUST HAVE ensured the below invariant
@@ -106,7 +107,8 @@ pub(crate) fn push_new_interval_event(
let event_id = next_interval_event_id_counter(storage)?; let event_id = next_interval_event_id_counter(storage)?;
let event_data = event.attach_source_height(env.block.height); let event_data = event.attach_source_height(env.block.height);
PENDING_INTERVAL_EVENTS.save(storage, event_id, &event_data) PENDING_INTERVAL_EVENTS.save(storage, event_id, &event_data)?;
Ok(event_id)
} }
pub(crate) fn update_rewarded_set( pub(crate) fn update_rewarded_set(
@@ -168,8 +170,11 @@ pub(crate) fn initialise_storage(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::support::tests::fixtures;
use crate::support::tests::test_helpers::TestSetup;
use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_dependencies;
use cosmwasm_std::Order; use cosmwasm_std::Order;
use rand_chacha::rand_core::RngCore;
fn read_entire_set(storage: &mut dyn Storage) -> HashMap<MixId, RewardedSetNodeStatus> { fn read_entire_set(storage: &mut dyn Storage) -> HashMap<MixId, RewardedSetNodeStatus> {
REWARDED_SET REWARDED_SET
@@ -211,4 +216,62 @@ mod tests {
assert!(current_set.get(&7).is_none()); assert!(current_set.get(&7).is_none());
assert!(current_set.get(&1).is_none()); assert!(current_set.get(&1).is_none());
} }
#[test]
fn pushing_new_epoch_event_returns_its_id() {
let mut test = TestSetup::new();
let env = test.env();
for _ in 0..500 {
let dummy_action = PendingEpochEventKind::Undelegate {
owner: Addr::unchecked("foomp"),
mix_id: test.rng.next_u32(),
proxy: None,
};
let id = push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap();
let expected = EPOCH_EVENT_ID_COUNTER.load(test.deps().storage).unwrap();
assert_eq!(expected, id);
}
test.execute_all_pending_events();
for _ in 0..10 {
let dummy_action = PendingEpochEventKind::Undelegate {
owner: Addr::unchecked("foomp"),
mix_id: test.rng.next_u32(),
proxy: None,
};
let id = push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap();
let expected = EPOCH_EVENT_ID_COUNTER.load(test.deps().storage).unwrap();
assert_eq!(expected, id);
}
}
#[test]
fn pushing_new_interval_event_returns_its_id() {
let mut test = TestSetup::new();
let env = test.env();
for _ in 0..500 {
let dummy_action = PendingIntervalEventKind::ChangeMixCostParams {
mix_id: test.rng.next_u32(),
new_costs: fixtures::mix_node_cost_params_fixture(),
};
let id = push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap();
let expected = INTERVAL_EVENT_ID_COUNTER.load(test.deps().storage).unwrap();
assert_eq!(expected, id);
}
test.execute_all_pending_events();
for _ in 0..10 {
let dummy_action = PendingIntervalEventKind::ChangeMixCostParams {
mix_id: test.rng.next_u32(),
new_costs: fixtures::mix_node_cost_params_fixture(),
};
let id = push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap();
let expected = INTERVAL_EVENT_ID_COUNTER.load(test.deps().storage).unwrap();
assert_eq!(expected, id);
}
}
} }
+164 -71
View File
@@ -10,7 +10,7 @@ use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::mixnode::{ use mixnet_contract_common::mixnode::{
MixNodeCostParams, MixNodeDetails, MixNodeRewarding, UnbondedMixnode, MixNodeCostParams, MixNodeDetails, MixNodeRewarding, UnbondedMixnode,
}; };
use mixnet_contract_common::{Layer, MixId, MixNode, MixNodeBond}; use mixnet_contract_common::{IdentityKey, Layer, MixId, MixNode, MixNodeBond};
pub(crate) fn must_get_mixnode_bond_by_owner( pub(crate) fn must_get_mixnode_bond_by_owner(
store: &dyn Storage, store: &dyn Storage,
@@ -26,18 +26,34 @@ pub(crate) fn must_get_mixnode_bond_by_owner(
.1) .1)
} }
pub(crate) fn attach_mix_details(
store: &dyn Storage,
bond_information: MixNodeBond,
) -> StdResult<MixNodeDetails> {
// if bond exists, rewarding details MUST also exist
let rewarding_details =
rewards_storage::MIXNODE_REWARDING.load(store, bond_information.mix_id)?;
// since this `Map` hasn't existed when contract was instantiated, some mixnodes might not
// have an entry here. But that's fine, because it means they have no pending changes
// (if there were supposed to be any changes, they would have been added during migration)
let pending_changes = storage::PENDING_MIXNODE_CHANGES
.may_load(store, bond_information.mix_id)?
.unwrap_or_default();
Ok(MixNodeDetails::new(
bond_information,
rewarding_details,
pending_changes,
))
}
pub(crate) fn get_mixnode_details_by_id( pub(crate) fn get_mixnode_details_by_id(
store: &dyn Storage, store: &dyn Storage,
mix_id: MixId, mix_id: MixId,
) -> StdResult<Option<MixNodeDetails>> { ) -> StdResult<Option<MixNodeDetails>> {
if let Some(bond_information) = storage::mixnode_bonds().may_load(store, mix_id)? { if let Some(bond_information) = storage::mixnode_bonds().may_load(store, mix_id)? {
// if bond exists, rewarding details MUST also exist attach_mix_details(store, bond_information).map(Some)
let rewarding_details =
rewards_storage::MIXNODE_REWARDING.load(store, bond_information.mix_id)?;
Ok(Some(MixNodeDetails::new(
bond_information,
rewarding_details,
)))
} else { } else {
Ok(None) Ok(None)
} }
@@ -53,13 +69,23 @@ pub(crate) fn get_mixnode_details_by_owner(
.item(store, address)? .item(store, address)?
.map(|record| record.1) .map(|record| record.1)
{ {
// if bond exists, rewarding details MUST also exist attach_mix_details(store, bond_information).map(Some)
let rewarding_details = } else {
rewards_storage::MIXNODE_REWARDING.load(store, bond_information.mix_id)?; Ok(None)
Ok(Some(MixNodeDetails::new( }
bond_information, }
rewarding_details,
))) pub(crate) fn get_mixnode_details_by_identity(
store: &dyn Storage,
mix_identity: IdentityKey,
) -> StdResult<Option<MixNodeDetails>> {
if let Some(bond_information) = storage::mixnode_bonds()
.idx
.identity_key
.item(store, mix_identity)?
.map(|record| record.1)
{
attach_mix_details(store, bond_information).map(Some)
} else { } else {
Ok(None) Ok(None)
} }
@@ -153,7 +179,13 @@ pub(crate) mod tests {
mix_node_cost_params_fixture, mix_node_fixture, TEST_COIN_DENOM, mix_node_cost_params_fixture, mix_node_fixture, TEST_COIN_DENOM,
}; };
use crate::support::tests::test_helpers::TestSetup; use crate::support::tests::test_helpers::TestSetup;
use cosmwasm_std::coin; use cosmwasm_std::{coin, Uint128};
pub(crate) struct DummyMixnode {
pub mix_id: MixId,
pub owner: Addr,
pub identity: IdentityKey,
}
pub(crate) const OWNER_EXISTS: &str = "mix-owner-existing"; pub(crate) const OWNER_EXISTS: &str = "mix-owner-existing";
pub(crate) const OWNER_UNBONDING: &str = "mix-owner-unbonding"; pub(crate) const OWNER_UNBONDING: &str = "mix-owner-unbonding";
@@ -161,33 +193,59 @@ pub(crate) mod tests {
pub(crate) const OWNER_UNBONDED_LEFTOVER: &str = "mix-owner-unbonded-leftover"; 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 // create a mixnode that is bonded, unbonded, in the process of unbonding and unbonded with leftover mix rewarding details
pub(crate) fn setup_mix_combinations(test: &mut TestSetup) -> Vec<MixId> { pub(crate) fn setup_mix_combinations(
let mix_id_exists = test.add_dummy_mixnode(OWNER_EXISTS, None); test: &mut TestSetup,
let mix_id_unbonding = test.add_dummy_mixnode(OWNER_UNBONDING, None); stake: Option<Uint128>,
let mix_id_unbonded = test.add_dummy_mixnode(OWNER_UNBONDED, None); ) -> Vec<DummyMixnode> {
let mix_id_unbonded_leftover = test.add_dummy_mixnode(OWNER_UNBONDED_LEFTOVER, None); let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_EXISTS, stake);
let mix_exists = DummyMixnode {
mix_id,
owner: Addr::unchecked(OWNER_EXISTS),
identity: keypair.public_key().to_base58_string(),
};
let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_UNBONDING, stake);
let mix_unbonding = DummyMixnode {
mix_id,
owner: Addr::unchecked(OWNER_UNBONDING),
identity: keypair.public_key().to_base58_string(),
};
let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_UNBONDED, stake);
let mix_unbonded = DummyMixnode {
mix_id,
owner: Addr::unchecked(OWNER_UNBONDED),
identity: keypair.public_key().to_base58_string(),
};
let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_UNBONDED_LEFTOVER, stake);
let mix_unbonded_leftover = DummyMixnode {
mix_id,
owner: Addr::unchecked(OWNER_UNBONDED_LEFTOVER),
identity: keypair.public_key().to_base58_string(),
};
// manually adjust delegation info as to indicate the rewarding information shouldnt get removed // manually adjust delegation info as to indicate the rewarding information shouldnt get removed
let mut rewarding_details = test.mix_rewarding(mix_id_unbonded_leftover); let mut rewarding_details = test.mix_rewarding(mix_unbonded_leftover.mix_id);
rewarding_details.delegates = Decimal::raw(12345); rewarding_details.delegates = Decimal::raw(12345);
rewarding_details.unique_delegations = 1; rewarding_details.unique_delegations = 1;
rewards_storage::MIXNODE_REWARDING rewards_storage::MIXNODE_REWARDING
.save( .save(
test.deps_mut().storage, test.deps_mut().storage,
mix_id_unbonded_leftover, mix_unbonded_leftover.mix_id,
&rewarding_details, &rewarding_details,
) )
.unwrap(); .unwrap();
test.immediately_unbond_mixnode(mix_id_unbonded); test.immediately_unbond_mixnode(mix_unbonded.mix_id);
test.immediately_unbond_mixnode(mix_id_unbonded_leftover); test.immediately_unbond_mixnode(mix_unbonded_leftover.mix_id);
test.start_unbonding_mixnode(mix_id_unbonding); test.start_unbonding_mixnode(mix_unbonding.mix_id);
vec![ vec![
mix_id_exists, mix_exists,
mix_id_unbonding, mix_unbonding,
mix_id_unbonded, mix_unbonded,
mix_id_unbonded_leftover, mix_unbonded_leftover,
] ]
} }
@@ -195,37 +253,35 @@ pub(crate) mod tests {
fn getting_mixnode_bond_by_owner() { fn getting_mixnode_bond_by_owner() {
let mut test = TestSetup::new(); let mut test = TestSetup::new();
let owner_exists = Addr::unchecked(OWNER_EXISTS); let nodes = setup_mix_combinations(&mut test, None);
let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); let mix_exists = &nodes[0];
let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); let mix_unbonding = &nodes[1];
let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); let mix_unbonded = &nodes[2];
let mix_unbonded_leftover = &nodes[3];
let ids = setup_mix_combinations(&mut test);
let mix_id_exists = ids[0];
let mix_id_unbonding = ids[1];
// if this is a normally bonded mixnode, all should be fine // if this is a normally bonded mixnode, all should be fine
let res = must_get_mixnode_bond_by_owner(test.deps().storage, &owner_exists).unwrap(); let res = must_get_mixnode_bond_by_owner(test.deps().storage, &mix_exists.owner).unwrap();
assert_eq!(res.mix_id, mix_id_exists); assert_eq!(res.mix_id, mix_exists.mix_id);
// if node is in the process of unbonding, we still should be capable of retrieving its details // if node is in the process of unbonding, we still should be capable of retrieving its details
let res = must_get_mixnode_bond_by_owner(test.deps().storage, &owner_unbonding).unwrap(); let res =
assert_eq!(res.mix_id, mix_id_unbonding); must_get_mixnode_bond_by_owner(test.deps().storage, &mix_unbonding.owner).unwrap();
assert_eq!(res.mix_id, mix_unbonding.mix_id);
// but if node has unbonded, the information is purged and query fails // but if node has unbonded, the information is purged and query fails
let res = must_get_mixnode_bond_by_owner(test.deps().storage, &owner_unbonded); let res = must_get_mixnode_bond_by_owner(test.deps().storage, &mix_unbonded.owner);
assert_eq!( assert_eq!(
res, res,
Err(MixnetContractError::NoAssociatedMixNodeBond { Err(MixnetContractError::NoAssociatedMixNodeBond {
owner: owner_unbonded owner: mix_unbonded.owner.clone()
}) })
); );
let res = must_get_mixnode_bond_by_owner(test.deps().storage, &owner_unbonded_leftover); let res = must_get_mixnode_bond_by_owner(test.deps().storage, &mix_unbonded_leftover.owner);
assert_eq!( assert_eq!(
res, res,
Err(MixnetContractError::NoAssociatedMixNodeBond { Err(MixnetContractError::NoAssociatedMixNodeBond {
owner: owner_unbonded_leftover owner: mix_unbonded_leftover.owner.clone()
}) })
); );
} }
@@ -234,26 +290,27 @@ pub(crate) mod tests {
fn getting_mixnode_details_by_id() { fn getting_mixnode_details_by_id() {
let mut test = TestSetup::new(); let mut test = TestSetup::new();
let ids = setup_mix_combinations(&mut test); let nodes = setup_mix_combinations(&mut test, None);
let mix_id_exists = ids[0]; let mix_exists = &nodes[0];
let mix_id_unbonding = ids[1]; let mix_unbonding = &nodes[1];
let mix_id_unbonded = ids[2]; let mix_unbonded = &nodes[2];
let mix_id_unbonded_leftover = ids[3]; let mix_unbonded_leftover = &nodes[3];
let res = get_mixnode_details_by_id(test.deps().storage, mix_id_exists) let res = get_mixnode_details_by_id(test.deps().storage, mix_exists.mix_id)
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert_eq!(res.bond_information.mix_id, mix_id_exists); assert_eq!(res.bond_information.mix_id, mix_exists.mix_id);
let res = get_mixnode_details_by_id(test.deps().storage, mix_id_unbonding) let res = get_mixnode_details_by_id(test.deps().storage, mix_unbonding.mix_id)
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert_eq!(res.bond_information.mix_id, mix_id_unbonding); assert_eq!(res.bond_information.mix_id, mix_unbonding.mix_id);
let res = get_mixnode_details_by_id(test.deps().storage, mix_id_unbonded).unwrap(); let res = get_mixnode_details_by_id(test.deps().storage, mix_unbonded.mix_id).unwrap();
assert!(res.is_none()); assert!(res.is_none());
let res = get_mixnode_details_by_id(test.deps().storage, mix_id_unbonded_leftover).unwrap(); let res =
get_mixnode_details_by_id(test.deps().storage, mix_unbonded_leftover.mix_id).unwrap();
assert!(res.is_none()) assert!(res.is_none())
} }
@@ -261,33 +318,69 @@ pub(crate) mod tests {
fn getting_mixnode_details_by_owner() { fn getting_mixnode_details_by_owner() {
let mut test = TestSetup::new(); let mut test = TestSetup::new();
let owner_exists = Addr::unchecked(OWNER_EXISTS); let nodes = setup_mix_combinations(&mut test, None);
let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); let mix_exists = &nodes[0];
let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); let mix_unbonding = &nodes[1];
let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); let mix_unbonded = &nodes[2];
let mix_unbonded_leftover = &nodes[3];
let ids = setup_mix_combinations(&mut test);
let mix_id_exists = ids[0];
let mix_id_unbonding = ids[1];
// if this is a normally bonded mixnode, all should be fine // if this is a normally bonded mixnode, all should be fine
let res = get_mixnode_details_by_owner(test.deps().storage, owner_exists) let res = get_mixnode_details_by_owner(test.deps().storage, mix_exists.owner.clone())
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert_eq!(res.bond_information.mix_id, mix_id_exists); assert_eq!(res.bond_information.mix_id, mix_exists.mix_id);
// if node is in the process of unbonding, we still should be capable of retrieving its details // if node is in the process of unbonding, we still should be capable of retrieving its details
let res = get_mixnode_details_by_owner(test.deps().storage, owner_unbonding) let res = get_mixnode_details_by_owner(test.deps().storage, mix_unbonding.owner.clone())
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert_eq!(res.bond_information.mix_id, mix_id_unbonding); assert_eq!(res.bond_information.mix_id, mix_unbonding.mix_id);
// but if node has unbonded, the information is purged and query fails // but if node has unbonded, the information is purged and query fails
let res = get_mixnode_details_by_owner(test.deps().storage, owner_unbonded).unwrap(); let res =
get_mixnode_details_by_owner(test.deps().storage, mix_unbonded.owner.clone()).unwrap();
assert!(res.is_none()); assert!(res.is_none());
let res = let res =
get_mixnode_details_by_owner(test.deps().storage, owner_unbonded_leftover).unwrap(); get_mixnode_details_by_owner(test.deps().storage, mix_unbonded_leftover.owner.clone())
.unwrap();
assert!(res.is_none());
}
#[test]
fn getting_mixnode_details_by_identity() {
let mut test = TestSetup::new();
let nodes = setup_mix_combinations(&mut test, None);
let mix_exists = &nodes[0];
let mix_unbonding = &nodes[1];
let mix_unbonded = &nodes[2];
let mix_unbonded_leftover = &nodes[3];
// if this is a normally bonded mixnode, all should be fine
let res = get_mixnode_details_by_identity(test.deps().storage, mix_exists.identity.clone())
.unwrap()
.unwrap();
assert_eq!(res.bond_information.mix_id, mix_exists.mix_id);
// if node is in the process of unbonding, we still should be capable of retrieving its details
let res =
get_mixnode_details_by_identity(test.deps().storage, mix_unbonding.identity.clone())
.unwrap()
.unwrap();
assert_eq!(res.bond_information.mix_id, mix_unbonding.mix_id);
// but if node has unbonded, the information is purged and query fails
let res =
get_mixnode_details_by_identity(test.deps().storage, mix_unbonded.identity.clone())
.unwrap();
assert!(res.is_none());
let res = get_mixnode_details_by_identity(
test.deps().storage,
mix_unbonded_leftover.identity.clone(),
)
.unwrap();
assert!(res.is_none()); assert!(res.is_none());
} }
+27 -59
View File
@@ -7,7 +7,10 @@ use crate::constants::{
MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT, MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT, MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT, MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT,
UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT, UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT, UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT, UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT,
}; };
use crate::mixnodes::helpers::{get_mixnode_details_by_id, get_mixnode_details_by_owner}; use crate::mixnodes::helpers::{
attach_mix_details, get_mixnode_details_by_id, get_mixnode_details_by_identity,
get_mixnode_details_by_owner,
};
use crate::rewards::storage as rewards_storage; use crate::rewards::storage as rewards_storage;
use cosmwasm_std::{Deps, Order, StdResult, Storage}; use cosmwasm_std::{Deps, Order, StdResult, Storage};
use cw_storage_plus::Bound; use cw_storage_plus::Bound;
@@ -46,18 +49,12 @@ pub fn query_mixnode_bonds_paged(
)) ))
} }
fn attach_rewarding_info( fn attach_node_details(
storage: &dyn Storage, storage: &dyn Storage,
read_bond: StdResult<(MixId, MixNodeBond)>, read_bond: StdResult<(MixId, MixNodeBond)>,
) -> StdResult<MixNodeDetails> { ) -> StdResult<MixNodeDetails> {
match read_bond { match read_bond {
Ok((_, bond)) => { Ok((_, bond)) => attach_mix_details(storage, bond),
// if we managed to read the bond we MUST be able to also read rewarding information.
// if we fail, this is a hard error and the query should definitely fail and we should investigate
// the reasons for that.
let mix_rewarding = rewards_storage::MIXNODE_REWARDING.load(storage, bond.mix_id)?;
Ok(MixNodeDetails::new(bond, mix_rewarding))
}
Err(err) => Err(err), Err(err) => Err(err),
} }
} }
@@ -76,7 +73,7 @@ pub fn query_mixnodes_details_paged(
let nodes = storage::mixnode_bonds() let nodes = storage::mixnode_bonds()
.range(deps.storage, start, None, Order::Ascending) .range(deps.storage, start, None, Order::Ascending)
.take(limit) .take(limit)
.map(|res| attach_rewarding_info(deps.storage, res)) .map(|res| attach_node_details(deps.storage, res))
.collect::<StdResult<Vec<MixNodeDetails>>>()?; .collect::<StdResult<Vec<MixNodeDetails>>>()?;
let start_next_after = nodes.last().map(|details| details.mix_id()); let start_next_after = nodes.last().map(|details| details.mix_id());
@@ -192,26 +189,12 @@ pub fn query_mixnode_details(deps: Deps<'_>, mix_id: MixId) -> StdResult<Mixnode
}) })
} }
// TODO: change the return type to be consistent with the other details queries
pub fn query_mixnode_details_by_identity( pub fn query_mixnode_details_by_identity(
deps: Deps<'_>, deps: Deps<'_>,
mix_identity: IdentityKey, mix_identity: IdentityKey,
) -> StdResult<Option<MixNodeDetails>> { ) -> StdResult<Option<MixNodeDetails>> {
if let Some(bond_information) = storage::mixnode_bonds() get_mixnode_details_by_identity(deps.storage, mix_identity)
.idx
.identity_key
.item(deps.storage, mix_identity)?
.map(|record| record.1)
{
// if bond exists, rewarding details MUST also exist
let rewarding_details =
rewards_storage::MIXNODE_REWARDING.load(deps.storage, bond_information.mix_id)?;
Ok(Some(MixNodeDetails::new(
bond_information,
rewarding_details,
)))
} else {
Ok(None)
}
} }
pub fn query_mixnode_rewarding_details( pub fn query_mixnode_rewarding_details(
@@ -459,10 +442,10 @@ pub(crate) mod tests {
fn obeys_limits() { fn obeys_limits() {
let mut deps = test_helpers::init_contract(); let mut deps = test_helpers::init_contract();
let _env = mock_env(); let _env = mock_env();
let mut rng = test_helpers::test_rng(); let rng = test_helpers::test_rng();
let limit = 2; let limit = 2;
test_helpers::add_dummy_unbonded_mixnodes(&mut rng, deps.as_mut(), 1000); test_helpers::add_dummy_unbonded_mixnodes(rng, deps.as_mut(), 1000);
let page1 = query_unbonded_mixnodes_paged(deps.as_ref(), None, Some(limit)).unwrap(); let page1 = query_unbonded_mixnodes_paged(deps.as_ref(), None, Some(limit)).unwrap();
assert_eq!(limit, page1.nodes.len() as u32); assert_eq!(limit, page1.nodes.len() as u32);
} }
@@ -471,8 +454,8 @@ pub(crate) mod tests {
fn has_default_limit() { fn has_default_limit() {
let mut deps = test_helpers::init_contract(); let mut deps = test_helpers::init_contract();
let _env = mock_env(); let _env = mock_env();
let mut rng = test_helpers::test_rng(); let rng = test_helpers::test_rng();
test_helpers::add_dummy_unbonded_mixnodes(&mut rng, deps.as_mut(), 1000); test_helpers::add_dummy_unbonded_mixnodes(rng, deps.as_mut(), 1000);
// query without explicitly setting a limit // query without explicitly setting a limit
let page1 = query_unbonded_mixnodes_paged(deps.as_ref(), None, None).unwrap(); let page1 = query_unbonded_mixnodes_paged(deps.as_ref(), None, None).unwrap();
@@ -487,8 +470,8 @@ pub(crate) mod tests {
fn has_max_limit() { fn has_max_limit() {
let mut deps = test_helpers::init_contract(); let mut deps = test_helpers::init_contract();
let _env = mock_env(); let _env = mock_env();
let mut rng = test_helpers::test_rng(); let rng = test_helpers::test_rng();
test_helpers::add_dummy_unbonded_mixnodes(&mut rng, deps.as_mut(), 1000); test_helpers::add_dummy_unbonded_mixnodes(rng, deps.as_mut(), 1000);
// query with a crazily high limit in an attempt to use too many resources // query with a crazily high limit in an attempt to use too many resources
let crazy_limit = 1000; let crazy_limit = 1000;
@@ -589,16 +572,11 @@ pub(crate) mod tests {
fn obeys_limits() { fn obeys_limits() {
let mut deps = test_helpers::init_contract(); let mut deps = test_helpers::init_contract();
let _env = mock_env(); let _env = mock_env();
let mut rng = test_helpers::test_rng(); let rng = test_helpers::test_rng();
let limit = 2; let limit = 2;
let owner = "owner"; let owner = "owner";
test_helpers::add_dummy_unbonded_mixnodes_with_owner( test_helpers::add_dummy_unbonded_mixnodes_with_owner(rng, deps.as_mut(), owner, 1000);
&mut rng,
deps.as_mut(),
owner,
1000,
);
let page1 = query_unbonded_mixnodes_by_owner_paged( let page1 = query_unbonded_mixnodes_by_owner_paged(
deps.as_ref(), deps.as_ref(),
owner.into(), owner.into(),
@@ -613,15 +591,10 @@ pub(crate) mod tests {
fn has_default_limit() { fn has_default_limit() {
let mut deps = test_helpers::init_contract(); let mut deps = test_helpers::init_contract();
let _env = mock_env(); let _env = mock_env();
let mut rng = test_helpers::test_rng(); let rng = test_helpers::test_rng();
let owner = "owner"; let owner = "owner";
test_helpers::add_dummy_unbonded_mixnodes_with_owner( test_helpers::add_dummy_unbonded_mixnodes_with_owner(rng, deps.as_mut(), owner, 1000);
&mut rng,
deps.as_mut(),
owner,
1000,
);
// query without explicitly setting a limit // query without explicitly setting a limit
let page1 = let page1 =
@@ -638,15 +611,10 @@ pub(crate) mod tests {
fn has_max_limit() { fn has_max_limit() {
let mut deps = test_helpers::init_contract(); let mut deps = test_helpers::init_contract();
let _env = mock_env(); let _env = mock_env();
let mut rng = test_helpers::test_rng(); let rng = test_helpers::test_rng();
let owner = "owner"; let owner = "owner";
test_helpers::add_dummy_unbonded_mixnodes_with_owner( test_helpers::add_dummy_unbonded_mixnodes_with_owner(rng, deps.as_mut(), owner, 1000);
&mut rng,
deps.as_mut(),
owner,
1000,
);
// query with a crazily high limit in an attempt to use too many resources // query with a crazily high limit in an attempt to use too many resources
let crazy_limit = 1000; let crazy_limit = 1000;
@@ -836,12 +804,12 @@ pub(crate) mod tests {
fn obeys_limits() { fn obeys_limits() {
let mut deps = test_helpers::init_contract(); let mut deps = test_helpers::init_contract();
let _env = mock_env(); let _env = mock_env();
let mut rng = test_helpers::test_rng(); let rng = test_helpers::test_rng();
let limit = 2; let limit = 2;
let identity = "foomp123"; let identity = "foomp123";
test_helpers::add_dummy_unbonded_mixnodes_with_identity( test_helpers::add_dummy_unbonded_mixnodes_with_identity(
&mut rng, rng,
deps.as_mut(), deps.as_mut(),
identity, identity,
1000, 1000,
@@ -860,10 +828,10 @@ pub(crate) mod tests {
fn has_default_limit() { fn has_default_limit() {
let mut deps = test_helpers::init_contract(); let mut deps = test_helpers::init_contract();
let _env = mock_env(); let _env = mock_env();
let mut rng = test_helpers::test_rng(); let rng = test_helpers::test_rng();
let identity = "foomp123"; let identity = "foomp123";
test_helpers::add_dummy_unbonded_mixnodes_with_identity( test_helpers::add_dummy_unbonded_mixnodes_with_identity(
&mut rng, rng,
deps.as_mut(), deps.as_mut(),
identity, identity,
1000, 1000,
@@ -888,10 +856,10 @@ pub(crate) mod tests {
fn has_max_limit() { fn has_max_limit() {
let mut deps = test_helpers::init_contract(); let mut deps = test_helpers::init_contract();
let _env = mock_env(); let _env = mock_env();
let mut rng = test_helpers::test_rng(); let rng = test_helpers::test_rng();
let identity = "foomp123"; let identity = "foomp123";
test_helpers::add_dummy_unbonded_mixnodes_with_identity( test_helpers::add_dummy_unbonded_mixnodes_with_identity(
&mut rng, rng,
deps.as_mut(), deps.as_mut(),
identity, identity,
1000, 1000,
+10 -9
View File
@@ -1,23 +1,27 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net> // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use crate::constants::{ use crate::constants::{
LAYER_DISTRIBUTION_KEY, MIXNODES_IDENTITY_IDX_NAMESPACE, MIXNODES_OWNER_IDX_NAMESPACE, LAYER_DISTRIBUTION_KEY, MIXNODES_IDENTITY_IDX_NAMESPACE, MIXNODES_OWNER_IDX_NAMESPACE,
MIXNODES_PK_NAMESPACE, MIXNODES_SPHINX_IDX_NAMESPACE, NODE_ID_COUNTER_KEY, MIXNODES_PK_NAMESPACE, MIXNODES_SPHINX_IDX_NAMESPACE, NODE_ID_COUNTER_KEY,
UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE, UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE, PENDING_MIXNODE_CHANGES_NAMESPACE, UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE,
UNBONDED_MIXNODES_PK_NAMESPACE, UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE, UNBONDED_MIXNODES_PK_NAMESPACE,
}; };
use cosmwasm_std::{StdResult, Storage}; use cosmwasm_std::{StdResult, Storage};
use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex, UniqueIndex}; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex, UniqueIndex};
use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::mixnode::UnbondedMixnode; use mixnet_contract_common::mixnode::{PendingMixNodeChanges, UnbondedMixnode};
use mixnet_contract_common::SphinxKey; use mixnet_contract_common::SphinxKey;
use mixnet_contract_common::{Addr, IdentityKey, Layer, LayerDistribution, MixId, MixNodeBond}; use mixnet_contract_common::{Addr, IdentityKey, Layer, LayerDistribution, MixId, MixNodeBond};
pub const LAYERS: Item<'_, LayerDistribution> = Item::new(LAYER_DISTRIBUTION_KEY);
pub const MIXNODE_ID_COUNTER: Item<MixId> = Item::new(NODE_ID_COUNTER_KEY);
pub const PENDING_MIXNODE_CHANGES: Map<MixId, PendingMixNodeChanges> =
Map::new(PENDING_MIXNODE_CHANGES_NAMESPACE);
// keeps track of `node_id -> IdentityKey, Owner, unbonding_height` so we'd known a bit more about past mixnodes // keeps track of `node_id -> IdentityKey, Owner, unbonding_height` so we'd known a bit more about past mixnodes
// if we ever decide it's too bloaty, we can deprecate it and start removing all data in // if we ever decide it's too bloaty, we can deprecate it and start removing all data in
// subsequent migrations // subsequent migrations
pub(crate) struct UnbondedMixnodeIndex<'a> { pub(crate) struct UnbondedMixnodeIndex<'a> {
pub(crate) owner: MultiIndex<'a, Addr, UnbondedMixnode, MixId>, pub(crate) owner: MultiIndex<'a, Addr, UnbondedMixnode, MixId>,
@@ -48,9 +52,6 @@ pub(crate) fn unbonded_mixnodes<'a>(
IndexedMap::new(UNBONDED_MIXNODES_PK_NAMESPACE, indexes) IndexedMap::new(UNBONDED_MIXNODES_PK_NAMESPACE, indexes)
} }
pub(crate) const LAYERS: Item<'_, LayerDistribution> = Item::new(LAYER_DISTRIBUTION_KEY);
pub const MIXNODE_ID_COUNTER: Item<MixId> = Item::new(NODE_ID_COUNTER_KEY);
pub(crate) struct MixnodeBondIndex<'a> { pub(crate) struct MixnodeBondIndex<'a> {
pub(crate) owner: UniqueIndex<'a, Addr, MixNodeBond>, pub(crate) owner: UniqueIndex<'a, Addr, MixNodeBond>,
+637 -60
View File
@@ -1,7 +1,19 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net> // Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use super::storage; use cosmwasm_std::{coin, Addr, Coin, DepsMut, Env, MessageInfo, Response, Storage};
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_decrease_event, new_pending_pledge_increase_event,
};
use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind};
use mixnet_contract_common::{Layer, MixId, MixNode};
use nym_contracts_common::signing::MessageSignature;
use crate::interval::storage as interval_storage; use crate::interval::storage as interval_storage;
use crate::interval::storage::push_new_interval_event; use crate::interval::storage::push_new_interval_event;
use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage;
@@ -13,19 +25,11 @@ use crate::mixnodes::signature_helpers::verify_mixnode_bonding_signature;
use crate::signing::storage as signing_storage; use crate::signing::storage as signing_storage;
use crate::support::helpers::{ use crate::support::helpers::{
ensure_bonded, ensure_epoch_in_progress_state, ensure_is_authorized, ensure_no_existing_bond, ensure_bonded, ensure_epoch_in_progress_state, ensure_is_authorized, ensure_no_existing_bond,
ensure_proxy_match, ensure_sent_by_vesting_contract, validate_pledge, ensure_no_pending_pledge_changes, ensure_proxy_match, ensure_sent_by_vesting_contract,
validate_pledge,
}; };
use cosmwasm_std::{coin, Addr, Coin, DepsMut, Env, MessageInfo, Response, Storage};
use mixnet_contract_common::error::MixnetContractError; use super::storage;
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};
use mixnet_contract_common::{Layer, MixId, MixNode};
use nym_contracts_common::signing::MessageSignature;
pub(crate) fn update_mixnode_layer( pub(crate) fn update_mixnode_layer(
mix_id: MixId, mix_id: MixId,
@@ -195,6 +199,7 @@ pub fn _try_increase_pledge(
) -> Result<Response, MixnetContractError> { ) -> Result<Response, MixnetContractError> {
let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())? let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())?
.ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })?; .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })?;
let mut pending_changes = mix_details.pending_changes;
let mix_id = mix_details.mix_id(); let mix_id = mix_details.mix_id();
// increasing pledge is only allowed if the epoch is currently not in the process of being advanced // increasing pledge is only allowed if the epoch is currently not in the process of being advanced
@@ -202,6 +207,7 @@ pub fn _try_increase_pledge(
ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?; ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?;
ensure_bonded(&mix_details.bond_information)?; ensure_bonded(&mix_details.bond_information)?;
ensure_no_pending_pledge_changes(&pending_changes)?;
let rewarding_denom = rewarding_denom(deps.storage)?; let rewarding_denom = rewarding_denom(deps.storage)?;
let pledge_increase = validate_pledge(increase, coin(1, rewarding_denom))?; let pledge_increase = validate_pledge(increase, coin(1, rewarding_denom))?;
@@ -213,7 +219,95 @@ pub fn _try_increase_pledge(
mix_id, mix_id,
amount: pledge_increase, amount: pledge_increase,
}; };
interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; let epoch_event_id = interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?;
pending_changes.pledge_change = Some(epoch_event_id);
storage::PENDING_MIXNODE_CHANGES.save(deps.storage, mix_id, &pending_changes)?;
Ok(Response::new().add_event(cosmos_event))
}
pub fn try_decrease_pledge(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
decrease_by: Coin,
) -> Result<Response, MixnetContractError> {
_try_decrease_pledge(deps, env, decrease_by, info.sender, None)
}
pub fn try_decrease_pledge_on_behalf(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
decrease_by: Coin,
owner: String,
) -> Result<Response, MixnetContractError> {
ensure_sent_by_vesting_contract(&info, deps.storage)?;
let proxy = info.sender;
let owner = deps.api.addr_validate(&owner)?;
_try_decrease_pledge(deps, env, decrease_by, owner, Some(proxy))
}
pub fn _try_decrease_pledge(
deps: DepsMut<'_>,
env: Env,
decrease_by: 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 mut pending_changes = mix_details.pending_changes;
let mix_id = mix_details.mix_id();
// decreasing pledge is only allowed if the epoch is currently not in the process of being advanced
ensure_epoch_in_progress_state(deps.storage)?;
ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?;
ensure_bonded(&mix_details.bond_information)?;
ensure_no_pending_pledge_changes(&pending_changes)?;
let minimum_pledge = mixnet_params_storage::minimum_mixnode_pledge(deps.storage)?;
// check that the denomination is correct
if decrease_by.denom != minimum_pledge.denom {
return Err(MixnetContractError::WrongDenom {
received: decrease_by.denom,
expected: minimum_pledge.denom,
});
}
// also check if the request contains non-zero amount
// (otherwise it's a no-op and we should we waste gas when resolving events?)
if decrease_by.amount.is_zero() {
return Err(MixnetContractError::ZeroCoinAmount);
}
// decreasing pledge can't result in the new pledge being lower than the minimum amount
let new_pledge_amount = mix_details
.original_pledge()
.amount
.saturating_sub(decrease_by.amount);
if new_pledge_amount < minimum_pledge.amount {
return Err(MixnetContractError::InvalidPledgeReduction {
current: mix_details.original_pledge().amount,
decrease_by: decrease_by.amount,
minimum: minimum_pledge.amount,
denom: minimum_pledge.denom,
});
}
let cosmos_event = new_pending_pledge_decrease_event(mix_id, &decrease_by);
// push the event to execute it at the end of the epoch
let epoch_event = PendingEpochEventKind::DecreasePledge {
mix_id,
decrease_by,
};
let epoch_event_id = interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?;
pending_changes.pledge_change = Some(epoch_event_id);
storage::PENDING_MIXNODE_CHANGES.save(deps.storage, mix_id, &pending_changes)?;
Ok(Response::new().add_event(cosmos_event)) Ok(Response::new().add_event(cosmos_event))
} }
@@ -246,6 +340,9 @@ pub(crate) fn _try_remove_mixnode(
proxy: Option<Addr>, proxy: Option<Addr>,
) -> Result<Response, MixnetContractError> { ) -> Result<Response, MixnetContractError> {
let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &owner)?; let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &owner)?;
let pending_changes = storage::PENDING_MIXNODE_CHANGES
.may_load(deps.storage, existing_bond.mix_id)?
.unwrap_or_default();
// unbonding is only allowed if the epoch is currently not in the process of being advanced // unbonding is only allowed if the epoch is currently not in the process of being advanced
ensure_epoch_in_progress_state(deps.storage)?; ensure_epoch_in_progress_state(deps.storage)?;
@@ -254,6 +351,9 @@ pub(crate) fn _try_remove_mixnode(
ensure_proxy_match(&proxy, &existing_bond.proxy)?; ensure_proxy_match(&proxy, &existing_bond.proxy)?;
ensure_bonded(&existing_bond)?; ensure_bonded(&existing_bond)?;
// if there are any pending requests to change the pledge, wait for them to resolve before allowing the unbonding
ensure_no_pending_pledge_changes(&pending_changes)?;
// set `is_unbonding` field // set `is_unbonding` field
let mut updated_bond = existing_bond.clone(); let mut updated_bond = existing_bond.clone();
updated_bond.is_unbonding = true; updated_bond.is_unbonding = true;
@@ -392,18 +492,22 @@ pub(crate) fn _try_update_mixnode_cost_params(
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use super::*; use cosmwasm_std::testing::mock_info;
use cosmwasm_std::{Order, StdResult, Uint128};
use mixnet_contract_common::mixnode::PendingMixNodeChanges;
use mixnet_contract_common::{
EpochState, EpochStatus, ExecuteMsg, Layer, LayerDistribution, Percent,
};
use crate::contract::execute; use crate::contract::execute;
use crate::mixnet_contract_settings::storage::minimum_mixnode_pledge; use crate::mixnet_contract_settings::storage::minimum_mixnode_pledge;
use crate::mixnodes::helpers::get_mixnode_details_by_id; use crate::mixnodes::helpers::get_mixnode_details_by_id;
use crate::support::tests::fixtures::{good_mixnode_pledge, TEST_COIN_DENOM}; use crate::support::tests::fixtures::{good_mixnode_pledge, TEST_COIN_DENOM};
use crate::support::tests::test_helpers::TestSetup; use crate::support::tests::test_helpers::TestSetup;
use crate::support::tests::{fixtures, test_helpers}; use crate::support::tests::{fixtures, test_helpers};
use cosmwasm_std::testing::mock_info;
use cosmwasm_std::{Order, StdResult, Uint128}; use super::*;
use mixnet_contract_common::{
EpochState, EpochStatus, ExecuteMsg, Layer, LayerDistribution, Percent,
};
#[test] #[test]
fn mixnode_add() { fn mixnode_add() {
@@ -531,7 +635,7 @@ pub mod tests {
let res = try_add_mixnode( let res = try_add_mixnode(
test.deps_mut(), test.deps_mut(),
env.clone(), env.clone(),
info.clone(), info,
mixnode.clone(), mixnode.clone(),
cost_params.clone(), cost_params.clone(),
signature.clone(), signature.clone(),
@@ -601,7 +705,7 @@ pub mod tests {
res, res,
MixnetContractError::SenderIsNotVestingContract { MixnetContractError::SenderIsNotVestingContract {
received: illegal_proxy, received: illegal_proxy,
vesting_contract vesting_contract,
} }
) )
} }
@@ -669,7 +773,7 @@ pub mod tests {
res, res,
Err(MixnetContractError::ProxyMismatch { Err(MixnetContractError::ProxyMismatch {
existing: "None".to_string(), existing: "None".to_string(),
incoming: vesting_contract.into_string() incoming: vesting_contract.into_string(),
}) })
); );
@@ -717,11 +821,66 @@ pub mod tests {
res, res,
MixnetContractError::SenderIsNotVestingContract { MixnetContractError::SenderIsNotVestingContract {
received: illegal_proxy, received: illegal_proxy,
vesting_contract vesting_contract,
} }
) )
} }
#[test]
fn mixnode_remove_is_not_allowed_if_there_are_pending_pledge_changes() {
let mut test = TestSetup::new();
let env = test.env();
// prior increase
let owner = "mix-owner1";
test.add_dummy_mixnode(owner, None);
let sender = mock_info(owner, &[test.coin(1000)]);
try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap();
let res = try_remove_mixnode(test.deps_mut(), env.clone(), sender);
assert_eq!(
res,
Err(MixnetContractError::PendingPledgeChange {
pending_event_id: 1
})
);
// prior decrease
let owner = "mix-owner2";
test.add_dummy_mixnode(owner, Some(Uint128::new(10000000000)));
let sender = mock_info(owner, &[]);
let amount = test.coin(1000);
try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap();
let sender = mock_info(owner, &[test.coin(1000)]);
let res = try_remove_mixnode(test.deps_mut(), env.clone(), sender);
assert_eq!(
res,
Err(MixnetContractError::PendingPledgeChange {
pending_event_id: 2
})
);
// artificial event
let owner = "mix-owner3";
let mix_id = test.add_dummy_mixnode(owner, None);
let pending_change = PendingMixNodeChanges {
pledge_change: Some(1234),
};
storage::PENDING_MIXNODE_CHANGES
.save(test.deps_mut().storage, mix_id, &pending_change)
.unwrap();
let sender = mock_info(owner, &[test.coin(1000)]);
let res = try_remove_mixnode(test.deps_mut(), env, sender);
assert_eq!(
res,
Err(MixnetContractError::PendingPledgeChange {
pending_event_id: 1234
})
);
}
#[test] #[test]
fn updating_mixnode_config() { fn updating_mixnode_config() {
let mut test = TestSetup::new(); let mut test = TestSetup::new();
@@ -760,7 +919,7 @@ pub mod tests {
res, res,
Err(MixnetContractError::ProxyMismatch { Err(MixnetContractError::ProxyMismatch {
existing: "None".to_string(), existing: "None".to_string(),
incoming: vesting_contract.into_string() incoming: vesting_contract.into_string(),
}) })
); );
// "normal" update succeeds // "normal" update succeeds
@@ -812,7 +971,7 @@ pub mod tests {
res, res,
MixnetContractError::SenderIsNotVestingContract { MixnetContractError::SenderIsNotVestingContract {
received: illegal_proxy, received: illegal_proxy,
vesting_contract vesting_contract,
} }
) )
} }
@@ -895,7 +1054,7 @@ pub mod tests {
res, res,
Err(MixnetContractError::ProxyMismatch { Err(MixnetContractError::ProxyMismatch {
existing: "None".to_string(), existing: "None".to_string(),
incoming: vesting_contract.into_string() incoming: vesting_contract.into_string(),
}) })
); );
// "normal" update succeeds // "normal" update succeeds
@@ -918,7 +1077,7 @@ pub mod tests {
assert_eq!( assert_eq!(
PendingIntervalEventKind::ChangeMixCostParams { PendingIntervalEventKind::ChangeMixCostParams {
mix_id, mix_id,
new_costs: update.clone() new_costs: update.clone(),
}, },
event.1.kind event.1.kind
); );
@@ -967,7 +1126,7 @@ pub mod tests {
res, res,
MixnetContractError::SenderIsNotVestingContract { MixnetContractError::SenderIsNotVestingContract {
received: illegal_proxy, received: illegal_proxy,
vesting_contract vesting_contract,
} }
) )
} }
@@ -1013,7 +1172,7 @@ pub mod tests {
info_alice, info_alice,
mixnode1, mixnode1,
cost_params.clone(), cost_params.clone(),
sig1 sig1,
) )
.is_ok()); .is_ok());
@@ -1025,12 +1184,15 @@ pub mod tests {
#[cfg(test)] #[cfg(test)]
mod increasing_mixnode_pledge { mod increasing_mixnode_pledge {
use super::*; use mixnet_contract_common::mixnode::PendingMixNodeChanges;
use mixnet_contract_common::{EpochState, EpochStatus};
use crate::mixnodes::helpers::tests::{ use crate::mixnodes::helpers::tests::{
setup_mix_combinations, OWNER_UNBONDED, OWNER_UNBONDED_LEFTOVER, OWNER_UNBONDING, setup_mix_combinations, OWNER_UNBONDED, OWNER_UNBONDED_LEFTOVER, OWNER_UNBONDING,
}; };
use crate::support::tests::test_helpers::TestSetup; use crate::support::tests::test_helpers::TestSetup;
use mixnet_contract_common::{EpochState, EpochStatus};
use super::*;
#[test] #[test]
fn cant_be_performed_if_epoch_transition_is_in_progress() { fn cant_be_performed_if_epoch_transition_is_in_progress() {
@@ -1108,7 +1270,7 @@ pub mod tests {
res, res,
Err(MixnetContractError::ProxyMismatch { Err(MixnetContractError::ProxyMismatch {
existing: "None".to_string(), existing: "None".to_string(),
incoming: "proxy".to_string() incoming: "proxy".to_string(),
}) })
); );
@@ -1123,7 +1285,7 @@ pub mod tests {
res, res,
Err(MixnetContractError::ProxyMismatch { Err(MixnetContractError::ProxyMismatch {
existing: "proxy".to_string(), existing: "proxy".to_string(),
incoming: "None".to_string() incoming: "None".to_string(),
}) })
); );
@@ -1138,7 +1300,7 @@ pub mod tests {
res, res,
Err(MixnetContractError::ProxyMismatch { Err(MixnetContractError::ProxyMismatch {
existing: "proxy".to_string(), existing: "proxy".to_string(),
incoming: "unrelated-proxy".to_string() incoming: "unrelated-proxy".to_string(),
}) })
) )
} }
@@ -1154,8 +1316,8 @@ pub mod tests {
let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); let owner_unbonded = Addr::unchecked(OWNER_UNBONDED);
let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER);
let ids = setup_mix_combinations(&mut test); let ids = setup_mix_combinations(&mut test, None);
let mix_id_unbonding = ids[1]; let mix_id_unbonding = ids[1].mix_id;
let res = try_increase_pledge( let res = try_increase_pledge(
test.deps_mut(), test.deps_mut(),
@@ -1214,11 +1376,66 @@ pub mod tests {
res, res,
Err(MixnetContractError::InsufficientPledge { Err(MixnetContractError::InsufficientPledge {
received: test.coin(0), received: test.coin(0),
minimum: test.coin(1) minimum: test.coin(1),
}) })
) )
} }
#[test]
fn is_not_allowed_if_there_are_pending_pledge_changes() {
let mut test = TestSetup::new();
let env = test.env();
// prior increase
let owner = "mix-owner1";
test.add_dummy_mixnode(owner, None);
let sender = mock_info(owner, &[test.coin(1000)]);
try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap();
let res = try_increase_pledge(test.deps_mut(), env.clone(), sender);
assert_eq!(
res,
Err(MixnetContractError::PendingPledgeChange {
pending_event_id: 1
})
);
// prior decrease
let owner = "mix-owner2";
test.add_dummy_mixnode(owner, Some(Uint128::new(10000000000)));
let sender = mock_info(owner, &[]);
let amount = test.coin(1000);
try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap();
let sender = mock_info(owner, &[test.coin(1000)]);
let res = try_increase_pledge(test.deps_mut(), env.clone(), sender);
assert_eq!(
res,
Err(MixnetContractError::PendingPledgeChange {
pending_event_id: 2
})
);
// artificial event
let owner = "mix-owner3";
let mix_id = test.add_dummy_mixnode(owner, None);
let pending_change = PendingMixNodeChanges {
pledge_change: Some(1234),
};
storage::PENDING_MIXNODE_CHANGES
.save(test.deps_mut().storage, mix_id, &pending_change)
.unwrap();
let sender = mock_info(owner, &[test.coin(1000)]);
let res = try_increase_pledge(test.deps_mut(), env, sender);
assert_eq!(
res,
Err(MixnetContractError::PendingPledgeChange {
pending_event_id: 1234
})
);
}
#[test] #[test]
fn with_valid_information_creates_pending_event() { fn with_valid_information_creates_pending_event() {
let mut test = TestSetup::new(); let mut test = TestSetup::new();
@@ -1238,38 +1455,398 @@ pub mod tests {
events[0].kind, events[0].kind,
PendingEpochEventKind::PledgeMore { PendingEpochEventKind::PledgeMore {
mix_id, mix_id,
amount: test.coin(1000) amount: test.coin(1000),
} }
); );
} }
#[test]
fn fails_for_illegal_proxy() {
let mut test = TestSetup::new();
let env = test.env();
let illegal_proxy = Addr::unchecked("not-vesting-contract");
let vesting_contract = test.vesting_contract();
let owner = "alice";
test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone());
let res = try_increase_pledge_on_behalf(
test.deps_mut(),
env,
mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]),
owner.to_string(),
)
.unwrap_err();
assert_eq!(
res,
MixnetContractError::SenderIsNotVestingContract {
received: illegal_proxy,
vesting_contract,
}
)
}
} }
#[test] #[cfg(test)]
fn fails_for_illegal_proxy() { mod decreasing_mixnode_pledge {
let mut test = TestSetup::new(); use mixnet_contract_common::mixnode::PendingMixNodeChanges;
let env = test.env(); use mixnet_contract_common::{EpochState, EpochStatus};
let illegal_proxy = Addr::unchecked("not-vesting-contract"); use crate::mixnodes::helpers::tests::{
let vesting_contract = test.vesting_contract(); setup_mix_combinations, OWNER_UNBONDED, OWNER_UNBONDED_LEFTOVER, OWNER_UNBONDING,
};
use crate::support::tests::test_helpers::TestSetup;
let owner = "alice"; use super::*;
test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone()); #[test]
fn cant_be_performed_if_epoch_transition_is_in_progress() {
let bad_states = vec![
EpochState::Rewarding {
last_rewarded: 0,
final_node_id: 0,
},
EpochState::ReconcilingEvents,
EpochState::AdvancingEpoch,
];
let res = try_increase_pledge_on_behalf( for bad_state in bad_states {
test.deps_mut(), let mut test = TestSetup::new();
env, let env = test.env();
mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]), let owner = "mix-owner";
owner.to_string(), let decrease = test.coin(1000);
)
.unwrap_err();
assert_eq!( test.add_dummy_mixnode(owner, Some(Uint128::new(100_000_000_000)));
res,
MixnetContractError::SenderIsNotVestingContract { let mut status = EpochStatus::new(test.rewarding_validator().sender);
received: illegal_proxy, status.state = bad_state;
vesting_contract interval_storage::save_current_epoch_status(test.deps_mut().storage, &status)
.unwrap();
let sender = mock_info(owner, &[]);
let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease);
assert!(matches!(
res,
Err(MixnetContractError::EpochAdvancementInProgress { .. })
));
} }
) }
#[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 decrease = test.coin(1000);
let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease);
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");
// just to make sure that after decrease the value would still be above the minimum
let stake = Uint128::new(100_000_000_000);
let decrease = test.coin(1000);
test.add_dummy_mixnode(owner_without_proxy.as_str(), Some(stake));
test.add_dummy_mixnode_with_illegal_proxy(
owner_with_proxy.as_str(),
Some(stake),
proxy.clone(),
);
let res = _try_decrease_pledge(
test.deps_mut(),
env.clone(),
decrease.clone(),
owner_without_proxy.clone(),
Some(proxy),
);
assert_eq!(
res,
Err(MixnetContractError::ProxyMismatch {
existing: "None".to_string(),
incoming: "proxy".to_string(),
})
);
let res = _try_decrease_pledge(
test.deps_mut(),
env.clone(),
decrease.clone(),
owner_with_proxy.clone(),
None,
);
assert_eq!(
res,
Err(MixnetContractError::ProxyMismatch {
existing: "proxy".to_string(),
incoming: "None".to_string(),
})
);
let res = _try_decrease_pledge(
test.deps_mut(),
env,
decrease,
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();
// just to make sure that after decrease the value would still be above the minimum
let stake = Uint128::new(100_000_000_000);
let decrease = test.coin(1000);
// 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, Some(stake));
let mix_id_unbonding = ids[1].mix_id;
let res = try_decrease_pledge(
test.deps_mut(),
env.clone(),
mock_info(owner_unbonding.as_str(), &[]),
decrease.clone(),
);
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_decrease_pledge(
test.deps_mut(),
env.clone(),
mock_info(owner_unbonded_leftover.as_str(), &[]),
decrease.clone(),
);
assert_eq!(
res,
Err(MixnetContractError::NoAssociatedMixNodeBond {
owner: owner_unbonded_leftover
})
);
let res = try_decrease_pledge(
test.deps_mut(),
env,
mock_info(owner_unbonded.as_str(), &[]),
decrease,
);
assert_eq!(
res,
Err(MixnetContractError::NoAssociatedMixNodeBond {
owner: owner_unbonded
})
)
}
#[test]
fn is_not_allowed_if_it_would_result_going_below_minimum_pledge() {
let mut test = TestSetup::new();
let env = test.env();
let owner = "mix-owner";
let minimum_pledge = minimum_mixnode_pledge(test.deps().storage).unwrap();
let pledge_amount = minimum_pledge.amount + Uint128::new(100);
let pledged = test.coin(pledge_amount.u128());
test.add_dummy_mixnode(owner, Some(pledge_amount));
let invalid_decrease = test.coin(150);
let valid_decrease = test.coin(50);
let sender = mock_info(owner, &[]);
let res = try_decrease_pledge(
test.deps_mut(),
env.clone(),
sender.clone(),
invalid_decrease.clone(),
);
assert_eq!(
res,
Err(MixnetContractError::InvalidPledgeReduction {
current: pledged.amount,
decrease_by: invalid_decrease.amount,
minimum: minimum_pledge.amount,
denom: minimum_pledge.denom,
})
);
let res = try_decrease_pledge(test.deps_mut(), env, sender, valid_decrease);
assert!(res.is_ok())
}
#[test]
fn provided_amount_has_to_be_nonzero() {
let mut test = TestSetup::new();
let env = test.env();
let stake = Uint128::new(100_000_000_000);
let decrease = test.coin(0);
let owner = "mix-owner";
test.add_dummy_mixnode(owner, Some(stake));
let sender = mock_info(owner, &[]);
let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease);
assert_eq!(res, Err(MixnetContractError::ZeroCoinAmount))
}
#[test]
fn is_not_allowed_if_there_are_pending_pledge_changes() {
let mut test = TestSetup::new();
let env = test.env();
let stake = Uint128::new(100_000_000_000);
let decrease = test.coin(1000);
// prior increase
let owner = "mix-owner1";
test.add_dummy_mixnode(owner, Some(stake));
let sender = mock_info(owner, &[test.coin(1000)]);
try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap();
let res = try_decrease_pledge(test.deps_mut(), env.clone(), sender, decrease.clone());
assert_eq!(
res,
Err(MixnetContractError::PendingPledgeChange {
pending_event_id: 1
})
);
// prior decrease
let owner = "mix-owner2";
test.add_dummy_mixnode(owner, Some(stake));
let sender = mock_info(owner, &[]);
let amount = test.coin(1000);
try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap();
let sender = mock_info(owner, &[test.coin(1000)]);
let res = try_decrease_pledge(test.deps_mut(), env.clone(), sender, decrease.clone());
assert_eq!(
res,
Err(MixnetContractError::PendingPledgeChange {
pending_event_id: 2
})
);
// artificial event
let owner = "mix-owner3";
let mix_id = test.add_dummy_mixnode(owner, Some(stake));
let pending_change = PendingMixNodeChanges {
pledge_change: Some(1234),
};
storage::PENDING_MIXNODE_CHANGES
.save(test.deps_mut().storage, mix_id, &pending_change)
.unwrap();
let sender = mock_info(owner, &[test.coin(1000)]);
let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease);
assert_eq!(
res,
Err(MixnetContractError::PendingPledgeChange {
pending_event_id: 1234
})
);
}
#[test]
fn with_valid_information_creates_pending_event() {
let mut test = TestSetup::new();
let env = test.env();
// just to make sure that after decrease the value would still be above the minimum
let stake = Uint128::new(100_000_000_000);
let decrease = test.coin(1000);
let owner = "mix-owner";
let mix_id = test.add_dummy_mixnode(owner, Some(stake));
let events = test.pending_epoch_events();
assert!(events.is_empty());
let sender = mock_info(owner, &[]);
try_decrease_pledge(test.deps_mut(), env, sender, decrease.clone()).unwrap();
let events = test.pending_epoch_events();
assert_eq!(
events[0].kind,
PendingEpochEventKind::DecreasePledge {
mix_id,
decrease_by: decrease,
}
);
}
#[test]
fn fails_for_illegal_proxy() {
let mut test = TestSetup::new();
let env = test.env();
let stake = Uint128::new(100_000_000_000);
let decrease = test.coin(1000);
let illegal_proxy = Addr::unchecked("not-vesting-contract");
let vesting_contract = test.vesting_contract();
let owner = "alice";
test.add_dummy_mixnode_with_illegal_proxy(owner, Some(stake), illegal_proxy.clone());
let res = try_decrease_pledge_on_behalf(
test.deps_mut(),
env,
mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]),
decrease,
owner.to_string(),
)
.unwrap_err();
assert_eq!(
res,
MixnetContractError::SenderIsNotVestingContract {
received: illegal_proxy,
vesting_contract,
}
)
}
} }
} }
+40
View File
@@ -1,2 +1,42 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> // Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use crate::interval::storage as interval_storage;
use crate::mixnodes::storage as mixnodes_storage;
use cosmwasm_std::DepsMut;
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::mixnode::PendingMixNodeChanges;
use mixnet_contract_common::PendingEpochEventKind;
use std::collections::BTreeMap;
pub fn insert_pending_pledge_changes(deps: DepsMut<'_>) -> Result<(), MixnetContractError> {
let last_executed = interval_storage::LAST_PROCESSED_EPOCH_EVENT.load(deps.storage)?;
let last_inserted = interval_storage::EPOCH_EVENT_ID_COUNTER.load(deps.storage)?;
let mut new_pending = BTreeMap::new();
for event_id in last_executed + 1..=last_inserted {
let event = interval_storage::PENDING_EPOCH_EVENTS.load(deps.storage, event_id)?;
match event.kind {
PendingEpochEventKind::PledgeMore { mix_id, .. }
| PendingEpochEventKind::DecreasePledge { mix_id, .. } => {
if new_pending.insert(mix_id, event_id).is_some() {
return Err(MixnetContractError::FailedMigration { comment: format!("mixnode {mix_id} has more than a single pledge change pending for this epoch. Run this migration again after the epoch has finished.") });
}
}
_ => (),
}
}
for (mix_id, event_id) in new_pending {
mixnodes_storage::PENDING_MIXNODE_CHANGES.save(
deps.storage,
mix_id,
&PendingMixNodeChanges {
pledge_change: Some(event_id),
},
)?;
}
Ok(())
}
+2 -2
View File
@@ -253,8 +253,8 @@ mod tests {
fn withdrawing_delegator_reward() { fn withdrawing_delegator_reward() {
let mut test = TestSetup::new(); let mut test = TestSetup::new();
let delegation_amount = Uint128::new(2500_000_000); let delegation_amount = Uint128::new(2_500_000_000);
let delegation_dec = 2500_000_000u32.into_base_decimal().unwrap(); let delegation_dec = 2_500_000_000_u32.into_base_decimal().unwrap();
let mix_id = test.add_dummy_mixnode("mix-owner", None); let mix_id = test.add_dummy_mixnode("mix-owner", None);
let delegator = "delegator"; let delegator = "delegator";
test.add_immediate_delegation(delegator, delegation_amount, mix_id); test.add_immediate_delegation(delegator, delegation_amount, mix_id);
+64 -52
View File
@@ -1,20 +1,8 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use super::storage;
use crate::delegations::storage as delegations_storage;
use crate::interval::storage as interval_storage;
use crate::interval::storage::{push_new_epoch_event, push_new_interval_event};
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::mixnodes::helpers::get_mixnode_details_by_owner;
use crate::mixnodes::storage as mixnodes_storage;
use crate::rewards::helpers;
use crate::rewards::helpers::update_and_save_last_rewarded;
use crate::support::helpers::{
ensure_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, ensure_is_owner,
ensure_proxy_match, ensure_sent_by_vesting_contract, send_to_proxy_or_owner,
};
use cosmwasm_std::{wasm_execute, Addr, DepsMut, Env, MessageInfo, Response}; use cosmwasm_std::{wasm_execute, Addr, DepsMut, Env, MessageInfo, Response};
use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::events::{ use mixnet_contract_common::events::{
new_active_set_update_event, new_mix_rewarding_event, new_active_set_update_event, new_mix_rewarding_event,
@@ -30,6 +18,21 @@ use mixnet_contract_common::reward_params::{
use mixnet_contract_common::{Delegation, EpochState, MixId}; use mixnet_contract_common::{Delegation, EpochState, MixId};
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
use crate::delegations::storage as delegations_storage;
use crate::interval::storage as interval_storage;
use crate::interval::storage::{push_new_epoch_event, push_new_interval_event};
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::mixnodes::helpers::get_mixnode_details_by_owner;
use crate::mixnodes::storage as mixnodes_storage;
use crate::rewards::helpers;
use crate::rewards::helpers::update_and_save_last_rewarded;
use crate::support::helpers::{
ensure_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, ensure_is_owner,
ensure_proxy_match, ensure_sent_by_vesting_contract, send_to_proxy_or_owner,
};
use super::storage;
pub(crate) fn try_reward_mixnode( pub(crate) fn try_reward_mixnode(
deps: DepsMut<'_>, deps: DepsMut<'_>,
env: Env, env: Env,
@@ -233,23 +236,22 @@ pub(crate) fn _try_withdraw_delegator_reward(
mix_id, mix_id,
address: owner.into_string(), address: owner.into_string(),
proxy: proxy.map(Addr::into_string), proxy: proxy.map(Addr::into_string),
}) });
} }
Some(delegation) => delegation, Some(delegation) => delegation,
}; };
// grab associated mixnode rewarding details // grab associated mixnode rewarding details
let mix_rewarding = let mix_rewarding =
storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::InconsistentState { storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::inconsistent_state(
comment: "mixnode rewarding got removed from the storage whilst there's still an existing delegation" "mixnode rewarding got removed from the storage whilst there's still an existing delegation"
.into(), ))?;
})?;
// see if the mixnode is not in the process of unbonding or whether it has already unbonded // see if the mixnode is not in the process of unbonding or whether it has already unbonded
// (in that case the expected path of getting your tokens back is via undelegation) // (in that case the expected path of getting your tokens back is via undelegation)
match mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)? { match mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)? {
Some(mix_bond) if mix_bond.is_unbonding => { Some(mix_bond) if mix_bond.is_unbonding => {
return Err(MixnetContractError::MixnodeIsUnbonding { mix_id }) return Err(MixnetContractError::MixnodeIsUnbonding { mix_id });
} }
None => return Err(MixnetContractError::MixnodeHasUnbonded { mix_id }), None => return Err(MixnetContractError::MixnodeHasUnbonded { mix_id }),
_ => (), _ => (),
@@ -376,17 +378,17 @@ pub(crate) fn try_update_rewarding_params(
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use super::*; use cosmwasm_std::testing::mock_info;
use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::storage as mixnodes_storage;
use crate::support::tests::test_helpers; use crate::support::tests::test_helpers;
use cosmwasm_std::testing::mock_info;
use super::*;
#[cfg(test)] #[cfg(test)]
mod mixnode_rewarding { mod mixnode_rewarding {
use super::*;
use crate::interval::pending_events;
use crate::support::tests::test_helpers::{find_attribute, TestSetup};
use cosmwasm_std::{Decimal, Uint128}; use cosmwasm_std::{Decimal, Uint128};
use mixnet_contract_common::events::{ use mixnet_contract_common::events::{
MixnetEventType, BOND_NOT_FOUND_VALUE, DELEGATES_REWARD_KEY, NO_REWARD_REASON_KEY, MixnetEventType, BOND_NOT_FOUND_VALUE, DELEGATES_REWARD_KEY, NO_REWARD_REASON_KEY,
OPERATOR_REWARD_KEY, PRIOR_DELEGATES_KEY, PRIOR_UNIT_REWARD_KEY, OPERATOR_REWARD_KEY, PRIOR_DELEGATES_KEY, PRIOR_UNIT_REWARD_KEY,
@@ -395,11 +397,17 @@ pub mod tests {
use mixnet_contract_common::helpers::compare_decimals; use mixnet_contract_common::helpers::compare_decimals;
use mixnet_contract_common::{EpochStatus, RewardedSetNodeStatus}; use mixnet_contract_common::{EpochStatus, RewardedSetNodeStatus};
use crate::interval::pending_events;
use crate::support::tests::test_helpers::{find_attribute, TestSetup};
use super::*;
#[cfg(test)] #[cfg(test)]
mod epoch_state_is_correctly_updated { mod epoch_state_is_correctly_updated {
use super::*;
use mixnet_contract_common::EpochState; use mixnet_contract_common::EpochState;
use super::*;
#[test] #[test]
fn when_target_mixnode_unbonded() { fn when_target_mixnode_unbonded() {
let mut test = TestSetup::new(); let mut test = TestSetup::new();
@@ -459,7 +467,7 @@ pub mod tests {
assert_eq!( assert_eq!(
EpochState::Rewarding { EpochState::Rewarding {
last_rewarded: mix_id_unbonded, last_rewarded: mix_id_unbonded,
final_node_id: mix_id_never_existed final_node_id: mix_id_never_existed,
}, },
interval_storage::current_epoch_status(test.deps().storage) interval_storage::current_epoch_status(test.deps().storage)
.unwrap() .unwrap()
@@ -477,7 +485,7 @@ pub mod tests {
assert_eq!( assert_eq!(
EpochState::Rewarding { EpochState::Rewarding {
last_rewarded: mix_id_unbonded_leftover, last_rewarded: mix_id_unbonded_leftover,
final_node_id: mix_id_never_existed final_node_id: mix_id_never_existed,
}, },
interval_storage::current_epoch_status(test.deps().storage) interval_storage::current_epoch_status(test.deps().storage)
.unwrap() .unwrap()
@@ -486,8 +494,8 @@ pub mod tests {
try_reward_mixnode( try_reward_mixnode(
test.deps_mut(), test.deps_mut(),
env.clone(), env,
sender.clone(), sender,
mix_id_never_existed, mix_id_never_existed,
performance, performance,
) )
@@ -578,7 +586,7 @@ pub mod tests {
assert_eq!( assert_eq!(
EpochState::Rewarding { EpochState::Rewarding {
last_rewarded: mix_id, last_rewarded: mix_id,
final_node_id: 100 final_node_id: 100,
}, },
current_state current_state
) )
@@ -1399,12 +1407,15 @@ pub mod tests {
#[cfg(test)] #[cfg(test)]
mod withdrawing_delegator_reward { mod withdrawing_delegator_reward {
use super::*; use cosmwasm_std::{coin, BankMsg, CosmosMsg, Decimal, Uint128};
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount;
use crate::interval::pending_events; use crate::interval::pending_events;
use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::fixtures::TEST_COIN_DENOM;
use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup}; use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup};
use cosmwasm_std::{coin, BankMsg, CosmosMsg, Decimal, Uint128};
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; use super::*;
#[test] #[test]
fn can_only_be_done_if_delegation_exists() { fn can_only_be_done_if_delegation_exists() {
@@ -1772,7 +1783,7 @@ pub mod tests {
res, res,
MixnetContractError::SenderIsNotVestingContract { MixnetContractError::SenderIsNotVestingContract {
received: illegal_proxy, received: illegal_proxy,
vesting_contract vesting_contract,
} }
) )
} }
@@ -1780,11 +1791,13 @@ pub mod tests {
#[cfg(test)] #[cfg(test)]
mod withdrawing_operator_reward { mod withdrawing_operator_reward {
use super::*; use cosmwasm_std::{coin, BankMsg, CosmosMsg, Uint128};
use crate::interval::pending_events; use crate::interval::pending_events;
use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::fixtures::TEST_COIN_DENOM;
use crate::support::tests::test_helpers::TestSetup; use crate::support::tests::test_helpers::TestSetup;
use cosmwasm_std::{coin, BankMsg, CosmosMsg, Uint128};
use super::*;
#[test] #[test]
fn can_only_be_done_if_bond_exists() { fn can_only_be_done_if_bond_exists() {
@@ -1929,7 +1942,7 @@ pub mod tests {
res, res,
MixnetContractError::SenderIsNotVestingContract { MixnetContractError::SenderIsNotVestingContract {
received: illegal_proxy, received: illegal_proxy,
vesting_contract vesting_contract,
} }
) )
} }
@@ -1937,10 +1950,12 @@ pub mod tests {
#[cfg(test)] #[cfg(test)]
mod updating_active_set { mod updating_active_set {
use super::*;
use crate::support::tests::test_helpers::TestSetup;
use mixnet_contract_common::{EpochState, EpochStatus}; use mixnet_contract_common::{EpochState, EpochStatus};
use crate::support::tests::test_helpers::TestSetup;
use super::*;
#[test] #[test]
fn cant_be_performed_if_epoch_transition_is_in_progress_unless_forced() { fn cant_be_performed_if_epoch_transition_is_in_progress_unless_forced() {
let bad_states = vec![ let bad_states = vec![
@@ -2094,7 +2109,7 @@ pub mod tests {
.unwrap() .unwrap()
.active_set_size; .active_set_size;
let env = test.env(); let env = test.env();
let res = try_update_active_set_size(test.deps_mut(), env, owner.clone(), 42, true); let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, true);
assert!(res.is_ok()); assert!(res.is_ok());
let new = storage::REWARDING_PARAMS let new = storage::REWARDING_PARAMS
.load(test.deps().storage) .load(test.deps().storage)
@@ -2140,11 +2155,14 @@ pub mod tests {
#[cfg(test)] #[cfg(test)]
mod updating_rewarding_params { mod updating_rewarding_params {
use super::*;
use crate::support::tests::test_helpers::{assert_decimals, TestSetup};
use cosmwasm_std::Decimal; use cosmwasm_std::Decimal;
use mixnet_contract_common::{EpochState, EpochStatus}; use mixnet_contract_common::{EpochState, EpochStatus};
use crate::support::tests::test_helpers::{assert_decimals, TestSetup};
use super::*;
#[test] #[test]
fn cant_be_performed_if_epoch_transition_is_in_progress_unless_forced() { fn cant_be_performed_if_epoch_transition_is_in_progress_unless_forced() {
let bad_states = vec![ let bad_states = vec![
@@ -2181,7 +2199,7 @@ pub mod tests {
test.deps_mut(), test.deps_mut(),
env.clone(), env.clone(),
owner.clone(), owner.clone(),
update.clone(), update,
false, false,
); );
assert!(matches!( assert!(matches!(
@@ -2189,13 +2207,8 @@ pub mod tests {
Err(MixnetContractError::EpochAdvancementInProgress { .. }) Err(MixnetContractError::EpochAdvancementInProgress { .. })
)); ));
let res_forced = try_update_rewarding_params( let res_forced =
test.deps_mut(), try_update_rewarding_params(test.deps_mut(), env.clone(), owner, update, true);
env.clone(),
owner,
update.clone(),
true,
);
assert!(res_forced.is_ok()) assert!(res_forced.is_ok())
} }
} }
@@ -2308,8 +2321,7 @@ pub mod tests {
let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap();
let env = test.env(); let env = test.env();
let res = let res = try_update_rewarding_params(test.deps_mut(), env, owner, update, true);
try_update_rewarding_params(test.deps_mut(), env, owner.clone(), update, true);
assert!(res.is_ok()); assert!(res.is_ok());
let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap();
assert_ne!(old, new); assert_ne!(old, new);
+45
View File
@@ -6,6 +6,7 @@ use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::storage as mixnodes_storage;
use cosmwasm_std::{wasm_execute, Addr, BankMsg, Coin, CosmosMsg, MessageInfo, Response, Storage}; use cosmwasm_std::{wasm_execute, Addr, BankMsg, Coin, CosmosMsg, MessageInfo, Response, Storage};
use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::mixnode::PendingMixNodeChanges;
use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixId, MixNodeBond}; use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixId, MixNodeBond};
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
@@ -46,6 +47,14 @@ where
owner: String, owner: String,
amount: Coin, amount: Coin,
) -> Result<Self, MixnetContractError>; ) -> Result<Self, MixnetContractError>;
fn maybe_add_track_vesting_decrease_mixnode_pledge(
self,
storage: &dyn Storage,
proxy: Option<Addr>,
owner: String,
amount: Coin,
) -> Result<Self, MixnetContractError>;
} }
impl VestingTracking for Response { impl VestingTracking for Response {
@@ -115,6 +124,33 @@ impl VestingTracking for Response {
Ok(self) Ok(self)
} }
} }
fn maybe_add_track_vesting_decrease_mixnode_pledge(
self,
storage: &dyn Storage,
proxy: Option<Addr>,
owner: String,
amount: Coin,
) -> Result<Self, MixnetContractError> {
if let Some(proxy) = proxy {
let vesting_contract = mixnet_params_storage::vesting_contract_address(storage)?;
// exactly the same possible halting behaviour as in `maybe_add_track_vesting_undelegation_message`.
if proxy != vesting_contract {
return Err(MixnetContractError::ProxyIsNotVestingContract {
received: proxy,
vesting_contract,
});
}
let msg = VestingContractExecuteMsg::TrackDecreasePledge { owner, amount };
let track_decrease_pledge_message = wasm_execute(proxy, &msg, vec![])?;
Ok(self.add_message(track_decrease_pledge_message))
} else {
// there's no proxy so nothing to do
Ok(self)
}
}
} }
// pub fn debug_with_visibility<S: Into<String>>(api: &dyn Api, msg: S) { // pub fn debug_with_visibility<S: Into<String>>(api: &dyn Api, msg: S) {
@@ -342,6 +378,15 @@ pub(crate) fn ensure_bonded(bond: &MixNodeBond) -> Result<(), MixnetContractErro
Ok(()) Ok(())
} }
pub(crate) fn ensure_no_pending_pledge_changes(
pending_changes: &PendingMixNodeChanges,
) -> Result<(), MixnetContractError> {
if let Some(pending_event_id) = pending_changes.pledge_change {
return Err(MixnetContractError::PendingPledgeChange { pending_event_id });
}
Ok(())
}
// check if the target address has already bonded a mixnode or gateway, // check if the target address has already bonded a mixnode or gateway,
// in either case, return an appropriate error // in either case, return an appropriate error
pub(crate) fn ensure_no_existing_bond( pub(crate) fn ensure_no_existing_bond(
+23 -19
View File
@@ -45,7 +45,7 @@ pub mod test_helpers {
use cosmwasm_std::testing::mock_info; use cosmwasm_std::testing::mock_info;
use cosmwasm_std::testing::MockApi; use cosmwasm_std::testing::MockApi;
use cosmwasm_std::testing::MockQuerier; use cosmwasm_std::testing::MockQuerier;
use cosmwasm_std::{coin, Addr, Api, BankMsg, CosmosMsg, Storage}; use cosmwasm_std::{coin, coins, Addr, Api, BankMsg, CosmosMsg, Storage};
use cosmwasm_std::{Coin, Order}; use cosmwasm_std::{Coin, Order};
use cosmwasm_std::{Decimal, Empty, MemoryStorage}; use cosmwasm_std::{Decimal, Empty, MemoryStorage};
use cosmwasm_std::{Deps, OwnedDeps}; use cosmwasm_std::{Deps, OwnedDeps};
@@ -62,7 +62,7 @@ pub mod test_helpers {
use mixnet_contract_common::rewarding::simulator::Simulator; use mixnet_contract_common::rewarding::simulator::Simulator;
use mixnet_contract_common::rewarding::RewardDistribution; use mixnet_contract_common::rewarding::RewardDistribution;
use mixnet_contract_common::{ use mixnet_contract_common::{
construct_family_join_permit, Delegation, EpochState, EpochStatus, Gateway, construct_family_join_permit, Delegation, EpochEventId, EpochState, EpochStatus, Gateway,
GatewayBondingPayload, IdentityKey, IdentityKeyRef, InitialRewardingParams, InstantiateMsg, GatewayBondingPayload, IdentityKey, IdentityKeyRef, InitialRewardingParams, InstantiateMsg,
Interval, MixId, MixNode, MixNodeBond, MixnodeBondingPayload, Percent, Interval, MixId, MixNode, MixNodeBond, MixnodeBondingPayload, Percent,
RewardedSetNodeStatus, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, RewardedSetNodeStatus, SignableGatewayBondingMsg, SignableMixNodeBondingMsg,
@@ -159,6 +159,10 @@ pub mod test_helpers {
coin(amount, rewarding_denom(self.deps().storage).unwrap()) coin(amount, rewarding_denom(self.deps().storage).unwrap())
} }
pub fn coins(&self, amount: u128) -> Vec<Coin> {
coins(amount, rewarding_denom(self.deps().storage).unwrap())
}
pub fn current_interval(&self) -> Interval { pub fn current_interval(&self) -> Interval {
interval_storage::current_interval(self.deps().storage).unwrap() interval_storage::current_interval(self.deps().storage).unwrap()
} }
@@ -189,8 +193,7 @@ pub mod test_helpers {
let family_head = FamilyHead::new(&identity); let family_head = FamilyHead::new(&identity);
let owner = head_mixnode.owner; let owner = head_mixnode.owner;
let nonce = let nonce = signing_storage::get_signing_nonce(self.deps().storage, owner).unwrap();
signing_storage::get_signing_nonce(self.deps().storage, owner.clone()).unwrap();
let proxy = if vesting { let proxy = if vesting {
Some(self.vesting_contract()) Some(self.vesting_contract())
@@ -243,6 +246,17 @@ pub mod test_helpers {
(mix_id, keys) (mix_id, keys)
} }
pub fn set_pending_pledge_change(&mut self, mix_id: MixId, event_id: Option<EpochEventId>) {
let mut changes = mixnodes_storage::PENDING_MIXNODE_CHANGES
.load(self.deps().storage, mix_id)
.unwrap_or_default();
changes.pledge_change = Some(event_id.unwrap_or(12345));
mixnodes_storage::PENDING_MIXNODE_CHANGES
.save(self.deps_mut().storage, mix_id, &changes)
.unwrap();
}
pub fn add_dummy_mixnode(&mut self, owner: &str, stake: Option<Uint128>) -> MixId { pub fn add_dummy_mixnode(&mut self, owner: &str, stake: Option<Uint128>) -> MixId {
let stake = self.make_mix_pledge(stake); let stake = self.make_mix_pledge(stake);
let (mixnode, owner_signature, _) = let (mixnode, owner_signature, _) =
@@ -541,13 +555,8 @@ pub mod test_helpers {
sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), sphinx_key: legit_sphinx_keys.public_key().to_base58_string(),
..tests::fixtures::mix_node_fixture() ..tests::fixtures::mix_node_fixture()
}; };
let msg = mixnode_bonding_sign_payload( let msg =
self.deps(), mixnode_bonding_sign_payload(self.deps(), sender, None, mixnode.clone(), stake);
sender,
None,
mixnode.clone(),
stake.clone(),
);
let owner_signature = ed25519_sign_message(msg, keypair.private_key()); let owner_signature = ed25519_sign_message(msg, keypair.private_key());
(mixnode, owner_signature, keypair) (mixnode, owner_signature, keypair)
@@ -570,13 +579,8 @@ pub mod test_helpers {
..tests::fixtures::gateway_fixture() ..tests::fixtures::gateway_fixture()
}; };
let msg = gateway_bonding_sign_payload( let msg =
self.deps(), gateway_bonding_sign_payload(self.deps(), sender, None, gateway.clone(), stake);
sender,
None,
gateway.clone(),
stake.clone(),
);
let owner_signature = ed25519_sign_message(msg, keypair.private_key()); let owner_signature = ed25519_sign_message(msg, keypair.private_key());
(gateway, owner_signature) (gateway, owner_signature)
@@ -1102,7 +1106,7 @@ pub mod test_helpers {
deps.branch(), deps.branch(),
&env, &env,
env.block.height, env.block.height,
Addr::unchecked(&format!("owner{}", i)), Addr::unchecked(format!("owner{}", i)),
mix_id, mix_id,
tests::fixtures::good_mixnode_pledge().pop().unwrap(), tests::fixtures::good_mixnode_pledge().pop().unwrap(),
None, None,
@@ -780,7 +780,7 @@ mod tests {
let err = app let err = app
.execute_contract( .execute_contract(
Addr::unchecked(TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string()), Addr::unchecked(TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string()),
flex_addr.clone(), flex_addr,
&proposal_wrong_exp, &proposal_wrong_exp,
&[], &[],
) )
@@ -856,12 +856,7 @@ mod tests {
let proposer = TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(); let proposer = TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string();
let res = app let res = app
.execute_contract( .execute_contract(Addr::unchecked(&proposer), flex_addr, &proposal, &[])
Addr::unchecked(&proposer),
flex_addr.clone(),
&proposal,
&[],
)
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
res.custom_attrs(1), res.custom_attrs(1),
@@ -262,7 +262,7 @@ fn paging_works() {
services: vec![ services: vec![
service_info(1, nym_address1.clone(), announcer1.clone()), service_info(1, nym_address1.clone(), announcer1.clone()),
service_info(2, nym_address1.clone(), announcer1.clone()), service_info(2, nym_address1.clone(), announcer1.clone()),
service_info(3, nym_address2.clone(), announcer1.clone()), service_info(3, nym_address2.clone(), announcer1),
], ],
per_page: 3, per_page: 3,
start_next_after: Some(3), start_next_after: Some(3),
@@ -272,8 +272,8 @@ fn paging_works() {
setup.query_all_with_limit(Some(3), Some(3)), setup.query_all_with_limit(Some(3), Some(3)),
PagedServicesListResponse { PagedServicesListResponse {
services: vec![ services: vec![
service_info(4, nym_address1.clone(), announcer2.clone()), service_info(4, nym_address1, announcer2.clone()),
service_info(5, nym_address2.clone(), announcer2.clone()), service_info(5, nym_address2, announcer2),
], ],
per_page: 3, per_page: 3,
start_next_after: Some(5), start_next_after: Some(5),
+4
View File
@@ -160,10 +160,14 @@ pub fn execute(
deps, deps,
), ),
ExecuteMsg::PledgeMore { amount } => try_pledge_more(deps, env, info, amount), ExecuteMsg::PledgeMore { amount } => try_pledge_more(deps, env, info, amount),
ExecuteMsg::DecreasePledge { amount } => try_decrease_pledge(deps, info, amount),
ExecuteMsg::UnbondMixnode {} => try_unbond_mixnode(info, deps), ExecuteMsg::UnbondMixnode {} => try_unbond_mixnode(info, deps),
ExecuteMsg::TrackUnbondMixnode { owner, amount } => { ExecuteMsg::TrackUnbondMixnode { owner, amount } => {
try_track_unbond_mixnode(&owner, amount, info, deps) try_track_unbond_mixnode(&owner, amount, info, deps)
} }
ExecuteMsg::TrackDecreasePledge { owner, amount } => {
try_track_decrease_mixnode_pledge(&owner, amount, info, deps)
}
ExecuteMsg::BondGateway { ExecuteMsg::BondGateway {
gateway, gateway,
owner_signature, owner_signature,
+4 -1
View File
@@ -1,5 +1,5 @@
use crate::storage::AccountStorageKey; use crate::storage::AccountStorageKey;
use cosmwasm_std::{Addr, OverflowError, StdError, Uint128}; use cosmwasm_std::{Addr, Coin, OverflowError, StdError, Uint128};
use mixnet_contract_common::MixId; use mixnet_contract_common::MixId;
use thiserror::Error; use thiserror::Error;
@@ -58,6 +58,9 @@ pub enum ContractError {
#[error("VESTING ({}): No bond found for account {0}", line!())] #[error("VESTING ({}): No bond found for account {0}", line!())]
NoBondFound(String), NoBondFound(String),
#[error("VESTING: Attempted to reduce mixnode bond pledge below zero! The current pledge is {current} and we attempted to reduce it by {decrease_by}.")]
InvalidBondPledgeReduction { current: Coin, decrease_by: Coin },
#[error("VESTING ({}): Action can only be executed by account owner -> {0}", line!())] #[error("VESTING ({}): Action can only be executed by account owner -> {0}", line!())]
NotOwner(String), NotOwner(String),
+1 -1
View File
@@ -5,7 +5,7 @@
#![warn(clippy::unwrap_used)] #![warn(clippy::unwrap_used)]
pub mod contract; pub mod contract;
mod errors; pub mod errors;
mod queries; mod queries;
mod queued_migrations; mod queued_migrations;
mod storage; mod storage;
+19 -1
View File
@@ -1,7 +1,7 @@
use crate::errors::ContractError; use crate::errors::ContractError;
use crate::vesting::Account; use crate::vesting::Account;
use cosmwasm_std::Order;
use cosmwasm_std::{Addr, Api, Storage, Uint128}; use cosmwasm_std::{Addr, Api, Storage, Uint128};
use cosmwasm_std::{Coin, Order};
use cw_storage_plus::{Item, Map}; use cw_storage_plus::{Item, Map};
use mixnet_contract_common::{IdentityKey, MixId}; use mixnet_contract_common::{IdentityKey, MixId};
use vesting_contract_common::PledgeData; use vesting_contract_common::PledgeData;
@@ -157,6 +157,24 @@ pub fn save_bond_pledge(
Ok(()) Ok(())
} }
pub fn decrease_bond_pledge(
key: AccountStorageKey,
amount: Coin,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
let mut existing = BOND_PLEDGES.load(storage, key)?;
if existing.amount.amount <= amount.amount {
// this shouldn't be possible!
// (but check for it anyway... just in case)
return Err(ContractError::InvalidBondPledgeReduction {
current: existing.amount,
decrease_by: amount,
});
}
existing.amount.amount -= amount.amount;
save_bond_pledge(key, &existing, storage)
}
pub fn load_gateway_pledge( pub fn load_gateway_pledge(
key: AccountStorageKey, key: AccountStorageKey,
storage: &dyn Storage, storage: &dyn Storage,
+2 -2
View File
@@ -325,12 +325,12 @@ pub mod helpers {
}; };
let env = mock_env(); let env = mock_env();
let info = mock_info("admin", &[]); let info = mock_info("admin", &[]);
instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); instantiate(deps.as_mut(), env, info, msg).unwrap();
deps deps
} }
pub fn vesting_account_mid_fixture(storage: &mut dyn Storage, env: &Env) -> Account { pub fn vesting_account_mid_fixture(storage: &mut dyn Storage, env: &Env) -> Account {
let start_time_ts = env.block.time.clone(); let start_time_ts = env.block.time;
let start_time = env.block.time.seconds() - 7200; let start_time = env.block.time.seconds() - 7200;
let periods = populate_vesting_periods( let periods = populate_vesting_periods(
start_time, start_time,
@@ -27,6 +27,12 @@ pub trait MixnodeBondingAccount {
storage: &mut dyn Storage, storage: &mut dyn Storage,
) -> Result<Response, ContractError>; ) -> Result<Response, ContractError>;
fn try_decrease_mixnode_pledge(
&self,
amount: Coin,
storage: &mut dyn Storage,
) -> Result<Response, ContractError>;
fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result<Response, ContractError>; fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result<Response, ContractError>;
fn try_track_unbond_mixnode( fn try_track_unbond_mixnode(
@@ -35,6 +41,12 @@ pub trait MixnodeBondingAccount {
storage: &mut dyn Storage, storage: &mut dyn Storage,
) -> Result<(), ContractError>; ) -> Result<(), ContractError>;
fn try_track_decrease_mixnode_pledge(
&self,
amount: Coin,
storage: &mut dyn Storage,
) -> Result<(), ContractError>;
fn try_update_mixnode_config( fn try_update_mixnode_config(
&self, &self,
new_config: MixNodeConfigUpdate, new_config: MixNodeConfigUpdate,
+31 -2
View File
@@ -19,8 +19,8 @@ use mixnet_contract_common::{
use vesting_contract_common::events::{ use vesting_contract_common::events::{
new_ownership_transfer_event, new_periodic_vesting_account_event, new_ownership_transfer_event, new_periodic_vesting_account_event,
new_staking_address_update_event, new_track_gateway_unbond_event, new_staking_address_update_event, new_track_gateway_unbond_event,
new_track_mixnode_unbond_event, new_track_reward_event, new_track_undelegation_event, new_track_mixnode_pledge_decrease_event, new_track_mixnode_unbond_event,
new_vested_coins_withdraw_event, new_track_reward_event, new_track_undelegation_event, new_vested_coins_withdraw_event,
}; };
use vesting_contract_common::messages::VestingSpecification; use vesting_contract_common::messages::VestingSpecification;
use vesting_contract_common::PledgeCap; use vesting_contract_common::PledgeCap;
@@ -278,6 +278,19 @@ pub fn try_pledge_more(
account.try_pledge_additional_tokens(additional_pledge, &env, deps.storage) account.try_pledge_additional_tokens(additional_pledge, &env, deps.storage)
} }
pub fn try_decrease_pledge(
deps: DepsMut<'_>,
info: MessageInfo,
amount: Coin,
) -> Result<Response, ContractError> {
let mix_denom = MIX_DENOM.load(deps.storage)?;
// perform basic validation - is it correct demon, is it non-zero, etc.
let decrease = validate_funds(&[amount], mix_denom)?;
let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
account.try_decrease_mixnode_pledge(decrease, deps.storage)
}
/// Unbond a mixnode, sends [mixnet_contract_common::ExecuteMsg::UnbondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. /// 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> { 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)?; let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
@@ -299,6 +312,22 @@ pub fn try_track_unbond_mixnode(
Ok(Response::new().add_event(new_track_mixnode_unbond_event())) Ok(Response::new().add_event(new_track_mixnode_unbond_event()))
} }
/// Tracks decreasing mixnode pledge. Invoked by the mixnet contract after successful event reconciliation.
/// A separate BankMsg containing the specified amount was sent in the same transaction.
pub fn try_track_decrease_mixnode_pledge(
owner: &str,
amount: Coin,
info: MessageInfo,
deps: DepsMut<'_>,
) -> Result<Response, ContractError> {
if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? {
return Err(ContractError::NotMixnetContract(info.sender));
}
let account = account_from_address(owner, deps.storage, deps.api)?;
account.try_track_decrease_mixnode_pledge(amount, deps.storage)?;
Ok(Response::new().add_event(new_track_mixnode_pledge_decrease_event()))
}
/// Track reward collection, invoked by the mixnert contract after sucessful reward compounding or claiming /// Track reward collection, invoked by the mixnert contract after sucessful reward compounding or claiming
pub fn try_track_reward( pub fn try_track_reward(
deps: DepsMut<'_>, deps: DepsMut<'_>,
@@ -11,9 +11,9 @@ use mixnet_contract_common::mixnode::MixNodeConfigUpdate;
use mixnet_contract_common::mixnode::MixNodeCostParams; use mixnet_contract_common::mixnode::MixNodeCostParams;
use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, MixNode}; use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, MixNode};
use vesting_contract_common::events::{ use vesting_contract_common::events::{
new_vesting_mixnode_bonding_event, new_vesting_mixnode_unbonding_event, new_vesting_decrease_pledge_event, new_vesting_mixnode_bonding_event,
new_vesting_pledge_more_event, new_vesting_update_mixnode_config_event, new_vesting_mixnode_unbonding_event, new_vesting_pledge_more_event,
new_vesting_update_mixnode_cost_params_event, new_vesting_update_mixnode_config_event, new_vesting_update_mixnode_cost_params_event,
}; };
use vesting_contract_common::PledgeData; use vesting_contract_common::PledgeData;
@@ -109,6 +109,51 @@ impl MixnodeBondingAccount for Account {
.add_event(new_vesting_pledge_more_event())) .add_event(new_vesting_pledge_more_event()))
} }
fn try_decrease_mixnode_pledge(
&self,
amount: Coin,
storage: &mut dyn Storage,
) -> Result<Response, ContractError> {
match self.load_mixnode_pledge(storage)? {
Some(pledge) => {
if pledge.amount.amount <= amount.amount {
return Err(ContractError::InvalidBondPledgeReduction {
current: pledge.amount,
decrease_by: amount,
});
}
}
None => {
return Err(ContractError::NoBondFound(
self.owner_address().as_str().to_string(),
));
}
}
let msg = MixnetExecuteMsg::DecreasePledgeOnBehalf {
owner: self.owner_address().into_string(),
decrease_by: amount,
};
let decrease_pledge_message =
wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?;
Ok(Response::new()
.add_message(decrease_pledge_message)
.add_event(new_vesting_decrease_pledge_event()))
}
fn try_track_decrease_mixnode_pledge(
&self,
amount: Coin,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
let new_balance = Uint128::new(self.load_balance(storage)?.u128() + amount.amount.u128());
self.save_balance(new_balance, storage)?;
self.decrease_mixnode_pledge(amount, storage)
}
fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result<Response, ContractError> { fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result<Response, ContractError> {
let msg = MixnetExecuteMsg::UnbondMixnodeOnBehalf { let msg = MixnetExecuteMsg::UnbondMixnodeOnBehalf {
owner: self.owner_address().into_string(), owner: self.owner_address().into_string(),
@@ -135,8 +180,7 @@ impl MixnodeBondingAccount for Account {
let new_balance = Uint128::new(self.load_balance(storage)?.u128() + amount.amount.u128()); let new_balance = Uint128::new(self.load_balance(storage)?.u128() + amount.amount.u128());
self.save_balance(new_balance, storage)?; self.save_balance(new_balance, storage)?;
self.remove_mixnode_pledge(storage)?; self.remove_mixnode_pledge(storage)
Ok(())
} }
fn try_update_mixnode_config( fn try_update_mixnode_config(
+12 -4
View File
@@ -1,10 +1,10 @@
use super::VestingPeriod; use super::VestingPeriod;
use crate::errors::ContractError; use crate::errors::ContractError;
use crate::storage::{ use crate::storage::{
count_subdelegations_for_mix, load_balance, load_bond_pledge, load_delegation_timestamps, count_subdelegations_for_mix, decrease_bond_pledge, load_balance, load_bond_pledge,
load_gateway_pledge, load_withdrawn, remove_bond_pledge, remove_delegation, load_delegation_timestamps, load_gateway_pledge, load_withdrawn, remove_bond_pledge,
remove_gateway_pledge, save_account, save_balance, save_bond_pledge, save_gateway_pledge, remove_delegation, remove_gateway_pledge, save_account, save_balance, save_bond_pledge,
save_withdrawn, AccountStorageKey, BlockTimestampSecs, DELEGATIONS, KEY, save_gateway_pledge, save_withdrawn, AccountStorageKey, BlockTimestampSecs, DELEGATIONS, KEY,
}; };
use crate::traits::VestingAccount; use crate::traits::VestingAccount;
use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128}; use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128};
@@ -247,6 +247,14 @@ impl Account {
remove_bond_pledge(self.storage_key(), storage) remove_bond_pledge(self.storage_key(), storage)
} }
pub fn decrease_mixnode_pledge(
&self,
amount: Coin,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
decrease_bond_pledge(self.storage_key, amount, storage)
}
pub fn load_gateway_pledge( pub fn load_gateway_pledge(
&self, &self,
storage: &dyn Storage, storage: &dyn Storage,
+8 -8
View File
@@ -68,7 +68,7 @@ mod tests {
cap: Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))), cap: Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))),
}; };
// Try creating an account when not admin // Try creating an account when not admin
let response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); let response = execute(deps.as_mut(), env.clone(), info, msg.clone());
assert!(response.is_err()); assert!(response.is_err());
let info = mock_info("admin", &coins(1_000_000_000_000, TEST_COIN_DENOM)); let info = mock_info("admin", &coins(1_000_000_000_000, TEST_COIN_DENOM));
@@ -142,7 +142,7 @@ mod tests {
assert!(old_owner_account.is_none()); assert!(old_owner_account.is_none());
// Not the owner // Not the owner
let response = execute(deps.as_mut(), env.clone(), info.clone(), msg); let response = execute(deps.as_mut(), env.clone(), info, msg);
assert!(response.is_err()); assert!(response.is_err());
// can't stake on behalf of the original owner anymore, but we can do it for the new one! // can't stake on behalf of the original owner anymore, but we can do it for the new one!
@@ -190,7 +190,7 @@ mod tests {
assert!(response.is_ok()); assert!(response.is_ok());
let info = mock_info("owner", &[]); let info = mock_info("owner", &[]);
let response = execute(deps.as_mut(), env.clone(), info, msg.clone()); let response = execute(deps.as_mut(), env.clone(), info, msg);
assert!(response.is_err()); assert!(response.is_err());
} }
@@ -202,7 +202,7 @@ mod tests {
let msg = ExecuteMsg::TransferOwnership { let msg = ExecuteMsg::TransferOwnership {
to_address: "new_owner".to_string(), to_address: "new_owner".to_string(),
}; };
let response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); let response = execute(deps.as_mut(), env.clone(), info.clone(), msg);
// Only owner can transfer // Only owner can transfer
assert!(response.is_err()); assert!(response.is_err());
@@ -213,7 +213,7 @@ mod tests {
}, },
}; };
env.block.time = Timestamp::from_nanos(env.block.time.nanos() + 100_000_000_000_000_000); env.block.time = Timestamp::from_nanos(env.block.time.nanos() + 100_000_000_000_000_000);
let response = execute(deps.as_mut(), env.clone(), info, msg.clone()); let response = execute(deps.as_mut(), env, info, msg);
// Only owner can withdraw // Only owner can withdraw
assert!(response.is_err()); assert!(response.is_err());
} }
@@ -510,7 +510,7 @@ mod tests {
assert_eq!( assert_eq!(
account.load_balance(&deps.storage).unwrap(), account.load_balance(&deps.storage).unwrap(),
Uint128::new(1000_000_000_000) Uint128::new(1_000_000_000_000)
); );
account account
@@ -588,7 +588,7 @@ mod tests {
}; };
let info = mock_info("admin", &coins(1_000_000_000_000, TEST_COIN_DENOM)); let info = mock_info("admin", &coins(1_000_000_000_000, TEST_COIN_DENOM));
let _response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); let _response = execute(deps.as_mut(), env.clone(), info, msg);
let account = load_account(Addr::unchecked("owner"), &deps.storage) let account = load_account(Addr::unchecked("owner"), &deps.storage)
.unwrap() .unwrap()
.unwrap(); .unwrap();
@@ -837,7 +837,7 @@ mod tests {
// but the additional one is going to fail // but the additional one is going to fail
let res = vesting_account let res = vesting_account
.try_delegate_to_mixnode(mix_id, delegation.clone(), &env, &mut deps.storage) .try_delegate_to_mixnode(mix_id, delegation, &env, &mut deps.storage)
.unwrap_err(); .unwrap_err();
assert_eq!( assert_eq!(
@@ -196,6 +196,8 @@ Follow these steps to update the information about your mix node which is public
You can either do this graphically via the Desktop Wallet, or the CLI. You can either do this graphically via the Desktop Wallet, or the CLI.
#### Updating node information via the Desktop Wallet #### Updating node information via the Desktop Wallet
Node operators can update node information and change node settings such as the amount of self-bond (increase or decrease) via the desktop wallet.
* Navigate to the `Bonding` page and click the `Node Settings` link in the top right corner: * Navigate to the `Bonding` page and click the `Node Settings` link in the top right corner:
![Bonding page](../images/wallet-screenshots/bonding.png) ![Bonding page](../images/wallet-screenshots/bonding.png)
+24 -6
View File
@@ -1,8 +1,11 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net> // Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use futures::Stream;
use nym_crypto::asymmetric::identity; use nym_crypto::asymmetric::identity;
use nym_gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver}; use nym_gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio_stream::StreamMap; use tokio_stream::StreamMap;
pub(crate) type GatewayMessages = Vec<Vec<u8>>; pub(crate) type GatewayMessages = Vec<Vec<u8>>;
@@ -20,11 +23,7 @@ impl GatewaysReader {
} }
} }
pub fn stream_map(&mut self) -> &mut StreamMap<String, MixnetMessageReceiver> { pub fn add_receivers(
&mut self.stream_map
}
pub fn add_recievers(
&mut self, &mut self,
id: identity::PublicKey, id: identity::PublicKey,
message_receiver: MixnetMessageReceiver, message_receiver: MixnetMessageReceiver,
@@ -35,8 +34,27 @@ impl GatewaysReader {
self.ack_map.insert(channel_id, ack_receiver); self.ack_map.insert(channel_id, ack_receiver);
} }
pub fn remove_recievers(&mut self, id: &str) { pub fn remove_receivers(&mut self, id: &str) {
self.stream_map.remove(id); self.stream_map.remove(id);
self.ack_map.remove(id); self.ack_map.remove(id);
} }
} }
impl Stream for GatewaysReader {
// just return whatever is returned by our main `stream_map`
type Item = <StreamMap<String, MixnetMessageReceiver> as Stream>::Item;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
// exhaust the ack map if possible
match Pin::new(&mut self.ack_map).poll_next(cx) {
Poll::Ready(None) => {
// this should have never happened!
return Poll::Ready(None);
}
Poll::Ready(Some(_item)) => (),
Poll::Pending => (),
}
Pin::new(&mut self.stream_map).poll_next(cx)
}
}
@@ -42,10 +42,10 @@ impl PacketReceiver {
match update { match update {
GatewayClientUpdate::New(id, (message_receiver, ack_receiver)) => { GatewayClientUpdate::New(id, (message_receiver, ack_receiver)) => {
self.gateways_reader self.gateways_reader
.add_recievers(id, message_receiver, ack_receiver); .add_receivers(id, message_receiver, ack_receiver);
} }
GatewayClientUpdate::Failure(id) => { GatewayClientUpdate::Failure(id) => {
self.gateways_reader.remove_recievers(&id.to_string()); self.gateways_reader.remove_receivers(&id.to_string());
} }
} }
} }
@@ -66,11 +66,12 @@ impl PacketReceiver {
// unwrap here is fine as it can only return a `None` if the PacketSender has died // unwrap here is fine as it can only return a `None` if the PacketSender has died
// and if that was the case, then the entire monitor is already in an undefined state // and if that was the case, then the entire monitor is already in an undefined state
update = self.clients_updater.next() => self.process_gateway_update(update.unwrap()), update = self.clients_updater.next() => self.process_gateway_update(update.unwrap()),
// similarly gateway reader will never return a `None` as it's implemented gateway_message = self.gateways_reader.next() => {
// as an infinite stream that returns Poll::Pending if it doesn't have anything let Some((_gateway_id, message)) = gateway_message else {
// to return log::error!("the gateways reader stream has terminated!");
Some((_gateway_id, message)) = self.gateways_reader.stream_map().next() => { continue
self.process_gateway_messages(message) };
self.process_gateway_messages(message)
} }
} }
} }
+1 -4
View File
@@ -27,10 +27,7 @@ async fn get_gateway_bond_annotated(
let gateways = cache let gateways = cache
.gateways_annotated_filtered() .gateways_annotated_filtered()
.await .await
.ok_or(ErrorResponse::new( .ok_or_else(|| ErrorResponse::new("no data available", Status::ServiceUnavailable))?;
"no data available",
Status::ServiceUnavailable,
))?;
gateways gateways
.into_iter() .into_iter()
+3 -3
View File
@@ -3191,7 +3191,7 @@ dependencies = [
[[package]] [[package]]
name = "nym-bin-common" name = "nym-bin-common"
version = "0.4.0" version = "0.5.0"
dependencies = [ dependencies = [
"atty", "atty",
"clap", "clap",
@@ -3841,7 +3841,7 @@ dependencies = [
[[package]] [[package]]
name = "nym-vesting-contract" name = "nym-vesting-contract"
version = "1.3.0" version = "1.3.1"
dependencies = [ dependencies = [
"cosmwasm-derive", "cosmwasm-derive",
"cosmwasm-std", "cosmwasm-std",
@@ -3859,7 +3859,7 @@ dependencies = [
[[package]] [[package]]
name = "nym-vesting-contract-common" name = "nym-vesting-contract-common"
version = "0.4.0" version = "0.5.0"
dependencies = [ dependencies = [
"cosmwasm-std", "cosmwasm-std",
"nym-contracts-common", "nym-contracts-common",
+2
View File
@@ -59,6 +59,7 @@ fn main() {
mixnet::bond::bond_gateway, mixnet::bond::bond_gateway,
mixnet::bond::bond_mixnode, mixnet::bond::bond_mixnode,
mixnet::bond::pledge_more, mixnet::bond::pledge_more,
mixnet::bond::decrease_pledge,
mixnet::bond::gateway_bond_details, mixnet::bond::gateway_bond_details,
mixnet::bond::get_pending_operator_rewards, mixnet::bond::get_pending_operator_rewards,
mixnet::bond::mixnode_bond_details, mixnet::bond::mixnode_bond_details,
@@ -114,6 +115,7 @@ fn main() {
vesting::bond::vesting_bond_gateway, vesting::bond::vesting_bond_gateway,
vesting::bond::vesting_bond_mixnode, vesting::bond::vesting_bond_mixnode,
vesting::bond::vesting_pledge_more, vesting::bond::vesting_pledge_more,
vesting::bond::vesting_decrease_pledge,
vesting::bond::vesting_unbond_gateway, vesting::bond::vesting_unbond_gateway,
vesting::bond::vesting_unbond_mixnode, vesting::bond::vesting_unbond_mixnode,
vesting::bond::vesting_update_mixnode_cost_params, vesting::bond::vesting_update_mixnode_cost_params,
@@ -160,6 +160,33 @@ pub async fn pledge_more(
)?) )?)
} }
#[tauri::command]
pub async fn decrease_pledge(
fee: Option<Fee>,
decrease_by: DecCoin,
state: tauri::State<'_, WalletState>,
) -> Result<TransactionExecuteResult, BackendError> {
let guard = state.read().await;
let decrease_by_base = guard.attempt_convert_to_base_coin(decrease_by.clone())?;
let fee_amount = guard.convert_tx_fee(fee.as_ref());
log::info!(
">>> Decrease pledge, pledge_decrease_display = {}, pledge_decrease_base = {}, fee = {:?}",
decrease_by,
decrease_by_base,
fee,
);
let res = guard
.current_client()?
.nyxd
.decrease_pledge(decrease_by_base, fee)
.await?;
log::info!("<<< tx hash = {}", res.transaction_hash);
log::trace!("<<< {:?}", res);
Ok(TransactionExecuteResult::from_execute_result(
res, fee_amount,
)?)
}
#[tauri::command] #[tauri::command]
pub async fn unbond_mixnode( pub async fn unbond_mixnode(
fee: Option<Fee>, fee: Option<Fee>,
@@ -151,6 +151,33 @@ pub async fn vesting_pledge_more(
)?) )?)
} }
#[tauri::command]
pub async fn vesting_decrease_pledge(
fee: Option<Fee>,
decrease_by: DecCoin,
state: tauri::State<'_, WalletState>,
) -> Result<TransactionExecuteResult, BackendError> {
let guard = state.read().await;
let decrease_by_base = guard.attempt_convert_to_base_coin(decrease_by.clone())?;
let fee_amount = guard.convert_tx_fee(fee.as_ref());
log::info!(
">>> Decrease pledge with locked tokens, pledge_decrease_display = {}, pledge_decrease_base = {}, fee = {:?}",
decrease_by,
decrease_by_base,
fee,
);
let res = guard
.current_client()?
.nyxd
.vesting_decrease_pledge(decrease_by_base, fee)
.await?;
log::info!("<<< tx hash = {}", res.transaction_hash);
log::trace!("<<< {:?}", res);
Ok(TransactionExecuteResult::from_execute_result(
res, fee_amount,
)?)
}
#[tauri::command] #[tauri::command]
pub async fn vesting_unbond_mixnode( pub async fn vesting_unbond_mixnode(
fee: Option<Fee>, fee: Option<Fee>,
+4
View File
@@ -14,6 +14,7 @@ import {
TBondMixNodeArgs, TBondMixNodeArgs,
TBondMixnodeSignatureArgs, TBondMixnodeSignatureArgs,
TBondMoreArgs, TBondMoreArgs,
TDecreaseBondArgs,
} from '../types'; } from '../types';
import { invokeWrapper } from './wrapper'; import { invokeWrapper } from './wrapper';
@@ -51,3 +52,6 @@ export const unbond = async (type: EnumNodeType) => {
}; };
export const bondMore = async (args: TBondMoreArgs) => invokeWrapper<TransactionExecuteResult>('pledge_more', args); export const bondMore = async (args: TBondMoreArgs) => invokeWrapper<TransactionExecuteResult>('pledge_more', args);
export const decreaseBond = async (args: TDecreaseBondArgs) =>
invokeWrapper<TransactionExecuteResult>('decrease_pledge', args);
+6
View File
@@ -126,3 +126,9 @@ export const vestingBondMore = async ({ fee, additionalPledge }: { fee?: Fee; ad
fee, fee,
additionalPledge, additionalPledge,
}); });
export const vestingDecreaseBond = async ({ fee, decreaseBy }: { fee?: Fee; decreaseBy: DecCoin }) =>
invokeWrapper<TransactionExecuteResult>('vesting_decrease_pledge', {
fee,
decreaseBy,
});
+5
View File
@@ -59,6 +59,11 @@ export type TBondMoreArgs = {
fee?: Fee; fee?: Fee;
}; };
export type TDecreaseBondArgs = {
decreaseBy: DecCoin;
fee?: Fee;
};
export type TNodeDescription = { export type TNodeDescription = {
name: string; name: string;
description: string; description: string;
@@ -23,12 +23,6 @@ pub(crate) async fn execute(
nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::DelegateVesting(args) => { nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::DelegateVesting(args) => {
nym_cli_commands::validator::mixnet::delegators::vesting_delegate_to_mixnode::vesting_delegate_to_mixnode(args, create_signing_client(global_args, network_details)?).await nym_cli_commands::validator::mixnet::delegators::vesting_delegate_to_mixnode::vesting_delegate_to_mixnode(args, create_signing_client(global_args, network_details)?).await
} }
nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::PledgeMore(args) => {
nym_cli_commands::validator::mixnet::delegators::pledge_more::pledge_more(args, create_signing_client(global_args, network_details)?).await
}
nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::PledgeMoreVesting(args) => {
nym_cli_commands::validator::mixnet::delegators::vesting_pledge_more::vesting_pledge_more(args, create_signing_client(global_args, network_details)?).await
}
nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::Undelegate(args) => { nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::Undelegate(args) => {
nym_cli_commands::validator::mixnet::delegators::undelegate_from_mixnode::undelegate_from_mixnode(args, create_signing_client(global_args, network_details)?).await nym_cli_commands::validator::mixnet::delegators::undelegate_from_mixnode::undelegate_from_mixnode(args, create_signing_client(global_args, network_details)?).await
} }
@@ -4,6 +4,8 @@
use nym_cli_commands::context::{create_signing_client, ClientArgs}; use nym_cli_commands::context::{create_signing_client, ClientArgs};
use nym_network_defaults::NymNetworkDetails; use nym_network_defaults::NymNetworkDetails;
pub(crate) mod settings;
pub(crate) async fn execute( pub(crate) async fn execute(
global_args: ClientArgs, global_args: ClientArgs,
gateway: nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGateway, gateway: nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGateway,
@@ -19,7 +21,16 @@ pub(crate) async fn execute(
nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::CreateGatewayBondingSignPayload(args) => { nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::CreateGatewayBondingSignPayload(args) => {
nym_cli_commands::validator::mixnet::operators::gateway::gateway_bonding_sign_payload::create_payload(args,create_signing_client(global_args, network_details)?).await nym_cli_commands::validator::mixnet::operators::gateway::gateway_bonding_sign_payload::create_payload(args,create_signing_client(global_args, network_details)?).await
} }
_ => unreachable!(), nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::Settings(settings) => {
settings::execute(global_args, settings, network_details).await?
}
nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::VestingBond(args) => {
nym_cli_commands::validator::mixnet::operators::gateway::vesting_bond_gateway::vesting_bond_gateway(args, create_signing_client(global_args, network_details)?).await
}
nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::VestingUnbond(_args) => {
nym_cli_commands::validator::mixnet::operators::gateway::vesting_unbond_gateway::vesting_unbond_gateway(create_signing_client(global_args, network_details)?).await
}
} }
Ok(()) Ok(())
} }
@@ -0,0 +1,21 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_cli_commands::context::{create_signing_client, ClientArgs};
use nym_network_defaults::NymNetworkDetails;
pub(crate) async fn execute(
global_args: ClientArgs,
settings: nym_cli_commands::validator::mixnet::operators::gateway::settings::MixnetOperatorsGatewaySettings,
network_details: &NymNetworkDetails,
) -> anyhow::Result<()> {
match settings.command {
nym_cli_commands::validator::mixnet::operators::gateway::settings::MixnetOperatorsGatewaySettingsCommands::UpdateConfig(args) => {
nym_cli_commands::validator::mixnet::operators::gateway::settings::update_config::update_config(args, create_signing_client(global_args, network_details)?).await
}
nym_cli_commands::validator::mixnet::operators::gateway::settings::MixnetOperatorsGatewaySettingsCommands::VestingUpdateConfig(args) => {
nym_cli_commands::validator::mixnet::operators::gateway::settings::vesting_update_config::vesting_update_config(args, create_signing_client(global_args, network_details)?).await
}
}
Ok(())
}
@@ -36,6 +36,18 @@ pub(crate) async fn execute(
nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::CreateMixnodeBondingSignPayload(args) => { nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::CreateMixnodeBondingSignPayload(args) => {
nym_cli_commands::validator::mixnet::operators::mixnode::mixnode_bonding_sign_payload::create_payload(args,create_signing_client(global_args, network_details)?).await nym_cli_commands::validator::mixnet::operators::mixnode::mixnode_bonding_sign_payload::create_payload(args,create_signing_client(global_args, network_details)?).await
} }
nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::PledgeMore(args) => {
nym_cli_commands::validator::mixnet::operators::mixnode::pledge_more::pledge_more(args, create_signing_client(global_args, network_details)?).await
}
nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::PledgeMoreVesting(args) => {
nym_cli_commands::validator::mixnet::operators::mixnode::vesting_pledge_more::vesting_pledge_more(args, create_signing_client(global_args, network_details)?).await
}
nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::DecreasePledge(args) => {
nym_cli_commands::validator::mixnet::operators::mixnode::decrease_pledge::decrease_pledge(args, create_signing_client(global_args, network_details)?).await
}
nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::DecreasePledgeVesting(args) => {
nym_cli_commands::validator::mixnet::operators::mixnode::vesting_decrease_pledge::vesting_decrease_pledge(args, create_signing_client(global_args, network_details)?).await
}
_ => unreachable!(), _ => unreachable!(),
} }
Ok(()) Ok(())