hats-finance / Possum-Labs--Portals--0xed8965d49b8aeca763447d56e6da7f4e0506b2d3

GNU General Public License v2.0
0 stars 2 forks source link

The remained-$PSM balance of the `fundingRewardPool` will be stuck forever #76

Open hats-bug-reporter[bot] opened 9 months ago

hats-bug-reporter[bot] commented 9 months ago

Github username: @0xmuxyz Twitter username: -- Submission hash (on-chain): 0xe9927d9b43daab555b6cfd3863985bdb608f357a2cbbe758e0228d4b66cd3999 Severity: medium

Description:

Description

Within the Portal, the fundingRewardPool would be defined to store the amount of PSM available for redemption against bTokens like this: \ https://github.com/hats-finance/Possum-Labs--Portals--0xed8965d49b8aeca763447d56e6da7f4e0506b2d3/blob/5e1855411121ccd883f15c0d3c8d2fd9fc1d8e4c/contracts/Portal.sol#L119

The fundingRewardPool would be increased until reach the fundingMaxRewards (bToken.totalSupply()) when the convert() would be called by an arbitrager like this: \ https://github.com/hats-finance/Possum-Labs--Portals--0xed8965d49b8aeca763447d56e6da7f4e0506b2d3/blob/5e1855411121ccd883f15c0d3c8d2fd9fc1d8e4c/contracts/Portal.sol#L638-L640

    function convert(address _token, uint256 _minReceived, uint256 _deadline) external nonReentrant {
        ...
        /// @dev Update the funding reward pool balance and the tracker of collected rewards
        if (bToken.totalSupply() > 0 && fundingRewardsCollected < fundingMaxRewards) { ///<----------- @audit
            uint256 newRewards = (FUNDING_REWARD_SHARE * AMOUNT_TO_CONVERT) / 100; ///<------- @audit
            fundingRewardPool += newRewards; ///<------- @audit
            fundingRewardsCollected += newRewards;
        }
        ...

When a funder would like to redeem their $bTokens for $PSM, the burnBtokens() would be called. Within the burnBtokens(), the amount of $PSM that a funder can receive (amountToReceive) would be determined based on the returned-value of the getBurnValuePSM(). \ https://github.com/hats-finance/Possum-Labs--Portals--0xed8965d49b8aeca763447d56e6da7f4e0506b2d3/blob/5e1855411121ccd883f15c0d3c8d2fd9fc1d8e4c/contracts/Portal.sol#L701 \ Then, the amount of $PSM that a funder can receive (amountToReceive) would be subtract from the fundingRewardPool like this: \ https://github.com/hats-finance/Possum-Labs--Portals--0xed8965d49b8aeca763447d56e6da7f4e0506b2d3/blob/5e1855411121ccd883f15c0d3c8d2fd9fc1d8e4c/contracts/Portal.sol#L707

    /// @notice Burn user bTokens to receive PSM
    ...
    /// @param _amount The amount of bTokens to burn
    function burnBtokens(uint256 _amount) external nonReentrant activePortalCheck {
        /// @dev Require that the burn amount is greater than zero
        if(_amount == 0) {revert InvalidInput();}

        /// @dev Calculate how many PSM the user receives based on the burn amount
        uint256 amountToReceive = getBurnValuePSM(_amount); ///<----------- @audit

        /// @dev Burn the bTokens from the user's balance
        bToken.burnFrom(msg.sender, _amount);

        /// @dev Reduce the funding reward pool by the amount of PSM payable to the user
        fundingRewardPool -= amountToReceive; ///<----------- @audit

        /// @dev Transfer the PSM to the user
        IERC20(PSM_ADDRESS).safeTransfer(msg.sender, amountToReceive);
        ...

Within the getBurnValuePSM(), the burnValue, which is the amountToReceive of $PSM in the burnBtokens() above, would be calculated and returned like this: \ https://github.com/hats-finance/Possum-Labs--Portals--0xed8965d49b8aeca763447d56e6da7f4e0506b2d3/blob/5e1855411121ccd883f15c0d3c8d2fd9fc1d8e4c/contracts/Portal.sol#L684

    /// @notice Calculate the current burn value of amount bTokens. Return value is amount PSM tokens
    /// @param _amount The amount of bTokens to burn
    function getBurnValuePSM(uint256 _amount) public view returns(uint256 burnValue) {
        burnValue = (fundingRewardPool * _amount) / bToken.totalSupply(); ///<-------------- @audit
    }

Since the funders can redeem their $bToken for $PSM anytime and the $PSM balance in the fundingRewardPoolis gradually increased via theconvert(), there will be the **remained-$PSM balance** of thefundingRewardPoolafter all funders redeem their $bTokens for $PSM if some funders redeem their $bTokens for $PSM **before** the $PSM balance of thefundingRewardPoolreach thefundingMaxRewards`.

Since the $PSM balance of fundingRewardPool would be excluded from the reserve0 in both the buyPortalEnergy() and the sellPortalEnergy(), the remained-$PSM balance of the fundingRewardPool would not be used for the reserve0 of the internal LP like this: \ https://github.com/hats-finance/Possum-Labs--Portals--0xed8965d49b8aeca763447d56e6da7f4e0506b2d3/blob/5e1855411121ccd883f15c0d3c8d2fd9fc1d8e4c/contracts/Portal.sol#L504 https://github.com/hats-finance/Possum-Labs--Portals--0xed8965d49b8aeca763447d56e6da7f4e0506b2d3/blob/5e1855411121ccd883f15c0d3c8d2fd9fc1d8e4c/contracts/Portal.sol#L555

So, this lead to that the remained-$PSM balance of the fundingRewardPool would be stuck in the fundingRewardPool forever.

POC (Scenario)

Assuming that:

Example scenario:

Recommendation

Consider adding a function that enable the owner to send the remained-$PSM balance of the fundingRewardPool to the reserve0 of the internal LP - after all funders redeemed all $bToken balance of them.

PossumLabsCrypto commented 9 months ago

Thank you for the in-depth report.

However, this is an invalid issue. You seem to have overlooked the second condition in the convert() function that checks if the total supply of bTokens is above zero. If everyone has burned their bTokens, no additional reward will accrue.

        /// @dev Update the funding reward pool balance and the tracker of collected rewards
        if (bToken.totalSupply() > 0 && fundingRewardsCollected < fundingMaxRewards) {
            uint256 newRewards = (FUNDING_REWARD_SHARE * AMOUNT_TO_CONVERT) / 100;
            fundingRewardPool += newRewards;
            fundingRewardsCollected += newRewards;
        }

It is also not possible that everyone burns their bTokens and a rest of rewards remains in the Portal because it´s a proportional redeeming at all times. Therefore, by definition the last person redeeming has 100% of the total supply of bTokens and therefore receives 100% of the rewardPool.