Platform: Code4rena
Start Date: 08/09/2023
Pot Size: $70,000 USDC
Total HM: 8
Participants: 84
Period: 6 days
Judge: gzeon
Total Solo HM: 2
Id: 285
League: ETH
Rank: 16/84
Findings: 3
Award: $869.10
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Bauchibred
Also found by: 0xnev, BARW, Ch_301, J4X, ast3ros, degensec, josephdara
520.8185 USDC - $520.82
https://github.com/code-423n4/2023-09-centrifuge/blob/main/src/InvestmentManager.sol#L535-L548
There is a missing ERC-1404 transfer restriction check for user redemptions similar to in _deposit()
, collectDeposit()
and collectRedeem()
. Users can circumvent this check and proceed with redemption of currencies by exchanging TT shares in liquidity pools without first checking validity of membership for transfer restrictions set by admin. This may allowed continued redemption of TT by users (investors) even when their membership has expired during the epoch and is expected to revert.
InvestmentManager.sol#L535-L548
function _redeem( uint128 trancheTokenAmount, uint128 currencyAmount, address liquidityPool, address receiver, address user ) internal { LiquidityPoolLike lPool = LiquidityPoolLike(liquidityPool); _decreaseRedemptionLimits(user, liquidityPool, currencyAmount, trancheTokenAmount); // decrease the possible deposit limits userEscrow.transferOut(lPool.asset(), user, receiver, currencyAmount); emit RedemptionProcessed(liquidityPool, user, trancheTokenAmount); }
Manual Analysis
function _redeem( uint128 trancheTokenAmount, uint128 currencyAmount, address liquidityPool, address receiver, address user ) internal { LiquidityPoolLike lPool = LiquidityPoolLike(liquidityPool); _decreaseRedemptionLimits(user, liquidityPool, currencyAmount, trancheTokenAmount); // decrease the possible deposit limits + require(liquidityPool.checkTransferRestriction(address(0), user, 0), "InvestmentManager/not-a-member"); userEscrow.transferOut(lPool.asset(), user, receiver, currencyAmount); emit RedemptionProcessed(liquidityPool, user, trancheTokenAmount); }
Context
#0 - c4-pre-sort
2023-09-15T07:34:27Z
raymondfam marked the issue as low quality report
#1 - c4-pre-sort
2023-09-15T07:34:50Z
raymondfam marked the issue as duplicate of #151
#2 - c4-pre-sort
2023-09-17T10:03:51Z
raymondfam marked the issue as not a duplicate
#3 - c4-pre-sort
2023-09-17T10:04:00Z
raymondfam marked the issue as duplicate of #14
#4 - c4-judge
2023-09-26T16:10:20Z
gzeon-c4 marked the issue as duplicate of #779
#5 - c4-judge
2023-09-26T16:12:11Z
gzeon-c4 marked the issue as satisfactory
🌟 Selected for report: castle_chain
Also found by: 0xAadi, 0xHelium, 0xLook, 0xblackskull, 0xfuje, 0xmystery, 0xnev, 0xpiken, 7ashraf, BARW, Bauchibred, Bughunter101, Ch_301, JP_Courses, Kaysoft, Krace, MohammedRizwan, SanketKogekar, Sathish9098, alexzoid, ast3ros, btk, catellatech, degensec, fatherOfBlocks, grearlake, imtybik, jkoppel, jolah1, klau5, lsaudit, m_Rassska, merlin, mrudenko, nobody2018, rokinot, rvierdiiev, sandy
12.7917 USDC - $12.79
https://github.com/code-423n4/2023-09-centrifuge/blob/main/src/LiquidityPool.sol#L97-L100
modifier withApproval(address owner) { require(msg.sender == owner, "LiquidityPool/no-approval"); _; }
The withApproval()
modifier is supposed to allow an authorized admin designated as ward to call certain specified functions such as withdraw()/redeem()/requestDeposit()/requestRedeem()/decreaseDepositRequest()/decreaseRedeemRequest()
. However, there is a lack of check of the wards
mapping allowing that to be performed, thus any authorized admin assigned as ward cannot call this functions as intended, breaking logic.
Manual Analysis
modifier withApproval(address owner) { - require(msg.sender == owner, "LiquidityPool/no-approval"); + require(msg.sender == owner || wards[msg.sender] == 1, "LiquidityPool/no-approval"); _; }
Context
#0 - c4-pre-sort
2023-09-15T22:18:47Z
raymondfam marked the issue as low quality report
#1 - c4-pre-sort
2023-09-15T22:19:00Z
raymondfam marked the issue as duplicate of #41
#2 - c4-judge
2023-09-25T16:09:59Z
gzeon-c4 changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-09-26T18:20:29Z
gzeon-c4 marked the issue as grade-b
🌟 Selected for report: ciphermarco
Also found by: 0x3b, 0xbrett8571, 0xmystery, 0xnev, K42, Kral01, Sathish9098, castle_chain, catellatech, cats, emerald7017, fouzantanveer, foxb868, grearlake, hals, jaraxxus, kaveyjoe, lsaudit, rokinot
335.4874 USDC - $335.49
Here is an outline of how a Tranche with their associated liquidity pools can be deployed.
PoolManager.addCurrency()
: Here, an asset is added to the global currencyIdToAddress
and currencyIdToAddress
mapping to signify support for currency asset to be allowed for liquidity poolsPoolManager.addPool()
: Here, an existing centrifuge RWA pool is added to the protocol to be allowed to be linked to liquidity pools, signified by the pool.createdAt
timestamp updated.PoolManager.allowPoolCurrency()
: Here, a specific currency assets are allowed to be used for centrifuge RWA pools that have been already added via addPool()
. This is sgnified by the pool.allowedCurrencies
mapping update.PoolManager.addTranche()
: Here, an specific tranche is added linked to a centrifuge RWA pool already added via addPool()
. This is signigied by the updates of the relevant data associated with tranche (RWA poolId, trancheId, TT decimals, TT name and TT symbol) and more importantly the tranche.createdAt
timestamp.PoolManager.deployTranche()
: Note that steps 1-4 are not permisionless, and requires a governance proposal and vote by CFG holders before addition of liquidity pools and tranches for deployment. Here, a successfully added tranche token can be deployed. Note that the salt is constructed by the RWA poolId and trancheId to ensure same TT address for any compatible EVM chain.PoolManager.deployLiquidityPool()
: This is the final step where successfully added tranches linked to specific RWA centrifuge pools can be deployed. This effectively means that a tranche is deployed for the RWA pool, since RWA pools can be linked to multiple tranches.LiquidityPool.requestDeposit()
: Investors first has to request investments before actual tranche tokens can be minted. This deposit requests are added to the orderbook on centrifuge to be matched with redemption orders and executed at the end of the epoch. At any point of time, user can cancel outstanding investment orders by passing in currencyAmount == 0
. This will communicate with the centrifuge chain to cancel any outstanding orders.InvestmentManager.handleExecutedCollectInvest()
: After the end of the epoch, once investment orders have been successfully executed on the centrifuge chain, the incoming router contract will communicate with the gateway contract to call the handleExecutedCollectInvest()
function. This mints the TT to the escrow contract and updates the necessary lpValues.maxDeposit
and lpValues.maxMint
for users to claim tranche tokens for successfully executed investment orders.LiquidityPool.deposit()/mint()
: Once gateway contract has successfully communicated with the investment manager contract, users can claim their associated amount of tranche tokens for successfully executed investment orders.LiquidityPool.requestRedeem()
: Users first has to request redemption before actual currencies previously escrowed are transferred back to them. This redemption requests are added to the orderbook on centrifuge to be matched with investment orders and executed at the end of the epoch. At any point of time, user can cancel outstanding redemption orders by passing in trancheTokenAmount == 0
. This will communicate with the centrifuge chain to cancel any outstanding orders.InvestmentManager.handleExecutedCollectRedeem()
: After the end of the epoch, once redemption orders have been successfully executed on the centrifuge chain, the incoming router contract will communicate with the gateway contract to call the handleExecutedCollectRedeem()
function. This burns the TT sent by user to escrow and updates the necessary lpValues.maxRedeem
and lpValues.maxWithdraw
for users to redeem underlying currencies for successfully executed redemption orders.LiquidityPool.redeem()/withdraw()
: Once gateway contract has successfully communicated with the investment manager contract, users can call these functions to claim their associated amount of underlying currencies for successfully executed redemption orders.LiquidityPool.decreaseDepositRequest()/decreaseRedeemRequest()
: At any point of time before end of epoch, user can request a decrease in outstanding investment/redemption orders. The outgoing router will communicate with centrifuge chain to executed the relevant decrease in order amounts.InvestmentManager.handleExecutedDecreaseInvestOrder()/handleExecutedDecreaseRedeemOrder()
: After the decrease is successfully executed on the centrifuge chain, the incoming router will communicate with the gateway contract which inturns communicates with the investment manager contract to call handleExecutedDecreaseInvestOrder()/handleExecutedDecreaseRedeemOrder()
. This executes the relevant return of tranche tokens/underlying currency back to user from escrow respectively.LiquidityPool.collectDeposit()/collectRedeem()
: In case a investment/redemption order executed on centrifuge chain does not occur successfully, this are backup functions where user can manually trigger to collect the outcome of the executed orders on Centrifuge Chain.Codebase Quality Categories | Comments |
---|---|
Testing | Unit test and Integration tests were present using Foundry, allowing ease of testing for PoCs |
Documentation | The documentation for Centrifuge is well written and provides a satisfactory overview of the protocol, though some features such as order cancellation and cross chain transfers were not documented |
Organization | Codebase is well organized with clear distinctions between the 18 contracts. |
LiquidityPool.convertToShares()/convertToAssets()
could be misleadingThe tranche token price denoted by latestPrice
is only updated after the previous epoch requested orders is executed. Furthermore, the latest update of price is based on the latest order be it redemption or investment, so if there are any inaccuracies/anomalies arising from latest order executed it can cause a inaccurate latestPrice
.
This functions are estimates useful for display purposes for users to estimate investment/redemption returns, so unknowing users would request investment/redemption orders based on previous epoch prices if the data is retrieved from these functions. Hence, sufficient warning needs to be given to users employing this functions for estimates.
Currently, a decrease in investment/redemption orders are supported via LiquidityPool.decreaseDepositRequest()/decreaseRedeemRequest()
. Consider supporting a increase in investment/redemption orders too for better UX so that they do not have to make another new separate order.
handle
functions for different function callsAny changes or actions made to the protocol sent from the incoming router needs to communicate with the gateway contract via the Gateway.handle()
function. However, currently this function checks the incoming message against all the relevant function calls until it matches. This can incur unecessary gas costs and may even cause revert on mainnet where gas costs are high with insufficient gas provided for the transaction.
Any approval calls for liquidity pools deployed with non-standard tokens suchs as USDT as underlying token will revert due to differences in approve()
function signatures, where USDT approve()
function signature do specify a return value.
Similarly, for tokens with non-standard (e.g. DAI) or even phantom (e.g. WETH) permit()
functions, the ERC-2612 spec could be broken which can affect any request for deposits utilizing the LiquidityPool.requestDepositWithPermit()
.
Hence, always take note deviating specifications when support is provided for certain tokens.
During pausing of protocol, consider providing a time buffer for users to react to price changes during the pause for their executed investment/redemption orders on the centrifuge chain. This could be allowing cancellation of already executed investment/redemption orders and returning funds from escrow back to them during the time buffer.
Consider separation of interfaces from main contract as well as a more consistent naming of interfaces to avoid confusion.
Protocol can choose not to provide relevant ERC1404 membership for users to interact with protocol, which is required to make investment/redemption orders.
The PauseAdmin
can instantaneously pause the protocol at any time to prevent any further interactions with protocol. Similarly, the DelayedAdmin
can pause the protocol albeit at a delay. This are fail safes for emergency scenarios but nevertheless the risk of centralization exists.
Day | Scope | Details |
---|---|---|
1-2 | Centrifuge Docs & Contest Details | Review Docs and understand key components of protocol, namely: Pool and Tranche deployment, Investment orders and redemption orders |
3-5 | Centrifuge contracts | Manual audits of contracts with the following flow, PoolManager.sol --> LiquidityPool.sol --> InvestmentManager.sol . The rest of the contracts were audited based on interactions with the above mentioned core contracts. |
6 | - | Finish up Analysis report |
40 hours
#0 - c4-pre-sort
2023-09-17T02:05:33Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2023-09-26T17:18:00Z
gzeon-c4 marked the issue as grade-a