// Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::contract::{ensure_staking_permission, validate_funds}; 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, StorableVestingAccountExt}; 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_migrate_mixnode_event, new_track_mixnode_pledge_decrease_event, new_track_mixnode_unbond_event, new_track_reward_event, new_track_undelegation_event, new_vested_coins_withdraw_event, }; use vesting_contract_common::{Account, PledgeCap, VestingContractError, VestingSpecification}; 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(VestingContractError::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(VestingContractError::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(VestingContractError::WrongDenom(amount.denom, mix_denom)); } let address = info.sender; let account = account_from_address(address.as_str(), deps.storage, deps.api)?; if address != account.owner_address() { return Err(VestingContractError::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(VestingContractError::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; let to_address = deps.api.addr_validate(&to_address)?; let mut account = account_from_address(address.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(VestingContractError::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(VestingContractError::StakingAccountExists( to_address.to_string(), )); } } let address = info.sender; let to_address = to_address.and_then(|x| deps.api.addr_validate(&x).ok()); let mut account = account_from_address(address.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(VestingContractError::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(VestingContractError::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())) } /// Track vesting mixnode being converted into the usage of liquid tokens. invoked by the mixnet contract after successful migration. pub fn try_track_migrate_mixnode( owner: &str, info: MessageInfo, deps: DepsMut<'_>, ) -> Result { if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? { return Err(VestingContractError::NotMixnetContract(info.sender)); } let account = account_from_address(owner, deps.storage, deps.api)?; account.try_track_migrated_mixnode(deps.storage)?; Ok(Response::new().add_event(new_track_migrate_mixnode_event())) } /// Track vesting delegation being converted into the usage of liquid tokens. invoked by the mixnet contract after successful migration. pub fn try_track_migrate_delegation( owner: &str, mix_id: MixId, info: MessageInfo, deps: DepsMut<'_>, ) -> Result { if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? { return Err(VestingContractError::NotMixnetContract(info.sender)); } let account = account_from_address(owner, deps.storage, deps.api)?; account.track_migrated_delegation(mix_id, deps.storage)?; Ok(Response::new().add_event(new_track_migrate_mixnode_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) } pub fn try_decrease_pledge( deps: DepsMut<'_>, info: MessageInfo, amount: Coin, ) -> Result { let mix_denom = MIX_DENOM.load(deps.storage)?; // perform basic validation - is it correct demon, is it non-zero, etc. let decrease = validate_funds(&[amount], mix_denom)?; let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; account.try_decrease_mixnode_pledge(decrease, 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(VestingContractError::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())) } /// Tracks decreasing mixnode pledge. Invoked by the mixnet contract after successful event reconciliation. /// A separate BankMsg containing the specified amount was sent in the same transaction. pub fn try_track_decrease_mixnode_pledge( owner: &str, amount: Coin, info: MessageInfo, deps: DepsMut<'_>, ) -> Result { if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? { return Err(VestingContractError::NotMixnetContract(info.sender)); } let account = account_from_address(owner, deps.storage, deps.api)?; account.try_track_decrease_mixnode_pledge(amount, deps.storage)?; Ok(Response::new().add_event(new_track_mixnode_pledge_decrease_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(VestingContractError::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(VestingContractError::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(VestingContractError::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(VestingContractError::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(VestingContractError::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::save_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, ))) }