The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: low
Valid

Flaw in `LiquidationPool: Position()` Function Results in Inaccurate Staker Position Calculation

Summary

The LiquidationPool::Position(...) function incorrectly calculates the position of a staker, potentially leading to conflicts.

Vulnerability Details

The issue lies in the flawed calculation of the staker's position within the LiquidationPool::Position(...) function. Specifically, the calculation includes the fee rewards of the user, but the fee reward calculation itself is flawed. The function considers the euro token balance of the liquidation pool manager and calculates rewards based on the staker's TST balance in the position (both consolidated and pending). However, this approach results in an inaccurate representation of the staker's position.

function position(address _holder) external view returns(Position memory _position, Reward[] memory _rewards) {
_position = positions[_holder];
(uint256 _pendingTST, uint256 _pendingEUROs) = holderPendingStakes(_holder);
_position.EUROs += _pendingEUROs;
_position.TST += _pendingTST;
@> if (_position.TST > 0) _position.EUROs += IERC20(EUROs).balanceOf(manager) * _position.TST / getTstTotal();
_rewards = findRewards(_holder);
}

GitHub: [83-90]

The problem arises from the fact that the staker's proportion is calculated based on the entire amount stored in the manager, while only a percentage of this amount is transferred from the manager's balance to the pool balance as a fee reward.

function distributeFees() public {
IERC20 eurosToken = IERC20(EUROs);
uint256 _feesForPool = eurosToken.balanceOf(address(this)) * poolFeePercentage / HUNDRED_PC;
if (_feesForPool > 0) {
eurosToken.approve(pool, _feesForPool);
LiquidationPool(pool).distributeFees(_feesForPool);
}
eurosToken.transfer(protocol, eurosToken.balanceOf(address(this)));
}

GitHub: [33-41]

This discrepancy results in the stake balance shown by the LiquidationPool::Position(...) function being higher than the actual position, potentially causing issues for any function or external protocol that relies on this information.

Impact

Can cause integration problem and loss of tokens if this function is used in critical functions.

Proof of Concept

NOTE
To setup and run the test, please read the details from readme file in the github: repo

Here is a test that proves that:

The test verifies that the LiquidationPool::Position(...) function returns an incorrect position of a staker.

