Summary
One of the intended purpose of providing EURO and TST to the LiquidationPool is for the users to get a portion of the borrowing fees as described in the README:
- **Stakers**: users adding TST and/or EUROs to the Liquidation Pool, in order to gain rewards from borrowing fees and vault liquidations
As confirmed by the sponsor the protocol
address in SmartVaultManagerV5
is the LiquidationPoolManager
address so each time a user mint or burn EUROs fees are sent to the LiquidationPoolManager.
The fee for the pool is split between the protocol and the Stakers based on poolFeePercentage
that is set as a reward distribution ratio between stackers and the protocol. The amount left after distributeFees
is sent to the protocol.
Unfortunately the current implementation only reward TST staker and not EUROs stacker
Vulnerability Details
As we can see bellow in the LiquidationPool
contract borrowing Fees reward are not transfered and added to the current user position if they didn't Stake TST.
function getTstTotal() private view returns (uint256 _tst) {
for (uint256 i = 0; i < holders.length; i++) {
_tst += positions[holders[i]].TST;
}
for (uint256 i = 0; i < pendingStakes.length; i++) {
_tst += pendingStakes[i].TST;
}
}
function distributeFees(uint256 _amount) external onlyManager {
uint256 tstTotal = getTstTotal();
@> if (tstTotal > 0) {
IERC20(EUROs).safeTransferFrom(msg.sender, address(this), _amount);
for (uint256 i = 0; i < holders.length; i++) {
address _holder = holders[i];
positions[_holder].EUROs += _amount * positions[_holder].TST / tstTotal;
}
for (uint256 i = 0; i < pendingStakes.length; i++) {
pendingStakes[i].EUROs += _amount * pendingStakes[i].TST / tstTotal;
}
}
}
Furthermore the protocol will get the total amount of the reward instead of sharing it with the users or leaving it in the LiquidationPoolManager:
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)));
}
Impact
This issue is reported as medium as there's a disruption of protocol functionality, the expected behavior for users staking TST and/or EUROs is to receive potentially BOTH rewards from borrowing fees and vault liquidations.
The current implementation doesn't incentivize users to stake EUROs which is a key mechanism.
Tools Used
Manual review
Recommendations
1 - Inform the protocol's users clearly that borrowers fees are only claimable by user staking TST but it could more interesting for the project tokenomics to allow user who stake EUROs to get rewarded with borrowers fees
2 - Or update the protocol logic to distribute rewards equally between TST and EUROs staker
function getStakeTotal() private view returns (uint256 _totalStaked) {
for (uint256 i = 0; i < holders.length; i++) {
_totalStaked += positions[holders[i]].TST;
_totalStaked += positions[holders[i]].EUROs;
}
for (uint256 i = 0; i < pendingStakes.length; i++) {
_totalStaked += pendingStakes[i].TST;
_totalStaked += pendingStakes[i].EUROs;
}
}
function distributeFees(uint256 _amount) external onlyManager {
uint256 totalStaked = getStakeTotal();
if (tstTotal > 0) {
IERC20(EUROs).safeTransferFrom(msg.sender, address(this), _amount);
for (uint256 i = 0; i < holders.length; i++) {
address _holder = holders[i];
positions[_holder].EUROs += _amount * (positions[_holder].TST + position.[_holder].EUROs) / totalStaked;
}
for (uint256 i = 0; i < pendingStakes.length; i++) {
pendingStakes[i].EUROs += _amount * (pendingStakes[i].TST + pendingStakes[i].EUROs) / totalStaked;
}
}
}