RIZ markets = # Competitive landscape I examined features, codebase and whitepapers of 60-70 lending protocols in order to identify those which support isolated markets. Out of those, following protocols support certain level of isolation of their markets: ## Aave The way Aave V3 implements isolation mode is not really compatible with what we want to achieve. It works like this: - Some assets are marked as "isolated" - Isolated assets can be borrowed without any special restrictions when other non-isolated assets are used as a collateral - User who supply isolated and non-isolated assets can choose whether to use one or more non-isolated assets as a collateral or alternatively, 1 and only 1 isolated asset - If isolated asset is used as a collateral, only stablecoins can be borrowed - There is a cap on the collective amount of stablecoins that can be borrowed against every isolated asset (debt ceiling) Pros of this approach: - Stablecoin liquidity is not fragmented across isolated and non-isolated markets Cons: - Lenders of stablecoins on Aave V3 are exposed to risk of isolated assets (limited with the debt ceiling) without being able to opt-in or opt-out for this risk - Debt ceiling limits general utility of isolated assets - Isolated assets can be borrowed against other assets without any special restrictions, putting all other assets at risk in case their price significantly goes up ## Morpho Blue Morpho Blue allows permissionless creation of isolated markets within a single non-uprgradeable contract. Market consists of collateral asset, loan asset, LLTV, IRM and Oracle. A user need to specify addresses of collateral asset, loan asset and contract that will return prices of these assets, together with IR and LLTV parameters. They market Morpho Blue as a layer on which other protocols can build their lending protocols. Currently, there are two protocols built on top of Moprho Blue: MetaMorpho with $12M TVL across 3 markets (wstETH/WETH, wstETH/USDC and wblB01/USDC) and Contango (margin trading protocol). It might be worth to check implementation of Morpho Blue on their [Github](https://github.com/morpho-org/morpho-blue). ## Ajna Protocol Ajna is one of many protocols allowing permissionless creation of pools with 2 assets: collateral asset and quote asset. Lenders supply quote assets and borrowers supply collateral asset and borrow quote asset. Lenders specify the range on which they would be willing to lend and then, all supplied quote asset is segragated into multiple backets based on their price range. The requirement for governance is minimized this way. Even though, the protocol seems interesting and promising (it can be accessed through Summer.fi), it has very few applicable points to RIZ. ## Ethereum Vault Connector (by Euler) Euer is developing Ethereum Vault Connector (EVC) as an ifrastructure layer that can help other protocols spin up their lending protocols. Every asset market is deployed as ERC-4626 vault and users can lend by supplying to these vaults. Protocol can determine which vaults can be used a collateral for borrowing from other vaults. Users can also opt-in which vaults they would use as a collateral and then, borrow from other vaults. Every vault has it's own price oracle and interest rate model and tracks borrow balances of the user. There is also EVC contract with Aave's LendingPool or Compound's Comptroller functions, which basically defines cross-collaterization between different vaults. In case of wide adoption of EVC, greater modularity and composability across multiple lending protocols would be achieved. It could eventually allow that assets from one protocol are used as a collateral on the other. However, EVC is still under development, it is not audited and it is far from being production-ready. Also, until wider adoption is achieved, EVC would basically represent alternative implementation of the lending protocol to widely adopted Aave and Compound codebases. That being said, it has very little relevance for RIZ implementation at this point. ## Market.xyz Market Protocol (market.xyz) is sunsetted protocol that was based on the concept of isolated markets only. They had pools consisting of multiple assets within the pool, ranging from 3 to 10+ assets. Many of these assets were LP tokens where pool would faciliate providing leveraged liquidity to AMMs. Every pool was based on the implementation of Compound V2 and on top of that, there was `PoolDirectory` contract which was used to deploy new pools, manage pools, add and manage assets within the pools. [Annoucement on sunsetting](https://marketxyz.medium.com/the-sunsetting-of-market-xyz-78b34d37ce07) is an interesting read describing challenges in management of isolated markets. It lays out some challenges that will Radiant face as well: *The prevailing infrastructure heavily rests upon governance, multisigs, and oracles, embracing an Optimistic viewpoint that these elements will operate fairly and effectively, devoid of any exploitative or malicious interferences.* *Despite significant improvements, oracle infrastructure for price feeds still faces challenges with oracle exploits, and the involvement of active governance not only generates operational burden on the wider community but also inadvertently accords disproportionate power to the custodians of the multisig. Consequently, governance necessitates the active management of pool parameters, creates the need for continuous involvement from pool administrators to oversee and regulate the pools efficiently.* The implementation of `PoolDirectory` and `Lens` contracts could've been useful, but it is not open-source. Their [docs](https://docs.market.xyz/contracts/pooldirectory) do provide some high-level overview of these contracts. ## Inverse finance Inverse finance has FiRM, fixed IR lending market where lenders can mint DOLA, Inverse's CDP. Inverse took some steps towards ensuring isolation between different assets on FiRM, but generally seems incompatible with potential RIZ implementation. ## Timeswap Timeswap allows permissionless creation of pools consisting of 2 assets where assets are lended and borrowed under fixed interest rate till maturity. When maturity is achieved, depending on the prices, borrower can repay their loan or have their collateral transferred to the lenders. Lenders will receive asset that they supplied or seized collateral. These pools resemble AMM pools and don't require oracle or liquidation. Even though very interesting concept, it is completely unapplicable to Radiant use case. ## Silo Finance Silo Finance is another lending protocol that markets creation of permissionless and risk-isolated markets. For every asset they list, they create an instance of "Silo", effectively a lending pool consisting of this asset, ETH and stablecoin (USDC on Arbitrum, XAI on Ethereum). Assets can be cross-collateralized only within a silo and not between silos. Silo has $94M TVL on Arbitrum across 21 silos. USDC TVL goes from 1k to 2M, showing how fragmented liquidity exists across stablecoin markets. Interesting idea from their docs: *You deposit your $UNI in UNI-ETH Silo (market). Someone else deposits $AMP in AMP-ETH Silo and borrows your $UNI. Behind the scene, SILO has moved ETH collateral from AMP-ETH to UNI-ETH. Your $UNI deposit is protected with ETH now, not $AMP. If $AMP goes bad, ETH lenders in the AMP-ETH are impacted.* Interesting approach that would give users feeling that they can deposit asset **A** on one isolated market **a** and use it as a collateral to borrow asset **B** on another isolated market **b**, but in the background, user would borrow USDC against asset **A** on market **a** and deposit borrowed USDC on market **b** to borrow **B** against it. This would require to have the same stablecoin across all isolated markets. Definitely worth checking Silo finance implementation on their [Github](https://github.com/silo-finance/silo-core-v1). ## Shoebill Finance Shoebill is just a Compound V2 fork that deloys new set of smart contracts for every isolated market. ## UniLend Even though Unilend V2 conceptually offers isolated markets, their implementation significantly deviates from Radiant's implementation. Their product, which is still testnet-only, revolves around concept of Dual Asset Lending Pool, which is effectively an isolated market consisting of two assets. Positions in the pool are tracked by issuing NFTs which describes collateral and debt position of the NFT owner. Innovative part is that NFT is transferable, making positions tradeable. Here is their [GitHub](https://github.com/UniLend/unilendv2/tree/main), but I don't think further analysis is required. ## Venus Protocol Venus Protocol implements isolated markets in very similar way how Radiant plans to. They have Core pool representing their main market and they have "Isolated pools", where each pool is theme-based. They currently have 5 pools: DeFi, Liquid staking BNB, GameFi, Stablecoins and Tron and each of the pools list between 4 and 8 assets. Their isolated pools are modestly successful - while TVL of the Core pool is $1.6B, the total TVL of Isolated pools is $5.6M, which could be a good benchmark for Radiant how successful RIZ could be, if implemented this way. Venus Protocol is a fork of Compound V2, but they built couple of innovations around isolated pools. They introduced `PoolRegistry.sol` - a central contract for adding and modifying pools and adding and modifying markets within the pools. Here is their [Github](https://github.com/VenusProtocol/isolated-pools/tree/develop). ## Conclusion Alongside **Venus protocol** that has the greatest level of similarity to what RIZ aim to achieve, **Morpho Blue**, **Silo Finance** and **Market.xyz** are protocols worth further exploring for a reference. # Challenges ## 1. Fragmentation of liquidity RIZ markets will face problem of fragmented liquidity which will be most amplified when it comes to stablecoins. In the bull market or when bull market is anticipated, lending protocols generally face shortage of stablecoins. E.g. on the Radiant's markets on Arbitrum, all stablecoin markets are utilized more than 85%. RIZ markets would struggle even more to attract stablecoins as not only that lenders of stablecoins risk missing out higher returns from general crypto market upward trends, but they also face risks of insolvencies from other volatile assets within isolated markets. High interest rates can attract some interest from stablecoin lenders, but there is a general question of economic sustainability of high interest rates. E.g. if isolated market consists of RWAs with 5% - 10% of real yield and stablecoins, then interest rates on stablecoins need to be lower than this real yield, so that leveraging would make sense. Two ideas were mentioned two tackle the fragmentation and both of them should be discarded: - One large stablecoin pool for all RIZ markets: if lended stablecoins can be accessed from all RIZ markets, it means there is no isolation between any RIZ markets, but only the isolation between main markets and RIZ markets. Having in mind that RIZ markets will include assets with completely different risk profile (e.g. RWAs, LPs, memecoins), stablecoins and all other assets are exposed to a risk of every single asset. E.g. if User A lends RWA and borrows USDC and RWA's value goes to $0 in a second (due to some off-chain circumstance), there is insolvency on USDC market. But this insolvency is then propagated to memecoin market, where User B who lended USDC and borrowed memecoin will not repay their debt as their collateral (USDC) can't be withdrawn. That propagates insolvency to memecoin market and all other markets. - Using collateral from main market to borrow on RIZ markets: this solution again breaks isolation of RIZ market and propagates possible insolvencies to the main market. If user A lends collateral on main market and uses it to borrow on the main market and RIZ market, and if borrowed asset on RIZ market spikes, insolvency is created on the main market as well. Even though there is no a clear solution to the fragmentation of liquidity, it can be balanced by creating less isolated markets containing more assets. All assets in a single isolated markets will share risks, but also stablecoin (and all other) liquidity. So, a good middle ground between isolating risks and unifying liquidity should be looked at. What could help with the unification of liquidity is building a separate product on top of the lending protocol which would be a **stablecoin-based investment vault**. It would allow users to deposit stablecoin and to opt-in which RIZ markets they are happy to lend to. Vault would then lend these stablecoins to various RIZ markets, with occasional rebalancing, aiming to achieve the highest yield. Cool side effect of this product would be optimization of interest rates across RIZ markets. Of course, this would be quite peripheral product and can be developed as a standalone product, even by giving grant to some external team. ## 2. Reputational risk Even though RIZ markets are implemented as isolated markets for a single reason - assets listed there bring significantly higher risk than assets on the main market, Radiant Capital is still responsible (and liable) for any incident that could happen on the RIZ markets. Even though other markets are protected against this risk, Radiant's reputation is not. In case of the incident, the space will be filled with the news about the incident and it could damage Radiant's reputation. The example is that sunsetted Market.xyz experienced exploit on their isolated market: [Decoding $220K Read-only Reentrancy Exploit](https://quillaudits.medium.com/decoding-220k-read-only-reentrancy-exploit-quillaudits-30871d728ad5) and now, the protocol is sunsetted. This risk can be partially mitigated by developing internal risk assesment framework for listing new assets, careful governance process and careful PR/marketing activities (e.g. continiously emphasizing the risks). ## 3. Management of large number of markets One of the biggest concerns is how to effectively manage large number of assets and protocol parameters, such as collateral factors, liquidation bonuses, interest rate parameters, supply and borrow caps. There is a huge operational burden of monitoring utilization of markets, DEX liquidity, price movements, liquidations and other risk- and performance-related factors, but there is also a huge burden on maintaining codebase and contract state of this large number of contracts. Couple of steps need to be taken to make this as easy as possible: - All RIZ markets should use the same implementation contracts. All RIZ markets should have the same LendingPool, AddressProvider, rToken, VariableDebtToken, etc. implementation. - One idea could be that implementation is also upgradeable. So, e.g. rARB -> rTokenProxy -> rTokenImplementation. If we would like to upgrade all rTokens, we wouldn't need to upgrade all contracts (e.g. 100 assets), but only rTokenProxy and all rTokens would be upgraded. - Development of centralized `RizRegistry.sol` contract and doing all management through it. This contracts would maintain the list of all RIZ markets and would support functions such as: - Upgrade of implementations. We pass implementation contract of e.g. LendingPool and all lending pools get upgraded - Adding (and retirement of) new RIZ. We would pass init params and new lending pool is deployed and registered within RizRegistry - Adding (and retirement of) new market to specific RIZ. We woudl pass IR params, oracle source, underlying asset address, collateral factor and other params and new rToken and variableDebtToken would be deployed and configured. - Changing lending pool and asset params such as collateral factors, liquidation bonuses, interest rate parameters, supply and borrow caps. - Eventually, we could develop a dashboard with nice overview of all RIZs (pools), markets and params. We could develop even `Lens` contract to faciliate easy querying. Finally, (just an idea), replicating this dashboard into private, guarded mode, could allow actions on chaning protocol params that will result in creating multisig proposals for adjustment of params.   ## 4. Revenue management Currently dLP zappers are receiving revenue in 10 different tokens because they collect revenue from every listed asset. If we extend this logic to RIZ markets which have potential to onboard 100+ assets, meaning 100+ tokens distributed to dLP zappers which is completely unsustainable. My proposal would be that every RIZ pool (consisting of multiple assets) has only one revenue asset. So, we will reduce number of assets in which revenue is distributed to number of different pools. Then, when we collect the revenue in various RIZ assets, we swap it into `revenueAsset` and distribute it further. It should be governance-based decision which asset to select as `revenueAsset`, but my proposal would be to follow these guidelines: - it should be one of the assets listed in that pool - it should be the most utilized asset from that pool. E.g. if RIZ pool is ETH LST pool, it would consist of stETH, rETH, cbETH, ETH, then it's safe to assume the most utilized asset would be ETH. If RIZ pool is RWA pool consisting of USDC and tokenized US bond asset, then it's safe to assume the most utilized asset will be USDC. Most utilized asset will generate the biggest revenue (e.g. my assumption that on LST RIZ pool, 99% of revenue will be in ETH), so it means that the least swapping will be needed by selecting the most utilized asset. - it should be stablecoin (preferably USDC/USDT across all RIZ pools) or some asset used as underlying asset of the vault that is also in the pool, such as ETH, BNB, MATIC for LSTs, DAI for gDAI, etc - in the best case, it will always be one of the assets already listed on the main/core market. By that way, number of reward tokens would not increase with addition of RIZ pools aTokens would still be minted to a treasury from all RIZ pools. Treasury (`MiddleFeeDistribution` on Arbitrum/BNB and `MultiFeeDistribution` on Mainnet) would be in charge to take its holding in various aTokens and swap it into `revenueAsset`. This swapping can take place when the reward is forwarded to MFD or we can have a bot that would run and regularly perform swaps. Currently, revenue (75% of paid interest rate) is divided as 85% to dLP zappers and 15% for operational expenses. This should be revised for RIZ pools. The fact we are listing some assets on isolated pools means that we anticipate that at some point some loans can default and create insolvency. As this is anticipated, part of the revenue should be set aside for covering bad debt and we can plan this by setting up risk fund and automatically redirecting portion of revenue to this risk fund. Therefore, some formula like 70% revenue going to dLP zappers, 10% for operational expenses and 20% to risk fund should be established. Exact percentages should be subject to governance decision. ## 5. Oracle providers As the number of listed assets grow, we will face the issue that Chainlink will not have price feeds for these assets. There are couple of way to mitigate this issue. First, Radiant should not depend on a single oracle provider in the codebase, meaning it should not have hard dependency on Chainlink. Instead, we should develop oracle aggregator that will serve prices for all Radiant's assets, regardless whether they are listed on the main market or RIZ markets. This aggregator will support multiple oracle providers underneath, and will allow us to get the price of asset A from Chainlink, asset B from RedStone, asset C from Pyth etc. Lending pool shouldn't be aware from which source the price is fetched as it calls aggregator only. It will give us more flexibility in listing assets, as Chainlink tends to be very conservative when it comes to supporting feeds. Secondly, Radiant should create strategic partnership with an oracle provider such as Pyth, Redstone, API3 or some similar oracle provider with smaller market share than Chainlink. Partnership would assume that they will support feed of an asset per our request. When we decide to list an asset, we reach out to them and they make its feed available. Thirdly, we need to decide how to get the oracle price to the chain. Usually, the fact that oracle provider supports specific asset doesn't mean that its price is available on chain per se. This price is available on oracle's network (e.g. Pythnet) and somebody needs to push it to e.g. Arbitrum. First option is push model in which Radiant (or oracle provider on our behalf) would push the prices on a regular basis (based on the heartbeat and price deviation parameters). This option can become costly, especially on chains where gas is expensive such us Ethereum Mainnet. Second option is pull model where users retrieve asset price from API (e.g. on our frontend) which is signed by the oracle provider and then, submit price update data toegther with price-sensitive operation they want to do (borrowing, redeeming, liquidating). Lending pool would accept price update data and pass it to oracle's smart contract. Oracle's smart contract would verify that the price came from their API and was not manipulated (by checking signature) and would update the price. After that, lending pool would have fresh price of the asset and could perform price-sensitive operation. This option transfers gas costs of updating the price to the users, minimizes overall gas costs as the price gets to the chain only when it's needed, but also makes integrations with our contracts harder, as external integrators become also dependant to the price oracle API. ## 6. RDNT Emissions Currently users who have any position on the Radiant core pool and they have zapped dLP are eligible for RDNT emissions as long as the value of zapped LP tokens is at least 5% of total value of their supplied assets. RDNT rewards are tracked in `ChefIncentivesController` which has helper contract `EligibilityDataProvider` which tracks eligibility statuses of the users. Neither of these two contracts scale well with number of assets because they have O(n) complexity with regards to number of assets. Determining eligibility in `EligibilityDataProvider` requires iterating through all assets lended by user and calculating rewards in `ChefIncentivesController` requires iterating through all assets that have emissions (times 2 because AToken and VariableDebtToken are checked for every asser). Adding 100+ assets across multiple RIZ markets makes it extremely unsustainable. My proposal would be to make eligibility requirement per RIZ pool. It means that user becomes eligible for emissions on a RIZ pool only if they have a position on that specific RIZ pool and their zapped LP tokens are worth at least 5% of their supplied assets on that RIZ pool. While zapping stays centralized and independent of specific RIZ pool, emissions are dependant of the RIZ pool. That's fair, but also logical consequence that user can't get any emissions on RIZ pool A if they haven't supplied anything on RIZ pool A. However, there is also side effect of lowering eligibility criteria when eligibility is a property of RIZ pool. Currently, if user supplied $1000-worth of assets on the core pool, eligibility threshold is $50-worth of zapped LP tokens. However, if user supplies $500 to core pool, $300 to RIZ pool A and $200 to RIZ pool B, then user becomes eligible on all 3 pools with $25-worth of LP tokens (because threshold on core pool became $25, $15 on RIZ pool A and $10 on RIZ pool B). It doesn't seem to be a problem (especially as RDNT rewards need to be generally lowered to accomodate growing number of assets), but it is important to keep in mind. Technically, this would mean deployment of `ChefIncentivesController` and `EligibilityDataProvider` contracts per RIZ pool. `EligibilityDataProvider` would interact with only one LendingPool (of specific RIZ market) to get user's `requiredUsdValue` and with shared MFD contract to get `lockedUsdValue`. Update of eligibility and rewards happens by calling CIC functions from 3 places and each of them would need to be slightly adjusted: - function `handleActionAfter` is called from `AToken` and `VariableDebtToken` to update eligibility and rewards based on change in user's position on the LendingPool. `AToken` and `VariableDebtToken` would still have only one (LendingPool-specific) CIC contract which they would call so no change would be required there - function `afterLockUpdate` is called from MFD contract to update eligibility based on change in user's amount of zapped LP tokens. This is the biggest change because MFD contract would not only store one CIC contract and call its function `afterLockUpdate`, but would need to store array of CIC contracts (one for every RIZ pool + core pool) and call `afterLockUpdate` on each of them. However, as the length of array is dependent on number of RIZ pools and not number of listed assets, impact on gas consumption is somewhat limited. - function `claimBounty` is called by `BountyManager` to update eligibility based on change of prices of supplied assets and zapped LP tokens. Change that would be required here is that bounty hunter specifies CIC contract (or lending pool) where change should happen. By this, no (significant) extra gas cost is added, though it makes off-chain process of identifying bounties slightly more complex. Furthermore, CIC contracts manages the list of rewarded rTokens. By having CIC contract per RIZ pool means this list will be much shorter, hence any interation through them will be significantly lower. To conclude, having `ChefIncentivesController` and `EligibilityDataProvider` contracts per RIZ pool allows us to keep the current way of RDNT emissions and limits increase in gas costs to MFD functions only (and it is O(log n) in the worst case). The only downside of this approach would be claiming process by the user. Now, user's RDNT rewards would be spread across multiple contracts and user would need to claim RDNT from each CIC contract separately. However, this can be solved by moving claim feature to MFD contract (or deploying a separate `RewardAggregator` just for claiming) and then, MFD's function would iterate through all CIC contracts and call individual claim functions. # Architectural overview ![Architecture](https://hackmd.io/_uploads/BkAd4gN56.png) ## RizRegistry `RizRegistry` will be a central contract for tracking and management of RIZ pools. It will extend existing `LendingPoolAddressesProviderRegistry`, so that it keeps the list of all address providers in the storage. It would keep also the addresses of all the implementations and addresses of all shared params and contracts. It will have functions for updating implementations and shared params which would update values in the storage, but also iterate through all RIZ pools (their address providers) and update implementations / params. It will also have functions for adding and retiring pools and adding new reserves to a specific pool, based on the values and implementation in the storage. In fact, `RizRegistry` would serve as a pool admin for all RIZ pools and it should have functions for all configuration that is done through `LendingPoolConfigurator` where it would just pass config to appropriate configurator. Toy implementation of the `RizRegistry`: ``` // contract should extend LendingPoolAddressesProviderRegistry contract RizRegistry { //implementation contracts bytes32 private constant LENDING_POOL = "LENDING_POOL"; bytes32 private constant LENDING_POOL_CONFIGURATOR = "LENDING_POOL_CONFIGURATOR"; bytes32 private constant LENDING_POOL_COLLATERAL_MANAGER = "COLLATERAL_MANAGER"; bytes32 private constant DATA_PROVIDER = "DATA_PROVIDER"; bytes32 private constant EDP = "EDP"; bytes32 private constant CIC = "CIC"; bytes32 private constant LEVERAGER = "LEVERAGER"; bytes32 private constant ATOKEN = "ATOKEN"; bytes32 private constant STABLE_DEBT_TOKEN = "STABLE_DEBT_TOKEN"; bytes32 private constant VARIABLE_DEBT_TOKEN = "VARIABLE_DEBT_TOKEN"; bytes32 private constant INTEREST_RATE_STRATEGY = "INTEREST_RATE_STRATEGY"; mapping(bytes32 => address) private _implementations; //list of RIZ pools mapping(address => uint256) private _addressesProviders; address[] private _addressesProvidersList; //shared parameters address private _protocolAdmin; address private _emergencyAdmin; address private _treasury; address private _lpToken; //shared contracts address private _mfd; address private _middleFeeDistribution; address private _wethGateway; address private _oracle; address private _bountyManager; address private _compounder; constructor(ImplementationParams implementationParams, address emergencyAdmin, ...) { _implementations[LENDING_POOL] = implementationParams.lendingPool; _implementations[LENDING_POOL_CONFIGURATOR] = implementationParams.lendingPoolConfigurator; //setting the rest of implementation addresses _emergencyAdmin = emergencyAdmin; //setting the rest of state variables } //getters and setters for all shared params and contracts with the example of emergencyAdmin function setEmergencyAdmin(address newEmergencyAdmin) { _emergencyAdmin = newEmergencyAdmin; for(uint i; i<_addressesProvidersList.length; i++) { ILendingPoolAddressesProvider(_addressesProvidersList[i]).setEmergencyAdmin(newEmergencyAdmin); } } function setImplementation(bytes32 implId, address implAddress) { _implementations[implId] = implAddress; for(uint i; i<_addressesProvidersList.length; i++) { ILendingPoolAddressesProvider(_addressesProvidersList[i]).setAddressAsProxy(implId, implAddress); } } function addPool(string memory marketId, uint256 eligibilityRequiredRatio, address revenueAsset) { // - deploy LendingPoolAddressesProvider with RIZ specific marketId // - register address provider at _addressesProviders and _addressesProvidersList // - set pool admin as this contract, emergency admin and treasury at LendingPoolAddressesProvider (shared for all) // - LendingPoolAddressesProvider.setLendingPoolImpl(sharedLPImpl) => will deploy new proxy or update existing // - LendingPoolAddressesProvider.setLendingPoolConfiguratorImpl(sharedLPCImpl) // - authorize LendingPool at WETHGateway (shared) // - deploy AaveProtocolDataProvider (effectively Lens contract) via LendingPoolAddressesProvider.setAddressAsProxy (make it proxy first) // - register oracle at LendingPoolAddressesProvider // - deploy EDP via LendingPoolAddressesProvider.setAddressAsProxy // - EDP.setRequiredDepositRatio(P2P_RATIO) // - deploy CIC via LendingPoolAddressesProvider.setAddressAsProxy // - EligibilityDataProvider.setChefIncentivesController(CIC) // - add CIC to minters and addresses on MFD // - deploy LendingPoolCollateralManager via LendingPoolAddressesProvider.setLendingPoolCollateralManager // - deploy leverager // - EDP.setLPToken } function addReserves( address lendingPoolAddressesProvider, InitReserveInput[] calldata initInputParams, ConfigureReserveInput[] calldata configureInputParams) { LendingPoolConfigurator configurator = ILendingPoolAddressesProvider(lendingPoolAddressesProvider).getLendingPoolConfigurator(); // - deploy interest rate // - deploy aTokens and debt tokens configurator.batchInitReserve(initInputParams); // - configure reserves uint256 length = configureInputParams.length; for (uint256 i = 0; i < length; ) { configurator.configureReserveAsCollateral( configureInputParams[i].asset, configureInputParams[i].baseLTV, configureInputParams[i].liquidationThreshold, configureInputParams[i].liquidationBonus ); if (configureInputParams[i].borrowingEnabled) { configurator.enableBorrowingOnReserve(configureInputParams[i].asset, configureInputParams[i].stableBorrowingEnabled); } configurator.setReserveFactor(configureInputParams[i].asset, configureInputParams[i].reserveFactor); unchecked { i++; } } // - mint aTokens } //all onlyPoolAdmin functions on LendingPoolConfigurator, example of enableBorrowingOnReserve function enableBorrowingOnReserve(address lendingPoolAddressesProvider, address asset, bool stableBorrowRateEnabled) { LendingPoolConfigurator configurator = ILendingPoolAddressesProvider(lendingPoolAddressesProvider).getLendingPoolConfigurator(); configurator.enableBorrowingOnReserve(asset, stableBorrowRateEnabled); } } ``` With this feature-rich `addPool()` function, many deployment steps can be omitted as most of deployment and configuration would be done with a single tx. Having a single tx for adding reserves allows us to mint first aTokens and consistently prevent the exploit we experienced in January 2024. ### RIZ dashboard On top of the `RIZRegistry`, we could develop comprehensive dashboard that would list all RIZ pools, listed assets and associated pool and asset parameters. This dashboard would show interest rates (and all params), LTV, liquidation threshold, liquidation bonus, reserve factor, required deposit ratio for RDNT emissions, allocation points used to calculate RDNT emissions, supply and borrow caps, revenue assets and any other important configurable parameter. It would be used primarily internally to get the overview of risk parameters and to help its management. In the next iterations, it can be expanded in a way to allow simulations, e.g. to allow changing slopes and kink params of the interest rate and see their effect on the borrow and supply rate. Finally, it can even allow changing some parameter and generating JSON file containing multisig tx that would change this parameter. Then, this JSON could be imported in the SAFE UI and submit multisig proposal. It would help changing protocol parameters without help from dev team. ## OracleRouter As the number of listed assets grow, we will face the issue that Chainlink will not have price feeds for these assets. There are couple of way to mitigate this issue. First, Radiant should not depend on a single oracle provider in the codebase, meaning it should not have hard dependency on Chainlink. Instead, we should develop oracle router that will serve prices for all Radiant's assets, regardless whether they are listed on the main market or RIZ markets. This router will support multiple oracle providers underneath, and will allow us to get the price of asset A from Chainlink, asset B from RedStone, asset C from Pyth etc. Lending pool shouldn't be aware from which source the price is fetched as it calls the router only. It will give us more flexibility in listing assets, as Chainlink tends to be very conservative when it comes to supporting feeds. Secondly, Radiant should create strategic partnership with an oracle provider such as Pyth, Redstone, API3 or some similar oracle provider with smaller market share than Chainlink. Partnership would assume that they will support feed of an asset per our request. When we decide to list an asset, we reach out to them and they make its feed available. Thirdly, we need to decide how to get the oracle price to the chain. Usually, the fact that oracle provider supports specific asset doesn't mean that its price is available on chain per se. This price is available on oracle's network (e.g. Pythnet) and somebody needs to push it to e.g. Arbitrum. First option is push model in which Radiant (or oracle provider on our behalf) would push the prices on a regular basis (based on the heartbeat and price deviation parameters). This option can become costly, especially on chains where gas is expensive such us Ethereum Mainnet. Second option is pull model where users retrieve asset price from API (e.g. on our frontend) which is signed by the oracle provider and then, submit price update data toegther with price-sensitive operation they want to do (borrowing, redeeming, liquidating). Lending pool would accept price update data and pass it to oracle's smart contract. Oracle's smart contract would verify that the price came from their API and was not manipulated (by checking signature) and would update the price. After that, lending pool would have fresh price of the asset and could perform price-sensitive operation. This option transfers gas costs of updating the price to the users, minimizes overall gas costs as the price gets to the chain only when it's needed, but also makes integrations with our contracts harder, as external integrators become also dependant to the price oracle API. After assesing benefits and drawbacks of each approach, pull model seems as more sustainable model to have when dealing with 100+ assets. When it comes to OracleRouter implementation, it would have: - `mapping(address => FeedData) feeds` in storage, where key would be an asset from the lending pool and the value would be feed data which would describe how to fetch the price. It would contain indicator of selected oracle provider, identifier at the oracle provider (feed address for Chainlink, token Id for Pyth, etc.) and acceptable heartbeat. This mapping can be even broken down into two mappings, for primary and fallback feed. - internal functions for fetching the prices from different providers (e.g. `getChainlinkPrice`, `getPythPrice`) which would be called based on selected oracle provider for a specific asset. - admin functions for setting feedData for every asset - function `getPrice` which would accept asset address, find primary feed in the mapping and based on the oracle provider, call internal function which would fetch and transform the price. If the price is not fresh enough, it would go to fallback feed and try fetching the prices from there. If both, primary and fallback feeds return too old prices, function call would revert. - function `updatePrices` which would accept bytes array describing off-chain prices for a group of asset and pass them to oracle provider supporting pull model (which should always be used as fallback feed at least). ## Revenue management Every RIZ pool (consisting of multiple assets) has only one revenue asset. When we collect the revenue in various RIZ assets, we swap it into `revenueAsset` and distribute it further. It should be governance-based decision which asset to select as `revenueAsset`, but my proposal would be to follow these guidelines: - it should be one of the assets listed in that pool - it should be the most utilized asset from that pool. E.g. if RIZ pool is ETH LST pool, it would consist of stETH, rETH, cbETH, ETH, then it's safe to assume the most utilized asset would be ETH. If RIZ pool is RWA pool consisting of USDC and tokenized US bond asset, then it's safe to assume the most utilized asset will be USDC. Most utilized asset will generate the biggest revenue (e.g. my assumption that on LST RIZ pool, 99% of revenue will be in ETH), so it means that the least swapping will be needed by selecting the most utilized asset. - it should be stablecoin (preferably USDC/USDT across all RIZ pools) or some asset used as underlying asset of the vault that is also in the pool, such as ETH, BNB, MATIC for LSTs, DAI for gDAI, etc - in the best case, it will always be one of the assets already listed on the main/core market. By that way, number of reward tokens would not increase with addition of RIZ pools - there was an alternative idea that `revenueAsset` would not be bluechip or stablecoin asset, but RDNT <> ETH LP tokens or RDNT tokens aTokens would still be minted to a treasury from all RIZ pools. Treasury (`MiddleFeeDistribution` on Arbitrum/BNB and `MultiFeeDistribution` on Mainnet) would be in charge to take its holding in various aTokens and swap it into `revenueAsset`. Treasury contract would need to maintain mapping between aTokens and `revenueAssets` in order to know to source and destination of the swap. This swapping can take place when the reward is forwarded to MFD or we can have a bot that would run and regularly perform swaps. If we go with bot option, it will open the opportunity to use DEX aggregators, ensuring best prices. Currently, revenue (75% of paid interest rate) is divided as 85% to dLP zappers and 15% for operational expenses. This should be revised for RIZ pools. The fact we are listing some assets on isolated pools means that we anticipate that at some point some loans can default and create insolvency. As this is anticipated, part of the revenue should be set aside for covering bad debt and we can plan this by setting up risk fund within safety module and automatically redirecting portion of revenue to this risk fund. Therefore, some formula like 70% revenue going to dLP zappers, 10% for operational expenses and 20% to risk fund should be established. Exact percentages should be subject to governance decision. ## RDNT emissions Currently users who have any position on the Radiant core pool and they have zapped dLP are eligible for RDNT emissions as long as the value of zapped LP tokens is at least 5% of total value of their supplied assets. RDNT rewards are tracked in `ChefIncentivesController` which has helper contract `EligibilityDataProvider` which tracks eligibility statuses of the users. Neither of these two contracts scale well with number of assets because they have O(n) complexity with regards to number of assets. Determining eligibility in `EligibilityDataProvider` requires iterating through all assets lended by user and calculating rewards in `ChefIncentivesController` requires iterating through all assets that have emissions (times 2 because AToken and VariableDebtToken are checked for every asser). Adding 100+ assets across multiple RIZ markets makes it extremely unsustainable. There are 2 alternatives how to support RDNT emissions: ### RIZ-specific incentives controller First proposal would be to replicate existing logic of emissions for every RIZ pool. In this option we should make eligibility requirement per RIZ pool. It means that user becomes eligible for emissions on a RIZ pool only if they have a position on that specific RIZ pool and their zapped LP tokens are worth at least 5% of their supplied assets on that RIZ pool. While zapping stays centralized and independent of specific RIZ pool, emissions are dependant of the RIZ pool. That's fair, but also logical consequence that user can't get any emissions on RIZ pool A if they haven't supplied anything on RIZ pool A. However, there is also side effect of lowering eligibility criteria when eligibility is a property of RIZ pool. Currently, if user supplied $1000-worth of assets on the core pool, eligibility threshold is $50-worth of zapped LP tokens. However, if user supplies $500 to core pool, $300 to RIZ pool A and $200 to RIZ pool B, then user becomes eligible on all 3 pools with $25-worth of LP tokens (because threshold on core pool became $25, $15 on RIZ pool A and $10 on RIZ pool B). It doesn't seem to be a problem (especially as RDNT rewards need to be generally lowered to accomodate growing number of assets), but it is important to keep in mind. Technically, this would mean deployment of `ChefIncentivesController` and `EligibilityDataProvider` contracts per RIZ pool. `EligibilityDataProvider` would interact with only one LendingPool (of specific RIZ market) to get user's `requiredUsdValue` and with shared MFD contract to get `lockedUsdValue`. Update of eligibility and rewards happens by calling CIC functions from 3 places and each of them would need to be slightly adjusted: - function `handleActionAfter` is called from `AToken` and `VariableDebtToken` to update eligibility and rewards based on change in user's position on the LendingPool. `AToken` and `VariableDebtToken` would still have only one (LendingPool-specific) CIC contract which they would call so no change would be required there - function `afterLockUpdate` is called from MFD contract to update eligibility based on change in user's amount of zapped LP tokens. This is the biggest change because MFD contract would not only store one CIC contract and call its function `afterLockUpdate`, but would need to store array of CIC contracts (one for every RIZ pool + core pool) and call `afterLockUpdate` on each of them. However, as the length of array is dependent on number of RIZ pools and not number of listed assets, impact on gas consumption is somewhat limited. It can be further improved in a way that `afterLockUpdate` function starts with simple check if user has position on that RIZ pool (it can be `mapping(address => bool)` in CIC contract) and if not, just return from this function. - function `claimBounty` is called by `BountyManager` to update eligibility based on change of prices of supplied assets and zapped LP tokens. Change that would be required here is that bounty hunter specifies CIC contract (or lending pool) where change should happen. By this, no (significant) extra gas cost is added, though it makes off-chain process of identifying bounties slightly more complex. Furthermore, CIC contracts manages the list of rewarded rTokens. By having CIC contract per RIZ pool means this list will be much shorter, hence any interation through them will be significantly lower. To conclude, having `ChefIncentivesController` and `EligibilityDataProvider` contracts per RIZ pool allows us to keep the current way of RDNT emissions and limits increase in gas costs to MFD functions only (and it is O(log n) in the worst case). There are two downsides of this approach, first being claiming process by the user. User's RDNT rewards would be spread across multiple contracts and user would need to claim RDNT from each CIC contract separately. However, this can be solved by moving claim feature to MFD contract (or deploying a separate `RewardAggregator` just for claiming) and then, MFD's function would iterate through all CIC contracts and call individual claim functions. Second downside is the scalability issue on MFD contract when the amount of zapped LP tokens change => it requires iterating through multiple CIC contracts, which can be gas expensive operation, especially on Ethereum Mainnet. ### Off-chain calculation of incentives Second approach would be to refactor complete incentives logic and move it off-chain. The idea would be to deploy new or expending subgraph which would index users' positions on all RIZ markets and their dLP position. Then, we would develop an app (expressjs?) which would calculate users rewards per block, based on the indexed data and summarize it on some interval (e.g. daily basis). Based on the rewards, merkle tree would be calculated and stored in the offchain (PostgreSQL?) database, together with reward data. Rewards and merkle paths would be made available through API to frontend dApp. At the same time, we would submit merkle tree root to `CentralIncentivesController` and send sufficient RDNT amount. Users would be able to claim their rewards by fetching amount and merkle path from the API and submitting them to `CentralIncentivesController`. This contract would verify amount and merkle path against the merkle root and allow claiming. We could also allow partial claims by storing amount of data that was claimed in the contract and making sure that submitted amount is less than total claimable amount. This approach would allow us to keep integrity of the data verifiable on chain, but would offload computation from the contract, making the whole process far more gas efficient. All txs would become less expensive and we wouldn't need bounties to set eligibility for users anymore. We could implement this for RIZ pools only and eventually replace existing on-chain mechanism on the core pool as well. The major drawback of this approach is that it might require more extensive development effort (compared to 1st alternative) and more expensive audits as its implementation significantly deverts from the current codebase. ## Stablecoin investment vault Stablecoin investment vault is an attempt to tackle the issue of fragmentation of stablecoin liquidity across RIZ pools. This vault would be separate, standalone product on top of the lending protocol. It would serve as the main gateway for stablecoin lenders looking to park their stablecoins and maximize the yield on them. It can support one stablecoin (USDT) or multiple with option that the vault internally swaps them when needed. It can be implemented as ERC4626 vault allowing users to deposit stablecoins. It would have rebalancing bot associated which would take deposited stablecoins and lend them to various RIZ pools in order to maximize the APR. Users would get the best APR without a need to check every RIZ pool and figure out the best interest rate and without a need to move them manually between RIZ pools. Side effect of this product would be optimization of interest rates across RIZ markets as vault would naturally lend on RIZ pool with the highest yield effectively decreasing utilization rate and borrow rate, leading to equalizing the interest rate across RIZ pools. Improvement of this product would be to divide the vault into 3 risk-based tranches, as [Level Finance](https://www.gate.io/learn/articles/understanding-level-finance-in-one-article/751) had. We would evaluate the risk of every RIZ pool and add based on their risk, group RIZ pools into one of three clusters A (high risk), B (moderate risk), C (low risk). The vault would have three tranches and users would select to which tranche they want their stableocins to be directed. Stablecoins going to Senior tranche would be lended only to C-cluster RIZ pools, Mezzanine tranche would lend to B and C clusters and Junior tranche would lend to A, B and C clusters. By that way, Senior tranche is the least risky, but also brings the lowest APR, while Junior tranche is the riskiest tranche with the highest risk. As this would be quite peripheral product, it can be developed as a standalone product, even by giving grant to some external team. ## Safety module With the current utilization rate on the core pool, on $496M of TVL, $24.6M of fees are collected annually, making it around 5%. If we assume 5% of fees will be collected on RIZ markets as well and if we take 75% of that as revenue and further take 20% of the revenue for the safety module, it means that 0.75% of RIZ TVL would be directed to safety module annually. It will be sufficient to cover bad debt only if we accumulate less than 0.75% of TVL on the annual basis, which seems quite unlikely (e.g. the exploit Radiant Capital had in January 2024 took 1% of TVL), so we need to come up with the mechanism that would allow us to build more substantial fund for covering bad debt. The proposal would be to to set up Safety module pool where users would be able to stake `safetyAsset`. One option is that users set locking duration which would translate into reward multiplier (the same approach as with zapping dLP). Second option would be that they have cooldown period, which means that users don't set locking duration and can unstake whenever they want, but they can't withdraw funds for x number of days (e.g. 30 days) after they submitted unstaking request. During that period, they don't accrue any reward. This approach is used by Aave. Users who stake `safetyAsset` into Safety module would get their reward in form of `revenueAsset` that is distributed to Safety module. If we aim for 10%-20% APR, Safety module could collect 3.75% - 7.5% of RIZ TVL which is by far more substential amount for covering bad debt. In case of an incident creating insolvency, up to x% (e.g. 30%) of the funds staked in the Safety module would be slashed to cover bad debt. Covering bad debt would be realized by swapping `safetyAsset` into asset that accrued bad debt and repaying the loan on behalf of the users who accumulated bad debt. There are 3 options for selection of `safetyAsset`. First option is RDNT token, second option is RDNT <> ETH LP token, third option is a stablecoin. If we decide to go with stablecoin, RDNT token would be protected from any bad debt incident, but it would further fragmentize stablecoin liquidity and wouldn't utilize available stablecoins to their best extent. Furthermore, taking into account assumed risk aversion of stablecoins lenders, it seems less likely that stablecoin lenders would provider their assets for slashing in case of insolvency, limiting ability of Safety module to attract substantial capital. Therefore, my preference would be RDNT or LP tokens as that would help further build RDNT liquidity and stabilize RDNT price. It would also align RDNT holders with their trust in Radiant Capital and it's ability to manage risk in particular. Finally, it case of a black swan event, Radiant DAO would take part of the hit, as RDNT tokens would be sold to cover bad debt. So, Radiant Capital would be additionally inclined to better manage the risk as any incident would damage RDNT price, while keeping bad debt away from RIZ markets would further build RDNT liquidity through funds staked in the Safety module. To further improve management of funds in the Saftey module, every RIZ pool could get its safety score which would translate in the maximum slashing that can happen to cover for the bed debt in that specific RIZ pool. RIZ pool which is evaluated as less risky, would allow higher % of Safety module funds to be slashed (e.g. 30%), while RIZ pool which we evaluate as more risky would limit slashing to lower % (e.g. 5%). Additional consideration that is required is how to manage bad debt that would be accrued beyond limits of the Safety module. We need to decide what to do with the bad debt that can't be covered even after maximum slashing of Safety module. My proposal would be to socialize the loss within RIZ pool which experienced insolvency. It would mean adjusting portfolios of all users in a way that portion of their rTokens and variableDebtTokens is burned, so they keep the same LTV as they had before the black swan event, but with smaller portfolio. This would be extremely complex process which would include lot of off-chain calculation and would require manual adjustments, but it seems as the only fair solution. If we decide to go this route, it would be best to add this feature to rToken and variableDebtToken in advance, together with scripts for calculation, so that portfolio rebalancing can happen quickly after a black swan event. Finally, as mechanism for socialization of the loss is required anyways, Safety module becomes an optional feature and the decision whether to implement it comes to pros and cons analysis. Pros would be greater safety of RIZ pools and greater upside potential for RDNT token in case of smooth functioning of RIZ pools. Drawbacks are more complex UX as suddenly we have too many places to deposit funds (core pool, dLP, RIZ pools, stablecoin vault, safety module), contagion of loss to more users (instead of containing it within a single RIZ pool) and the increased risk for RDNT token.