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: 22/84
Findings: 2
Award: $533.61
🌟 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
The current transfer restrictions effectively prevent non-member and non-KYC-verified accounts from owning the tokens but not from utilizing them. As a result, these accounts can still interact with tranche tokens in other protocols.
The Tranche contract has functions like mint
, transfer
, and transferFrom
which are all guarded by a restricted
modifier.
https://github.com/code-423n4/2023-09-centrifuge/blob/6e735d42c1e0cfe52bbbd74f0e8b0c3541073060/src/token/Tranche.sol#L59 https://github.com/code-423n4/2023-09-centrifuge/blob/6e735d42c1e0cfe52bbbd74f0e8b0c3541073060/src/token/Tranche.sol#L63 https://github.com/code-423n4/2023-09-centrifuge/blob/6e735d42c1e0cfe52bbbd74f0e8b0c3541073060/src/token/Tranche.sol#L72
modifier restricted(address from, address to, uint256 value) { uint8 restrictionCode = detectTransferRestriction(from, to, value); require(restrictionCode == restrictionManager.SUCCESS_CODE(), messageForTransferRestriction(restrictionCode)); _; }
However, this restricted
modifier only validates the to
address and not the from
address. The actual check occurs in restrictionManager.detectTransferRestriction
, which focuses solely on hasMember(to)
but ignores hasMember(from)
.
function detectTransferRestriction(address from, address to, uint256 value) public view returns (uint8) { if (!hasMember(to)) { return DESTINATION_NOT_A_MEMBER_RESTRICTION_CODE; } return SUCCESS_CODE; }
Moreover, the functions approve
, increaseAllowance
, and decreaseAllowance
are not covered by any restrictions.
This creates a loophole: an account that has neither member status nor KYC verification can still make use of the tranche tokens in other DeFi platforms by having another account member approve
or increase their allowance.
Manual
ERC20
#0 - c4-pre-sort
2023-09-15T23:32:27Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-09-15T23:32:35Z
raymondfam marked the issue as duplicate of #17
#2 - c4-pre-sort
2023-09-17T05:16:19Z
raymondfam marked the issue as not a duplicate
#3 - c4-pre-sort
2023-09-17T05:16:24Z
raymondfam marked the issue as low quality report
#4 - c4-pre-sort
2023-09-17T05:16:36Z
raymondfam marked the issue as duplicate of #14
#5 - c4-judge
2023-09-26T16:08:47Z
gzeon-c4 marked the issue as not a duplicate
#6 - c4-judge
2023-09-26T16:08:58Z
gzeon-c4 marked the issue as duplicate of #779
#7 - c4-judge
2023-09-26T16:11:50Z
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
Membership consists of a whitelist of users who have signed relevant agreements and successfully completed the Know Your Customer (KYC) process. This allows them to invest in specific tranches. Membership is generally valid for one year, after which it expires.
However, investors can only redeem their tranche tokens if their membership is active, creating a potential barrier for those who no longer wish to invest.
// Check if user is allowed to hold the restricted tranche tokens _isAllowedToInvest(lPool.poolId(), lPool.trancheId(), lPool.asset(), user);
Only check membership if a user want to deposit, not when he wants to redeem.
View function in Investment Manager and Liquidity pool such as totalAssets, convertToShares, convertToAssets, maxDeposit, maxMint, maxWithdraw, previewDeposit, ... are not accurate because they used the price from latest update that can be staled.
This is because the process in Centrifuge Chain is asynchronous. The price is updated when there is a message from Centrifuge Chain to the current chain and function _updateLiquidityPoolPrice
is called.
https://github.com/code-423n4/2023-09-centrifuge/blob/6e735d42c1e0cfe52bbbd74f0e8b0c3541073060/src/InvestmentManager.sol#L319-L419 https://github.com/code-423n4/2023-09-centrifuge/blob/72b7a3ae44993459757b0c4f1623fe765cd02d8f/src/LiquidityPool.sol#L111-L137 https://github.com/code-423n4/2023-09-centrifuge/blob/72b7a3ae44993459757b0c4f1623fe765cd02d8f/src/LiquidityPool.sol#L154-L172 https://github.com/code-423n4/2023-09-centrifuge/blob/72b7a3ae44993459757b0c4f1623fe765cd02d8f/src/LiquidityPool.sol#L186-L194
The trance token decimal is set by admin from Centrifuge Chain.
tranche.decimals = decimals;
There is no check that is has to be less than or equal to 18 decimals. If the decimal is more than 18, the converting will revert.
Approve function of USDT requires setting allowance to 0 first.
// To change the approve amount you first have to reduce the addresses` // allowance to zero by calling `approve(_spender, 0)` if it is not // already 0 to mitigate the race condition described here: // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 require(!((_value != 0) && (allowed[msg.sender][_spender] != 0)));
https://forum.openzeppelin.com/t/can-not-call-the-function-approve-of-the-usdt-contract/2130/2
Function handleTransfer in PoolManager.sol should set the allowance to 0 first before setting to the new value, it may affect the operation of the pool if the currency is USDT.
function handleTransfer(uint128 currency, address recipient, uint128 amount) public onlyGateway { address currencyAddress = currencyIdToAddress[currency]; require(currencyAddress != address(0), "PoolManager/unknown-currency"); EscrowLike(escrow).approve(currencyAddress, address(this), amount); SafeTransferLib.safeTransferFrom(currencyAddress, address(escrow), recipient, amount); }
Add approve 0 first before setting to the new value.
EscrowLike(escrow).approve(currencyAddress, address(this), 0);
The bridge that send messages from and to Centrifuge Chain to other chain is Axelar. The gas fee is paid by Centrifuge. It can be subjected to DOS from users.
function send(bytes calldata message) public onlyGateway { axelarGateway.callContract(axelarCentrifugeChainId, centrifugeGatewayPrecompileAddress, message); }
In the standard implementation, the msg.sender will attach the gas fee for bridging transactions.
Please see here for details: https://docs.axelar.dev/dev/general-message-passing/gas-services/pay-gas
The name of the variable currencyAmountInPriceDecimals
should be trancheTokenAmountInPriceDecimals
because it is the amount of tranche tokens that the user will receive.
uint256 currencyAmountInPriceDecimals = _toPriceDecimals(currencyAmount, currencyDecimals, liquidityPool).mulDiv( 10 ** PRICE_DECIMALS, price, MathLib.Rounding.Down );
Instance:
https://github.com/code-423n4/2023-09-centrifuge/blob/72b7a3ae44993459757b0c4f1623fe765cd02d8f/src/LiquidityPool.sol#L96 : only msg.sender can call, no ward https://github.com/code-423n4/2023-09-centrifuge/blob/6e735d42c1e0cfe52bbbd74f0e8b0c3541073060/src/UserEscrow.sol#L39: only authorized admin, no destination address
#0 - c4-pre-sort
2023-09-17T01:39:38Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2023-09-26T17:39:21Z
gzeon-c4 marked the issue as grade-c
#2 - c4-judge
2023-09-29T11:51:41Z
gzeon-c4 marked the issue as grade-b