diff --git a/common/client-libs/validator-client/src/nyxd/traits/vesting_query_client.rs b/common/client-libs/validator-client/src/nyxd/traits/vesting_query_client.rs index 4aeb307a55..2f8931f98a 100644 --- a/common/client-libs/validator-client/src/nyxd/traits/vesting_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/traits/vesting_query_client.rs @@ -229,6 +229,32 @@ pub trait VestingQueryClient { .await } + async fn get_vesting_delegation( + &self, + address: &str, + mix_id: MixId, + block_timestamp_secs: u64, + ) -> Result { + self.query_vesting_contract(VestingQueryMsg::GetDelegation { + address: address.to_string(), + mix_id, + block_timestamp_secs, + }) + .await + } + + async fn get_total_delegation_amount( + &self, + address: &str, + mix_id: MixId, + ) -> Result { + self.query_vesting_contract(VestingQueryMsg::GetTotalDelegationAmount { + address: address.to_string(), + mix_id, + }) + .await + } + async fn get_delegation_timestamps( &self, address: &str, diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index ed605dd64a..0ec3d14304 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -260,6 +260,15 @@ pub enum QueryMsg { GetCurrentVestingPeriod { address: String, }, + GetDelegation { + address: String, + mix_id: MixId, + block_timestamp_secs: u64, + }, + GetTotalDelegationAmount { + address: String, + mix_id: MixId, + }, GetDelegationTimes { address: String, mix_id: MixId, diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index 16192fcb5e..268068b53c 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -1,38 +1,14 @@ use crate::errors::ContractError; -use crate::storage::{ - account_from_address, save_account, BlockTimestampSecs, ACCOUNTS, ADMIN, DELEGATIONS, - MIXNET_CONTRACT_ADDRESS, MIX_DENOM, -}; -use crate::traits::{ - DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, NodeFamilies, VestingAccount, -}; -use crate::vesting::{populate_vesting_periods, Account}; -use contracts_common::signing::MessageSignature; -use contracts_common::ContractBuildInformation; +pub use crate::queries::*; +use crate::storage::{ADMIN, MIXNET_CONTRACT_ADDRESS, MIX_DENOM}; +pub use crate::transactions::*; +use crate::vesting::Account; use cosmwasm_std::{ - coin, entry_point, to_binary, Addr, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, Order, - QueryResponse, Response, StdResult, Timestamp, Uint128, + entry_point, to_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, + Uint128, }; -use cw_storage_plus::Bound; -use mixnet_contract_common::families::FamilyHead; -use mixnet_contract_common::gateway::GatewayConfigUpdate; -use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use mixnet_contract_common::{Gateway, MixId, MixNode}; use semver::Version; -use vesting_contract_common::events::{ - new_ownership_transfer_event, new_periodic_vesting_account_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_vested_coins_withdraw_event, -}; -use vesting_contract_common::messages::{ - ExecuteMsg, InitMsg, MigrateMsg, QueryMsg, VestingSpecification, -}; -use vesting_contract_common::{ - AccountVestingCoins, AccountsResponse, AllDelegationsResponse, BaseVestingAccountInfo, - DelegationTimesResponse, OriginalVestingResponse, Period, PledgeCap, PledgeData, - VestingCoinsResponse, VestingDelegation, -}; +use vesting_contract_common::messages::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg}; // version info for migration info const CONTRACT_NAME: &str = "crate:nym-vesting-contract"; @@ -209,453 +185,6 @@ pub fn execute( } } -pub fn try_create_family( - info: MessageInfo, - deps: DepsMut, - label: String, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_create_family(deps.storage, label) -} -pub fn try_join_family( - info: MessageInfo, - deps: DepsMut, - join_permit: MessageSignature, - family_head: FamilyHead, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_join_family(deps.storage, join_permit, family_head) -} -pub fn try_leave_family( - info: MessageInfo, - deps: DepsMut, - family_head: FamilyHead, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_leave_family(deps.storage, family_head) -} -pub fn try_kick_family_member( - info: MessageInfo, - deps: DepsMut, - member: String, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_head_kick_member(deps.storage, &member) -} - -/// Update locked_pledge_cap, the hard cap for staking/bonding with unvested tokens. -/// -/// Callable by ADMIN only, see [instantiate]. -pub fn try_update_locked_pledge_cap( - address: String, - cap: PledgeCap, - info: MessageInfo, - deps: DepsMut, -) -> Result { - if info.sender != ADMIN.load(deps.storage)? { - return Err(ContractError::NotAdmin(info.sender.as_str().to_string())); - } - let mut account = account_from_address(&address, deps.storage, deps.api)?; - - account.pledge_cap = Some(cap); - save_account(&account, deps.storage)?; - Ok(Response::default()) -} - -/// Update config for a mixnode bonded with vesting account, sends [mixnet_contract_common::ExecuteMsg::UpdateMixnodeConfig] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. -pub fn try_update_mixnode_config( - new_config: MixNodeConfigUpdate, - info: MessageInfo, - deps: DepsMut, -) -> Result { - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_update_mixnode_config(new_config, deps.storage) -} - -pub fn try_update_gateway_config( - new_config: GatewayConfigUpdate, - info: MessageInfo, - deps: DepsMut, -) -> Result { - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_update_gateway_config(new_config, deps.storage) -} - -pub fn try_update_mixnode_cost_params( - new_costs: MixNodeCostParams, - info: MessageInfo, - deps: DepsMut, -) -> Result { - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_update_mixnode_cost_params(new_costs, deps.storage) -} - -/// Updates mixnet contract address, for cases when a new mixnet contract is deployed. -/// -/// Callable by ADMIN only, see [instantiate]. -pub fn try_update_mixnet_address( - address: String, - info: MessageInfo, - deps: DepsMut<'_>, -) -> Result { - if info.sender != ADMIN.load(deps.storage)? { - return Err(ContractError::NotAdmin(info.sender.as_str().to_string())); - } - let mixnet_contract_address = deps.api.addr_validate(&address)?; - - MIXNET_CONTRACT_ADDRESS.save(deps.storage, &mixnet_contract_address)?; - Ok(Response::default()) -} - -/// Withdraw already vested coins. -pub fn try_withdraw_vested_coins( - amount: Coin, - env: Env, - info: MessageInfo, - deps: DepsMut<'_>, -) -> Result { - let mix_denom = MIX_DENOM.load(deps.storage)?; - if amount.denom != mix_denom { - return Err(ContractError::WrongDenom(amount.denom, mix_denom)); - } - - let address = info.sender.clone(); - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - if address != account.owner_address() { - return Err(ContractError::NotOwner(account.owner_address().to_string())); - } - let spendable_coins = account.spendable_coins(None, &env, deps.storage)?; - if amount.amount <= spendable_coins.amount { - let new_balance = account.withdraw(&amount, deps.storage)?; - - let send_tokens = BankMsg::Send { - to_address: account.owner_address().as_str().to_string(), - amount: vec![amount.clone()], - }; - - Ok(Response::new() - .add_message(send_tokens) - .add_event(new_vested_coins_withdraw_event( - &address, - &amount, - &coin(new_balance, &amount.denom), - ))) - } else { - Err(ContractError::InsufficientSpendable( - account.owner_address().as_str().to_string(), - spendable_coins.amount.u128(), - )) - } -} - -/// Transfer ownership of the entire vesting account. -fn try_transfer_ownership( - to_address: String, - info: MessageInfo, - deps: DepsMut<'_>, -) -> Result { - let address = info.sender.clone(); - let to_address = deps.api.addr_validate(&to_address)?; - let mut account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - if address == account.owner_address() { - account.transfer_ownership(&to_address, deps.storage)?; - Ok(Response::new().add_event(new_ownership_transfer_event(&address, &to_address))) - } else { - Err(ContractError::NotOwner(account.owner_address().to_string())) - } -} - -/// Set or update staking address for a vesting account. -fn try_update_staking_address( - to_address: Option, - info: MessageInfo, - deps: DepsMut<'_>, -) -> Result { - if let Some(ref to_address) = to_address { - if account_from_address(to_address, deps.storage, deps.api).is_ok() { - // do not allow setting staking address to an existing account's address - return Err(ContractError::StakingAccountExists(to_address.to_string())); - } - } - - let address = info.sender.clone(); - let to_address = to_address.and_then(|x| deps.api.addr_validate(&x).ok()); - let mut account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - if address == account.owner_address() { - let old = account.staking_address().cloned(); - account.update_staking_address(to_address.clone(), deps.storage)?; - Ok(Response::new().add_event(new_staking_address_update_event(&old, &to_address))) - } else { - Err(ContractError::NotOwner(account.owner_address().to_string())) - } -} - -/// Bond a gateway, sends [mixnet_contract_common::ExecuteMsg::BondGatewayOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. -pub fn try_bond_gateway( - gateway: Gateway, - owner_signature: MessageSignature, - amount: Coin, - info: MessageInfo, - env: Env, - deps: DepsMut<'_>, -) -> Result { - let mix_denom = MIX_DENOM.load(deps.storage)?; - let pledge = validate_funds(&[amount], mix_denom)?; - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_bond_gateway(gateway, owner_signature, pledge, &env, deps.storage) -} - -/// Unbond a gateway, sends [mixnet_contract_common::ExecuteMsg::UnbondGatewayOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. -pub fn try_unbond_gateway(info: MessageInfo, deps: DepsMut<'_>) -> Result { - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_unbond_gateway(deps.storage) -} - -/// Track gateway unbonding, invoked by the mixnet contract after succesful unbonding, message containes coins returned including any accrued rewards. -pub fn try_track_unbond_gateway( - owner: &str, - amount: Coin, - info: MessageInfo, - deps: DepsMut<'_>, -) -> Result { - 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_unbond_gateway(amount, deps.storage)?; - Ok(Response::new().add_event(new_track_gateway_unbond_event())) -} - -/// Bond a mixnode, sends [mixnet_contract_common::ExecuteMsg::BondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. -pub fn try_bond_mixnode( - mix_node: MixNode, - cost_params: MixNodeCostParams, - owner_signature: MessageSignature, - amount: Coin, - info: MessageInfo, - env: Env, - deps: DepsMut<'_>, -) -> Result { - let mix_denom = MIX_DENOM.load(deps.storage)?; - let pledge = validate_funds(&[amount], mix_denom)?; - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_bond_mixnode( - mix_node, - cost_params, - owner_signature, - pledge, - &env, - deps.storage, - ) -} - -pub fn try_pledge_more( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - amount: Coin, -) -> Result { - let mix_denom = MIX_DENOM.load(deps.storage)?; - let additional_pledge = validate_funds(&[amount], mix_denom)?; - - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_pledge_additional_tokens(additional_pledge, &env, deps.storage) -} - -/// 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 { - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_unbond_mixnode(deps.storage) -} - -/// Track mixnode unbonding, invoked by the mixnet contract after succesful unbonding, message containes coins returned including any accrued rewards. -pub fn try_track_unbond_mixnode( - owner: &str, - amount: Coin, - info: MessageInfo, - deps: DepsMut<'_>, -) -> Result { - 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_unbond_mixnode(amount, deps.storage)?; - Ok(Response::new().add_event(new_track_mixnode_unbond_event())) -} - -/// Track reward collection, invoked by the mixnert contract after sucessful reward compounding or claiming -fn try_track_reward( - deps: DepsMut<'_>, - info: MessageInfo, - amount: Coin, - address: &str, -) -> Result { - if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? { - return Err(ContractError::NotMixnetContract(info.sender)); - } - let account = account_from_address(address, deps.storage, deps.api)?; - account.track_reward(amount, deps.storage)?; - Ok(Response::new().add_event(new_track_reward_event())) -} - -/// Track undelegation, invoked by the mixnet contract after sucessful undelegation, message contains coins returned with any accrued rewards. -fn try_track_undelegation( - address: &str, - mix_id: MixId, - amount: Coin, - info: MessageInfo, - deps: DepsMut<'_>, -) -> Result { - if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? { - return Err(ContractError::NotMixnetContract(info.sender)); - } - let account = account_from_address(address, deps.storage, deps.api)?; - - account.track_undelegation(mix_id, amount, deps.storage)?; - Ok(Response::new().add_event(new_track_undelegation_event())) -} - -/// Delegate to mixnode, sends [mixnet_contract_common::ExecuteMsg::DelegateToMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].. -fn try_delegate_to_mixnode( - mix_id: MixId, - amount: Coin, - on_behalf_of: Option, - info: MessageInfo, - env: Env, - deps: DepsMut<'_>, -) -> Result { - // TODO - // as of 01.02.23 - // thus restricting it to 25, which is more than double of that, doesn't seem too unreasonable. - - // while this might not be the best workaround, if user wishes to delegate more tokens towards the same node - // they could remove the existing delegation (thus removing all separate entries from the storage) - // and re-delegate it with the reclaimed amount (which will include all rewards). - - let mix_denom = MIX_DENOM.load(deps.storage)?; - let amount = validate_funds(&[amount], mix_denom)?; - - let account = match on_behalf_of { - Some(account_owner) => { - let account = account_from_address(&account_owner, deps.storage, deps.api)?; - ensure_staking_permission(&info.sender, &account)?; - account - } - // you're the owner, you can do what you want - None => account_from_address(info.sender.as_str(), deps.storage, deps.api)?, - }; - - account.try_delegate_to_mixnode(mix_id, amount, &env, deps.storage) -} - -/// Claims operator reward, sends [mixnet_contract_common::ExecuteMsg::ClaimOperatorRewardOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. -fn try_claim_operator_reward( - deps: DepsMut<'_>, - info: MessageInfo, -) -> Result { - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_claim_operator_reward(deps.storage) -} - -/// Claims delegator reward, sends [mixnet_contract_common::ExecuteMsg::ClaimDelegatorRewardOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. -fn try_claim_delegator_reward( - deps: DepsMut<'_>, - info: MessageInfo, - mix_id: MixId, -) -> Result { - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - - account.try_claim_delegator_reward(mix_id, deps.storage) -} - -/// Undelegates from a mixnode, sends [mixnet_contract_common::ExecuteMsg::UndelegateFromMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. -fn try_undelegate_from_mixnode( - mix_id: MixId, - on_behalf_of: Option, - info: MessageInfo, - deps: DepsMut<'_>, -) -> Result { - let account = match on_behalf_of { - Some(account_owner) => { - let account = account_from_address(&account_owner, deps.storage, deps.api)?; - ensure_staking_permission(&info.sender, &account)?; - account - } - // you're the owner, you can do what you want - None => account_from_address(info.sender.as_str(), deps.storage, deps.api)?, - }; - - account.try_undelegate_from_mixnode(mix_id, deps.storage) -} - -/// Creates a new periodic vesting account, and deposits funds to vest into the contract. -/// -/// Callable by ADMIN only, see [instantiate]. -pub(crate) fn try_create_periodic_vesting_account( - owner_address: &str, - staking_address: Option, - vesting_spec: Option, - cap: Option, - info: MessageInfo, - env: Env, - deps: DepsMut<'_>, -) -> Result { - if info.sender != ADMIN.load(deps.storage)? { - return Err(ContractError::NotAdmin(info.sender.as_str().to_string())); - } - - let mix_denom = MIX_DENOM.load(deps.storage)?; - - let account_exists = account_from_address(owner_address, deps.storage, deps.api).is_ok(); - if account_exists { - return Err(ContractError::AccountAlreadyExists( - owner_address.to_string(), - )); - } - - let vesting_spec = vesting_spec.unwrap_or_default(); - - let coin = validate_funds(&info.funds, mix_denom)?; - - let owner_address = deps.api.addr_validate(owner_address)?; - let staking_address = if let Some(staking_address) = staking_address { - let staking_account_exists = - account_from_address(&staking_address, deps.storage, deps.api).is_ok(); - if staking_account_exists { - return Err(ContractError::StakingAccountAlreadyExists(staking_address)); - } - Some(deps.api.addr_validate(&staking_address)?) - } else { - None - }; - 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); - - let response = Response::new(); - - Account::new( - owner_address.clone(), - staking_address.clone(), - coin.clone(), - start_time, - periods, - cap, - deps.storage, - )?; - - Ok(response.add_event(new_periodic_vesting_account_event( - &owner_address, - &coin, - &staking_address, - start_time, - ))) -} - #[entry_point] pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { let query_res = match msg { @@ -757,6 +286,19 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { to_binary(&try_get_current_vesting_period(&address, deps, env)?) } + QueryMsg::GetDelegation { + address, + mix_id, + block_timestamp_secs, + } => to_binary(&try_get_delegation( + deps, + &address, + mix_id, + block_timestamp_secs, + )?), + QueryMsg::GetTotalDelegationAmount { address, mix_id } => { + to_binary(&try_get_delegation_amount(deps, &address, mix_id)?) + } QueryMsg::GetDelegationTimes { address, mix_id } => { to_binary(&try_get_delegation_times(deps, &address, mix_id)?) } @@ -768,313 +310,7 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result, - env: Env, -) -> Result { - let account = account_from_address(address, deps.storage, deps.api)?; - account.get_current_vesting_period(env.block.time) -} - -/// Loads mixnode bond from vesting contract storage. -pub fn try_get_mixnode(address: &str, deps: Deps<'_>) -> Result, ContractError> { - let account = account_from_address(address, deps.storage, deps.api)?; - account.load_mixnode_pledge(deps.storage) -} - -/// Loads gateway bond from vesting contract storage. -pub fn try_get_gateway(address: &str, deps: Deps<'_>) -> Result, 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_from_address(address, deps.storage, deps.api) -} - -/// Gets build information of this contract. -pub fn get_contract_version() -> ContractBuildInformation { - // as per docs - // env! macro will expand to the value of the named environment variable at - // compile time, yielding an expression of type `&'static str` - ContractBuildInformation { - build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(), - build_version: env!("VERGEN_BUILD_SEMVER").to_string(), - commit_sha: option_env!("VERGEN_GIT_SHA").unwrap_or("NONE").to_string(), - commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP") - .unwrap_or("NONE") - .to_string(), - commit_branch: option_env!("VERGEN_GIT_BRANCH") - .unwrap_or("NONE") - .to_string(), - rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(), - } -} - -pub fn try_get_all_accounts( - deps: Deps<'_>, - start_after: Option, - limit: Option, -) -> Result { - let limit = limit.unwrap_or(150).min(250) as usize; - - let start = start_after - .map(|raw| deps.api.addr_validate(&raw).map(Bound::exclusive)) - .transpose()?; - - let accounts = ACCOUNTS - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|res| { - res.map(|(_, account)| BaseVestingAccountInfo { - account_id: account.storage_key(), - owner: account.owner_address, - }) - }) - .collect::>>()?; - - let start_next_after = accounts.last().map(|acc| acc.owner.clone()); - - Ok(AccountsResponse { - accounts, - start_next_after, - }) -} - -pub fn try_get_all_accounts_vesting_coins( - deps: Deps<'_>, - env: Env, - start_after: Option, - limit: Option, -) -> Result { - let limit = limit.unwrap_or(150).min(250) as usize; - - let start = start_after - .map(|raw| deps.api.addr_validate(&raw).map(Bound::exclusive)) - .transpose()?; - - let accounts = ACCOUNTS - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|res| { - res.map(|(_, account)| { - account - .get_vesting_coins(None, &env, deps.storage) - .map(|still_vesting| AccountVestingCoins { - account_id: account.storage_key(), - owner: account.owner_address, - still_vesting, - }) - }) - }) - .collect::, _>>>()??; - - let start_next_after = accounts.last().map(|acc| acc.owner.clone()); - - Ok(VestingCoinsResponse { - accounts, - start_next_after, - }) -} - -/// Gets currently locked coins, see [crate::traits::VestingAccount::locked_coins] -pub fn try_get_locked_coins( - vesting_account_address: &str, - block_time: Option, - env: Env, - deps: Deps<'_>, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - account.locked_coins(block_time, &env, deps.storage) -} - -/// Returns currently locked coins, see [crate::traits::VestingAccount::spendable_coins] -pub fn try_get_spendable_coins( - vesting_account_address: &str, - block_time: Option, - env: Env, - deps: Deps<'_>, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - account.spendable_coins(block_time, &env, deps.storage) -} - -/// Returns coins that have vested, see [crate::traits::VestingAccount::get_vested_coins] -pub fn try_get_vested_coins( - vesting_account_address: &str, - block_time: Option, - env: Env, - deps: Deps<'_>, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - account.get_vested_coins(block_time, &env, deps.storage) -} - -/// Returns coins that are vesting, see [crate::traits::VestingAccount::get_vesting_coins] -pub fn try_get_vesting_coins( - vesting_account_address: &str, - block_time: Option, - env: Env, - deps: Deps<'_>, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - account.get_vesting_coins(block_time, &env, deps.storage) -} - -/// See [crate::traits::VestingAccount::get_start_time] -pub fn try_get_start_time( - vesting_account_address: &str, - deps: Deps<'_>, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - Ok(account.get_start_time()) -} - -/// See [crate::traits::VestingAccount::get_end_time] -pub fn try_get_end_time( - vesting_account_address: &str, - deps: Deps<'_>, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - Ok(account.get_end_time()) -} - -/// See [crate::traits::VestingAccount::get_original_vesting] -pub fn try_get_original_vesting( - vesting_account_address: &str, - deps: Deps<'_>, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - account.get_original_vesting() -} - -pub fn try_get_historical_vesting_staking_reward( - vesting_account_address: &str, - deps: Deps<'_>, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - account.get_historical_vested_staking_rewards(deps.storage) -} - -pub fn try_get_spendable_vested_coins( - vesting_account_address: &str, - deps: Deps<'_>, - env: Env, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - account.spendable_vested_coins(None, &env, deps.storage) -} - -pub fn try_get_spendable_reward_coins( - vesting_account_address: &str, - deps: Deps<'_>, - env: Env, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - account.spendable_reward_coins(None, &env, deps.storage) -} - -pub fn try_get_delegated_coins( - vesting_account_address: &str, - deps: Deps<'_>, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - let denom = MIX_DENOM.load(deps.storage)?; - let amount = account.total_delegations(deps.storage)?; - Ok(Coin { denom, amount }) -} - -pub fn try_get_pledged_coins( - vesting_account_address: &str, - deps: Deps<'_>, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - let denom = MIX_DENOM.load(deps.storage)?; - let amount = account.total_pledged(deps.storage)?; - Ok(Coin { denom, amount }) -} - -pub fn try_get_staked_coins( - vesting_account_address: &str, - deps: Deps<'_>, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - let denom = MIX_DENOM.load(deps.storage)?; - let amount = account.total_staked(deps.storage)?; - Ok(Coin { denom, amount }) -} - -pub fn try_get_withdrawn_coins( - vesting_account_address: &str, - deps: Deps<'_>, -) -> Result { - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - let denom = MIX_DENOM.load(deps.storage)?; - let amount = account.load_withdrawn(deps.storage)?; - Ok(Coin { denom, amount }) -} - -/// Returns timestamps at which delegations were made -pub fn try_get_delegation_times( - deps: Deps<'_>, - vesting_account_address: &str, - mix_id: MixId, -) -> Result { - let owner = deps.api.addr_validate(vesting_account_address)?; - let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - - let delegation_timestamps = DELEGATIONS - .prefix((account.storage_key(), mix_id)) - .keys(deps.storage, None, None, Order::Ascending) - .collect::>>()?; - - Ok(DelegationTimesResponse { - owner, - account_id: account.storage_key(), - mix_id, - delegation_timestamps, - }) -} - -pub fn try_get_all_delegations( - deps: Deps<'_>, - start_after: Option<(u32, MixId, BlockTimestampSecs)>, - limit: Option, -) -> Result { - let limit = limit.unwrap_or(100).min(200) as usize; - - let start = start_after.map(Bound::exclusive); - let delegations = DELEGATIONS - .range(deps.storage, start, None, Order::Ascending) - .map(|kv| { - kv.map( - |((account_id, mix_id, block_timestamp), amount)| VestingDelegation { - account_id, - mix_id, - block_timestamp, - amount, - }, - ) - }) - .collect::>>()?; - - let start_next_after = if delegations.len() < limit { - None - } else { - delegations - .last() - .map(|delegation| delegation.storage_key()) - }; - - Ok(AllDelegationsResponse { - delegations, - start_next_after, - }) -} - -fn validate_funds(funds: &[Coin], mix_denom: String) -> Result { +pub(crate) fn validate_funds(funds: &[Coin], mix_denom: String) -> Result { if funds.is_empty() || funds[0].amount.is_zero() { return Err(ContractError::EmptyFunds); } @@ -1090,7 +326,10 @@ fn validate_funds(funds: &[Coin], mix_denom: String) -> Result Result<(), ContractError> { +pub(crate) fn ensure_staking_permission( + addr: &Addr, + account: &Account, +) -> Result<(), ContractError> { if let Some(staking_address) = account.staking_address() { if staking_address == addr { return Ok(()); diff --git a/contracts/vesting/src/lib.rs b/contracts/vesting/src/lib.rs index ad4fd4f5f4..702530b927 100644 --- a/contracts/vesting/src/lib.rs +++ b/contracts/vesting/src/lib.rs @@ -6,8 +6,10 @@ pub mod contract; mod errors; +mod queries; mod queued_migrations; mod storage; mod support; mod traits; +mod transactions; pub mod vesting; diff --git a/contracts/vesting/src/queries.rs b/contracts/vesting/src/queries.rs new file mode 100644 index 0000000000..a33bb84256 --- /dev/null +++ b/contracts/vesting/src/queries.rs @@ -0,0 +1,357 @@ +// Copyright 2021-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::errors::ContractError; +use crate::storage; +use crate::storage::{account_from_address, BlockTimestampSecs, ACCOUNTS, DELEGATIONS, MIX_DENOM}; +use crate::traits::VestingAccount; +use crate::vesting::Account; +use contracts_common::ContractBuildInformation; +use cosmwasm_std::{Coin, Deps, Env, Order, StdResult, Timestamp, Uint128}; +use cw_storage_plus::Bound; +use mixnet_contract_common::MixId; +use vesting_contract_common::{ + AccountVestingCoins, AccountsResponse, AllDelegationsResponse, BaseVestingAccountInfo, + DelegationTimesResponse, OriginalVestingResponse, Period, PledgeData, VestingCoinsResponse, + VestingDelegation, +}; + +/// Get current vesting period for a given [crate::vesting::Account]. +pub fn try_get_current_vesting_period( + address: &str, + deps: Deps<'_>, + env: Env, +) -> Result { + let account = account_from_address(address, deps.storage, deps.api)?; + account.get_current_vesting_period(env.block.time) +} + +/// Loads mixnode bond from vesting contract storage. +pub fn try_get_mixnode(address: &str, deps: Deps<'_>) -> Result, ContractError> { + let account = account_from_address(address, deps.storage, deps.api)?; + account.load_mixnode_pledge(deps.storage) +} + +/// Loads gateway bond from vesting contract storage. +pub fn try_get_gateway(address: &str, deps: Deps<'_>) -> Result, 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_from_address(address, deps.storage, deps.api) +} + +/// Gets build information of this contract. +pub fn get_contract_version() -> ContractBuildInformation { + // as per docs + // env! macro will expand to the value of the named environment variable at + // compile time, yielding an expression of type `&'static str` + ContractBuildInformation { + build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(), + build_version: env!("VERGEN_BUILD_SEMVER").to_string(), + commit_sha: option_env!("VERGEN_GIT_SHA").unwrap_or("NONE").to_string(), + commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP") + .unwrap_or("NONE") + .to_string(), + commit_branch: option_env!("VERGEN_GIT_BRANCH") + .unwrap_or("NONE") + .to_string(), + rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(), + } +} + +pub fn try_get_all_accounts( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit.unwrap_or(150).min(250) as usize; + + let start = start_after + .map(|raw| deps.api.addr_validate(&raw).map(Bound::exclusive)) + .transpose()?; + + let accounts = ACCOUNTS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| { + res.map(|(_, account)| BaseVestingAccountInfo { + account_id: account.storage_key(), + owner: account.owner_address, + }) + }) + .collect::>>()?; + + let start_next_after = accounts.last().map(|acc| acc.owner.clone()); + + Ok(AccountsResponse { + accounts, + start_next_after, + }) +} + +pub fn try_get_all_accounts_vesting_coins( + deps: Deps<'_>, + env: Env, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit.unwrap_or(150).min(250) as usize; + + let start = start_after + .map(|raw| deps.api.addr_validate(&raw).map(Bound::exclusive)) + .transpose()?; + + let accounts = ACCOUNTS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| { + res.map(|(_, account)| { + account + .get_vesting_coins(None, &env, deps.storage) + .map(|still_vesting| AccountVestingCoins { + account_id: account.storage_key(), + owner: account.owner_address, + still_vesting, + }) + }) + }) + .collect::, _>>>()??; + + let start_next_after = accounts.last().map(|acc| acc.owner.clone()); + + Ok(VestingCoinsResponse { + accounts, + start_next_after, + }) +} + +/// Gets currently locked coins, see [crate::traits::VestingAccount::locked_coins] +pub fn try_get_locked_coins( + vesting_account_address: &str, + block_time: Option, + env: Env, + deps: Deps<'_>, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + account.locked_coins(block_time, &env, deps.storage) +} + +/// Returns currently locked coins, see [crate::traits::VestingAccount::spendable_coins] +pub fn try_get_spendable_coins( + vesting_account_address: &str, + block_time: Option, + env: Env, + deps: Deps<'_>, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + account.spendable_coins(block_time, &env, deps.storage) +} + +/// Returns coins that have vested, see [crate::traits::VestingAccount::get_vested_coins] +pub fn try_get_vested_coins( + vesting_account_address: &str, + block_time: Option, + env: Env, + deps: Deps<'_>, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + account.get_vested_coins(block_time, &env, deps.storage) +} + +/// Returns coins that are vesting, see [crate::traits::VestingAccount::get_vesting_coins] +pub fn try_get_vesting_coins( + vesting_account_address: &str, + block_time: Option, + env: Env, + deps: Deps<'_>, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + account.get_vesting_coins(block_time, &env, deps.storage) +} + +/// See [crate::traits::VestingAccount::get_start_time] +pub fn try_get_start_time( + vesting_account_address: &str, + deps: Deps<'_>, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + Ok(account.get_start_time()) +} + +/// See [crate::traits::VestingAccount::get_end_time] +pub fn try_get_end_time( + vesting_account_address: &str, + deps: Deps<'_>, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + Ok(account.get_end_time()) +} + +/// See [crate::traits::VestingAccount::get_original_vesting] +pub fn try_get_original_vesting( + vesting_account_address: &str, + deps: Deps<'_>, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + account.get_original_vesting() +} + +pub fn try_get_historical_vesting_staking_reward( + vesting_account_address: &str, + deps: Deps<'_>, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + account.get_historical_vested_staking_rewards(deps.storage) +} + +pub fn try_get_spendable_vested_coins( + vesting_account_address: &str, + deps: Deps<'_>, + env: Env, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + account.spendable_vested_coins(None, &env, deps.storage) +} + +pub fn try_get_spendable_reward_coins( + vesting_account_address: &str, + deps: Deps<'_>, + env: Env, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + account.spendable_reward_coins(None, &env, deps.storage) +} + +pub fn try_get_delegated_coins( + vesting_account_address: &str, + deps: Deps<'_>, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + let denom = MIX_DENOM.load(deps.storage)?; + let amount = account.total_delegations(deps.storage)?; + Ok(Coin { denom, amount }) +} + +pub fn try_get_pledged_coins( + vesting_account_address: &str, + deps: Deps<'_>, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + let denom = MIX_DENOM.load(deps.storage)?; + let amount = account.total_pledged(deps.storage)?; + Ok(Coin { denom, amount }) +} + +pub fn try_get_staked_coins( + vesting_account_address: &str, + deps: Deps<'_>, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + let denom = MIX_DENOM.load(deps.storage)?; + let amount = account.total_staked(deps.storage)?; + Ok(Coin { denom, amount }) +} + +pub fn try_get_withdrawn_coins( + vesting_account_address: &str, + deps: Deps<'_>, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + let denom = MIX_DENOM.load(deps.storage)?; + let amount = account.load_withdrawn(deps.storage)?; + Ok(Coin { denom, amount }) +} + +/// Returns timestamps at which delegations were made +pub fn try_get_delegation_times( + deps: Deps<'_>, + vesting_account_address: &str, + mix_id: MixId, +) -> Result { + let owner = deps.api.addr_validate(vesting_account_address)?; + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + + let delegation_timestamps = + storage::load_delegation_timestamps((account.storage_key(), mix_id), deps.storage)?; + + Ok(DelegationTimesResponse { + owner, + account_id: account.storage_key(), + mix_id, + delegation_timestamps, + }) +} + +pub fn try_get_all_delegations( + deps: Deps<'_>, + start_after: Option<(u32, MixId, BlockTimestampSecs)>, + limit: Option, +) -> Result { + let limit = limit.unwrap_or(100).min(200) as usize; + + let start = start_after.map(Bound::exclusive); + let delegations = DELEGATIONS + .range(deps.storage, start, None, Order::Ascending) + .map(|kv| { + kv.map( + |((account_id, mix_id, block_timestamp), amount)| VestingDelegation { + account_id, + mix_id, + block_timestamp, + amount, + }, + ) + }) + .collect::>>()?; + + let start_next_after = if delegations.len() < limit { + None + } else { + delegations + .last() + .map(|delegation| delegation.storage_key()) + }; + + Ok(AllDelegationsResponse { + delegations, + start_next_after, + }) +} + +pub fn try_get_delegation( + deps: Deps<'_>, + vesting_account_address: &str, + mix_id: MixId, + block_timestamp_secs: BlockTimestampSecs, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + + let storage_key = (account.storage_key(), mix_id, block_timestamp_secs); + let delegation_amount = DELEGATIONS.load(deps.storage, storage_key)?; + + Ok(VestingDelegation { + account_id: account.storage_key(), + mix_id, + block_timestamp: block_timestamp_secs, + amount: delegation_amount, + }) +} + +pub fn try_get_delegation_amount( + deps: Deps<'_>, + vesting_account_address: &str, + mix_id: MixId, +) -> Result { + let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; + + let amount = DELEGATIONS + .prefix((account.storage_key(), mix_id)) + .range(deps.storage, None, None, Order::Ascending) + .map(|kv_res| kv_res.map(|kv| kv.1)) + .try_fold(Uint128::zero(), |acc, x_res| x_res.map(|x| x + acc))?; + let denom = MIX_DENOM.load(deps.storage)?; + + Ok(Coin { denom, amount }) +} diff --git a/contracts/vesting/src/transactions.rs b/contracts/vesting/src/transactions.rs new file mode 100644 index 0000000000..c43e084cff --- /dev/null +++ b/contracts/vesting/src/transactions.rs @@ -0,0 +1,473 @@ +// Copyright 2021-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::{ensure_staking_permission, validate_funds}; +use crate::errors::ContractError; +use crate::storage::{ + account_from_address, save_account, ADMIN, MIXNET_CONTRACT_ADDRESS, MIX_DENOM, +}; +use crate::traits::{ + DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, NodeFamilies, VestingAccount, +}; +use crate::vesting::{populate_vesting_periods, Account}; +use contracts_common::signing::MessageSignature; +use cosmwasm_std::{coin, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Timestamp}; +use mixnet_contract_common::families::FamilyHead; +use mixnet_contract_common::{ + Gateway, GatewayConfigUpdate, MixId, MixNode, MixNodeConfigUpdate, MixNodeCostParams, +}; +use vesting_contract_common::events::{ + new_ownership_transfer_event, new_periodic_vesting_account_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_vested_coins_withdraw_event, +}; +use vesting_contract_common::messages::VestingSpecification; +use vesting_contract_common::PledgeCap; + +pub fn try_create_family( + info: MessageInfo, + deps: DepsMut, + label: String, +) -> Result { + let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; + account.try_create_family(deps.storage, label) +} +pub fn try_join_family( + info: MessageInfo, + deps: DepsMut, + join_permit: MessageSignature, + family_head: FamilyHead, +) -> Result { + let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; + account.try_join_family(deps.storage, join_permit, family_head) +} +pub fn try_leave_family( + info: MessageInfo, + deps: DepsMut, + family_head: FamilyHead, +) -> Result { + let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; + account.try_leave_family(deps.storage, family_head) +} +pub fn try_kick_family_member( + info: MessageInfo, + deps: DepsMut, + member: String, +) -> Result { + let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; + account.try_head_kick_member(deps.storage, &member) +} + +/// Update locked_pledge_cap, the hard cap for staking/bonding with unvested tokens. +/// +/// Callable by ADMIN only, see [instantiate]. +pub fn try_update_locked_pledge_cap( + address: String, + cap: PledgeCap, + info: MessageInfo, + deps: DepsMut, +) -> Result { + if info.sender != ADMIN.load(deps.storage)? { + return Err(ContractError::NotAdmin(info.sender.as_str().to_string())); + } + let mut account = account_from_address(&address, deps.storage, deps.api)?; + + account.pledge_cap = Some(cap); + save_account(&account, deps.storage)?; + Ok(Response::default()) +} + +/// Update config for a mixnode bonded with vesting account, sends [mixnet_contract_common::ExecuteMsg::UpdateMixnodeConfig] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. +pub fn try_update_mixnode_config( + new_config: MixNodeConfigUpdate, + info: MessageInfo, + deps: DepsMut, +) -> Result { + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_update_mixnode_config(new_config, deps.storage) +} + +pub fn try_update_gateway_config( + new_config: GatewayConfigUpdate, + info: MessageInfo, + deps: DepsMut, +) -> Result { + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_update_gateway_config(new_config, deps.storage) +} + +pub fn try_update_mixnode_cost_params( + new_costs: MixNodeCostParams, + info: MessageInfo, + deps: DepsMut, +) -> Result { + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_update_mixnode_cost_params(new_costs, deps.storage) +} + +/// Updates mixnet contract address, for cases when a new mixnet contract is deployed. +/// +/// Callable by ADMIN only, see [instantiate]. +pub fn try_update_mixnet_address( + address: String, + info: MessageInfo, + deps: DepsMut<'_>, +) -> Result { + if info.sender != ADMIN.load(deps.storage)? { + return Err(ContractError::NotAdmin(info.sender.as_str().to_string())); + } + let mixnet_contract_address = deps.api.addr_validate(&address)?; + + MIXNET_CONTRACT_ADDRESS.save(deps.storage, &mixnet_contract_address)?; + Ok(Response::default()) +} + +/// Withdraw already vested coins. +pub fn try_withdraw_vested_coins( + amount: Coin, + env: Env, + info: MessageInfo, + deps: DepsMut<'_>, +) -> Result { + let mix_denom = MIX_DENOM.load(deps.storage)?; + if amount.denom != mix_denom { + return Err(ContractError::WrongDenom(amount.denom, mix_denom)); + } + + let address = info.sender.clone(); + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + if address != account.owner_address() { + return Err(ContractError::NotOwner(account.owner_address().to_string())); + } + let spendable_coins = account.spendable_coins(None, &env, deps.storage)?; + if amount.amount <= spendable_coins.amount { + let new_balance = account.withdraw(&amount, deps.storage)?; + + let send_tokens = BankMsg::Send { + to_address: account.owner_address().as_str().to_string(), + amount: vec![amount.clone()], + }; + + Ok(Response::new() + .add_message(send_tokens) + .add_event(new_vested_coins_withdraw_event( + &address, + &amount, + &coin(new_balance, &amount.denom), + ))) + } else { + Err(ContractError::InsufficientSpendable( + account.owner_address().as_str().to_string(), + spendable_coins.amount.u128(), + )) + } +} + +/// Transfer ownership of the entire vesting account. +pub fn try_transfer_ownership( + to_address: String, + info: MessageInfo, + deps: DepsMut<'_>, +) -> Result { + let address = info.sender.clone(); + let to_address = deps.api.addr_validate(&to_address)?; + let mut account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + if address == account.owner_address() { + account.transfer_ownership(&to_address, deps.storage)?; + Ok(Response::new().add_event(new_ownership_transfer_event(&address, &to_address))) + } else { + Err(ContractError::NotOwner(account.owner_address().to_string())) + } +} + +/// Set or update staking address for a vesting account. +pub fn try_update_staking_address( + to_address: Option, + info: MessageInfo, + deps: DepsMut<'_>, +) -> Result { + if let Some(ref to_address) = to_address { + if account_from_address(to_address, deps.storage, deps.api).is_ok() { + // do not allow setting staking address to an existing account's address + return Err(ContractError::StakingAccountExists(to_address.to_string())); + } + } + + let address = info.sender.clone(); + let to_address = to_address.and_then(|x| deps.api.addr_validate(&x).ok()); + let mut account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + if address == account.owner_address() { + let old = account.staking_address().cloned(); + account.update_staking_address(to_address.clone(), deps.storage)?; + Ok(Response::new().add_event(new_staking_address_update_event(&old, &to_address))) + } else { + Err(ContractError::NotOwner(account.owner_address().to_string())) + } +} + +/// Bond a gateway, sends [mixnet_contract_common::ExecuteMsg::BondGatewayOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. +pub fn try_bond_gateway( + gateway: Gateway, + owner_signature: MessageSignature, + amount: Coin, + info: MessageInfo, + env: Env, + deps: DepsMut<'_>, +) -> Result { + let mix_denom = MIX_DENOM.load(deps.storage)?; + let pledge = validate_funds(&[amount], mix_denom)?; + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_bond_gateway(gateway, owner_signature, pledge, &env, deps.storage) +} + +/// Unbond a gateway, sends [mixnet_contract_common::ExecuteMsg::UnbondGatewayOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. +pub fn try_unbond_gateway(info: MessageInfo, deps: DepsMut<'_>) -> Result { + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_unbond_gateway(deps.storage) +} + +/// Track gateway unbonding, invoked by the mixnet contract after succesful unbonding, message containes coins returned including any accrued rewards. +pub fn try_track_unbond_gateway( + owner: &str, + amount: Coin, + info: MessageInfo, + deps: DepsMut<'_>, +) -> Result { + 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_unbond_gateway(amount, deps.storage)?; + Ok(Response::new().add_event(new_track_gateway_unbond_event())) +} + +/// Bond a mixnode, sends [mixnet_contract_common::ExecuteMsg::BondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. +pub fn try_bond_mixnode( + mix_node: MixNode, + cost_params: MixNodeCostParams, + owner_signature: MessageSignature, + amount: Coin, + info: MessageInfo, + env: Env, + deps: DepsMut<'_>, +) -> Result { + let mix_denom = MIX_DENOM.load(deps.storage)?; + let pledge = validate_funds(&[amount], mix_denom)?; + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_bond_mixnode( + mix_node, + cost_params, + owner_signature, + pledge, + &env, + deps.storage, + ) +} + +pub fn try_pledge_more( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + amount: Coin, +) -> Result { + let mix_denom = MIX_DENOM.load(deps.storage)?; + let additional_pledge = validate_funds(&[amount], mix_denom)?; + + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_pledge_additional_tokens(additional_pledge, &env, deps.storage) +} + +/// 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 { + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_unbond_mixnode(deps.storage) +} + +/// Track mixnode unbonding, invoked by the mixnet contract after succesful unbonding, message containes coins returned including any accrued rewards. +pub fn try_track_unbond_mixnode( + owner: &str, + amount: Coin, + info: MessageInfo, + deps: DepsMut<'_>, +) -> Result { + 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_unbond_mixnode(amount, deps.storage)?; + Ok(Response::new().add_event(new_track_mixnode_unbond_event())) +} + +/// Track reward collection, invoked by the mixnert contract after sucessful reward compounding or claiming +pub fn try_track_reward( + deps: DepsMut<'_>, + info: MessageInfo, + amount: Coin, + address: &str, +) -> Result { + if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? { + return Err(ContractError::NotMixnetContract(info.sender)); + } + let account = account_from_address(address, deps.storage, deps.api)?; + account.track_reward(amount, deps.storage)?; + Ok(Response::new().add_event(new_track_reward_event())) +} + +/// Track undelegation, invoked by the mixnet contract after sucessful undelegation, message contains coins returned with any accrued rewards. +pub fn try_track_undelegation( + address: &str, + mix_id: MixId, + amount: Coin, + info: MessageInfo, + deps: DepsMut<'_>, +) -> Result { + if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? { + return Err(ContractError::NotMixnetContract(info.sender)); + } + let account = account_from_address(address, deps.storage, deps.api)?; + + account.track_undelegation(mix_id, amount, deps.storage)?; + Ok(Response::new().add_event(new_track_undelegation_event())) +} + +/// Delegate to mixnode, sends [mixnet_contract_common::ExecuteMsg::DelegateToMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].. +pub fn try_delegate_to_mixnode( + mix_id: MixId, + amount: Coin, + on_behalf_of: Option, + info: MessageInfo, + env: Env, + deps: DepsMut<'_>, +) -> Result { + // TODO + // as of 01.02.23 + // thus restricting it to 25, which is more than double of that, doesn't seem too unreasonable. + + // while this might not be the best workaround, if user wishes to delegate more tokens towards the same node + // they could remove the existing delegation (thus removing all separate entries from the storage) + // and re-delegate it with the reclaimed amount (which will include all rewards). + + let mix_denom = MIX_DENOM.load(deps.storage)?; + let amount = validate_funds(&[amount], mix_denom)?; + + let account = match on_behalf_of { + Some(account_owner) => { + let account = account_from_address(&account_owner, deps.storage, deps.api)?; + ensure_staking_permission(&info.sender, &account)?; + account + } + // you're the owner, you can do what you want + None => account_from_address(info.sender.as_str(), deps.storage, deps.api)?, + }; + + account.try_delegate_to_mixnode(mix_id, amount, &env, deps.storage) +} + +/// Claims operator reward, sends [mixnet_contract_common::ExecuteMsg::ClaimOperatorRewardOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. +pub fn try_claim_operator_reward( + deps: DepsMut<'_>, + info: MessageInfo, +) -> Result { + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_claim_operator_reward(deps.storage) +} + +/// Claims delegator reward, sends [mixnet_contract_common::ExecuteMsg::ClaimDelegatorRewardOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. +pub fn try_claim_delegator_reward( + deps: DepsMut<'_>, + info: MessageInfo, + mix_id: MixId, +) -> Result { + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + + account.try_claim_delegator_reward(mix_id, deps.storage) +} + +/// Undelegates from a mixnode, sends [mixnet_contract_common::ExecuteMsg::UndelegateFromMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. +pub fn try_undelegate_from_mixnode( + mix_id: MixId, + on_behalf_of: Option, + info: MessageInfo, + deps: DepsMut<'_>, +) -> Result { + let account = match on_behalf_of { + Some(account_owner) => { + let account = account_from_address(&account_owner, deps.storage, deps.api)?; + ensure_staking_permission(&info.sender, &account)?; + account + } + // you're the owner, you can do what you want + None => account_from_address(info.sender.as_str(), deps.storage, deps.api)?, + }; + + account.try_undelegate_from_mixnode(mix_id, deps.storage) +} + +/// Creates a new periodic vesting account, and deposits funds to vest into the contract. +/// +/// Callable by ADMIN only, see [instantiate]. +pub fn try_create_periodic_vesting_account( + owner_address: &str, + staking_address: Option, + vesting_spec: Option, + cap: Option, + info: MessageInfo, + env: Env, + deps: DepsMut<'_>, +) -> Result { + if info.sender != ADMIN.load(deps.storage)? { + return Err(ContractError::NotAdmin(info.sender.as_str().to_string())); + } + + let mix_denom = MIX_DENOM.load(deps.storage)?; + + let account_exists = account_from_address(owner_address, deps.storage, deps.api).is_ok(); + if account_exists { + return Err(ContractError::AccountAlreadyExists( + owner_address.to_string(), + )); + } + + let vesting_spec = vesting_spec.unwrap_or_default(); + + let coin = validate_funds(&info.funds, mix_denom)?; + + let owner_address = deps.api.addr_validate(owner_address)?; + let staking_address = if let Some(staking_address) = staking_address { + let staking_account_exists = + account_from_address(&staking_address, deps.storage, deps.api).is_ok(); + if staking_account_exists { + return Err(ContractError::StakingAccountAlreadyExists(staking_address)); + } + Some(deps.api.addr_validate(&staking_address)?) + } else { + None + }; + 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); + + let response = Response::new(); + + Account::new( + owner_address.clone(), + staking_address.clone(), + coin.clone(), + start_time, + periods, + cap, + deps.storage, + )?; + + Ok(response.add_event(new_periodic_vesting_account_event( + &owner_address, + &coin, + &staking_address, + start_time, + ))) +}