diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index 008306ec87..1cc501a3a0 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -18,4 +18,6 @@ pub enum MixnetContractError { #[from] source: cosmwasm_std::StdError, }, + #[error("Division by zero at {}", line!())] + DivisionByZero, } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index b70f774e41..7447dde7bd 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -222,11 +222,17 @@ impl DelegatorRewardParams { } pub fn determine_delegation_reward(&self, delegation_amount: Uint128) -> u128 { + if self.sigma == 0 { + return 0; + } + // change all values into their fixed representations let delegation_amount = U128::from_num(delegation_amount.u128()); let circulating_supply = U128::from_num(self.reward_params.circulating_supply()); let scaled_delegation_amount = delegation_amount / circulating_supply; + + // Div by zero checked above let delegator_reward = (ONE - self.profit_margin) * (scaled_delegation_amount / self.sigma) * self.node_profit; @@ -465,12 +471,16 @@ impl MixNodeBond { pub fn operator_reward(&self, params: &RewardParams) -> u128 { let reward = self.reward(params); + if reward.sigma == 0 { + return 0; + } let profit = if reward.reward < params.node.operator_cost() { U128::from_num(0u128) } else { reward.reward - params.node.operator_cost() }; let operator_base_reward = reward.reward.min(params.node.operator_cost()); + // Div by zero checked above let operator_reward = (self.profit_margin() + (ONE - self.profit_margin()) * reward.lambda / reward.sigma) * profit; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs index 96d618c9a0..654253b14d 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -57,8 +57,12 @@ impl NodeEpochRewards { pub fn operator_reward(&self, profit_margin: U128) -> Result { let reward = self.node_profit(); let operator_base_reward = reward.min(self.operator_cost()); - let operator_reward = - (profit_margin + (ONE - profit_margin) * self.lambda() / self.sigma()) * reward; + let div_by_zero_check = if let Some(value) = self.lambda().checked_div(self.sigma()) { + value + } else { + return Err(MixnetContractError::DivisionByZero); + }; + let operator_reward = (profit_margin + (ONE - profit_margin) * div_by_zero_check) * reward; let reward = (operator_reward + operator_base_reward).max(U128::from_num(0u128)); @@ -80,8 +84,15 @@ impl NodeEpochRewards { let circulating_supply = U128::from_num(epoch_reward_params.circulating_supply()); let scaled_delegation_amount = delegation_amount / circulating_supply; - let delegator_reward = - (ONE - profit_margin) * scaled_delegation_amount / self.sigma() * self.node_profit(); + + let check_div_by_zero = + if let Some(value) = scaled_delegation_amount.checked_div(self.sigma()) { + value + } else { + return Err(MixnetContractError::DivisionByZero); + }; + + let delegator_reward = (ONE - profit_margin) * check_div_by_zero * self.node_profit(); let reward = delegator_reward.max(U128::ZERO); if let Some(int_reward) = reward.checked_cast() { @@ -198,6 +209,11 @@ impl RewardParams { let denom = self.active_set_work_factor() * U128::from_num(self.rewarded_set_size()) - (self.active_set_work_factor() - ONE) * U128::from_num(self.idle_nodes().u128()); + if denom == 0 { + return U128::ZERO; + } + + // Div by zero checked above if self.in_active_set() { // work_active = factor / (factor * self.network.k[month] - (factor - 1) * idle_nodes) self.active_set_work_factor() / denom * self.rewarded_set_size() diff --git a/contracts/mixnet/src/delegations/transactions.rs b/contracts/mixnet/src/delegations/transactions.rs index 6c95ebbce6..ff39ac9c6f 100644 --- a/contracts/mixnet/src/delegations/transactions.rs +++ b/contracts/mixnet/src/delegations/transactions.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage::{self, PENDING_DELEGATION_EVENTS}; // use crate::contract::debug_with_visibility; +// use crate::contract::debug_with_visibility; use crate::error::ContractError; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::storage as mixnodes_storage; @@ -44,6 +45,8 @@ pub(crate) fn _try_reconcile_all_delegation_events( let mut response = Response::new(); + // debug_with_visibility(api, "Reconciling delegation events"); + for (key, delegation_event) in pending_delegation_events { match delegation_event { DelegationEvent::Delegate(delegation) => { @@ -253,7 +256,7 @@ pub struct ReconcileUndelegateResponse { pub(crate) fn try_reconcile_undelegation( storage: &mut dyn Storage, - _api: &dyn Api, + api: &dyn Api, pending_undelegate: &PendingUndelegate, ) -> Result { let delegation_map = storage::delegations(); @@ -283,6 +286,7 @@ pub(crate) fn try_reconcile_undelegation( let reward = crate::rewards::transactions::calculate_delegator_reward( storage, + api, pending_undelegate.proxy_storage_key(), &pending_undelegate.mix_identity(), )?; diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 7191e76a03..eeaf378ef3 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -191,6 +191,7 @@ pub(crate) fn _try_remove_mixnode( crate::rewards::transactions::_try_compound_operator_reward( deps.storage, + deps.api, env.block.height, &owner, None, diff --git a/contracts/mixnet/src/rewards/queries.rs b/contracts/mixnet/src/rewards/queries.rs index 4c087a7751..0b29580914 100644 --- a/contracts/mixnet/src/rewards/queries.rs +++ b/contracts/mixnet/src/rewards/queries.rs @@ -39,7 +39,7 @@ pub fn query_operator_reward(deps: Deps, owner: String) -> Result Result { let owner = deps.api.addr_validate(info.sender.as_str())?; - let reward = _try_compound_operator_reward(deps.storage, env.block.height, &owner, None)?; + let reward = + _try_compound_operator_reward(deps.storage, deps.api, env.block.height, &owner, None)?; Ok(Response::new().add_event(new_compound_operator_reward_event(&owner, reward))) } pub fn _try_compound_operator_reward( storage: &mut dyn Storage, + api: &dyn Api, block_height: u64, owner: &Addr, proxy: Option, @@ -72,7 +80,7 @@ pub fn _try_compound_operator_reward( } let mut updated_bond = bond.clone(); - let reward = calculate_operator_reward(storage, owner, &bond)?; + let reward = calculate_operator_reward(storage, api, owner, &bond)?; updated_bond.accumulated_rewards = Some(updated_bond.accumulated_rewards() - reward); updated_bond.pledge_amount.amount += reward; mixnodes().replace( @@ -94,6 +102,7 @@ pub fn _try_compound_operator_reward( pub fn calculate_operator_reward( storage: &dyn Storage, + api: &dyn Api, owner: &Addr, bond: &StoredMixnodeBond, ) -> Result { @@ -111,19 +120,22 @@ pub fn calculate_operator_reward( Ok(Uint128::zero()), |acc, height| -> Result { let accumulated_reward = acc?; - if let Some(bond) = - mixnodes().may_load_at_height(storage, bond.identity().as_str(), height)? + if let Some(bond) = mixnodes() + .may_load_at_height(storage, bond.identity().as_str(), height) + .ok() + .flatten() { if let Some(ref epoch_rewards) = bond.epoch_rewards { - let epoch_reward_params = - epoch_reward_params_for_id(storage, epoch_rewards.epoch_id())?; // Compound rewards from previous heights - let reward_at_height = epoch_rewards.delegation_reward( - bond.pledge_amount().amount + accumulated_reward, - bond.profit_margin(), - epoch_reward_params, - )?; - return Ok(accumulated_reward + reward_at_height); + match epoch_rewards.operator_reward(bond.profit_margin()) { + Ok(reward) => return Ok(accumulated_reward + reward), + Err(err) => { + debug_with_visibility( + api, + format!("Failed to calculate operator reward: {:?}", err), + ); + } + }; } }; Ok(accumulated_reward) @@ -204,7 +216,7 @@ pub fn _try_compound_delegator_reward( &deps.api.addr_validate(owner_address)?, proxy.as_ref(), ); - let reward = calculate_delegator_reward(deps.storage, key.clone(), mix_identity)?; + let reward = calculate_delegator_reward(deps.storage, deps.api, key.clone(), mix_identity)?; let mut compounded_delegation = reward; // Might want to introduce paging here @@ -261,6 +273,7 @@ pub fn _try_compound_delegator_reward( // + last_reward_claimed height is correctly used pub fn calculate_delegator_reward( storage: &dyn Storage, + api: &dyn Api, key: Vec, mix_identity: &str, ) -> Result { @@ -296,7 +309,15 @@ pub fn calculate_delegator_reward( .fold(Uint128::zero(), |total, delegation| { total + delegation.amount.amount }); + // debug_with_visibility( + // api, + // format!("delegation at height {} - {}", height, delegation_at_height), + // ); if delegation_at_height != Uint128::zero() { + // debug_with_visibility( + // api, + // format!("Loading bond {} at height {}", mix_identity, height), + // ); if let Some(bond) = mixnodes() .may_load_at_height(storage, mix_identity, height) .ok() @@ -304,17 +325,40 @@ pub fn calculate_delegator_reward( { if let Some(ref epoch_rewards) = bond.epoch_rewards { // Compound rewards from previous heights - let epoch_reward_params = - epoch_reward_params_for_id(storage, epoch_rewards.epoch_id())?; - let reward_at_height = match epoch_rewards.delegation_reward( - delegation_at_height + accumulated_reward, - bond.profit_margin(), - epoch_reward_params, - ) { - Ok(reward) => reward, - Err(_err) => Uint128::zero(), - }; - return Ok(accumulated_reward + reward_at_height); + match epoch_reward_params_for_id(storage, epoch_rewards.epoch_id()) { + Ok(params) => { + let reward_at_height = match epoch_rewards.delegation_reward( + delegation_at_height + accumulated_reward, + bond.profit_margin(), + params, + ) { + Ok(reward) => { + // debug_with_visibility( + // api, + // format!("Reward at height {} - {}", height, reward), + // ); + reward + } + Err(err) => { + debug_with_visibility( + api, + format!( + "Error calculating reward at {} - {}", + height, err + ), + ); + Uint128::zero() + } + }; + return Ok(accumulated_reward + reward_at_height); + } + Err(_err) => { + debug_with_visibility( + api, + format!("No epoch reward params for epoch {}", height), + ); + } + } } } }; @@ -1181,7 +1225,8 @@ pub mod tests { } let alice_reward = - calculate_delegator_reward(&deps.storage, key.clone(), &node_identity_1).unwrap(); + calculate_delegator_reward(&deps.storage, &deps.api, key.clone(), &node_identity_1) + .unwrap(); assert_eq!(alice_reward, Uint128::new(304552)); let mix_0 = mixnodes.load(&deps.storage, &node_identity_1).unwrap(); @@ -1231,8 +1276,9 @@ pub mod tests { ); let operator_reward = - calculate_operator_reward(&deps.storage, &Addr::unchecked("alice"), &mix_1).unwrap(); - assert_eq!(operator_reward, Uint128::new(190345)); + calculate_operator_reward(&deps.storage, &deps.api, &Addr::unchecked("alice"), &mix_1) + .unwrap(); + assert_eq!(operator_reward, Uint128::new(352532)); assert_eq!( mix_1_reward_result.sigma(),