Centrifuge - grearlake's results

The institutional ecosystem for on-chain credit.

General Information

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

Centrifuge

Findings Distribution

Researcher Performance

Rank: 59/84

Findings: 2

Award: $47.48

QA:
grade-b
Analysis:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

12.7917 USDC - $12.79

Labels

bug
grade-b
QA (Quality Assurance)
sufficient quality report
Q-21

External Links

1, There might not enough currency token for Centrifuge chain for success call handleExecutedCollectRedeem function When Centrifuge chain send message to pool, Gateway#handle will handle message:

} else if (Messages.isExecutedCollectRedeem(message)) { ( uint64 poolId, bytes16 trancheId, address investor, uint128 currency, uint128 currencyPayout, uint128 trancheTokensPayout ) = Messages.parseExecutedCollectRedeem(message); investmentManager.handleExecutedCollectRedeem( poolId, trancheId, investor, currency, currencyPayout, trancheTokensPayout );

In InvestmentManager#handleExecutedCollectRedeem, it will try to transfer currency from escrow to recipient that request in previous escrow:

userEscrow.transferIn( _currency, address(escrow), recipient, currencyPayout );

But problem is there is no guardian that escrow will have enough, since ward in contract is able to taken out by ward, and ward can be set to any address by root because escrow is rely on root, as it is described in Deployer.sol:

Escrow(address(escrow)).rely(address(root));

Moreover, there even is a chance that all currency that inside escrow can be stolen because of malicious ward in escrow that set by root.

To mitigration, escrow should be independent, not able to be fully controlled by any address for safe, and make sure that escrow have enough tokens to transfer for user when epoch end

2, Should not use draft proposal RestrictionManager.sol use ERC1404, which is draft proposal (https://github.com/ethereum/eips/issues/1404), which is not recognized. Instead, can use others like (https://eips.ethereum.org/EIPS/eip-1450)

3, Ward in InvestmentManager can mint unlimited tranche token ERC20#mint() is used to mint token to address:

function mint(address to, uint256 value) public virtual auth { require( to != address(0) && to != address(this), "ERC20/invalid-address" ); unchecked { balanceOf[to] = balanceOf[to] + value; // note: we don't need an overflow check here b/c balanceOf[to] <= totalSupply and there is an overflow check below } totalSupply = totalSupply + value; emit Transfer(address(0), to, value); }

If any person that have ward that set by root in Root#relyContract(), they can call this function to mint unlimited tranche token to any address. To mitigrate this, make sure address called to this function is only contract address that will call to this function

5, Tranche token name only can up to 128 character, tranche token symbol only can up to 32 character When update tranche token metadata, Messages#formatUpdateTrancheTokenMetadata is called:

function formatUpdateTrancheTokenMetadata( uint64 poolId, bytes16 trancheId, string memory tokenName, string memory tokenSymbol ) internal pure returns (bytes memory) { // TODO(nuno): Now, we encode `tokenName` as a 128-bytearray by first encoding `tokenName` // to bytes32 and then we encode three empty bytes32's, which sum up to a total of 128 bytes. // Add support to actually encode `tokenName` fully as a 128 bytes string. return abi.encodePacked( uint8(Call.UpdateTrancheTokenMetadata), poolId, trancheId, _stringToBytes128(tokenName), _stringToBytes32(tokenSymbol) ); }

_stringToBytes128(tokenName) mean only 128 characters is saved, if more than 128 character, the rest will be ignored, same with _stringToBytes32(tokenSymbol)

6, Strict checking condition in UserEscrow#transferOut In UserEscrow#transferOut(), there is a require check that explained by user:

/// @dev transferOut can only be initiated by the destination address or an authorized admin. /// The check is just an additional protection to secure destination funds in case of compromized auth. /// Since userEscrow is not able to decrease the allowance for the receiver, /// a transfer is only possible in case receiver has received the full allowance from destination address.

But this checking condition could also make user hard to directly transfer redeem to other user. To directly transfer, they have approve from outside of contract, which increase risk of losing token by approve() full balance to other address

#0 - c4-pre-sort

2023-09-17T01:26:35Z

raymondfam marked the issue as sufficient quality report

#1 - c4-judge

2023-09-26T17:45:28Z

gzeon-c4 marked the issue as grade-b

Awards

34.6879 USDC - $34.69

Labels

analysis-advanced
grade-b
sufficient quality report
edited-by-warden
A-10

External Links

[01] Summary of Codebase

1.1 Description

Centrifuge is a transparent market project on Ethereum, where allows borrowers and lenders to transact without unnecessary intermediaries. In liquidity pool, user can invest stablecoins and receive back shares. These shares can be be used to redeem to get stablecoins back to gain profit with diffirent rates at risks based on tranch

1.2 Flow

1.2.1 Add currency

  • Centrifuge chain add currency that could be used to create pool through Gateway#handle() -> PoolManager#addCurrency()
  • Check that currency was added previously or not
  • Check that currency token have decimals <= 18 or not, because contract only support decimals that euqal or smaller
  • approve UserEscrow and InvestmentManager contract to use token in PoolManager contract

1.2.2 Add pool

  • Centrifuge chain add pool through Gateway#handle() -> PoolManager#addPool()
  • Check that poolId haven't used before

1.2.3 Add currency to pool

  • Centrifuge chain allow pool to support currency for investing through Gateway#handle() -> PoolManager#allowPoolCurrency(). One pool can have multiple currencies that being supported
  • Check that pool and currency is valid to add
  • currency is marked as allowed with that pool

1.2.4 Add tranche to pool

  • Centrifuge chain add tranche to pool through Gateway#handle() -> PoolManager#addTranche(). One pool can have multiple tranches
  • Tranche is created with input data and poolId that it linked with and creation time

1.2.5 Deploy tranche

  • Tranche is deployed when anyone call PoolManager#deployTranche() with correct poolId and trancheId
  • a ERC20 tranche token address is created

1.2.6 Deploy liquidity pool

  • Liquidity pool is deployed when anyone call PoolManager#deployLiquidityPool() with correct poolId, trancheId linked with poolId and currency that accepted to investment in that pool

1.2.7 Investing currency to pool

  • To be able to investing, user first need to be approved to be invested by Centrifuge chain by being added to members list in RestrictionManager contract.
  • User call LiquidityPool#requestDeposit() with amount of token they want to invest. Only owner of assets can call this function due to withApproval modifier. Currency then will be transfered to escrow and an announcement about request invest will be sent to Centrifuge chain

1.2.7.1 Decrease investment

  • If user want to decrease number of currency that they want to invest, they can call LiquidityPool#decreaseDepositRequest. Only owner of assets can call this function due to withApproval modifier
  • A message about decrease invest will be sent to Centrifuge chain, and deposited assets will be refunded to user
  • To be able to decrease investment, user must call this function before the end of epoch

1.2.7.2 Cancel investment

  • If user want to cancel investment , they can call LiquidityPool#requestDeposit with currency amount = 0. Only owner of assets can call this function due to withApproval modifier
  • A message about cancel invest will be sent to Centrifuge chain, and deposited assets will be refunded to user
  • To be able to cancel investment, user must call this function before the end of epoch

1.2.8 Collect share

  • After epoch end, user will receive number of shares based on currency amount and rate that returned by Centrifuge chain
  • In case Centrifuge chain, by somehow, failed to trigger ExecutedCollectInvest messages to transfer payout to user after epoch end, user will manually call LiquidityPool#collectInvest to redeem shares
  • User then call LiquidityPool#deposit() or LiquidityPool#mint() to collect share for deposited assets

1.2.9 Request share redemption

  • User call LiquidityPool#requestRedeem() with amount of share they want to redeem. Only owner of shares can call this function due to withApproval modifier. Tranche token then will be transfered to escrow and an announcement about request redeem will be sent to Centrifuge chain

1.2.9.1 Decrease redeem

  • If user want to decrease number of share that they want to redeem, they can call LiquidityPool#decreaseRedeemRequest. Only owner of assets can call this function due to withApproval modifier
  • A message about decrease invest will be sent to Centrifuge chain, and share will be refunded to user
  • To be able to decrease redeem, user must call this function before the end of epoch

1.2.9.2 Cancel redeem

  • If user want to cancel redeem , they can call LiquidityPool#requestRedeem with share amount = 0. Only owner of assets can call this function due to withApproval modifier
  • A message about cancel redeem will be sent to Centrifuge chain, and requested share will be refunded to user
  • To be able to cancel redeem, user must call this function before the end of epoch

1.2.10 Redeem shares

  • After epoch end, user will receive number of shares based on share amount and rate that returned by Centrifuge chain
  • In case Centrifuge chain, by somehow, failed to trigger ExecutedCollectRedeem messages to transfer payout to user after epoch end, user will manually call LiquidityPool#collectDeposit to redeem shares
  • User then call LiquidityPool#withdraw() or LiquidityPool#redeem() to collect assets for deposited share

[02] Centralization risks

2.1 Wards can taken out token value in Escrow anytime

Since Wards can taken any token value out in Escrow anytime, functions that need to transfer token from escrow can be failed, which lead to break contract

2.2 Wards can mint unlimited tranche token to any address

Wards can call token/ERC20#mint() to mint unlimited tranche token to any address

[03] Architecture Improvements

3.1 Contract should support more types of token in future

Currently, contract does not support types of token like fee-on-transfer (USDT, USDC, ...), rebasing token, ... Contract should have improvement to support these types of tokens to have more choice for user to invest

[03] Time spent:

Day 1: Have a overview about in-scope contracts Day 2: Understand workflow of contracts Day 3 + 4: Deep dive in 3 main contracts: LiquidityPool, PoolManager, InvestmentManager Day 5: Checking the rest of contracts, Finishing Analysis

Time spent:

45 hours

#0 - c4-pre-sort

2023-09-17T02:07:18Z

raymondfam marked the issue as sufficient quality report

#1 - c4-judge

2023-09-26T17:16:38Z

gzeon-c4 marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter