From bf4e18be15facfa5aaa1f67d5596b11856c862de Mon Sep 17 00:00:00 2001 From: Drazen Urch Date: Sun, 21 Nov 2021 21:17:01 +0100 Subject: [PATCH] Finalize bonding/unbonding --- contracts/mixnet/src/transactions.rs | 1 - contracts/vesting/src/contract.rs | 93 +++++++++------ contracts/vesting/src/errors.rs | 8 ++ contracts/vesting/src/messages.rs | 15 +-- contracts/vesting/src/storage.rs | 28 ++++- contracts/vesting/src/vesting.rs | 168 +++++++++++++++++++++++---- 6 files changed, 247 insertions(+), 66 deletions(-) diff --git a/contracts/mixnet/src/transactions.rs b/contracts/mixnet/src/transactions.rs index df0705c122..4a70547651 100644 --- a/contracts/mixnet/src/transactions.rs +++ b/contracts/mixnet/src/transactions.rs @@ -204,7 +204,6 @@ pub(crate) fn _try_remove_mixnode( if let Some(proxy) = &proxy { let msg = VestingContractExecuteMsg::TrackUnbond { owner: owner.to_owned(), - mix_identity: mix_identity.clone(), amount: coins(mixnode_bond.bond_amount.amount.u128(), DENOM)[0].clone(), }; diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index 89dd6777ad..0a2bfb5ffc 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -2,13 +2,15 @@ use crate::errors::ContractError; use crate::messages::{ExecuteMsg, InitMsg, QueryMsg}; use crate::storage::{get_account, get_account_balance, set_account_balance}; use crate::vesting::{ - populate_vesting_periods, DelegationAccount, PeriodicVestingAccount, VestingAccount, + populate_vesting_periods, BondingAccount, DelegationAccount, PeriodicVestingAccount, + VestingAccount, }; +use config::defaults::DENOM; use cosmwasm_std::{ attr, entry_point, to_binary, Addr, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Timestamp, Uint128, }; -use mixnet_contract::IdentityKey; +use mixnet_contract::{IdentityKey, MixNode}; pub const NUM_VESTING_PERIODS: usize = 8; pub const VESTING_PERIOD: u64 = 3 * 30 * 86400; @@ -38,10 +40,9 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::DelegateToMixnode { - mix_identity, - amount, - } => try_delegate_to_mixnode(mix_identity, amount, info, env, deps), + ExecuteMsg::DelegateToMixnode { mix_identity } => { + try_delegate_to_mixnode(mix_identity, info, env, deps) + } ExecuteMsg::UndelegateFromMixnode { mix_identity } => { try_undelegate_from_mixnode(mix_identity, info, deps) } @@ -57,49 +58,54 @@ pub fn execute( mix_identity, amount, } => try_track_undelegation(owner, mix_identity, amount, deps), - ExecuteMsg::BondMixnode { - mix_identity, - amount, - } => try_bond_mixnode(mix_identity, amount, info, env, deps), - ExecuteMsg::UnbondMixnode { - mix_identity, - amount, - } => try_unbond_mixnode(mix_identity, amount, info, env, deps), - ExecuteMsg::TrackUnbond { - owner, - mix_identity, - amount, - } => try_track_unbond(owner, mix_identity, amount, deps), + ExecuteMsg::BondMixnode { mix_node } => try_bond_mixnode(mix_node, info, env, deps), + ExecuteMsg::UnbondMixnode {} => try_unbond_mixnode(info, deps), + ExecuteMsg::TrackUnbond { owner, amount } => try_track_unbond(owner, amount, deps), } } pub fn try_bond_mixnode( - mix_identity: IdentityKey, - amount: Coin, + mix_node: MixNode, info: MessageInfo, env: Env, deps: DepsMut, ) -> Result { - unimplemented!() + let owner = deps.api.addr_validate(info.sender.as_str())?; + let bond = validate_funds(&info.funds)?; + if let Some(account) = get_account(deps.storage, &owner) { + account.try_bond_mixnode(mix_node, bond, &env, deps.storage) + } else { + Err(ContractError::NoAccountForAddress( + owner.as_str().to_string(), + )) + } } -pub fn try_unbond_mixnode( - mix_identity: IdentityKey, - amount: Coin, - info: MessageInfo, - env: Env, - deps: DepsMut, -) -> Result { - unimplemented!() +pub fn try_unbond_mixnode(info: MessageInfo, deps: DepsMut) -> Result { + let owner = deps.api.addr_validate(info.sender.as_str())?; + if let Some(account) = get_account(deps.storage, &owner) { + account.try_unbond_mixnode() + } else { + Err(ContractError::NoAccountForAddress( + owner.as_str().to_string(), + )) + } } pub fn try_track_unbond( owner: Addr, - mix_identity: IdentityKey, amount: Coin, deps: DepsMut, ) -> Result { - unimplemented!() + let owner = deps.api.addr_validate(owner.as_str())?; + if let Some(account) = get_account(deps.storage, &owner) { + account.track_unbond(amount, deps.storage)?; + Ok(Response::default()) + } else { + Err(ContractError::NoAccountForAddress( + owner.as_str().to_string(), + )) + } } pub fn try_withdraw_vested_coins( @@ -167,12 +173,12 @@ fn try_track_undelegation( fn try_delegate_to_mixnode( mix_identity: IdentityKey, - amount: Coin, info: MessageInfo, env: Env, deps: DepsMut, ) -> Result { let delegate_addr = info.sender; + let amount = validate_funds(&info.funds)?; let address = deps.api.addr_validate(delegate_addr.as_str())?; if let Some(account) = get_account(deps.storage, &address) { account.try_delegate_to_mixnode(mix_identity, amount, &env, deps.storage) @@ -209,7 +215,7 @@ fn try_create_periodic_vesting_account( if info.sender != ADMIN_ADDRESS { return Err(ContractError::NotAdmin(info.sender.as_str().to_string())); } - let coin = info.funds[0].clone(); + let coin = validate_funds(&info.funds)?; let address = deps.api.addr_validate(&address)?; let start_time = start_time.unwrap_or_else(|| env.block.time.seconds()); let periods = populate_vesting_periods(start_time, NUM_VESTING_PERIODS); @@ -413,3 +419,22 @@ pub fn try_get_delegated_vesting( Err(ContractError::NoAccountForAddress(vesting_account_address)) } } + +fn validate_funds(funds: &[Coin]) -> Result { + if funds.is_empty() || funds[0].amount.is_zero() { + return Err(ContractError::EmptyFunds); + } + + if funds.len() > 1 { + return Err(ContractError::MultipleDenoms); + } + + if funds[0].denom != DENOM { + return Err(ContractError::WrongDenom( + funds[0].denom.clone(), + DENOM.to_string(), + )); + } + + Ok(funds[0].clone()) +} diff --git a/contracts/vesting/src/errors.rs b/contracts/vesting/src/errors.rs index 1c611c17dd..761b2c7ea4 100644 --- a/contracts/vesting/src/errors.rs +++ b/contracts/vesting/src/errors.rs @@ -21,4 +21,12 @@ pub enum ContractError { NotDelegate(String), #[error("Total vesting amount is inprobably low -> {0}, this is likely an error")] ImprobableVestingAmount(u128), + #[error("Address {0} has already bonded a node")] + AlreadyBonded(String), + #[error("Recieved empty funds vector")] + EmptyFunds, + #[error("Recieved wrong denom: {0}, expected {1}")] + WrongDenom(String, String), + #[error("Recieved multiple denoms, expected 1")] + MultipleDenoms, } diff --git a/contracts/vesting/src/messages.rs b/contracts/vesting/src/messages.rs index f10122f4e5..925592494d 100644 --- a/contracts/vesting/src/messages.rs +++ b/contracts/vesting/src/messages.rs @@ -1,5 +1,6 @@ use cosmwasm_std::{Addr, Coin, Timestamp}; use mixnet_contract::IdentityKey; +use mixnet_contract::MixNode; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -11,7 +12,6 @@ pub struct InitMsg {} pub enum ExecuteMsg { DelegateToMixnode { mix_identity: IdentityKey, - amount: Coin, }, UndelegateFromMixnode { mix_identity: IdentityKey, @@ -29,18 +29,13 @@ pub enum ExecuteMsg { amount: Coin, }, BondMixnode { - mix_identity: IdentityKey, - amount: Coin, - }, - UnbondMixnode { - mix_identity: IdentityKey, - amount: Coin, + mix_node: MixNode, }, + UnbondMixnode {}, TrackUnbond { - mix_identity: IdentityKey, owner: Addr, - amount: Coin - } + amount: Coin, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] diff --git a/contracts/vesting/src/storage.rs b/contracts/vesting/src/storage.rs index b9d454a32d..fddba856a7 100644 --- a/contracts/vesting/src/storage.rs +++ b/contracts/vesting/src/storage.rs @@ -5,7 +5,7 @@ use cosmwasm_storage::{bucket, bucket_read, Bucket, ReadonlyBucket}; use crate::{ errors::ContractError, - vesting::{DelegationData, PeriodicVestingAccount}, + vesting::{BondData, DelegationData, PeriodicVestingAccount}, }; // storage prefixes // all of them must be unique and presumably not be a prefix of a different one @@ -16,6 +16,7 @@ use crate::{ const PREFIX_ACCOUNTS: &[u8] = b"ac"; const PREFIX_ACCOUNT_DELEGATIONS: &[u8] = b"ad"; const PREFIX_ACCOUNT_BALANCE: &[u8] = b"ab"; +const PREFIX_ACCOUNT_MIXBOND: &[u8] = b"am"; // Contract-level stuff fn accounts_mut(storage: &mut dyn Storage) -> Bucket { @@ -34,6 +35,14 @@ fn account_delegations(storage: &dyn Storage) -> ReadonlyBucket Bucket { + bucket(storage, PREFIX_ACCOUNT_DELEGATIONS) +} + +fn account_bond(storage: &dyn Storage) -> ReadonlyBucket { + bucket_read(storage, PREFIX_ACCOUNT_DELEGATIONS) +} + fn account_balance(storage: &dyn Storage) -> ReadonlyBucket { bucket_read(storage, PREFIX_ACCOUNT_BALANCE) } @@ -72,6 +81,23 @@ pub fn set_account_delegations( account_delegations_mut(storage).save(address.as_bytes(), &delegations) } +pub fn get_account_bond(storage: &dyn Storage, address: &Addr) -> Option { + // Due to using may_load this should be safe to unwrap + account_bond(storage).may_load(address.as_bytes()).unwrap() +} + +pub fn drop_account_bond(storage: &mut dyn Storage, address: &Addr) -> StdResult<()> { + Ok(account_bond_mut(storage).remove(address.as_bytes())) +} + +pub fn set_account_bond( + storage: &mut dyn Storage, + address: &Addr, + bond: BondData, +) -> StdResult<()> { + account_bond_mut(storage).save(address.as_bytes(), &bond) +} + pub fn get_account_balance(storage: &dyn Storage, address: &Addr) -> Option { // Due to using may_load this should be safe to unwrap account_balance(storage) diff --git a/contracts/vesting/src/vesting.rs b/contracts/vesting/src/vesting.rs index fd0897d169..732a9b81fe 100644 --- a/contracts/vesting/src/vesting.rs +++ b/contracts/vesting/src/vesting.rs @@ -1,13 +1,13 @@ use crate::contract::{NUM_VESTING_PERIODS, VESTING_PERIOD}; use crate::errors::ContractError; use crate::storage::{ - get_account_balance, get_account_delegations, set_account, set_account_balance, - set_account_delegations, + drop_account_bond, get_account_balance, get_account_bond, get_account_delegations, set_account, + set_account_balance, set_account_bond, set_account_delegations, }; use config::defaults::{DEFAULT_MIXNET_CONTRACT_ADDRESS, DENOM}; use cosmwasm_std::{attr, wasm_execute, Addr, Coin, Env, Response, Storage, Timestamp, Uint128}; -use mixnet_contract::ExecuteMsg as MixnetExecuteMsg; use mixnet_contract::IdentityKey; +use mixnet_contract::{ExecuteMsg as MixnetExecuteMsg, MixNode}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -84,7 +84,7 @@ pub trait DelegationAccount { &self, block_time: Timestamp, mix_identity: IdentityKey, - delegation_amount: Coin, + delegation: Coin, storage: &mut dyn Storage, ) -> Result<(), ContractError>; // track_undelegation performs internal vesting accounting necessary when a @@ -97,6 +97,27 @@ pub trait DelegationAccount { ) -> Result<(), ContractError>; } +pub trait BondingAccount { + fn try_bond_mixnode( + &self, + mix_node: MixNode, + amount: Coin, + env: &Env, + storage: &mut dyn Storage, + ) -> Result; + + fn try_unbond_mixnode(&self) -> Result; + + fn track_bond( + &self, + block_time: Timestamp, + bond: Coin, + storage: &mut dyn Storage, + ) -> Result<(), ContractError>; + + fn track_unbond(&self, amount: Coin, storage: &mut dyn Storage) -> Result<(), ContractError>; +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct VestingPeriod { pub start_time: u64, @@ -116,13 +137,6 @@ pub struct PeriodicVestingAccount { coin: Coin, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct DelegationData { - mix_identity: IdentityKey, - amount: Uint128, - block_time: Timestamp, -} - impl PeriodicVestingAccount { pub fn new( address: Addr, @@ -254,10 +268,10 @@ impl DelegationAccount for PeriodicVestingAccount { data: None, }) } else { - return Err(ContractError::InsufficientBalance( + Err(ContractError::InsufficientBalance( self.address.as_str().to_string(), self.get_balance(storage).u128(), - )); + )) } } @@ -293,7 +307,7 @@ impl DelegationAccount for PeriodicVestingAccount { &self, block_time: Timestamp, mix_identity: IdentityKey, - delegation_amount: Coin, + delegation: Coin, storage: &mut dyn Storage, ) -> Result<(), ContractError> { let mut delegations = @@ -304,13 +318,13 @@ impl DelegationAccount for PeriodicVestingAccount { }; delegations.push(DelegationData { mix_identity, - amount: delegation_amount.amount, + amount: delegation.amount, block_time, }); let new_balance = if let Some(balance) = get_account_balance(storage, &self.address) { - // We've checked that delegation_amount < balance in the caller function - Uint128(balance.u128() - delegation_amount.amount.u128()) + // We've checked that delegation < balance in the caller function + Uint128(balance.u128() - delegation.amount.u128()) } else { return Err(ContractError::NoBalanceForAddress( self.address.as_str().to_string(), @@ -335,7 +349,7 @@ impl DelegationAccount for PeriodicVestingAccount { .into_iter() .filter(|d| d.mix_identity != mix_identity) .collect(); - + let new_balance = if let Some(balance) = get_account_balance(storage, &self.address) { Uint128(balance.u128() + amount.amount.u128()) } else { @@ -346,8 +360,6 @@ impl DelegationAccount for PeriodicVestingAccount { set_account_balance(storage, &self.address, new_balance)?; - // Since we're always removing the entire delegation we can just drop the key - // TODO: track balance here as well, maybe Ok(set_account_delegations( storage, &self.address, @@ -356,6 +368,109 @@ impl DelegationAccount for PeriodicVestingAccount { } } +impl BondingAccount for PeriodicVestingAccount { + fn try_bond_mixnode( + &self, + mix_node: MixNode, + bond: Coin, + env: &Env, + storage: &mut dyn Storage, + ) -> Result { + if bond.amount < self.get_balance(storage) { + let msg = MixnetExecuteMsg::BondMixnodeOnBehalf { + mix_node, + owner: self.address.clone(), + }; + let messages = + vec![ + wasm_execute(DEFAULT_MIXNET_CONTRACT_ADDRESS, &msg, vec![bond.clone()])?.into(), + ]; + let attributes = vec![attr("action", "bond mixnode on behalf")]; + self.track_bond(env.block.time, bond, storage)?; + + Ok(Response { + submessages: Vec::new(), + messages, + attributes, + data: None, + }) + } else { + Err(ContractError::InsufficientBalance( + self.address.as_str().to_string(), + self.get_balance(storage).u128(), + )) + } + } + + fn track_bond( + &self, + block_time: Timestamp, + bond: Coin, + storage: &mut dyn Storage, + ) -> Result<(), ContractError> { + let bond = if let Some(_bond) = get_account_bond(storage, &self.address) { + return Err(ContractError::AlreadyBonded( + self.address.as_str().to_string(), + )); + } else { + BondData { + block_time, + amount: bond.amount, + } + }; + + let new_balance = if let Some(balance) = get_account_balance(storage, &self.address) { + // We've checked that bond.amount < balance in the caller function + Uint128(balance.u128() - bond.amount.u128()) + } else { + return Err(ContractError::NoBalanceForAddress( + self.address.as_str().to_string(), + )); + }; + + set_account_balance(storage, &self.address, new_balance)?; + Ok(set_account_bond(storage, &self.address, bond)?) + } + + fn try_unbond_mixnode(&self) -> Result { + let msg = MixnetExecuteMsg::UnbondMixnodeOnBehalf { + owner: self.address.clone(), + }; + let messages = vec![wasm_execute( + DEFAULT_MIXNET_CONTRACT_ADDRESS, + &msg, + vec![Coin { + amount: Uint128(0), + denom: DENOM.to_string(), + }], + )? + .into()]; + + let attributes = vec![attr("action", "unbond mixnode on behalf")]; + + Ok(Response { + submessages: Vec::new(), + messages, + attributes, + data: None, + }) + } + + fn track_unbond(&self, amount: Coin, storage: &mut dyn Storage) -> Result<(), ContractError> { + let new_balance = if let Some(balance) = get_account_balance(storage, &self.address) { + Uint128(balance.u128() + amount.amount.u128()) + } else { + return Err(ContractError::NoBalanceForAddress( + self.address.as_str().to_string(), + )); + }; + + set_account_balance(storage, &self.address, new_balance)?; + drop_account_bond(storage, &self.address)?; + Ok(()) + } +} + impl VestingAccount for PeriodicVestingAccount { fn locked_coins( &self, @@ -467,6 +582,19 @@ impl VestingAccount for PeriodicVestingAccount { } } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct DelegationData { + mix_identity: IdentityKey, + amount: Uint128, + block_time: Timestamp, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct BondData { + amount: Uint128, + block_time: Timestamp, +} + fn lt(x: u64, y: u64) -> bool { x < y }