Stubbing away
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::mixnode::NodeRewardParams;
|
||||
use crate::StateParams;
|
||||
use crate::{Gateway, IdentityKey, MixNode};
|
||||
use cosmwasm_std::Addr;
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -59,6 +58,15 @@ pub enum ExecuteMsg {
|
||||
// nonce of the current rewarding interval
|
||||
rewarding_interval_nonce: u32,
|
||||
},
|
||||
DelegateToMixnodeOnBehalf {
|
||||
mix_identity: IdentityKey,
|
||||
delegate_addr: Addr,
|
||||
coin: Coin,
|
||||
},
|
||||
UnDelegateFromMixnodeOnBehalf {
|
||||
mix_identity: IdentityKey,
|
||||
delegate_addr: Addr,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
|
||||
@@ -141,6 +141,27 @@ pub fn execute(
|
||||
ExecuteMsg::FinishMixnodeRewarding {
|
||||
rewarding_interval_nonce,
|
||||
} => transactions::try_finish_mixnode_rewarding(deps, info, rewarding_interval_nonce),
|
||||
ExecuteMsg::DelegateToMixnodeOnBehalf {
|
||||
mix_identity,
|
||||
delegate_addr,
|
||||
coin
|
||||
} => transactions::try_delegate_to_mixnode_on_behalf(
|
||||
deps,
|
||||
env,
|
||||
info,
|
||||
mix_identity,
|
||||
delegate_addr,
|
||||
coin
|
||||
),
|
||||
ExecuteMsg::UnDelegateFromMixnodeOnBehalf {
|
||||
mix_identity,
|
||||
delegate_addr,
|
||||
} => transactions::try_remove_delegation_from_mixnode_on_behalf(
|
||||
deps,
|
||||
info,
|
||||
mix_identity,
|
||||
delegate_addr,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ use crate::queries;
|
||||
use crate::storage::*;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::{
|
||||
attr, coins, BankMsg, Coin, Decimal, DepsMut, Env, MessageInfo, Response, StdResult, Uint128,
|
||||
attr, coins, Addr, BankMsg, Coin, Decimal, DepsMut, Env, MessageInfo, Response, StdResult,
|
||||
Uint128,
|
||||
};
|
||||
use cosmwasm_storage::ReadonlyBucket;
|
||||
use mixnet_contract::mixnode::NodeRewardParams;
|
||||
@@ -656,6 +657,34 @@ pub(crate) fn try_delegate_to_mixnode(
|
||||
// check if the delegation contains any funds of the appropriate denomination
|
||||
validate_delegation_stake(&info.funds)?;
|
||||
|
||||
let delegate_addr = info.sender.clone();
|
||||
let amount = info.funds[0].amount;
|
||||
|
||||
_try_delegate_to_mixnode(deps, env, mix_identity, &delegate_addr, amount)
|
||||
}
|
||||
|
||||
pub(crate) fn try_delegate_to_mixnode_on_behalf(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
_info: MessageInfo,
|
||||
mix_identity: IdentityKey,
|
||||
delegate_addr: Addr,
|
||||
coin: Coin,
|
||||
) -> Result<Response, ContractError> {
|
||||
// check if the delegation contains any funds of the appropriate denomination
|
||||
validate_delegation_stake(&[coin.clone()])?;
|
||||
let amount = coin.amount;
|
||||
|
||||
_try_delegate_to_mixnode(deps, env, mix_identity, &delegate_addr, amount)
|
||||
}
|
||||
|
||||
fn _try_delegate_to_mixnode(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
mix_identity: IdentityKey,
|
||||
delegate_addr: &Addr,
|
||||
amount: Uint128,
|
||||
) -> Result<Response, ContractError> {
|
||||
// check if the target node actually exists
|
||||
let mut current_bond = match mixnodes_read(deps.storage).load(mix_identity.as_bytes()) {
|
||||
Ok(bond) => bond,
|
||||
@@ -666,14 +695,12 @@ pub(crate) fn try_delegate_to_mixnode(
|
||||
}
|
||||
};
|
||||
|
||||
let amount = info.funds[0].amount;
|
||||
|
||||
// update total_delegation of this node
|
||||
current_bond.total_delegation.amount += info.funds[0].amount;
|
||||
current_bond.total_delegation.amount += amount;
|
||||
mixnodes(deps.storage).save(mix_identity.as_bytes(), ¤t_bond)?;
|
||||
|
||||
let mut delegation_bucket = mix_delegations(deps.storage, &mix_identity);
|
||||
let sender_bytes = info.sender.as_bytes();
|
||||
let sender_bytes = delegate_addr.as_bytes();
|
||||
|
||||
// write the delegation
|
||||
let new_amount = match delegation_bucket.may_load(sender_bytes)? {
|
||||
@@ -684,7 +711,7 @@ pub(crate) fn try_delegate_to_mixnode(
|
||||
let new_delegation = RawDelegationData::new(new_amount, env.block.height);
|
||||
delegation_bucket.save(sender_bytes, &new_delegation)?;
|
||||
|
||||
reverse_mix_delegations(deps.storage, &info.sender).save(mix_identity.as_bytes(), &())?;
|
||||
reverse_mix_delegations(deps.storage, delegate_addr).save(mix_identity.as_bytes(), &())?;
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
@@ -693,9 +720,28 @@ pub(crate) fn try_remove_delegation_from_mixnode(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
mix_identity: IdentityKey,
|
||||
) -> Result<Response, ContractError> {
|
||||
let sender = info.sender.clone();
|
||||
_try_remove_delegation_from_mixnode(deps, info, mix_identity, &sender)
|
||||
}
|
||||
|
||||
pub(crate) fn try_remove_delegation_from_mixnode_on_behalf(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
mix_identity: IdentityKey,
|
||||
delegate_addr: Addr,
|
||||
) -> Result<Response, ContractError> {
|
||||
_try_remove_delegation_from_mixnode(deps, info, mix_identity, &delegate_addr)
|
||||
}
|
||||
|
||||
fn _try_remove_delegation_from_mixnode(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
mix_identity: IdentityKey,
|
||||
delegate_addr: &Addr,
|
||||
) -> Result<Response, ContractError> {
|
||||
let mut delegation_bucket = mix_delegations(deps.storage, &mix_identity);
|
||||
let sender_bytes = info.sender.as_bytes();
|
||||
let sender_bytes = delegate_addr.as_bytes();
|
||||
match delegation_bucket.may_load(sender_bytes)? {
|
||||
Some(delegation) => {
|
||||
// remove delegation from the buckets
|
||||
|
||||
@@ -6,6 +6,10 @@ use cosmwasm_std::{
|
||||
Timestamp,
|
||||
};
|
||||
use mixnet_contract::IdentityKey;
|
||||
|
||||
pub const NUM_VESTING_PERIODS: u64 = 8;
|
||||
pub const VESTING_PERIOD: u64 = 3 * 30 * 86400;
|
||||
|
||||
/// Instantiate the contract.
|
||||
///
|
||||
/// `deps` contains Storage, API and Querier
|
||||
@@ -38,10 +42,10 @@ pub fn execute(
|
||||
}
|
||||
ExecuteMsg::CreatePeriodicVestingAccount {
|
||||
address,
|
||||
coins,
|
||||
coin,
|
||||
start_time,
|
||||
periods,
|
||||
} => try_create_periodic_vesting_account(address, coins, start_time, periods, env, deps),
|
||||
} => try_create_periodic_vesting_account(address, coin, start_time, env, deps),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,15 +67,26 @@ fn try_undelegate_from_mixnode(
|
||||
|
||||
fn try_create_periodic_vesting_account(
|
||||
address: Addr,
|
||||
coins: Vec<Coin>,
|
||||
coin: Coin,
|
||||
start_time: Option<u64>,
|
||||
periods: Option<Vec<VestingPeriod>>,
|
||||
env: Env,
|
||||
deps: DepsMut,
|
||||
) -> Result<Response, ContractError> {
|
||||
let start_time = start_time.unwrap_or_else(|| env.block.time.seconds());
|
||||
let periods = periods.unwrap_or_else(|| vec![VestingPeriod::default(); 8]);
|
||||
let account = crate::vesting::PeriodicVestingAccount::new(address, coins, start_time, periods);
|
||||
let mut periods = Vec::new();
|
||||
// There are eight 3 month periods in two years
|
||||
for i in 0..(NUM_VESTING_PERIODS - 1) {
|
||||
let period = VestingPeriod {
|
||||
start_time: start_time + i * VESTING_PERIOD,
|
||||
};
|
||||
periods.push(period);
|
||||
}
|
||||
let account = crate::vesting::PeriodicVestingAccount::new(
|
||||
address,
|
||||
coin,
|
||||
Timestamp::from_seconds(start_time),
|
||||
periods,
|
||||
);
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -105,8 +120,7 @@ fn try_get_locked_coins(
|
||||
env: Env,
|
||||
deps: Deps,
|
||||
) -> Result<Vec<Coin>, ContractError> {
|
||||
let block_time = block_time.unwrap_or_else(|| env.block.time);
|
||||
|
||||
let block_time = block_time.unwrap_or(env.block.time);
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -115,7 +129,7 @@ fn try_get_spendable_coins(
|
||||
env: Env,
|
||||
deps: Deps,
|
||||
) -> Result<Vec<Coin>, ContractError> {
|
||||
let block_time = block_time.unwrap_or_else(|| env.block.time);
|
||||
let block_time = block_time.unwrap_or(env.block.time);
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -124,7 +138,7 @@ fn try_get_vested_coins(
|
||||
env: Env,
|
||||
deps: Deps,
|
||||
) -> Result<Vec<Coin>, ContractError> {
|
||||
let block_time = block_time.unwrap_or_else(|| env.block.time);
|
||||
let block_time = block_time.unwrap_or(env.block.time);
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -133,7 +147,7 @@ fn try_get_vesting_coins(
|
||||
env: Env,
|
||||
deps: Deps,
|
||||
) -> Result<Vec<Coin>, ContractError> {
|
||||
let block_time = block_time.unwrap_or_else(|| env.block.time);
|
||||
let block_time = block_time.unwrap_or(env.block.time);
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ mod contract;
|
||||
mod errors;
|
||||
mod messages;
|
||||
mod vesting;
|
||||
mod storage;
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::vesting::VestingPeriod;
|
||||
use cosmwasm_std::{Addr, Coin, Timestamp};
|
||||
use mixnet_contract::IdentityKey;
|
||||
use mixnet_contract::{IdentityKey};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -17,7 +17,7 @@ pub enum ExecuteMsg {
|
||||
},
|
||||
CreatePeriodicVestingAccount {
|
||||
address: Addr,
|
||||
coins: Vec<Coin>,
|
||||
coin: Coin,
|
||||
start_time: Option<u64>,
|
||||
periods: Option<Vec<VestingPeriod>>,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
use cosmwasm_std::{Order, StdResult, Storage, Uint128};
|
||||
use cosmwasm_storage::{
|
||||
bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton,
|
||||
Singleton,
|
||||
};
|
||||
use mixnet_contract::IdentityKey;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::vesting::{DelegationData, PeriodicVestingAccount};
|
||||
// storage prefixes
|
||||
// all of them must be unique and presumably not be a prefix of a different one
|
||||
// keeping them as short as possible is also desirable as they are part of each stored key
|
||||
// it's not as important for singletons, but is a nice optimisation for buckets
|
||||
|
||||
// singletons
|
||||
|
||||
// buckets
|
||||
const PREFIX_ACCOUNTS: &[u8] = b"ac";
|
||||
const PREFIX_ACCOUNT_DELEGATIONS: &[u8] = b"ad";
|
||||
// Contract-level stuff
|
||||
|
||||
pub fn accounts_mut(storage: &mut dyn Storage) -> Bucket<PeriodicVestingAccount> {
|
||||
bucket(storage, PREFIX_ACCOUNTS)
|
||||
}
|
||||
|
||||
pub fn accounts(storage: &dyn Storage) -> ReadonlyBucket<PeriodicVestingAccount> {
|
||||
bucket_read(storage, PREFIX_ACCOUNTS)
|
||||
}
|
||||
|
||||
pub fn account_delegations_mut(
|
||||
storage: &mut dyn Storage,
|
||||
) -> Bucket<HashMap<IdentityKey, Vec<DelegationData>>> {
|
||||
bucket(storage, PREFIX_ACCOUNT_DELEGATIONS)
|
||||
}
|
||||
|
||||
pub fn account_delegations(
|
||||
storage: &dyn Storage,
|
||||
) -> ReadonlyBucket<HashMap<IdentityKey, Vec<DelegationData>>> {
|
||||
bucket_read(storage, PREFIX_ACCOUNT_DELEGATIONS)
|
||||
}
|
||||
|
||||
pub fn get_account(storage: &dyn Storage, address: &str) -> Option<PeriodicVestingAccount> {
|
||||
// Due to using may_load this should be safe to unwrap
|
||||
accounts(storage).may_load(address.as_bytes()).unwrap()
|
||||
}
|
||||
|
||||
pub fn get_account_delegations(
|
||||
storage: &dyn Storage,
|
||||
address: &str,
|
||||
) -> Option<HashMap<IdentityKey, Vec<DelegationData>>> {
|
||||
// Due to using may_load this should be safe to unwrap
|
||||
account_delegations(storage)
|
||||
.may_load(address.as_bytes())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn set_account_delegations(
|
||||
storage: &mut dyn Storage,
|
||||
address: &str,
|
||||
delegations: HashMap<IdentityKey, Vec<DelegationData>>,
|
||||
) -> StdResult<()> {
|
||||
account_delegations_mut(storage).save(address.as_bytes(), &delegations)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::helpers::identity_and_owner_to_bytes;
|
||||
use crate::support::tests::helpers::{
|
||||
gateway_bond_fixture, gateway_fixture, mix_node_fixture, mixnode_bond_fixture,
|
||||
raw_delegation_fixture,
|
||||
};
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::testing::{mock_dependencies, MockStorage};
|
||||
use cosmwasm_std::{coin, Addr, Uint128};
|
||||
use mixnet_contract::{Gateway, MixNode};
|
||||
|
||||
#[test]
|
||||
fn mixnode_single_read_retrieval() {
|
||||
let mut storage = MockStorage::new();
|
||||
let bond1 = mixnode_bond_fixture();
|
||||
let bond2 = mixnode_bond_fixture();
|
||||
mixnodes(&mut storage).save(b"bond1", &bond1).unwrap();
|
||||
mixnodes(&mut storage).save(b"bond2", &bond2).unwrap();
|
||||
|
||||
let res1 = mixnodes_read(&storage).load(b"bond1").unwrap();
|
||||
let res2 = mixnodes_read(&storage).load(b"bond2").unwrap();
|
||||
assert_eq!(bond1, res1);
|
||||
assert_eq!(bond2, res2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_mixnode_bond() {
|
||||
let mut storage = MockStorage::new();
|
||||
let node_owner: Addr = Addr::unchecked("node-owner");
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
|
||||
// produces an error if target mixnode doesn't exist
|
||||
let res = read_mixnode_bond(&storage, node_owner.as_bytes());
|
||||
assert!(res.is_err());
|
||||
|
||||
// returns appropriate value otherwise
|
||||
let bond_value = 1000;
|
||||
|
||||
let mixnode_bond = MixNodeBond {
|
||||
bond_amount: coin(bond_value, DENOM),
|
||||
total_delegation: coin(0, DENOM),
|
||||
owner: node_owner.clone(),
|
||||
layer: Layer::One,
|
||||
block_height: 12_345,
|
||||
mix_node: MixNode {
|
||||
identity_key: node_identity.clone(),
|
||||
..mix_node_fixture()
|
||||
},
|
||||
profit_margin_percent: Some(10),
|
||||
};
|
||||
|
||||
mixnodes(&mut storage)
|
||||
.save(node_identity.as_bytes(), &mixnode_bond)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Uint128(bond_value),
|
||||
read_mixnode_bond(&storage, node_identity.as_bytes()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_mixnode_delegations_read_retrieval() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity1: IdentityKey = "foo1".into();
|
||||
let delegation_owner1 = Addr::unchecked("bar1");
|
||||
let node_identity2: IdentityKey = "foo2".into();
|
||||
let delegation_owner2 = Addr::unchecked("bar2");
|
||||
let raw_delegation1 = RawDelegationData::new(1u128.into(), 1000);
|
||||
let raw_delegation2 = RawDelegationData::new(2u128.into(), 2000);
|
||||
|
||||
mix_delegations(&mut deps.storage, &node_identity1)
|
||||
.save(delegation_owner1.as_bytes(), &raw_delegation1)
|
||||
.unwrap();
|
||||
mix_delegations(&mut deps.storage, &node_identity2)
|
||||
.save(delegation_owner2.as_bytes(), &raw_delegation2)
|
||||
.unwrap();
|
||||
|
||||
let res1 = all_mix_delegations_read::<RawDelegationData>(&deps.storage)
|
||||
.load(&*identity_and_owner_to_bytes(
|
||||
&node_identity1,
|
||||
&delegation_owner1,
|
||||
))
|
||||
.unwrap();
|
||||
let res2 = all_mix_delegations_read::<RawDelegationData>(&deps.storage)
|
||||
.load(&*identity_and_owner_to_bytes(
|
||||
&node_identity2,
|
||||
&delegation_owner2,
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(raw_delegation1, res1);
|
||||
assert_eq!(raw_delegation2, res2);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod increasing_mix_delegated_stakes {
|
||||
use super::*;
|
||||
use crate::queries::query_mixnode_delegations_paged;
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
|
||||
#[test]
|
||||
fn when_there_are_no_delegations() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
let total_increase = increase_mix_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
42,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// there was no increase
|
||||
assert!(total_increase.is_zero());
|
||||
|
||||
// there are no 'new' delegations magically added
|
||||
assert!(
|
||||
query_mixnode_delegations_paged(deps.as_ref(), node_identity, None, None)
|
||||
.unwrap()
|
||||
.delegations
|
||||
.is_empty()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_there_is_a_single_delegation() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
let delegation_blockstamp = 42;
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
let delegator_address = Addr::unchecked("bob");
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegator_address.as_bytes(),
|
||||
&RawDelegationData::new(1000u128.into(), delegation_blockstamp),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let total_increase = increase_mix_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(Uint128(1), total_increase);
|
||||
|
||||
// amount is incremented, block height remains the same
|
||||
assert_eq!(
|
||||
RawDelegationData::new(1001u128.into(), 42),
|
||||
mix_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_there_is_a_single_delegation_depending_on_blockstamp() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
let delegation_blockstamp = 42;
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
let delegator_address = Addr::unchecked("bob");
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegator_address.as_bytes(),
|
||||
&RawDelegationData::new(1000u128.into(), delegation_blockstamp),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let total_increase = increase_mix_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + MINIMUM_BLOCK_AGE_FOR_REWARDING - 1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// there was no increase
|
||||
assert!(total_increase.is_zero());
|
||||
|
||||
// amount is not incremented
|
||||
assert_eq!(
|
||||
RawDelegationData::new(1000u128.into(), delegation_blockstamp),
|
||||
mix_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
let total_increase = increase_mix_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + MINIMUM_BLOCK_AGE_FOR_REWARDING,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// there is an increase now, that the lock period has passed
|
||||
assert_eq!(Uint128(1), total_increase);
|
||||
|
||||
// amount is incremented
|
||||
assert_eq!(
|
||||
RawDelegationData::new(1001u128.into(), delegation_blockstamp),
|
||||
mix_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_there_are_multiple_delegations() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
let delegation_blockstamp = 42;
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
for i in 0..100 {
|
||||
let delegator_address = Addr::unchecked(format!("address{}", i));
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegator_address.as_bytes(),
|
||||
&RawDelegationData::new(1000u128.into(), delegation_blockstamp),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let total_increase = increase_mix_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(Uint128(100), total_increase);
|
||||
|
||||
for i in 0..100 {
|
||||
let delegator_address = Addr::unchecked(format!("address{}", i));
|
||||
assert_eq!(
|
||||
raw_delegation_fixture(1001),
|
||||
mix_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_there_are_more_delegations_than_page_size() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
let delegation_blockstamp = 42;
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 {
|
||||
let delegator_address = Addr::unchecked(format!("address{}", i));
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegator_address.as_bytes(),
|
||||
&RawDelegationData::new(1000u128.into(), delegation_blockstamp),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let total_increase = increase_mix_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Uint128(queries::DELEGATION_PAGE_MAX_LIMIT as u128 * 10),
|
||||
total_increase
|
||||
);
|
||||
|
||||
for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 {
|
||||
let delegator_address = Addr::unchecked(format!("address{}", i));
|
||||
assert_eq!(
|
||||
raw_delegation_fixture(1001),
|
||||
mix_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod reverse_mix_delegations {
|
||||
use super::*;
|
||||
use crate::support::tests::helpers;
|
||||
|
||||
#[test]
|
||||
fn reverse_mix_delegation_exists() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
let delegation_owner = Addr::unchecked("bar");
|
||||
|
||||
reverse_mix_delegations(&mut deps.storage, &delegation_owner)
|
||||
.save(node_identity.as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
reverse_mix_delegations_read(deps.as_ref().storage, &delegation_owner)
|
||||
.may_load(node_identity.as_bytes())
|
||||
.unwrap()
|
||||
.is_some(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reverse_mix_delegation_returns_none_if_delegation_doesnt_exist() {
|
||||
let mut deps = helpers::init_contract();
|
||||
|
||||
let node_identity1: IdentityKey = "foo1".into();
|
||||
let node_identity2: IdentityKey = "foo2".into();
|
||||
let delegation_owner1 = Addr::unchecked("bar");
|
||||
let delegation_owner2 = Addr::unchecked("bar2");
|
||||
|
||||
assert!(
|
||||
reverse_mix_delegations_read(deps.as_ref().storage, &delegation_owner1)
|
||||
.may_load(node_identity1.as_bytes())
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
// add delegation for a different node
|
||||
reverse_mix_delegations(&mut deps.storage, &delegation_owner1)
|
||||
.save(node_identity2.as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
reverse_mix_delegations_read(deps.as_ref().storage, &delegation_owner1)
|
||||
.may_load(node_identity1.as_bytes())
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
// add delegation from a different owner
|
||||
reverse_mix_delegations(&mut deps.storage, &delegation_owner2)
|
||||
.save(node_identity1.as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
reverse_mix_delegations_read(deps.as_ref().storage, &delegation_owner1)
|
||||
.may_load(node_identity1.as_bytes())
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
use cosmwasm_std::{Addr, Coin, DepsMut, Env, Timestamp};
|
||||
use crate::contract::{NUM_VESTING_PERIODS, VESTING_PERIOD};
|
||||
use crate::errors::ContractError;
|
||||
use crate::storage::{get_account_delegations, set_account_delegations};
|
||||
use config::defaults::{DEFAULT_MIXNET_CONTRACT_ADDRESS, DENOM};
|
||||
use cosmwasm_std::{Addr, Coin, DepsMut, Env, Timestamp, Uint128};
|
||||
use mixnet_contract::ExecuteMsg as MixnetExecuteMsg;
|
||||
use mixnet_contract::IdentityKey;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
trait VestingAccount {
|
||||
// locked_coins returns the set of coins that are not spendable (i.e. locked),
|
||||
@@ -10,10 +16,37 @@ trait VestingAccount {
|
||||
// To get spendable coins of a vesting account, first the total balance must
|
||||
// be retrieved and the locked tokens can be subtracted from the total balance.
|
||||
// Note, the spendable balance can be negative.
|
||||
fn locked_coins(&self, block_time: Option<Timestamp>, env: Env, deps: DepsMut) -> Vec<Coin>;
|
||||
fn locked_coins(&self, block_time: Timestamp, env: Env, deps: DepsMut) -> Coin;
|
||||
|
||||
// Calculates the total spendable balance that can be sent to other accounts.
|
||||
fn spendable_coins(&self, block_time: Option<Timestamp>, env: Env, deps: DepsMut) -> Vec<Coin>;
|
||||
fn spendable_coins(&self, block_time: Timestamp, env: Env, deps: DepsMut) -> Coin;
|
||||
|
||||
fn get_vested_coins(&self, block_time: Timestamp) -> Coin;
|
||||
fn get_vesting_coins(&self, block_time: Timestamp) -> Coin;
|
||||
|
||||
fn get_start_time(&self) -> Timestamp;
|
||||
fn get_end_time(&self) -> Timestamp;
|
||||
|
||||
fn get_original_vesting(&self) -> Coin;
|
||||
fn get_delegated_free(&self, env: Env, deps: DepsMut) -> Coin;
|
||||
fn get_delegated_vesting(&self, env: Env, deps: DepsMut) -> Coin;
|
||||
}
|
||||
|
||||
trait DelegationAccount {
|
||||
// TODO: Add delegate on behalf messages in the mixnet contract.
|
||||
// As the vesting account is delegating on behalf of the token holders/vesters
|
||||
fn try_delegate_to_mixnode(
|
||||
&self,
|
||||
mix_identity: IdentityKey,
|
||||
amount: Coin,
|
||||
deps: DepsMut,
|
||||
) -> Result<(), ContractError>;
|
||||
|
||||
fn try_undelegate_from_mixnode(
|
||||
&self,
|
||||
mix_identity: IdentityKey,
|
||||
deps: DepsMut,
|
||||
) -> Result<(), ContractError>;
|
||||
|
||||
// track_delegation performs internal vesting accounting necessary when
|
||||
// delegating from a vesting account. It accepts the current block time, the
|
||||
@@ -22,57 +55,269 @@ trait VestingAccount {
|
||||
fn track_delegation(
|
||||
&self,
|
||||
block_time: Timestamp,
|
||||
delegation_amount: Vec<Coin>,
|
||||
original_vesting_balance: Vec<Coin>,
|
||||
env: Env,
|
||||
mix_identity: IdentityKey,
|
||||
delegation_amount: Coin,
|
||||
deps: DepsMut,
|
||||
);
|
||||
) -> Result<(), ContractError>;
|
||||
// track_undelegation performs internal vesting accounting necessary when a
|
||||
// vesting account performs an undelegation.
|
||||
fn track_undelegation(&self, delegation_amount: Vec<Coin>, env: Env, deps: DepsMut);
|
||||
|
||||
fn get_vested_coins(&self, block_time: Timestamp, env: Env, deps: DepsMut) -> Vec<Coin>;
|
||||
fn get_vesting_coins(&self, block_time: Timestamp, env: Env, deps: DepsMut) -> Vec<Coin>;
|
||||
|
||||
fn get_start_time(&self, env: Env, deps: DepsMut) -> Timestamp;
|
||||
fn get_end_time(&self, env: Env, deps: DepsMut) -> Timestamp;
|
||||
|
||||
fn get_original_vesting(&self, env: Env, deps: DepsMut) -> Vec<Coin>;
|
||||
fn get_delegated_free(&self, env: Env, deps: DepsMut) -> Vec<Coin>;
|
||||
fn get_delegated_vesting(&self, env: Env, deps: DepsMut) -> Vec<Coin>;
|
||||
}
|
||||
|
||||
trait DelegationAccount {
|
||||
fn try_delegate_to_mixnode(mix_identity: IdentityKey, amount: Vec<Coin>);
|
||||
fn try_undelegate_from_mixnode(mix_identity: IdentityKey);
|
||||
fn track_undelegation(&self, mix_identity: IdentityKey, deps: DepsMut);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct VestingPeriod {
|
||||
secs: u64,
|
||||
pub start_time: u64,
|
||||
}
|
||||
|
||||
impl VestingPeriod {
|
||||
pub fn end_time(&self) -> Timestamp {
|
||||
Timestamp::from_seconds(self.start_time + VESTING_PERIOD)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct PeriodicVestingAccount {
|
||||
address: Addr,
|
||||
start_time: u64,
|
||||
start_time: Timestamp,
|
||||
periods: Vec<VestingPeriod>,
|
||||
coin: Coin,
|
||||
}
|
||||
|
||||
impl Default for VestingPeriod {
|
||||
fn default() -> Self {
|
||||
// 90 days
|
||||
VestingPeriod { secs: 90 * 86400 }
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct DelegationData {
|
||||
amount: Uint128,
|
||||
block_time: Timestamp,
|
||||
}
|
||||
|
||||
impl PeriodicVestingAccount {
|
||||
pub fn new(
|
||||
address: Addr,
|
||||
coins: Vec<Coin>,
|
||||
start_time: u64,
|
||||
coin: Coin,
|
||||
start_time: Timestamp,
|
||||
periods: Vec<VestingPeriod>,
|
||||
// TODO: Store coins into contract to keep track of current balance.
|
||||
) -> Self {
|
||||
PeriodicVestingAccount {
|
||||
address,
|
||||
start_time,
|
||||
periods,
|
||||
coin,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tokens_per_period(&self) -> u128 {
|
||||
// Remainder tokens will be lumped into the last period.
|
||||
self.coin.amount.u128() / NUM_VESTING_PERIODS as u128
|
||||
}
|
||||
|
||||
fn get_next_vesting_period(&self, block_time: Timestamp) -> usize {
|
||||
// Returns the index of the next vesting period. Unless the current time is somehow in the past or vesting has not started yet.
|
||||
// In case vesting is over it will always return NUM_VESTING_PERIODS.
|
||||
self.periods
|
||||
.iter()
|
||||
.map(|period| period.start_time)
|
||||
.collect::<Vec<u64>>()
|
||||
.binary_search(&block_time.seconds())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn get_current_vesting_period(&self, block_time: Timestamp) -> usize {
|
||||
if self.get_next_vesting_period(block_time) > 0 {
|
||||
self.get_next_vesting_period(block_time) - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn get_delegated_with_op(
|
||||
&self,
|
||||
op: &dyn Fn(u64, u64) -> bool,
|
||||
env: Env,
|
||||
deps: DepsMut,
|
||||
) -> Coin {
|
||||
let period = self.get_current_vesting_period(env.block.time);
|
||||
if period == 0 {
|
||||
return Coin {
|
||||
amount: Uint128(0),
|
||||
denom: DENOM.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let end_time = if period >= NUM_VESTING_PERIODS as usize {
|
||||
u64::MAX
|
||||
} else {
|
||||
self.periods[period].end_time().seconds()
|
||||
};
|
||||
|
||||
if let Some(delegations) = get_account_delegations(deps.storage, self.address.as_str()) {
|
||||
let mut delegated = Coin {
|
||||
amount: Uint128(0),
|
||||
denom: DENOM.to_string(),
|
||||
};
|
||||
for (_mix_identity, delegation_data) in delegations.iter() {
|
||||
for delegation in delegation_data {
|
||||
if op(delegation.block_time.seconds(), end_time) {
|
||||
delegated.amount += delegation.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
delegated
|
||||
} else {
|
||||
Coin {
|
||||
amount: Uint128(0),
|
||||
denom: DENOM.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_balance(&self) -> Coin {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl DelegationAccount for PeriodicVestingAccount {
|
||||
fn try_delegate_to_mixnode(
|
||||
&self,
|
||||
mix_identity: IdentityKey,
|
||||
coin: Coin,
|
||||
deps: DepsMut,
|
||||
) -> Result<(), ContractError> {
|
||||
let querier = deps.querier;
|
||||
let msg = MixnetExecuteMsg::DelegateToMixnodeOnBehalf {
|
||||
mix_identity,
|
||||
delegate_addr: self.address.clone(),
|
||||
coin,
|
||||
};
|
||||
querier.query_wasm_smart(DEFAULT_MIXNET_CONTRACT_ADDRESS, &msg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_undelegate_from_mixnode(
|
||||
&self,
|
||||
mix_identity: IdentityKey,
|
||||
deps: DepsMut,
|
||||
) -> Result<(), ContractError> {
|
||||
let querier = deps.querier;
|
||||
let msg = MixnetExecuteMsg::UnDelegateFromMixnodeOnBehalf {
|
||||
mix_identity,
|
||||
delegate_addr: self.address.clone(),
|
||||
};
|
||||
querier.query_wasm_smart(DEFAULT_MIXNET_CONTRACT_ADDRESS, &msg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn track_delegation(
|
||||
&self,
|
||||
block_time: Timestamp,
|
||||
mix_identity: IdentityKey,
|
||||
delegation_amount: Coin,
|
||||
deps: DepsMut,
|
||||
) -> Result<(), ContractError> {
|
||||
let mut delegations = if let Some(delegations) =
|
||||
get_account_delegations(deps.storage, self.address.as_str())
|
||||
{
|
||||
delegations
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
let delegation = delegations.entry(mix_identity).or_insert_with(Vec::new);
|
||||
delegation.push(DelegationData {
|
||||
amount: delegation_amount.amount,
|
||||
block_time,
|
||||
});
|
||||
set_account_delegations(deps.storage, self.address.as_str(), delegations)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn track_undelegation(&self, mix_identity: IdentityKey, deps: DepsMut) {
|
||||
// This has to exist in storage at this point.
|
||||
let mut delegations = get_account_delegations(deps.storage, self.address.as_str()).unwrap();
|
||||
// Since we're always removing the entire delegation we can just drop the key
|
||||
delegations.remove(&mix_identity);
|
||||
}
|
||||
}
|
||||
|
||||
impl VestingAccount for PeriodicVestingAccount {
|
||||
fn locked_coins(&self, block_time: Timestamp, env: Env, deps: DepsMut) -> Coin {
|
||||
// Returns 0 in case of underflow.
|
||||
Coin {
|
||||
amount: Uint128(
|
||||
self.get_vesting_coins(block_time)
|
||||
.amount
|
||||
.u128()
|
||||
.saturating_sub(self.get_delegated_vesting(env, deps).amount.u128()),
|
||||
),
|
||||
denom: DENOM.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn spendable_coins(&self, block_time: Timestamp, env: Env, deps: DepsMut) -> Coin {
|
||||
Coin {
|
||||
amount: Uint128(
|
||||
self.get_balance()
|
||||
.amount
|
||||
.u128()
|
||||
.saturating_sub(self.locked_coins(block_time, env, deps).amount.u128()),
|
||||
),
|
||||
denom: DENOM.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vested_coins(&self, block_time: Timestamp) -> Coin {
|
||||
let period = self.get_current_vesting_period(block_time);
|
||||
|
||||
match period {
|
||||
// We're in the first period, or the vesting has not started yet.
|
||||
0 => Coin {
|
||||
amount: Uint128(0),
|
||||
denom: DENOM.to_string(),
|
||||
},
|
||||
1..=7 => Coin {
|
||||
amount: Uint128(self.tokens_per_period() * period as u128),
|
||||
denom: DENOM.to_string(),
|
||||
},
|
||||
_ => Coin {
|
||||
amount: self.coin.amount,
|
||||
denom: DENOM.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vesting_coins(&self, block_time: Timestamp) -> Coin {
|
||||
Coin {
|
||||
amount: Uint128(
|
||||
self.get_original_vesting().amount.u128()
|
||||
- self.get_vested_coins(block_time).amount.u128(),
|
||||
),
|
||||
denom: DENOM.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_start_time(&self) -> Timestamp {
|
||||
self.start_time
|
||||
}
|
||||
|
||||
fn get_end_time(&self) -> Timestamp {
|
||||
self.periods[(NUM_VESTING_PERIODS - 1) as usize].end_time()
|
||||
}
|
||||
|
||||
fn get_original_vesting(&self) -> Coin {
|
||||
self.coin.clone()
|
||||
}
|
||||
|
||||
fn get_delegated_free(&self, env: Env, deps: DepsMut) -> Coin {
|
||||
self.get_delegated_with_op(<, env, deps)
|
||||
}
|
||||
|
||||
fn get_delegated_vesting(&self, env: Env, deps: DepsMut) -> Coin {
|
||||
self.get_delegated_with_op(&ge, env, deps)
|
||||
}
|
||||
}
|
||||
|
||||
fn lt(x: u64, y: u64) -> bool {
|
||||
x < y
|
||||
}
|
||||
|
||||
fn ge(x: u64, y: u64) -> bool {
|
||||
x >= y
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user