Open c4-submissions opened 1 year ago
Accounting mess up indeed.
raymondfam marked the issue as low quality report
raymondfam marked the issue as primary issue
raymondfam marked the issue as remove high or low quality report
raymondfam marked the issue as high quality report
hieronx (sponsor) confirmed
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.
gzeon-c4 marked the issue as selected for report
gzeon-c4 marked the issue as satisfactory
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
LiquidityPool.deposit()
will cause the Escrow contract doesn't have enough shares to allow other investors to claim their maxDeposit or maxMint values for their deposited assetsProof of Concept
Before an investor can claim their deposits, they first needs to request the deposit and wait for the Centrigue Chain to validate it in the next epoch.
Investors can request deposits at different epochs without the need to claim all the approved deposits before requesting a new deposit, in the end, the maxDeposit and maxMint values that the investor can claim will be increased accordingly based on all the request deposits that the investor makes.
When the requestDeposit of the investor is processed in the Centrifuge chain, a number of TrancheShares will be minted based on the price at the moment when the request was processed and the total amount of deposited assets, this TrancheShares will be deposited to the Escrow contract, and the TrancheShares will be waiting for the investors to claim their deposits.
When investors decide to claim their deposit they can use the
LiquidityPool.deposit()
function, this function receives as arguments the number of assets that are being claimed and the address of the account to claim the deposits for.The
LiquidityPool.deposit()
function calls theInvestmentManager::processDeposit()
which will validate that the amount of assets being claimed doesn't exceed the investor's deposit limits, will compute the deposit price in theInvestmentManager::calculateDepositPrice()
, which basically computes an average price for all the request deposits that have been accepted in the Centrifuge Chain, each of those request deposits could've been executed at a different price, so, this function, based on the values of maxDeposit and maxMint will estimate an average price for all the unclaimed deposits, later, using this computed price for the deposits will compute the equivalent of TrancheTokens for the CurrencyAmount being claimed, and finally, processDeposit() will transferFrom the escrow to the investor account the computed amount of TranchTokens.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.LiquidityPool.deposit()
Coded PoC
I used the
LiquidityPool.t.sol
test file as the base file for this PoC, please add the below testPoC to the LiquidityPool.t.sol fileIn this PoC I demonstrate that Alice (A second investor) won't be able to claim her maxDeposit or maxMint amounts after the first investor uses the
LiquidityPool.deposit()
function to claim his maxDeposit() assets. The first investor makes two requestDeposit, each of them at a different epoch and at a different price, Alice on the other hand only does 1 requestDeposit in the second epoch.Run this PoC two times, check the comments on the last 4 lines, one time we want to test Alice claiming her deposits using LiquidityPool::deposit(), and the second time using LiquidityPool::mint()
Tools Used
Manual Audit
Recommended Mitigation Steps
I'd recommend to add a check to the computed value of the
_trancheTokenAmount
in theInvestmentManager::processDeposit()
, if the_trancheTokenAmount
exceeds themaxMint()
of the user, update it and set it to be the maxMint(), in this way, the rounding differences will be discarded before doing the actual transfer of shares from the Escrow to the user, and this will prevent the Escrow from not having all the required TranchToken for the other investorsif (_trancheTokenAmount > orderbook[user][liquidityPool].maxMint) _trancheTokenAmount = orderbook[user][liquidityPool].maxMint;
_deposit(_trancheTokenAmount, _currencyAmount, liquidityPool, user); trancheTokenAmount = uint256(_trancheTokenAmount); }
After applying the suggested recommendation, you can use the provided PoC on this report to verify that the problem has been solved.
Assessed type
Math