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

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

Quote functions return incorrect values during funding phase #63

Open hats-bug-reporter[bot] opened 1 year ago

hats-bug-reporter[bot] commented 1 year ago

Github username: @0xfuje Twitter username: 0xfuje Submission hash (on-chain): 0x7c559946325e3b283826358af8a423908409d282cb208fc006ce5faeaeb2152b Severity: low

Description:

Description

The problem is that portalEnergy quote functions do not take into account when constantProduct have not yet been set, since it will only have a non-zero value after activatePortal(). Any time the user calls the functions before the active phase it will return a misleading value.

    function quoteSellPortalEnergy(uint256 _amountInput) external view returns(uint256) {
        /// @dev Calculate the PSM token reserve (output)
        uint256 reserve0 = IERC20(PSM_ADDRESS).balanceOf(address(this)) - fundingRewardPool;

        /// @dev Calculate the reserve of portalEnergy (input)
        uint256 reserve1 = constantProduct / reserve0;

        /// @dev Calculate the amount of PSM tokens received based on the amount of portalEnergy sold
        uint256 amountReceived = (_amountInput * reserve0) / (_amountInput + reserve1);

        return (amountReceived);
    }

  1. Assuming multiple portals have opened: aka portalEnergyToken has a market
  2. User calls quoteSellPortalEnergy() with 100 * 1e18
  3. Saw that he will get outstanding value in PSM for his portalEnergy
  4. Buys 100 * 1e18 portal energy token
  5. After the funding phase he finds out that his portalEnergyToken sell is worth much less PSM
  6. Tries to cause reputational damage to the project

Proof of Concept

  1. setup a foundry test repository with deployed contracts
  2. copy the below PoC in the test contract
  3. run forge test --match-test testInaccurateQuotesDuringFundingPhase -vvvv

    function testInaccurateQuotesDuringFundingPhase() public {
        // initial funding of Portal
        vm.startPrank(deployer);
        psmToken.approve(address(portal), 250_000_000 * 1e18);
        portal.contributeFunding(250_000_000 * 1e18);
        vm.stopPrank();
    
        // user calls quote functions
        vm.startPrank(user);
        // buy portal energy returns zero
        assertEq(portal.quoteBuyPortalEnergy(1_000_000 * 1e18), 0);
        // sell portal energy returns whole PSM balance
        assertEq(portal.quoteSellPortalEnergy(1), 250_000_000 * 1e18);
    }

Recommendation

Consider calculating the _constantProduct in memory in the quote functions before the funding phase (without touching the storage variable constantProduct). In the active phase, the storage var can be used loaded to memory. Based on the constantProduct = fundingBalance ** 2 / FUNDING_EXCHANGE_RATIO calculation:

    function quoteSellPortalEnergy(uint256 _amountInput) external view returns(uint256) {
        uint256 reserve0 = IERC20(PSM_ADDRESS).balanceOf(address(this)) - fundingRewardPool;

        if (constantProduct == 0) {
            uint256 _constantProduct = fundingBalance ** 2 / FUNDING_EXCHANGE_RATIO;
        } else {
            uint256 _constantProduct = constantProduct;
        }

        uint256 reserve1 = _constantProduct / reserve0;

        uint256 amountReceived = (_amountInput * reserve0) / (_amountInput + reserve1);

        return (amountReceived);
    }
PossumLabsCrypto commented 1 year ago

Thanks for the submission!

That´s a valid finding, although I want to mention that it is impossible to acquire Portal Energy Tokens before a Portal was activated. Therefore, steps 4-6 will never happen.

Nonetheless, this is a valid low severity because the contract provides confusing information, i.e. doesn´t work as intended.

I would prefer to disable the quoteBuy/SellPortalEnergy functions alltogether before the Portal is activated.

Let me know if you agree with this solution:

function quoteSellPortalEnergy(uint256 _amountInput) external activePortalCheck view returns(uint256) { }

0xfuje commented 1 year ago

Thanks for the feedback!

Just out of curiosity: in the scenario where multiple Portals are opened: is it still impossible to acquire portal energy before portal was activated?

Agree with the solution, this way there is no added complexity.

PossumLabsCrypto commented 1 year ago

Thanks for the confirmation. 🤝

There are only two primary ways to acquire Portal Energy:

  1. stake principal and generate PE
  2. buy it from the Portal with PSM via the internal LP

stake() cannot be called before the Portal is activated. buyPortalEnergy cannot be called by anyone who doesn´t have an initialized account. A new account can only be initialized by calling stake().

Therefore, it´s impossible to acquire Portal Energy of a Portal that is inactive. :)