Skip to content

Commit

Permalink
refactor(farm_manager): remove rewards validation on position withdrawal
Browse files Browse the repository at this point in the history
  • Loading branch information
mantricjavier committed Oct 23, 2024
1 parent a1cd399 commit 3f8d4dc
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 50 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.3"
version = "0.1.4"
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
14 changes: 6 additions & 8 deletions contracts/farm-manager/src/position/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ fn close_position_in_full(position: &mut Position, expires_at: u64) -> Uint128 {
position.lp_asset.amount
}

/// Withdraws the given position. The position needs to have expired.
/// Withdraws the given position. If the position has not expired, i.e. the unlocking period has not
/// passed, the position can be withdrawn with a penalty fee using the`emergency_unlock` param.
pub(crate) fn withdraw_position(
mut deps: DepsMut,
env: Env,
Expand All @@ -307,16 +308,13 @@ pub(crate) fn withdraw_position(
ContractError::Unauthorized
);

if emergency_unlock.is_none() || emergency_unlock.is_some() && !emergency_unlock.unwrap() {
// check if the user has pending rewards. Can't withdraw a position without claiming pending rewards first.
// if the user wants to perform an emergency withdrawal, this is not checked
validate_no_pending_rewards(deps.as_ref(), &env, &info)?;
}

let current_time = env.block.time.seconds();
let mut messages: Vec<CosmosMsg> = vec![];

// check if the emergency unlock is requested, will pull the whole position out whether it's open, closed or expired, paying the penalty
// check if the emergency unlock is requested, will pull the whole position out whether it's
// open, closed or expired.
// If the position already expired, the position can be withdrawn without penalty, even if the
// emergency_unlock is requested
if emergency_unlock.is_some() && emergency_unlock.unwrap() && !position.is_expired(current_time)
{
let emergency_unlock_penalty = CONFIG.load(deps.storage)?.emergency_unlock_penalty;
Expand Down
215 changes: 175 additions & 40 deletions contracts/farm-manager/tests/integration.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
extern crate core;

use amm::constants::{LP_SYMBOL, MONTH_IN_SECONDS};
use std::cell::RefCell;

use cosmwasm_std::{coin, Addr, Coin, Decimal, StdResult, Timestamp, Uint128};
use cw_utils::PaymentError;

use amm::constants::{LP_SYMBOL, MONTH_IN_SECONDS};
use amm::farm_manager::{
Config, Curve, Farm, FarmAction, FarmParams, FarmsBy, LpWeightResponse, Position,
PositionAction, PositionsBy, PositionsResponse, RewardsResponse,
};
use cosmwasm_std::{coin, Addr, Coin, Decimal, StdResult, Timestamp, Uint128};
use cw_utils::PaymentError;
use farm_manager::state::MAX_ITEMS_LIMIT;
use farm_manager::ContractError;

Expand Down Expand Up @@ -1702,7 +1701,7 @@ pub fn test_manage_position() {
.query_farms(None, None, None, |result| {
let farms_response = result.unwrap();
assert_eq!(farms_response.farms.len(), 1);
assert_eq!(farms_response.farms[0].claimed_amount, Uint128::new(2_000),);
assert_eq!(farms_response.farms[0].claimed_amount, Uint128::new(2_000));
})
.manage_position(
&creator,
Expand Down Expand Up @@ -1906,26 +1905,21 @@ pub fn test_manage_position() {
},
vec![],
|result| {
let err = result.unwrap_err().downcast::<ContractError>().unwrap();
match err {
ContractError::PendingRewards { .. } => {}
_ => panic!("Wrong error type, should return ContractError::PendingRewards"),
}
result.unwrap();
},
)
.query_balance("uusdy".to_string(), &creator, |balance| {
assert_eq!(balance, Uint128::new(999_994_000));
})
.claim(&creator, vec![], |result| {
result.unwrap();
})
.query_balance("uusdy".to_string(), &creator, |balance| {
assert_eq!(balance, Uint128::new(1000_000_000));
})
.query_farms(None, None, None, |result| {
let farms_response = result.unwrap();
assert_eq!(
farms_response.farms[0].farm_asset.amount,
farms_response.farms[0].claimed_amount
);
//assert!(farms_response.farms[0].is_expired(5));
})
.query_rewards(&creator, |result| {
let rewards_response = result.unwrap();
Expand All @@ -1936,23 +1930,9 @@ pub fn test_manage_position() {
_ => panic!("shouldn't return this but RewardsResponse"),
}
})
.claim(&creator, vec![], |result| {
result.unwrap();
})
.query_balance("uusdy".to_string(), &creator, |balance| {
assert_eq!(balance, Uint128::new(1000_000_000));
assert_eq!(balance, Uint128::new(1_000_000_000));
})
.manage_position(
&creator,
PositionAction::Withdraw {
identifier: "p-1".to_string(),
emergency_unlock: None,
},
vec![],
|result| {
result.unwrap();
},
)
.query_positions(
Some(PositionsBy::Receiver(other.to_string())),
Some(false),
Expand Down Expand Up @@ -2299,6 +2279,161 @@ pub fn test_manage_position() {
);
}

#[test]
#[allow(clippy::inconsistent_digit_grouping)]
pub fn test_withdrawing_open_positions_updates_weight() {
let lp_denom = format!("factory/{MOCK_CONTRACT_ADDR_1}/{LP_SYMBOL}").to_string();
let another_lp = format!("factory/{MOCK_CONTRACT_ADDR_1}/2.{LP_SYMBOL}").to_string();
let invalid_lp_denom = format!("factory/{MOCK_CONTRACT_ADDR_2}/{LP_SYMBOL}").to_string();

let mut suite = TestingSuite::default_with_balances(vec![
coin(1_000_000_000u128, "uom"),
coin(1_000_000_000u128, "uusdy"),
coin(1_000_000_000u128, "uosmo"),
coin(1_000_000_000u128, lp_denom.clone()),
coin(1_000_000_000u128, invalid_lp_denom.clone()),
coin(1_000_000_000u128, another_lp.clone()),
]);

let creator = suite.creator();

suite.instantiate_default();

let farm_manager = suite.farm_manager_addr.clone();
let pool_manager = suite.pool_manager_addr.clone();

// send some lp tokens to the pool manager, to simulate later the creation of a position
// on behalf of a user by the pool manager
suite.send_tokens(&creator, &pool_manager, &[coin(100_000, lp_denom.clone())]);

suite
.manage_farm(
&creator,
FarmAction::Fill {
params: FarmParams {
lp_denom: lp_denom.clone(),
start_epoch: Some(2),
preliminary_end_epoch: Some(6),
curve: None,
farm_asset: Coin {
denom: "uusdy".to_string(),
amount: Uint128::new(8_000u128),
},
farm_identifier: None,
},
},
vec![coin(8_000, "uusdy"), coin(1_000, "uom")],
|result| {
result.unwrap();
},
)
.query_lp_weight(&farm_manager, &lp_denom, 0, |result| {
let err = result.unwrap_err().to_string();

assert_eq!(
err,
"Generic error: Querier contract error: There's no snapshot of the LP \
weight in the contract for the epoch 0"
);
})
.manage_position(
&creator,
PositionAction::Create {
identifier: Some("creator_position".to_string()),
unlocking_duration: 86_400,
receiver: None,
},
vec![coin(2_000, lp_denom.clone())],
|result| {
result.unwrap();
},
)
.query_lp_weight(&farm_manager, &lp_denom, 1, |result| {
let lp_weight = result.unwrap();
assert_eq!(
lp_weight,
LpWeightResponse {
lp_weight: Uint128::new(2_000),
epoch_id: 1,
}
);
});

suite
.add_one_epoch()
.add_one_epoch()
.query_current_epoch(|result| {
let epoch_response = result.unwrap();
assert_eq!(epoch_response.epoch.id, 2);
})
.query_rewards(&creator, |result| {
let rewards_response = result.unwrap();
match rewards_response {
RewardsResponse::RewardsResponse { total_rewards, .. } => {
assert_eq!(total_rewards.len(), 1);
assert_eq!(
total_rewards[0],
Coin {
denom: "uusdy".to_string(),
amount: Uint128::new(2_000),
}
);
}
_ => panic!("shouldn't return this but RewardsResponse"),
}
})
.query_farms(None, None, None, |result| {
let farms_response = result.unwrap();
assert_eq!(farms_response.farms.len(), 1);
assert_eq!(farms_response.farms[0].claimed_amount, Uint128::zero());
});

// withdraw the position
suite
.manage_position(
&creator,
PositionAction::Withdraw {
identifier: "u-creator_position".to_string(),
emergency_unlock: None,
},
vec![],
|result| {
let err = result.unwrap_err().downcast::<ContractError>().unwrap();
match err {
ContractError::Unauthorized { .. } => {}
_ => panic!("Wrong error type, should return ContractError::Unauthorized"),
}
},
)
.manage_position(
&creator,
PositionAction::Withdraw {
identifier: "u-creator_position".to_string(),
emergency_unlock: Some(true),
},
vec![],
|result| {
result.unwrap();
},
)
// the weight is updated after the position is withdrawn with the emergency flag
.query_lp_weight(&farm_manager, &lp_denom, 3, |result| {
let lp_weight = result.unwrap();
assert_eq!(
lp_weight,
LpWeightResponse {
lp_weight: Uint128::zero(),
epoch_id: 3,
}
);
})
.query_farms(None, None, None, |result| {
let farms_response = result.unwrap();
assert_eq!(farms_response.farms.len(), 1);
assert_eq!(farms_response.farms[0].claimed_amount, Uint128::zero());
});
}

#[test]
#[allow(clippy::inconsistent_digit_grouping)]
pub fn test_expand_position_unsuccessfully() {
Expand Down Expand Up @@ -2846,7 +2981,7 @@ fn claiming_rewards_with_multiple_positions_arent_inflated() {
open: true,
expiring_at: None,
receiver: other.clone(),
}
},
]
);
},
Expand Down Expand Up @@ -3069,7 +3204,7 @@ fn claiming_rewards_with_multiple_positions_arent_inflated() {
start_epoch: 16u64,
preliminary_end_epoch: 20u64,
last_epoch_claimed: 15u64,
}
},
]
);
});
Expand Down Expand Up @@ -4732,7 +4867,7 @@ fn test_rewards_query_overlapping_farms() {
lp_denom_2.clone(),
vec![coin(15000, "uom"), coin(35000, "uusdy")]
),
]
],
}
);
});
Expand Down Expand Up @@ -5266,7 +5401,7 @@ fn position_fill_attack_is_not_possible() {
identifier: "u-nice_position".to_string(),
lp_asset: Coin {
denom: lp_denom.clone(),
amount: Uint128::new(5_000)
amount: Uint128::new(5_000),
},
unlocking_duration: 86_400,
open: true,
Expand Down Expand Up @@ -5385,7 +5520,7 @@ fn positions_can_handled_by_pool_manager_for_the_user() {
identifier: "u-nice_position".to_string(),
lp_asset: Coin {
denom: lp_denom.clone(),
amount: Uint128::new(5_000)
amount: Uint128::new(5_000),
},
unlocking_duration: 86_400,
open: true,
Expand Down Expand Up @@ -5423,7 +5558,7 @@ fn positions_can_handled_by_pool_manager_for_the_user() {
identifier: "u-nice_position".to_string(),
lp_asset: Coin {
denom: lp_denom.clone(),
amount: Uint128::new(10_000)
amount: Uint128::new(10_000),
},
unlocking_duration: 86_400,
open: true,
Expand Down Expand Up @@ -5481,7 +5616,7 @@ fn positions_can_handled_by_pool_manager_for_the_user() {
identifier: "u-nice_position".to_string(),
lp_asset: Coin {
denom: lp_denom.clone(),
amount: Uint128::new(10_000)
amount: Uint128::new(10_000),
},
unlocking_duration: 86_400,
open: true,
Expand Down Expand Up @@ -5711,8 +5846,8 @@ fn test_positions_limits() {
match err {
ContractError::MaxPositionsPerUserExceeded { .. } => {}
_ => panic!(
"Wrong error type, should return ContractError::MaxPositionsPerUserExceeded"
),
"Wrong error type, should return ContractError::MaxPositionsPerUserExceeded"
),
}
},
)
Expand All @@ -5732,8 +5867,8 @@ fn test_positions_limits() {
match err {
ContractError::MaxPositionsPerUserExceeded { .. } => {}
_ => panic!(
"Wrong error type, should return ContractError::MaxPositionsPerUserExceeded"
),
"Wrong error type, should return ContractError::MaxPositionsPerUserExceeded"
),
}
},
);
Expand Down

0 comments on commit 3f8d4dc

Please sign in to comment.