Platform: Code4rena
Start Date: 08/09/2023
End Date: 14/09/2023
Period: 6 days
Status: Completed
Pot Size: $70,000 USDC
Participants: 84
Reporter: liveactionllama
Judge: gzeon
Id: 285
League: ETH
ciphermarco | 1/84 | $11,760.71 | 2 | 0 | 0 | 1 | 1 | 0 | 0 | Grade A |
alexfilippov314 | 2/84 | $11,324.58 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 0 |
0x3b | 3/84 | $3,092.33 | 2 | 0 | 0 | 1 | 0 | 0 | 0 | Grade B |
jaraxxus | 4/84 | $2,386.72 | 2 | 0 | 0 | 1 | 0 | 0 | 0 | Grade B |
twicek | 5/84 | $2,352.03 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
J4X | 6/84 | $1,663.91 | 2 | 0 | 0 | 2 | 0 | 0 | 0 | 0 |
0xStalin | 7/84 | $1,486.01 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
Aymen0909 | 8/84 | $1,275.95 | 2 | 0 | 0 | 2 | 0 | 0 | 0 | 0 |
bin2chen | 9/84 | $1,247.37 | 2 | 0 | 0 | 2 | 0 | 0 | 0 | 0 |
imtybik | 10/84 | $1,155.88 | 2 | 0 | 0 | 1 | 0 | Grade B | 0 | 0 |
Auditor per page
The institutional ecosystem for onchain credit.
View our website » Read the documentation » Try the app »
Automated findings output for the audit can be found here within 24 hours of audit opening.
Note for C4 wardens: Anything included in the automated findings output is considered a publicly known issue and is ineligible for awards.
Founded in 2017, Centrifuge is the institutional platform for credit onchain. Centrifuge was the first protocol where MakerDAO minted DAI against a real-world asset, the first onchain securitization, and Centrifuge launched the RWA Market with Aave. Centrifuge’s multi-chain strategy allows investors to access native RWA yields on the network of their choice.
Centrifuge works based on a hub-and-spoke model. RWA pools are managed by borrowers on Centrifuge Chain, an application-specific blockchain built purposely for managing real world assets. Liquidity Pools are deployed on any other L1 or L2 where there is demand for RWA, and each Liquidity Pool deployment communicates directly with Centrifuge Chain using messaging layers.
Investors can invest in multiple tranches for each RWA pool. Each of these tranches is a separate deployment of a Liquidity Pool and a Tranche Token.
RestrictionManager
that manages transfer restrictions. Prices for tranche tokens are computed on Centrifuge.The deployment of these tranches and the management of investments is controlled by the underlying InvestmentManager, TokenManager, Gateway, and Routers.
Escrow
and UserEscrow
, and more.Messages
and handles routing to/from Centrifuge.[!NOTE]
The coding style of theliquidity-pools
code base is heavily inspired by MakerDAO's coding style. Composition over inheritance, no upgradeable proxies but rather using contract migrations, and as few dependencies as possible. Authentication uses theward
pattern, in which addresses can berelied
ordenied
to get access. Key parameter updates of contracts are executed throughfile
methods.
Using the Centrifuge protocol, issuers can launch pools of real-world assets. Each pool can have 1 or more tranches that investors can buy. The purpose of these tranches is to give investors different kinds of risk exposure and yield on the same asset class. Each pool has 1 pool currency. The decimals of this pool currency define the decimals of the tranche tokens that are issued per tranche. Both deposit (also known as investments) and redemptions in tranches of Centrifuge pool happen asynchronously, through an epoch mechanism. Prices for tranches are calculated on Centrifuge Chain based on the Net Asset Value of the real world assets in the pool. More information on this can be found in the documentation.
Because of the epoch mechanism, as well as the fact that Liquidity Pools communicate with Centrifuge Chain through messaging layers, deposits and redemptions cannot be executed automatically, and rather are executed asynchronously. A key goal if Liquidity Pools is to increase composability of Centrifuge assets, by leveraging ERC4626. However, ERC4626 assumes atomic deposits and withdrawals, thus the Liquidity Pool contracts are extended with methods for requesting deposits & redemptions. There is also support for permits when requesting deposits/redemptions. More details on this in User flows
below.
The communication between Liquidity Pools and Centrifuge Chain uses external general message passing protocols. Messages are encoded using a compacted ABI encoding scheme, as implemented in src/gateway/Messages.sol
.
While there is 1 native pool currency, Liquidity Pools (acronym: LP) are built to support deposits in multiple currencies. Each Liquidity Pool is linked to 1 currency (asset) and 1 tranche token (share), but Liquidity Pools can be deployed linked to the same tranche token (share). The Liquidity Pool contract therefore passes through the ERC20 methods to the underlying share implementation. To support this, the ERC20 of the tranche token uses ERC2771 context, and the tranche token contract ensures that all Liquidity Pools are considered trusted forwarders for this.
The other challenge with supporting multiple currencies is that the decimals between the tranche token (which is based on the native pool currency decimals) and the investment currency (or asset) can differ. Therefore, all price calculations and conversions between shares and assets (or tranche tokens and currencies) need to account for these differences. This is accomplished by normalizing all balances and prices to 18 decimal fixed point integers, doing the calculations using these normalized values, and then unnormalizing back to the intended decimals. Currencies with more than 18 decimals are not supported and blocked in the contracts.
The Root
contract is a ward
on all other contracts. The PauseAdmin
can instantaneously pause the protocol. The DelayedAdmin
can make itself ward
on any contract through Root.relyContract
, but this needs to go through the timelock specified in Root.delay
. The Root.delay
will initially be set to 48 hours.
By default, all actions in Liquidity Pools should occur through messages coming in from a router, that was transported from Centrifuge Chain. This includes any upgrades, which can be triggered through a ScheduleUpgrade
message. This also calls Root.relyContract
, and also is protected by the timelock.
Some possible emergency scenarios are described below:
Someone gains control over a router and triggers a malicious ScheduleUpgrade
message
pause()
to block further incoming messages from the router. The delayed admin calls cancelRely()
to cancel the scheduled rely from the router exploiter.scheduleRely()
to remove the router from the gateway contract 48h later.Someone controls 1 pause admin and triggers a malicious pause()
ward
on the pause admin and can trigger PauseAdmin.removePauser
.root.unpause()
.Someone gains control over a router and triggers a malicious Transfer
message
This scenario is not fully protected, as funds currently locked in the Escrow
contract can be transferred out. However, there are two important factors that reduce the capital at stake.
Liquidity management
, liquidity is constantly transferred between different blockchains. Since funds are actually withdrawn from the pool by a borrower, this leads to most funds not being stuck in the Escrow. In practice, only funds currently in process of being invested are in the Escrow
contract.requestRedeem()
ed, but not yet withdrawn, these are held in the UserEscrow
contract. The key design principle of this contract is that once tokens are transferred in, they are locked to a specific destination, and can only be transferred out to this destination. Even a ward
on the UserEscrow
contract cannot transfer tokens to any other destination.The full relationships of wards
can be seen below.
When investors deposit in a currency that is not equivalent to the native pool currency, this needs to be swapped in order to execute the investment. And vice versa for redemptions. These swaps occur on Centrifuge Chain. These swaps also guarantee that sufficient liquidity is in the escrow contract to fulfill any orders. Note that locking, for example, USDC in Liquidity Pools on Ethereum, leads to Wrapped Ethereum LP on USDC, which will be non-fungible with USDC locked in Liquidity Pools on Arbitrum, which leads to Wrapped Arbitrum LP on USDC.
An example flow for how this works is visualized below:
[!WARNING]
src/gateway/Messages.sol
is a large file but contains only repetitive encoding/decoding functions. All files excludingFactory.sol
in thesrc/util
directory are imported libraries.
Contract | SLOC | Purpose | Libraries used |
---|---|---|---|
src/LiquidityPool.sol | 225 | A ERC-4626 compatible contract that enables investors to deposit and withdraw stablecoins to invest in tranches of pools | SafeMath |
src/InvestmentManager.sol | 527 | Main contract LiquidityPools interact with for both incoming and outgoing investment transactions. | SafeMath, SafeTransfer |
src/PoolManager.sol | 261 | Manages which pools & tranches exist | SafeTransfer |
src/Escrow.sol | 17 | Token holding contract | SafeTransfer |
src/UserEscrow.sol | 30 | Token holding contract with locked destinations | SafeTransfer |
src/Root.sol | 66 | Core contract that is a ward on all other deployed contracts | |
src/admins/PauseAdmin.sol | 30 | Simple pausing contract | |
src/admins/DelayedAdmin.sol | 24 | Admin contract that can trigger the timelock on Root | |
src/token/Tranche.sol | 76 | Tranche token contract that inherits from ERC20 | |
src/token/ERC20.sol | 183 | ERC20 implementation with mint/burn & permit functionality | |
src/token/RestrictionManager.sol | 49 | ERC1404 based contract that checks transfer restrictions | |
src/gateway/Gateway.sol | 328 | Incoming & outgoing message parsing | |
src/gateway/Messages.sol | 619 | Message encoding & decoding | |
src/gateway/routers/axelar/Router.sol | 88 | Routing contract that integrates with Axelar | |
src/util/Auth.sol | 18 | Simple authentication contract | |
src/util/Context.sol | 6 | ERC2771 base contract | OZ Context |
src/util/Factory.sol | 93 | Factory contract for deploying LPs and tranche tokens | |
src/util/SafeTransferLib.sol | 17 | Safe transfer lib | SafeTransfer |
src/util/BytesLib.sol
src/util/MathLib.sol
DelayedAdmin
can trigger become ward on any contract and abuse the system, but should not be able to get additional wards before root.delay
(the timelock should be enforced).uint128
for calculations and all messages use uint128
types for values, only uint128
values are supported (anything larger should revert).scripts
) are out of scope.- If you have a public code repo, please share it here: N/A - How many contracts are in scope?: 18 - Total SLoC for these contracts?: 2657 - How many external imports are there?: 0 - How many separate interfaces and struct definitions are there for the contracts within scope?: 2 interfaces, 3 structs - Does most of your code generally use composition or inheritance?: Composition - How many external calls?: Only ERC20 transfers - What is the overall line coverage percentage provided by your tests?: 80% - Is this an upgrade of an existing system?: No - Check all that apply (e.g. timelock, NFT, AMM, ERC20, rollups, etc.): Timelock, DeFi, Multi-chain - Is there a need to understand a separate part of the codebase / get context in order to audit this part of the protocol?: No - Please describe required context: Liquidity Pools interact with Centrifuge Chain over general message passing protocols, but auditors can assume Centrifuge Chain is a blackbox - Does it use an oracle?: No - Describe any novel or unique curve logic or mathematical models your code uses: None - Is this either a fork of or an alternate implementation of another project?: No - Does it use a side-chain?: No - Describe any specific areas you would like addressed: Loss of funds, stuck funds, bypass of timelock, price manipulation
Make sure Foundry is installed.
git clone https://github.com/code-423n4/2023-09-centrifuge.git cd 2023-09-centrifuge forge test