Skip to main content

vesting_contract/
transactions.rs

1// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::contract::{ensure_staking_permission, validate_funds};
5use crate::errors::ContractError;
6use crate::storage::{
7    account_from_address, save_account, ADMIN, MIXNET_CONTRACT_ADDRESS, MIX_DENOM,
8};
9use crate::traits::{
10    DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, NodeFamilies, VestingAccount,
11};
12use crate::vesting::{populate_vesting_periods, Account};
13use contracts_common::signing::MessageSignature;
14use cosmwasm_std::{coin, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Timestamp};
15use mixnet_contract_common::families::FamilyHead;
16use mixnet_contract_common::{
17    Gateway, GatewayConfigUpdate, MixId, MixNode, MixNodeConfigUpdate, MixNodeCostParams,
18};
19use vesting_contract_common::events::{
20    new_ownership_transfer_event, new_periodic_vesting_account_event,
21    new_staking_address_update_event, new_track_gateway_unbond_event,
22    new_track_mixnode_pledge_decrease_event, new_track_mixnode_unbond_event,
23    new_track_reward_event, new_track_undelegation_event, new_vested_coins_withdraw_event,
24};
25use vesting_contract_common::messages::VestingSpecification;
26use vesting_contract_common::PledgeCap;
27
28pub fn try_create_family(
29    info: MessageInfo,
30    deps: DepsMut,
31    label: String,
32) -> Result<Response, ContractError> {
33    let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?;
34    account.try_create_family(deps.storage, label)
35}
36pub fn try_join_family(
37    info: MessageInfo,
38    deps: DepsMut,
39    join_permit: MessageSignature,
40    family_head: FamilyHead,
41) -> Result<Response, ContractError> {
42    let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?;
43    account.try_join_family(deps.storage, join_permit, family_head)
44}
45pub fn try_leave_family(
46    info: MessageInfo,
47    deps: DepsMut,
48    family_head: FamilyHead,
49) -> Result<Response, ContractError> {
50    let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?;
51    account.try_leave_family(deps.storage, family_head)
52}
53pub fn try_kick_family_member(
54    info: MessageInfo,
55    deps: DepsMut,
56    member: String,
57) -> Result<Response, ContractError> {
58    let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?;
59    account.try_head_kick_member(deps.storage, &member)
60}
61
62/// Update locked_pledge_cap, the hard cap for staking/bonding with unvested tokens.
63///
64/// Callable by ADMIN only, see [instantiate].
65pub fn try_update_locked_pledge_cap(
66    address: String,
67    cap: PledgeCap,
68    info: MessageInfo,
69    deps: DepsMut,
70) -> Result<Response, ContractError> {
71    if info.sender != ADMIN.load(deps.storage)? {
72        return Err(ContractError::NotAdmin(info.sender.as_str().to_string()));
73    }
74    let mut account = account_from_address(&address, deps.storage, deps.api)?;
75
76    account.pledge_cap = Some(cap);
77    save_account(&account, deps.storage)?;
78    Ok(Response::default())
79}
80
81/// Update config for a mixnode bonded with vesting account, sends [mixnet_contract_common::ExecuteMsg::UpdateMixnodeConfig] to [crate::storage::MIXNET_CONTRACT_ADDRESS].
82pub fn try_update_mixnode_config(
83    new_config: MixNodeConfigUpdate,
84    info: MessageInfo,
85    deps: DepsMut,
86) -> Result<Response, ContractError> {
87    let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
88    account.try_update_mixnode_config(new_config, deps.storage)
89}
90
91pub fn try_update_gateway_config(
92    new_config: GatewayConfigUpdate,
93    info: MessageInfo,
94    deps: DepsMut,
95) -> Result<Response, ContractError> {
96    let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
97    account.try_update_gateway_config(new_config, deps.storage)
98}
99
100pub fn try_update_mixnode_cost_params(
101    new_costs: MixNodeCostParams,
102    info: MessageInfo,
103    deps: DepsMut,
104) -> Result<Response, ContractError> {
105    let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
106    account.try_update_mixnode_cost_params(new_costs, deps.storage)
107}
108
109/// Updates mixnet contract address, for cases when a new mixnet contract is deployed.
110///
111/// Callable by ADMIN only, see [instantiate].
112pub fn try_update_mixnet_address(
113    address: String,
114    info: MessageInfo,
115    deps: DepsMut<'_>,
116) -> Result<Response, ContractError> {
117    if info.sender != ADMIN.load(deps.storage)? {
118        return Err(ContractError::NotAdmin(info.sender.as_str().to_string()));
119    }
120    let mixnet_contract_address = deps.api.addr_validate(&address)?;
121
122    MIXNET_CONTRACT_ADDRESS.save(deps.storage, &mixnet_contract_address)?;
123    Ok(Response::default())
124}
125
126/// Withdraw already vested coins.
127pub fn try_withdraw_vested_coins(
128    amount: Coin,
129    env: Env,
130    info: MessageInfo,
131    deps: DepsMut<'_>,
132) -> Result<Response, ContractError> {
133    let mix_denom = MIX_DENOM.load(deps.storage)?;
134    if amount.denom != mix_denom {
135        return Err(ContractError::WrongDenom(amount.denom, mix_denom));
136    }
137
138    let address = info.sender.clone();
139    let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
140    if address != account.owner_address() {
141        return Err(ContractError::NotOwner(account.owner_address().to_string()));
142    }
143    let spendable_coins = account.spendable_coins(None, &env, deps.storage)?;
144    if amount.amount <= spendable_coins.amount {
145        let new_balance = account.withdraw(&amount, deps.storage)?;
146
147        let send_tokens = BankMsg::Send {
148            to_address: account.owner_address().as_str().to_string(),
149            amount: vec![amount.clone()],
150        };
151
152        Ok(Response::new()
153            .add_message(send_tokens)
154            .add_event(new_vested_coins_withdraw_event(
155                &address,
156                &amount,
157                &coin(new_balance, &amount.denom),
158            )))
159    } else {
160        Err(ContractError::InsufficientSpendable(
161            account.owner_address().as_str().to_string(),
162            spendable_coins.amount.u128(),
163        ))
164    }
165}
166
167/// Transfer ownership of the entire vesting account.
168pub fn try_transfer_ownership(
169    to_address: String,
170    info: MessageInfo,
171    deps: DepsMut<'_>,
172) -> Result<Response, ContractError> {
173    let address = info.sender.clone();
174    let to_address = deps.api.addr_validate(&to_address)?;
175    let mut account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
176    if address == account.owner_address() {
177        account.transfer_ownership(&to_address, deps.storage)?;
178        Ok(Response::new().add_event(new_ownership_transfer_event(&address, &to_address)))
179    } else {
180        Err(ContractError::NotOwner(account.owner_address().to_string()))
181    }
182}
183
184/// Set or update staking address for a vesting account.
185pub fn try_update_staking_address(
186    to_address: Option<String>,
187    info: MessageInfo,
188    deps: DepsMut<'_>,
189) -> Result<Response, ContractError> {
190    if let Some(ref to_address) = to_address {
191        if account_from_address(to_address, deps.storage, deps.api).is_ok() {
192            // do not allow setting staking address to an existing account's address
193            return Err(ContractError::StakingAccountExists(to_address.to_string()));
194        }
195    }
196
197    let address = info.sender.clone();
198    let to_address = to_address.and_then(|x| deps.api.addr_validate(&x).ok());
199    let mut account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
200    if address == account.owner_address() {
201        let old = account.staking_address().cloned();
202        account.update_staking_address(to_address.clone(), deps.storage)?;
203        Ok(Response::new().add_event(new_staking_address_update_event(&old, &to_address)))
204    } else {
205        Err(ContractError::NotOwner(account.owner_address().to_string()))
206    }
207}
208
209/// Bond a gateway, sends [mixnet_contract_common::ExecuteMsg::BondGatewayOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].
210pub fn try_bond_gateway(
211    gateway: Gateway,
212    owner_signature: MessageSignature,
213    amount: Coin,
214    info: MessageInfo,
215    env: Env,
216    deps: DepsMut<'_>,
217) -> Result<Response, ContractError> {
218    let mix_denom = MIX_DENOM.load(deps.storage)?;
219    let pledge = validate_funds(&[amount], mix_denom)?;
220    let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
221    account.try_bond_gateway(gateway, owner_signature, pledge, &env, deps.storage)
222}
223
224/// Unbond a gateway, sends [mixnet_contract_common::ExecuteMsg::UnbondGatewayOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].
225pub fn try_unbond_gateway(info: MessageInfo, deps: DepsMut<'_>) -> Result<Response, ContractError> {
226    let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
227    account.try_unbond_gateway(deps.storage)
228}
229
230/// Track gateway unbonding, invoked by the mixnet contract after succesful unbonding, message containes coins returned including any accrued rewards.
231pub fn try_track_unbond_gateway(
232    owner: &str,
233    amount: Coin,
234    info: MessageInfo,
235    deps: DepsMut<'_>,
236) -> Result<Response, ContractError> {
237    if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? {
238        return Err(ContractError::NotMixnetContract(info.sender));
239    }
240    let account = account_from_address(owner, deps.storage, deps.api)?;
241    account.try_track_unbond_gateway(amount, deps.storage)?;
242    Ok(Response::new().add_event(new_track_gateway_unbond_event()))
243}
244
245/// Bond a mixnode, sends [mixnet_contract_common::ExecuteMsg::BondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].
246pub fn try_bond_mixnode(
247    mix_node: MixNode,
248    cost_params: MixNodeCostParams,
249    owner_signature: MessageSignature,
250    amount: Coin,
251    info: MessageInfo,
252    env: Env,
253    deps: DepsMut<'_>,
254) -> Result<Response, ContractError> {
255    let mix_denom = MIX_DENOM.load(deps.storage)?;
256    let pledge = validate_funds(&[amount], mix_denom)?;
257    let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
258    account.try_bond_mixnode(
259        mix_node,
260        cost_params,
261        owner_signature,
262        pledge,
263        &env,
264        deps.storage,
265    )
266}
267
268pub fn try_pledge_more(
269    deps: DepsMut<'_>,
270    env: Env,
271    info: MessageInfo,
272    amount: Coin,
273) -> Result<Response, ContractError> {
274    let mix_denom = MIX_DENOM.load(deps.storage)?;
275    let additional_pledge = validate_funds(&[amount], mix_denom)?;
276
277    let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
278    account.try_pledge_additional_tokens(additional_pledge, &env, deps.storage)
279}
280
281pub fn try_decrease_pledge(
282    deps: DepsMut<'_>,
283    info: MessageInfo,
284    amount: Coin,
285) -> Result<Response, ContractError> {
286    let mix_denom = MIX_DENOM.load(deps.storage)?;
287    // perform basic validation - is it correct demon, is it non-zero, etc.
288    let decrease = validate_funds(&[amount], mix_denom)?;
289
290    let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
291    account.try_decrease_mixnode_pledge(decrease, deps.storage)
292}
293
294/// Unbond a mixnode, sends [mixnet_contract_common::ExecuteMsg::UnbondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].
295pub fn try_unbond_mixnode(info: MessageInfo, deps: DepsMut<'_>) -> Result<Response, ContractError> {
296    let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
297    account.try_unbond_mixnode(deps.storage)
298}
299
300/// Track mixnode unbonding, invoked by the mixnet contract after succesful unbonding, message containes coins returned including any accrued rewards.
301pub fn try_track_unbond_mixnode(
302    owner: &str,
303    amount: Coin,
304    info: MessageInfo,
305    deps: DepsMut<'_>,
306) -> Result<Response, ContractError> {
307    if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? {
308        return Err(ContractError::NotMixnetContract(info.sender));
309    }
310    let account = account_from_address(owner, deps.storage, deps.api)?;
311    account.try_track_unbond_mixnode(amount, deps.storage)?;
312    Ok(Response::new().add_event(new_track_mixnode_unbond_event()))
313}
314
315/// Tracks decreasing mixnode pledge. Invoked by the mixnet contract after successful event reconciliation.
316/// A separate BankMsg containing the specified amount was sent in the same transaction.
317pub fn try_track_decrease_mixnode_pledge(
318    owner: &str,
319    amount: Coin,
320    info: MessageInfo,
321    deps: DepsMut<'_>,
322) -> Result<Response, ContractError> {
323    if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? {
324        return Err(ContractError::NotMixnetContract(info.sender));
325    }
326    let account = account_from_address(owner, deps.storage, deps.api)?;
327    account.try_track_decrease_mixnode_pledge(amount, deps.storage)?;
328    Ok(Response::new().add_event(new_track_mixnode_pledge_decrease_event()))
329}
330
331/// Track reward collection, invoked by the mixnert contract after sucessful reward compounding or claiming
332pub fn try_track_reward(
333    deps: DepsMut<'_>,
334    info: MessageInfo,
335    amount: Coin,
336    address: &str,
337) -> Result<Response, ContractError> {
338    if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? {
339        return Err(ContractError::NotMixnetContract(info.sender));
340    }
341    let account = account_from_address(address, deps.storage, deps.api)?;
342    account.track_reward(amount, deps.storage)?;
343    Ok(Response::new().add_event(new_track_reward_event()))
344}
345
346/// Track undelegation, invoked by the mixnet contract after sucessful undelegation, message contains coins returned with any accrued rewards.
347pub fn try_track_undelegation(
348    address: &str,
349    mix_id: MixId,
350    amount: Coin,
351    info: MessageInfo,
352    deps: DepsMut<'_>,
353) -> Result<Response, ContractError> {
354    if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? {
355        return Err(ContractError::NotMixnetContract(info.sender));
356    }
357    let account = account_from_address(address, deps.storage, deps.api)?;
358
359    account.track_undelegation(mix_id, amount, deps.storage)?;
360    Ok(Response::new().add_event(new_track_undelegation_event()))
361}
362
363/// Delegate to mixnode, sends [mixnet_contract_common::ExecuteMsg::DelegateToMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]..
364pub fn try_delegate_to_mixnode(
365    mix_id: MixId,
366    amount: Coin,
367    on_behalf_of: Option<String>,
368    info: MessageInfo,
369    env: Env,
370    deps: DepsMut<'_>,
371) -> Result<Response, ContractError> {
372    // TODO
373    // as of 01.02.23
374    // thus restricting it to 25, which is more than double of that, doesn't seem too unreasonable.
375
376    // while this might not be the best workaround, if user wishes to delegate more tokens towards the same node
377    // they could remove the existing delegation (thus removing all separate entries from the storage)
378    // and re-delegate it with the reclaimed amount (which will include all rewards).
379
380    let mix_denom = MIX_DENOM.load(deps.storage)?;
381    let amount = validate_funds(&[amount], mix_denom)?;
382
383    let account = match on_behalf_of {
384        Some(account_owner) => {
385            let account = account_from_address(&account_owner, deps.storage, deps.api)?;
386            ensure_staking_permission(&info.sender, &account)?;
387            account
388        }
389        // you're the owner, you can do what you want
390        None => account_from_address(info.sender.as_str(), deps.storage, deps.api)?,
391    };
392
393    account.try_delegate_to_mixnode(mix_id, amount, &env, deps.storage)
394}
395
396/// Claims operator reward, sends [mixnet_contract_common::ExecuteMsg::ClaimOperatorRewardOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].
397pub fn try_claim_operator_reward(
398    deps: DepsMut<'_>,
399    info: MessageInfo,
400) -> Result<Response, ContractError> {
401    let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
402    account.try_claim_operator_reward(deps.storage)
403}
404
405/// Claims delegator reward, sends [mixnet_contract_common::ExecuteMsg::ClaimDelegatorRewardOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].
406pub fn try_claim_delegator_reward(
407    deps: DepsMut<'_>,
408    info: MessageInfo,
409    mix_id: MixId,
410) -> Result<Response, ContractError> {
411    let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
412
413    account.try_claim_delegator_reward(mix_id, deps.storage)
414}
415
416/// Undelegates from a mixnode, sends [mixnet_contract_common::ExecuteMsg::UndelegateFromMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].
417pub fn try_undelegate_from_mixnode(
418    mix_id: MixId,
419    on_behalf_of: Option<String>,
420    info: MessageInfo,
421    deps: DepsMut<'_>,
422) -> Result<Response, ContractError> {
423    let account = match on_behalf_of {
424        Some(account_owner) => {
425            let account = account_from_address(&account_owner, deps.storage, deps.api)?;
426            ensure_staking_permission(&info.sender, &account)?;
427            account
428        }
429        // you're the owner, you can do what you want
430        None => account_from_address(info.sender.as_str(), deps.storage, deps.api)?,
431    };
432
433    account.try_undelegate_from_mixnode(mix_id, deps.storage)
434}
435
436/// Creates a new periodic vesting account, and deposits funds to vest into the contract.
437///
438/// Callable by ADMIN only, see [instantiate].
439pub fn try_create_periodic_vesting_account(
440    owner_address: &str,
441    staking_address: Option<String>,
442    vesting_spec: Option<VestingSpecification>,
443    cap: Option<PledgeCap>,
444    info: MessageInfo,
445    env: Env,
446    deps: DepsMut<'_>,
447) -> Result<Response, ContractError> {
448    if info.sender != ADMIN.load(deps.storage)? {
449        return Err(ContractError::NotAdmin(info.sender.as_str().to_string()));
450    }
451
452    let mix_denom = MIX_DENOM.load(deps.storage)?;
453
454    let account_exists = account_from_address(owner_address, deps.storage, deps.api).is_ok();
455    if account_exists {
456        return Err(ContractError::AccountAlreadyExists(
457            owner_address.to_string(),
458        ));
459    }
460
461    let vesting_spec = vesting_spec.unwrap_or_default();
462
463    let coin = validate_funds(&info.funds, mix_denom)?;
464
465    let owner_address = deps.api.addr_validate(owner_address)?;
466    let staking_address = if let Some(staking_address) = staking_address {
467        let staking_account_exists =
468            account_from_address(&staking_address, deps.storage, deps.api).is_ok();
469        if staking_account_exists {
470            return Err(ContractError::StakingAccountAlreadyExists(staking_address));
471        }
472        Some(deps.api.addr_validate(&staking_address)?)
473    } else {
474        None
475    };
476    let start_time = vesting_spec
477        .start_time()
478        .unwrap_or_else(|| env.block.time.seconds());
479
480    let periods = populate_vesting_periods(start_time, vesting_spec);
481
482    let start_time = Timestamp::from_seconds(start_time);
483
484    let response = Response::new();
485
486    Account::new(
487        owner_address.clone(),
488        staking_address.clone(),
489        coin.clone(),
490        start_time,
491        periods,
492        cap,
493        deps.storage,
494    )?;
495
496    Ok(response.add_event(new_periodic_vesting_account_event(
497        &owner_address,
498        &coin,
499        &staking_address,
500        start_time,
501    )))
502}