Skip to content

Commit

Permalink
fix(farm_manager): reconciles the state of the user when exiting a farm
Browse files Browse the repository at this point in the history
  • Loading branch information
mantricjavier committed Oct 29, 2024
1 parent d05d2f6 commit ea75561
Show file tree
Hide file tree
Showing 7 changed files with 1,017 additions and 17 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion contracts/farm-manager/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "farm-manager"
version = "0.1.4"
version = "0.1.5"
authors = ["Kerber0x <kerber0x@protonmail.com>"]
edition.workspace = true
description = "The Farm Manager is a contract that allows to manage multiple pool farms in a single contract."
Expand Down
18 changes: 11 additions & 7 deletions contracts/farm-manager/src/farm/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ pub(crate) fn claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result<Respon
&info.sender,
lp_denom,
&current_epoch.id,
true,
)?;
}
_ => return Err(ContractError::Unauthorized),
Expand Down Expand Up @@ -386,11 +387,12 @@ fn compute_farm_emissions(

/// Syncs the address lp weight history for the given address and epoch_id, removing all the previous
/// entries as the user has already claimed those epochs, and setting the weight for the current epoch.
fn sync_address_lp_weight_history(
pub fn sync_address_lp_weight_history(
storage: &mut dyn Storage,
address: &Addr,
lp_denom: &str,
current_epoch_id: &u64,
save_last_lp_weight: bool,
) -> Result<(), ContractError> {
let (earliest_epoch_id, _) = get_earliest_address_lp_weight(storage, address, lp_denom)?;
let (latest_epoch_id, latest_address_lp_weight) =
Expand All @@ -401,12 +403,14 @@ fn sync_address_lp_weight_history(
LP_WEIGHT_HISTORY.remove(storage, (address, lp_denom, epoch_id));
}

// save the latest weight for the current epoch
LP_WEIGHT_HISTORY.save(
storage,
(address, lp_denom, *current_epoch_id),
&latest_address_lp_weight,
)?;
if save_last_lp_weight {
// save the latest weight for the current epoch
LP_WEIGHT_HISTORY.save(
storage,
(address, lp_denom, *current_epoch_id),
&latest_address_lp_weight,
)?;
}

Ok(())
}
12 changes: 10 additions & 2 deletions contracts/farm-manager/src/position/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use amm::farm_manager::Position;

use crate::helpers::{validate_identifier, validate_lp_denom};
use crate::position::helpers::{
calculate_weight, create_penalty_share_msg, get_latest_address_weight,
calculate_weight, create_penalty_share_msg, get_latest_address_weight, reconcile_user_state,
validate_no_pending_rewards, AUTO_POSITION_ID_PREFIX, EXPLICIT_POSITION_ID_PREFIX,
PENALTY_FEE_SHARE,
};
Expand Down Expand Up @@ -274,6 +274,8 @@ pub(crate) fn close_position(

POSITIONS.save(deps.storage, &identifier, &position)?;

reconcile_user_state(deps, &info.sender, &position)?;

Ok(Response::default().add_attributes(attributes))
}

Expand Down Expand Up @@ -412,14 +414,20 @@ pub(crate) fn withdraw_position(
messages.push(
BankMsg::Send {
to_address: position.receiver.to_string(),
amount: vec![position.lp_asset],
amount: vec![position.lp_asset.clone()],
}
.into(),
);
}

POSITIONS.remove(deps.storage, &identifier)?;

// if the position to remove was open, i.e. withdrawn via the emergency unlock feature, then
// we need to reconcile the user state
if position.open {
reconcile_user_state(deps, &info.sender, &position)?;
}

Ok(Response::default()
.add_attributes(vec![
("action", "withdraw_position".to_string()),
Expand Down
85 changes: 81 additions & 4 deletions contracts/farm-manager/src/position/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use cosmwasm_std::{
ensure, Addr, BankMsg, Coin, CosmosMsg, Decimal, Decimal256, Deps, Env, MessageInfo, Order,
StdError, Storage, Uint128,
ensure, Addr, BankMsg, Coin, CosmosMsg, Decimal, Decimal256, Deps, DepsMut, Env, MessageInfo,
Order, StdError, Storage, Uint128,
};

use amm::farm_manager::{Config, EpochId, RewardsResponse};
use amm::farm_manager::{Config, EpochId, Position, RewardsResponse};

use crate::farm::commands::sync_address_lp_weight_history;
use crate::queries::query_rewards;
use crate::state::{get_positions_by_receiver, LP_WEIGHT_HISTORY, MAX_ITEMS_LIMIT};
use crate::state::{
get_positions_by_receiver, has_any_lp_weight, CONFIG, LAST_CLAIMED_EPOCH, LP_WEIGHT_HISTORY,
MAX_ITEMS_LIMIT,
};
use crate::ContractError;

const SECONDS_IN_DAY: u64 = 86400;
Expand Down Expand Up @@ -176,3 +180,76 @@ pub fn validate_no_pending_rewards(

Ok(())
}

/// Reconciles a user's state by updating or removing stale data based on their current open positions.
///
/// This function checks for two primary conditions:
/// 1. If the user has no more open positions, it clears the LAST_CLAIMED_EPOCH state item.
/// 2. If the user has no more open positions for a specific LP denom, it wipes the LP weight history for that denom.
///
/// Why do we need to do this?
/// If the lp history and the LAST_CLAIMED_EPOCH for the user is not cleared when fully existing the farm,
/// if the user would create a new position in the future for the same denom, the contract would try to
/// claim rewards for old epochs that would be irrelevant, as the LAST_CLAIMED_EPOCH is recorded when
/// the user claims rewards. At that point, the user weight would be zero for the given LP, which renders
/// the computation for those epochs useless. Additionally, if the user were be the only user in the farm,
/// exiting the farms would record the lp weight for both the user and contract as zero. If the LAST_CLAIMED_EPOCH
/// and lp weight history were not cleared, if the user opens another position for the same LP denom in the future,
/// as the contract would try to claim previous epoch rewards there would be a DivideByZero error as the
/// total_lp_weight would be zero when calculating user's share of the rewards.
pub fn reconcile_user_state(
deps: DepsMut,
receiver: &Addr,
position: &Position,
) -> Result<(), ContractError> {
let receiver_open_positions = get_positions_by_receiver(
deps.storage,
receiver.as_ref(),
Some(true),
None,
Some(MAX_ITEMS_LIMIT),
)?;

// if the user has no more open positions, clear the last claimed epoch
if receiver_open_positions.is_empty() {
LAST_CLAIMED_EPOCH.remove(deps.storage, receiver);
}

// if the user has no more open positions for the position's LP denom, wipe the LP weight
// history for that denom
if receiver_open_positions
.iter()
.filter(|p| p.lp_asset.denom == position.lp_asset.denom)
.collect::<Vec<_>>()
.is_empty()
{
// if it doesn't have any it means it was already cleared up when closing the position,
// but it is different if the user emergency exits an open position.
// if withdrawing a position after closing it, this won't be triggered as it was already
// called when closing the position.
if has_any_lp_weight(deps.storage, receiver, &position.lp_asset.denom)? {
clear_lp_weight_history(deps, receiver, &position.lp_asset.denom)?;
}
}

Ok(())
}

/// Clears the lp weight history.
fn clear_lp_weight_history(
deps: DepsMut,
address: &Addr,
lp_denom: &str,
) -> Result<(), ContractError> {
let config = CONFIG.load(deps.storage)?;
let current_epoch = amm::epoch_manager::get_current_epoch(
deps.as_ref(),
config.epoch_manager_addr.to_string(),
)?;

// by passing the false flag the lp weight for the current epoch won't be saved, which we want
// as we want to clear the whole lp weight history for this lp denom.
sync_address_lp_weight_history(deps.storage, address, lp_denom, &current_epoch.id, false)?;

Ok(())
}
19 changes: 19 additions & 0 deletions contracts/farm-manager/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,25 @@ pub fn get_earliest_address_lp_weight(
}
}

/// Checks if a user has any LP weight for the given LP denom.
pub fn has_any_lp_weight(
storage: &dyn Storage,
address: &Addr,
lp_denom: &str,
) -> Result<bool, ContractError> {
let lp_weight_history_result = LP_WEIGHT_HISTORY
.prefix((address, lp_denom))
.range(storage, None, None, Order::Ascending)
.next()
.transpose();

match lp_weight_history_result {
Ok(Some(_)) => Ok(true),
Ok(None) => Ok(false),
Err(std_err) => Err(std_err.into()),
}
}

/// Gets the latest entry of an address in the address lp weight history.
/// If the address has no open positions, returns 0 for the weight.
pub fn get_latest_address_lp_weight(
Expand Down
Loading

0 comments on commit ea75561

Please sign in to comment.