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: 21/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
https://github.com/code-423n4/2023-09-centrifuge/blob/main/src/token/RestrictionManager.sol#L27-L34
As we know from README.md
Removing an investor from the memberlist in the Restriction Manager locks their tokens. This is expected behavior.
to check this, the modifier restricted
is implemented in mint()
,transferFrom()
and transfer()
However, the restricted
modifier in Tranche.sol
will sub-call to restrictionManager.detectTransferRestriction()
File: RestrictionManager.sol 27: // --- ERC1404 implementation --- 28: function detectTransferRestriction(address from, address to, uint256 value) public view returns (uint8) { 29: if (!hasMember(to)) { 30: return DESTINATION_NOT_A_MEMBER_RESTRICTION_CODE; 31: } 32: 33: return SUCCESS_CODE; 34: }
You can notice it only checks the to
address if has a member or not
So in case the from
is removed investor he still able to transfer his Tranche Token to an active address (investor) in order to withdraw it later
The removed investors are still able to withdraw/transfer their locked Tranche Token
Manual Review
check the from
address if he is a member or not
Invalid Validation
#0 - c4-pre-sort
2023-09-15T16:09:45Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-09-15T16:09:57Z
raymondfam marked the issue as duplicate of #17
#2 - c4-pre-sort
2023-09-17T05:24:14Z
raymondfam marked the issue as not a duplicate
#3 - c4-pre-sort
2023-09-17T05:24:21Z
raymondfam marked the issue as duplicate of #14
#4 - c4-pre-sort
2023-09-17T05:24:27Z
raymondfam marked the issue as low quality report
#5 - c4-judge
2023-09-26T16:10:24Z
gzeon-c4 marked the issue as duplicate of #779
#6 - c4-judge
2023-09-26T16:11:53Z
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
01- The Invest could get blacklisted in the supported currency
02- The UserEscrow.transferOut()
will revert
03- Liquidity Pool is not ERC-4626 compatible contract
04- The tokens could have a different decimal in each chains
05- User could lose his Tranch tokens
06- The deployer could set delay
to a value bigger than MAX_DELAY
(4 weeks)
07- The admin could restart the scheduled delay period
08- No need for full allowance in case transferOut()
get triggered by LiquidityPool.withdraw()
with a different receiver
in case _currency == USDT
and user
is blacklisted the transactions from Centrifuge chains will keep ever. and AXELAR will keep charging this protocol fee for every transaction
File: InvestmentManager.sol SafeTransferLib.safeTransferFrom(_currency, address(escrow), user, currencyPayout);
GitHub: [L277-L292] (https://github.com/code-423n4/2023-09-centrifuge/blob/main/src/InvestmentManager.sol#L277-L292)
UserEscrow.transferOut()
will revertIn case lpValues.maxWithdraw < _currency
is true and there are not enough funds in UserEscrow.sol
, the UserEscrow.transferOut()
will revert
File: InvestmentManager.sol if (lpValues.maxWithdraw < _currency) { lpValues.maxWithdraw = 0; } else { lpValues.maxWithdraw = lpValues.maxWithdraw - _currency; } if (lpValues.maxRedeem < trancheTokens) { lpValues.maxRedeem = 0; } else {
GitHub: L639-L646
from README.md
Liquidity Pool: A ERC-4626 compatible contract that enables investors to deposit and withdraw stablecoins to invest in tranches of pools.
However, the totalSupply()
will return the supply of all the Liquidity Pools that are with the same Tranch Token. It should return only the supply of the current Liquidity Pools
File: LiquidityPool.sol function totalSupply() public view returns (uint256) { return share.totalSupply(); }
GitHub: L283-L285
Because some tokens have a different decimal in a different chain
So, in case currencyAddress
has a different decimal in the Centrifuge Chain. the same thing with handleTransfer()
it just takes amount
and transfers it to the recipient
File: PoolManager.sol function transfer(address currencyAddress, bytes32 recipient, uint128 amount) public { uint128 currency = currencyAddressToId[currencyAddress]; require(currency != 0, "PoolManager/unknown-currency"); SafeTransferLib.safeTransferFrom(currencyAddress, msg.sender, address(escrow), amount); gateway.transfer(currency, msg.sender, recipient, amount); }
GitHub: L128-L134
In case the user passes a non-supported destinationChainId
in transferTrancheTokensToEVM()
. he will lose his funds
File: PoolManager.sol function transferTrancheTokensToEVM( uint64 poolId, bytes16 trancheId, uint64 destinationChainId, address destinationAddress, uint128 amount ) public { TrancheTokenLike trancheToken = TrancheTokenLike(getTrancheToken(poolId, trancheId)); require(address(trancheToken) != address(0), "PoolManager/unknown-token"); trancheToken.burn(msg.sender, amount); gateway.transferTrancheTokensToEVM( poolId, trancheId, msg.sender, destinationChainId, destinationAddress, amount ); }
GitHub: L149-L163
delay
to a value bigger than MAX_DELAY
(4 weeks)The deployer could set delay
to a value bigger than MAX_DELAY
(4 weeks)
File: Root.sol constructor(address _escrow, uint256 _delay) { escrow = _escrow; delay = _delay;
GitHub: L34-L36
admin could invoke scheduleRely()
function two times for the same target
. this will restart the scheduled delay period that was passed
File: Root.sol function scheduleRely(address target) external auth { schedule[target] = block.timestamp + delay; emit RelyScheduled(target, schedule[target]); }
GitHub: L65-L68
transferOut()
get triggered by LiquidityPool.withdraw()
with a different receiver
users don't need to set any allowance or full allowance to invoke LiquidityPool.withdraw()
with a different receiver
File: UserEscrow.sol receiver == destination || (ERC20Like(token).allowance(destination, receiver) == type(uint256).max), "UserEscrow/receiver-has-no-allowance"
GitHub: L43-L44
#0 - c4-pre-sort
2023-09-17T01:16:09Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2023-09-26T17:49:24Z
gzeon-c4 marked the issue as grade-b