code-423n4 / 2023-09-centrifuge-findings

16 stars 14 forks source link

Investors claiming their maxDeposit by using the LiquidityPool.deposit() will cause that other users won't be able to claim their maxDeposit/maxMint #118

Open c4-submissions opened 1 year ago

c4-submissions commented 1 year ago

Lines of code

https://github.com/code-423n4/2023-09-centrifuge/blob/main/src/LiquidityPool.sol#L141-L144 https://github.com/code-423n4/2023-09-centrifuge/blob/main/src/InvestmentManager.sol#L427-L441

Vulnerability details

Impact

Proof of Concept

The problem occurs when an investor hasn't claimed their deposits and has requested multiple deposits on different epochs at different prices. The InvestmentManager::calculateDepositPrice() function will compute an equivalent/average price for all the requestDeposits that haven't been claimed yet. Because of the different prices that the request deposits where processed at, the computed price will compute the most accurate average of the deposit's price, but there is a slight rounding error that causes the computed value of trancheTokenAmount to be slightly different from what it should exactly be.

Coded PoC

    function testDepositAtDifferentPricesPoC(uint64 poolId, bytes16 trancheId, uint128 currencyId) public {
        vm.assume(currencyId > 0);

        uint8 TRANCHE_TOKEN_DECIMALS = 18; // Like DAI
        uint8 INVESTMENT_CURRENCY_DECIMALS = 6; // 6, like USDC

        ERC20 currency = _newErc20("Currency", "CR", INVESTMENT_CURRENCY_DECIMALS);
        address lPool_ =
            deployLiquidityPool(poolId, TRANCHE_TOKEN_DECIMALS, "", "", trancheId, currencyId, address(currency));
        LiquidityPool lPool = LiquidityPool(lPool_);
        homePools.updateTrancheTokenPrice(poolId, trancheId, currencyId, 1000000000000000000);

        //@audit-info => Add Alice as a Member
        address alice = address(0x23232323);
        homePools.updateMember(poolId, trancheId, alice, type(uint64).max);

        // invest
        uint256 investmentAmount = 100000000; // 100 * 10**6
        homePools.updateMember(poolId, trancheId, self, type(uint64).max);
        currency.approve(address(investmentManager), investmentAmount);
        currency.mint(self, investmentAmount);
        lPool.requestDeposit(investmentAmount, self);

        // trigger executed collectInvest at a price of 1.25
        uint128 _currencyId = poolManager.currencyAddressToId(address(currency)); // retrieve currencyId
        uint128 currencyPayout = 100000000; // 100 * 10**6                                          
        uint128 firstTrancheTokenPayout = 80000000000000000000; // 100 * 10**18 / 1.25, rounded down
        homePools.isExecutedCollectInvest(
            poolId, trancheId, bytes32(bytes20(self)), _currencyId, currencyPayout, firstTrancheTokenPayout
        );

        // assert deposit & mint values adjusted
        assertEq(lPool.maxDeposit(self), currencyPayout);
        assertEq(lPool.maxMint(self), firstTrancheTokenPayout);

        // deposit price should be ~1.25*10**18 === 1250000000000000000
        assertEq(investmentManager.calculateDepositPrice(self, address(lPool)), 1250000000000000000);

        // second investment in a different epoch => different price
        currency.approve(address(investmentManager), investmentAmount);
        currency.mint(self, investmentAmount);
        lPool.requestDeposit(investmentAmount, self);

        // trigger executed collectInvest at a price of 2
        currencyPayout = 100000000; // 100 * 10**6
        uint128 secondTrancheTokenPayout = 50000000000000000000; // 100 * 10**18 / 1.4, rounded down
        homePools.isExecutedCollectInvest(
            poolId, trancheId, bytes32(bytes20(self)), _currencyId, currencyPayout, secondTrancheTokenPayout
        );

        // Alice invests the same amount as the other investor in the second epoch - Price is at 2
        currency.mint(alice, investmentAmount);

        vm.startPrank(alice);
        currency.approve(address(investmentManager), investmentAmount);
        lPool.requestDeposit(investmentAmount, alice);
        vm.stopPrank();

        homePools.isExecutedCollectInvest(
            poolId, trancheId, bytes32(bytes20(alice)), _currencyId, currencyPayout, secondTrancheTokenPayout
        );

        uint128 AliceTrancheTokenPayout = 50000000000000000000; // 100 * 10**18 / 1.4, rounded down

        //@audit-info => At this point, the Escrow contract should have the firstTrancheTokenPayout + secondTrancheTokenPayout + AliceTrancheTokenPayout
        assertEq(lPool.balanceOf(address(escrow)),firstTrancheTokenPayout + secondTrancheTokenPayout + AliceTrancheTokenPayout);

        // Investor collects his the deposited assets using the LiquidityPool::deposit()
        lPool.deposit(lPool.maxDeposit(self), self);

        // Alice tries to collect her deposited assets and gets her transactions reverted because the Escrow doesn't have the required TokenShares for Alice!
        vm.startPrank(alice);

        //@audit-info => Run the PoC one time to test Alice trying to claim their deposit using LiquidityPool.deposit()
        lPool.deposit(lPool.maxDeposit(alice), alice);

        //@audit-info => Run the PoC a second time, but now using LiquidityPool.mint()
        // lPool.mint(lPool.maxMint(alice), alice);
        vm.stopPrank();
    }

Tools Used

Manual Audit

Recommended Mitigation Steps

Assessed type

Math

raymondfam commented 1 year ago

Accounting mess up indeed.

c4-pre-sort commented 1 year ago

raymondfam marked the issue as low quality report

c4-pre-sort commented 1 year ago

raymondfam marked the issue as primary issue

c4-pre-sort commented 1 year ago

raymondfam marked the issue as remove high or low quality report

c4-pre-sort commented 1 year ago

raymondfam marked the issue as high quality report

c4-sponsor commented 1 year ago

hieronx (sponsor) confirmed

gzeon-c4 commented 1 year ago

https://github.com/code-423n4/2023-09-centrifuge-findings/issues/210#issuecomment-1728512207

gzeon-c4 commented 1 year ago

This issue described a protocol specific issue with multiple deposit/withdrawal where #34 is a generic 4626 rounding issue, and therefore not marked as duplicate of #34. In terms of severity, this does not directly lead to a loss of fund but will affect the availability of the protocol, hence Med.

c4-judge commented 1 year ago

gzeon-c4 marked the issue as selected for report

c4-judge commented 1 year ago

gzeon-c4 marked the issue as satisfactory

hieronx commented 1 year ago

Mitigated in https://github.com/centrifuge/liquidity-pools/pull/166