function test_PositionFunctionReturnsIncorrectPositionOfStaker() public {
//////////////////////////////////
/// Setup ///
//////////////////////////////////
uint256 amount = 1000 ether; // 1000 tokens
////////////////////////////////////////
/// Alice deposits in Pool ///
////////////////////////////////////////
tokens.tstToken.mint(alice, amount * 2);
tokens.eurosToken.mint(alice, amount * 2);
vm.startPrank(alice);
tokens.tstToken.approve(address(contracts.liquidationPool), amount);
tokens.eurosToken.approve(address(contracts.liquidationPool), amount);
contracts.liquidationPool.increasePosition(amount, amount);
vm.stopPrank();
///////////////////////////////////////
/// Bob creates smart Vault ///
///////////////////////////////////////
(address vault, uint256 tokenIdMinted) = _createSmartVault(bob);
////////////////////////////////////////////////////
/// checking if it is minted correctly ///
////////////////////////////////////////////////////
assertEq(contracts.smartVaultManagerV5.ownerOf(tokenIdMinted), bob, "Bob is not the owner");
////////////////////////////////////////////////////
/// Bob deposits some tokens in the vault ///
////////////////////////////////////////////////////
tokens.arbToken.mint(bob, amount);
vm.startPrank(bob);
tokens.arbToken.transfer(vault, amount);
vm.stopPrank();
/////////////////////////////////////////////////////////
/// Checking If Bob's Vualt balance is correct ///
/////////////////////////////////////////////////////////
uint256 bobArbBalanceInVault = tokens.arbToken.balanceOf(vault);
assertEq(bobArbBalanceInVault, amount, "Vault Balance is not correct");
////////////////////////////////////////////////////
/// Bob mints some euros from the vault ///
////////////////////////////////////////////////////
vm.startPrank(bob);
uint256 eurosBobWant = 100 ether; // 100 euros
SmartVaultV3(payable(vault)).mint(bob, eurosBobWant);
vm.stopPrank();
//////////////////////////////////////////////////////
/// Checking if the correct amount of euros ///
/// has been minted from the vault ///
//////////////////////////////////////////////////////
uint256 bobEurosBalance = tokens.eurosToken.balanceOf(bob);
assertEq(bobEurosBalance, eurosBobWant, "Bob's Euro Balance is not correct");
////////////////////////////////////////////////////////////////////////
/// Checking if the correct fee is generated ///
/// feeGenerated = eurosBobWant * PROTOCOL_FEE_RATE / 100000 ///
/// feeGenerated = 100 * 500 / 100000 ///
/// feeGenerated = 0.5 euros ///
////////////////////////////////////////////////////////////////////////
uint256 feeGenerated = (eurosBobWant * constants.PROTOCOL_FEE_RATE) / 100000;
uint256 expectedFeeGenerated = 0.5 ether; // 0.5 euros
uint256 eurosFeeInLiquidationManager = tokens.eurosToken.balanceOf(address(contracts.liquidationPoolManager));
assertEq(eurosFeeInLiquidationManager, feeGenerated, "fee is not equal");
assertEq(feeGenerated, expectedFeeGenerated, "fee generated is not same");
///////////////////////////////////////////////////////
/// Skipping some time to consolidate rewards ///
///////////////////////////////////////////////////////
skip(block.timestamp + 3 days);
////////////////////////////////////////////////////////////////
/// Getting alice's position before fee is distributed ///
////////////////////////////////////////////////////////////////
(LiquidationPool.Position memory alicePosition, ) = contracts.liquidationPool.position(alice);
uint256 feeThatWillBeReceivedByAliceAccordingToPositionFunction = alicePosition.EUROs - amount;
//////////////////////////////////////////////////////////////
/// alice deposits some more tokens to the pool ///
/// this will consolidate the rewards and distribute ///
/// the fee to alice ///
//////////////////////////////////////////////////////////////
vm.startPrank(alice);
tokens.tstToken.approve(address(contracts.liquidationPool), amount);
tokens.eurosToken.approve(address(contracts.liquidationPool), amount);
contracts.liquidationPool.increasePosition(amount, amount);
vm.stopPrank();
////////////////////////////////////////////////////////////////
/// Getting alice's position after fee is distributed ///
////////////////////////////////////////////////////////////////
(alicePosition, ) = contracts.liquidationPool.position(alice);
uint256 feeActuallyReceivedByAlice = alicePosition.EUROs - (amount * 2);
////////////////////////////////////////////////////////////////
/// Checking if alice has received the correct fee ///
////////////////////////////////////////////////////////////////
vm.expectRevert();
require(feeActuallyReceivedByAlice == feeThatWillBeReceivedByAliceAccordingToPositionFunction, "fee is not equal");
// position function returned twice the fee since it does not take into account the pool fee percentage
assertEq(feeActuallyReceivedByAlice *2, feeThatWillBeReceivedByAliceAccordingToPositionFunction, "fee is not equal");
}

Link to original Test: Link

Output:

Running 1 test for test/unit/Unit.t.sol:Unit
[PASS] test_PositionFunctionReturnsIncorrectPositionOfStaker() (gas: 3739347)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 13.51ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

Tools Used

  • Manual Reveiw

  • Foundry

Recommendations

It is recommended to make the following changes to the LiquidationPool::Position(...) function. Note that corresponding changes should be made to the ILiquidationPoolManager interface.

function position(address _holder) external view returns(Position memory _position, Reward[] memory _rewards) {
_position = positions[_holder];
(uint256 _pendingTST, uint256 _pendingEUROs) = holderPendingStakes(_holder);
_position.EUROs += _pendingEUROs;
_position.TST += _pendingTST;
- if (_position.TST > 0) _position.EUROs += IERC20(EUROs).balanceOf(manager) * _position.TST / getTstTotal();
+ if (_position.TST > 0){
+ uint256 rewardsEuro = (IERC20(EUROs).balanceOf(manager) * ILiquidationPoolManager(manager).poolFeePercentage()) / ILiquidationPoolManager(manager).HUNDRED_PC();
+ _position.EUROs += (rewardsEuro * _position.TST) / getTstTotal();
+ }
_rewards = findRewards(_holder);
}
Updates

Lead Judging Commences

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

incorrect-position

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.