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:
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -4,4 +4,4 @@ pub mod messages;
|
||||
mod storage;
|
||||
mod support;
|
||||
mod traits;
|
||||
mod vesting;
|
||||
pub mod vesting;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user