Feature/flexible vesting + utility queries (#1083)

* Flexible vesting schedules

* Utility queries

* make vesting specification fields public

* Address review comments

Co-authored-by: Tommy Verrall <tommyvez@protonmail.com>
This commit is contained in:
Drazen Urch
2022-01-28 14:07:07 +01:00
committed by GitHub
parent 6333042826
commit 538616af54
9 changed files with 135 additions and 45 deletions
@@ -7,6 +7,8 @@ use crate::nymd::NymdClient;
use async_trait::async_trait;
use cosmwasm_std::{Coin, Timestamp};
use vesting_contract::messages::QueryMsg as VestingQueryMsg;
use vesting_contract::vesting::Account;
use vesting_contract::vesting::PledgeData;
#[async_trait]
pub trait VestingQueryClient {
@@ -55,6 +57,10 @@ pub trait VestingQueryClient {
vesting_account_address: &str,
block_time: Option<Timestamp>,
) -> Result<Coin, NymdError>;
async fn get_account(&self, address: &str) -> Result<Account, NymdError>;
async fn get_mixnode_pledge(&self, address: &str) -> Result<Option<PledgeData>, NymdError>;
async fn get_gateway_pledge(&self, address: &str) -> Result<Option<PledgeData>, NymdError>;
}
#[async_trait]
@@ -173,4 +179,29 @@ impl<C: CosmWasmClient + Sync + Send> VestingQueryClient for NymdClient<C> {
.query_contract_smart(self.vesting_contract_address()?, &request)
.await
}
async fn get_account(&self, address: &str) -> Result<Account, NymdError> {
let request = VestingQueryMsg::GetAccount {
address: address.to_string(),
};
self.client
.query_contract_smart(self.vesting_contract_address()?, &request)
.await
}
async fn get_mixnode_pledge(&self, address: &str) -> Result<Option<PledgeData>, NymdError> {
let request = VestingQueryMsg::GetMixnode {
address: address.to_string(),
};
self.client
.query_contract_smart(self.vesting_contract_address()?, &request)
.await
}
async fn get_gateway_pledge(&self, address: &str) -> Result<Option<PledgeData>, NymdError> {
let request = VestingQueryMsg::GetGateway {
address: address.to_string(),
};
self.client
.query_contract_smart(self.vesting_contract_address()?, &request)
.await
}
}
@@ -9,7 +9,7 @@ use crate::nymd::{cosmwasm_coin_to_cosmos_coin, NymdClient};
use async_trait::async_trait;
use cosmwasm_std::Coin;
use mixnet_contract_common::{Gateway, IdentityKey, IdentityKeyRef, MixNode};
use vesting_contract::messages::ExecuteMsg as VestingExecuteMsg;
use vesting_contract::messages::{ExecuteMsg as VestingExecuteMsg, VestingSpecification};
#[async_trait]
pub trait VestingSigningClient {
@@ -66,7 +66,7 @@ pub trait VestingSigningClient {
&self,
owner_address: &str,
staking_address: Option<String>,
start_time: Option<u64>,
vesting_spec: Option<VestingSpecification>,
amount: Coin,
) -> Result<ExecuteResult, NymdError>;
}
@@ -277,14 +277,14 @@ impl<C: SigningCosmWasmClient + Sync + Send> VestingSigningClient for NymdClient
&self,
owner_address: &str,
staking_address: Option<String>,
start_time: Option<u64>,
vesting_spec: Option<VestingSpecification>,
amount: Coin,
) -> Result<ExecuteResult, NymdError> {
let fee = self.operation_fee(Operation::CreatePeriodicVestingAccount);
let req = VestingExecuteMsg::CreateAccount {
owner_address: owner_address.to_string(),
staking_address,
start_time,
vesting_spec,
};
self.client
.execute(
+25 -15
View File
@@ -1,10 +1,10 @@
use crate::errors::ContractError;
use crate::messages::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg};
use crate::messages::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg, VestingSpecification};
use crate::storage::{account_from_address, ADMIN, MIXNET_CONTRACT_ADDRESS};
use crate::traits::{
DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, VestingAccount,
};
use crate::vesting::{populate_vesting_periods, Account};
use crate::vesting::{populate_vesting_periods, Account, PledgeData};
use config::defaults::DENOM;
use cosmwasm_std::{
coin, entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse,
@@ -17,12 +17,6 @@ use vesting_contract_common::events::{
new_track_mixnode_unbond_event, new_track_undelegation_event, new_vested_coins_withdraw_event,
};
// We're using a 24 month vesting period with 3 months sub-periods.
// There are 8 three month periods in two years
// and duration of a single period is 30 days.
pub const NUM_VESTING_PERIODS: usize = 8;
pub const VESTING_PERIOD: u64 = 3 * 30 * 86400;
#[entry_point]
pub fn instantiate(
deps: DepsMut,
@@ -59,11 +53,11 @@ pub fn execute(
ExecuteMsg::CreateAccount {
owner_address,
staking_address,
start_time,
vesting_spec,
} => try_create_periodic_vesting_account(
&owner_address,
staking_address,
start_time,
vesting_spec,
info,
env,
deps,
@@ -284,7 +278,7 @@ fn try_undelegate_from_mixnode(
fn try_create_periodic_vesting_account(
owner_address: &str,
staking_address: Option<String>,
start_time: Option<u64>,
vesting_spec: Option<VestingSpecification>,
info: MessageInfo,
env: Env,
deps: DepsMut,
@@ -293,6 +287,8 @@ fn try_create_periodic_vesting_account(
return Err(ContractError::NotAdmin(info.sender.as_str().to_string()));
}
let vesting_spec = vesting_spec.unwrap_or_default();
let coin = validate_funds(&info.funds)?;
let owner_address = deps.api.addr_validate(owner_address)?;
let staking_address = if let Some(staking_address) = staking_address {
@@ -300,8 +296,11 @@ fn try_create_periodic_vesting_account(
} else {
None
};
let start_time = start_time.unwrap_or_else(|| env.block.time.seconds());
let periods = populate_vesting_periods(start_time, NUM_VESTING_PERIODS);
let start_time = vesting_spec
.start_time()
.unwrap_or_else(|| env.block.time.seconds());
let periods = populate_vesting_periods(start_time, vesting_spec);
let start_time = Timestamp::from_seconds(start_time);
Account::new(
@@ -389,14 +388,25 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result<QueryResponse, Contr
deps,
)?),
QueryMsg::GetAccount { address } => to_binary(&try_get_account(&address, deps)?),
QueryMsg::GetMixnode { address } => to_binary(&try_get_mixnode(&address, deps)?),
QueryMsg::GetGateway { address } => to_binary(&try_get_gateway(&address, deps)?),
};
Ok(query_res?)
}
pub fn try_get_account(address: &str, deps: Deps) -> Result<Option<Account>, ContractError> {
pub fn try_get_mixnode(address: &str, deps: Deps) -> Result<Option<PledgeData>, ContractError> {
let account = account_from_address(address, deps.storage, deps.api)?;
Ok(Some(account))
account.load_mixnode_pledge(deps.storage)
}
pub fn try_get_gateway(address: &str, deps: Deps) -> Result<Option<PledgeData>, ContractError> {
let account = account_from_address(address, deps.storage, deps.api)?;
account.load_gateway_pledge(deps.storage)
}
pub fn try_get_account(address: &str, deps: Deps) -> Result<Account, ContractError> {
account_from_address(address, deps.storage, deps.api)
}
pub fn try_get_locked_coins(
+1 -1
View File
@@ -4,4 +4,4 @@ pub mod messages;
mod storage;
mod support;
mod traits;
mod vesting;
pub mod vesting;
+40 -1
View File
@@ -13,6 +13,39 @@ pub struct InitMsg {
#[serde(rename_all = "snake_case")]
pub struct MigrateMsg {}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, Default)]
pub struct VestingSpecification {
start_time: Option<u64>,
period_seconds: Option<u64>,
num_periods: Option<u64>,
}
impl VestingSpecification {
pub fn new(
start_time: Option<u64>,
period_seconds: Option<u64>,
num_periods: Option<u64>,
) -> Self {
Self {
start_time,
period_seconds,
num_periods,
}
}
pub fn start_time(&self) -> Option<u64> {
self.start_time
}
pub fn period_seconds(&self) -> u64 {
self.period_seconds.unwrap_or(3 * 30 * 86400)
}
pub fn num_periods(&self) -> u64 {
self.num_periods.unwrap_or(8)
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
@@ -26,7 +59,7 @@ pub enum ExecuteMsg {
CreateAccount {
owner_address: String,
staking_address: Option<String>,
start_time: Option<u64>,
vesting_spec: Option<VestingSpecification>,
},
WithdrawVestedCoins {
amount: Coin,
@@ -103,4 +136,10 @@ pub enum QueryMsg {
GetAccount {
address: String,
},
GetMixnode {
address: String,
},
GetGateway {
address: String,
},
}
+4 -3
View File
@@ -1,7 +1,7 @@
#[cfg(test)]
pub mod helpers {
use crate::contract::{instantiate, NUM_VESTING_PERIODS};
use crate::messages::InitMsg;
use crate::contract::instantiate;
use crate::messages::{InitMsg, VestingSpecification};
use crate::vesting::{populate_vesting_periods, Account};
use config::defaults::DENOM;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier};
@@ -20,7 +20,8 @@ pub mod helpers {
pub fn vesting_account_fixture(storage: &mut dyn Storage, env: &Env) -> Account {
let start_time = env.block.time;
let periods = populate_vesting_periods(start_time.seconds(), NUM_VESTING_PERIODS);
let periods =
populate_vesting_periods(start_time.seconds(), VestingSpecification::default());
Account::new(
Addr::unchecked("owner"),
+6 -3
View File
@@ -1,5 +1,4 @@
use super::{PledgeData, VestingPeriod};
use crate::contract::NUM_VESTING_PERIODS;
use crate::errors::ContractError;
use crate::storage::{
load_balance, load_bond_pledge, load_gateway_pledge, remove_bond_pledge, remove_delegation,
@@ -57,6 +56,10 @@ impl Account {
Ok(account)
}
pub fn num_vesting_periods(&self) -> usize {
self.periods.len()
}
pub fn storage_key(&self) -> u32 {
self.storage_key
}
@@ -81,11 +84,11 @@ impl Account {
pub fn tokens_per_period(&self) -> Result<u128, ContractError> {
let amount = self.coin.amount.u128();
if amount < NUM_VESTING_PERIODS as u128 {
if amount < self.num_vesting_periods() as u128 {
Err(ContractError::ImprobableVestingAmount(amount))
} else {
// Remainder tokens will be lumped into the last period.
Ok(amount / NUM_VESTING_PERIODS as u128)
Ok(amount / self.num_vesting_periods() as u128)
}
}
@@ -1,4 +1,3 @@
use crate::contract::NUM_VESTING_PERIODS;
use crate::errors::ContractError;
use crate::storage::{delete_account, save_account, DELEGATIONS};
use crate::traits::VestingAccount;
@@ -97,7 +96,7 @@ impl VestingAccount for Account {
}
fn get_end_time(&self) -> Timestamp {
self.periods[(NUM_VESTING_PERIODS - 1) as usize].end_time()
self.periods[(self.num_vesting_periods() - 1) as usize].end_time()
}
fn get_original_vesting(&self) -> Coin {
+23 -16
View File
@@ -1,4 +1,3 @@
use crate::contract::VESTING_PERIOD;
use cosmwasm_std::{Timestamp, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -6,14 +5,17 @@ use serde::{Deserialize, Serialize};
mod account;
pub use account::*;
use crate::messages::VestingSpecification;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VestingPeriod {
pub start_time: u64,
pub period_seconds: u64,
}
impl VestingPeriod {
pub fn end_time(&self) -> Timestamp {
Timestamp::from_seconds(self.start_time + VESTING_PERIOD)
Timestamp::from_seconds(self.start_time + self.period_seconds as u64)
}
}
@@ -23,11 +25,15 @@ pub struct PledgeData {
block_time: Timestamp,
}
pub fn populate_vesting_periods(start_time: u64, n: usize) -> Vec<VestingPeriod> {
let mut periods = Vec::with_capacity(n as usize);
for i in 0..n {
pub fn populate_vesting_periods(
start_time: u64,
vesting_spec: VestingSpecification,
) -> Vec<VestingPeriod> {
let mut periods = Vec::with_capacity(vesting_spec.num_periods() as usize);
for i in 0..vesting_spec.num_periods() {
let period = VestingPeriod {
start_time: start_time + i as u64 * VESTING_PERIOD,
start_time: start_time + i as u64 * vesting_spec.period_seconds(),
period_seconds: vesting_spec.period_seconds(),
};
periods.push(period);
}
@@ -36,7 +42,7 @@ pub fn populate_vesting_periods(start_time: u64, n: usize) -> Vec<VestingPeriod>
#[cfg(test)]
mod tests {
use crate::contract::{execute, NUM_VESTING_PERIODS, VESTING_PERIOD};
use crate::contract::execute;
use crate::messages::ExecuteMsg;
use crate::storage::load_account;
use crate::support::tests::helpers::{init_contract, vesting_account_fixture};
@@ -56,7 +62,7 @@ mod tests {
let msg = ExecuteMsg::CreateAccount {
owner_address: "owner".to_string(),
staking_address: Some("staking".to_string()),
start_time: None,
vesting_spec: None,
};
let response = execute(deps.as_mut(), env.clone(), info, msg.clone());
assert!(response.is_err());
@@ -165,17 +171,18 @@ mod tests {
fn test_period_logic() {
let mut deps = init_contract();
let env = mock_env();
let num_vesting_periods = 8;
let vesting_period = 3 * 30 * 86400;
let account = vesting_account_fixture(&mut deps.storage, &env);
assert_eq!(account.periods().len(), NUM_VESTING_PERIODS as usize);
assert_eq!(account.periods().len(), 8);
assert_eq!(account.periods().len(), num_vesting_periods as usize);
let current_period = account.get_current_vesting_period(Timestamp::from_seconds(0));
assert_eq!(0, current_period);
let block_time =
Timestamp::from_seconds(account.start_time().seconds() + VESTING_PERIOD + 1);
Timestamp::from_seconds(account.start_time().seconds() + vesting_period + 1);
let current_period = account.get_current_vesting_period(block_time);
assert_eq!(current_period, 1);
let vested_coins = account.get_vested_coins(Some(block_time), &env).unwrap();
@@ -183,19 +190,19 @@ mod tests {
assert_eq!(
vested_coins.amount,
Uint128::new(
account.get_original_vesting().amount.u128() / NUM_VESTING_PERIODS as u128
account.get_original_vesting().amount.u128() / num_vesting_periods as u128
)
);
assert_eq!(
vesting_coins.amount,
Uint128::new(
account.get_original_vesting().amount.u128()
- account.get_original_vesting().amount.u128() / NUM_VESTING_PERIODS as u128
- account.get_original_vesting().amount.u128() / num_vesting_periods as u128
)
);
let block_time =
Timestamp::from_seconds(account.start_time().seconds() + 5 * VESTING_PERIOD + 1);
Timestamp::from_seconds(account.start_time().seconds() + 5 * vesting_period + 1);
let current_period = account.get_current_vesting_period(block_time);
assert_eq!(current_period, 5);
let vested_coins = account.get_vested_coins(Some(block_time), &env).unwrap();
@@ -203,7 +210,7 @@ mod tests {
assert_eq!(
vested_coins.amount,
Uint128::new(
5 * account.get_original_vesting().amount.u128() / NUM_VESTING_PERIODS as u128
5 * account.get_original_vesting().amount.u128() / num_vesting_periods as u128
)
);
assert_eq!(
@@ -211,7 +218,7 @@ mod tests {
Uint128::new(
account.get_original_vesting().amount.u128()
- 5 * account.get_original_vesting().amount.u128()
/ NUM_VESTING_PERIODS as u128
/ num_vesting_periods as u128
)
);
}