💻Technical
Technical Specifications
Overview

Borrowers and lenders can deposit and withdraw into the
LiquidityWarehousecontract by calling the relevant functions.Borrowers can call
depositBorrowerto deposit assets andwithdrawBorrowerto withdraw assetsLenders can call
depositLenderto deposit assets andwithdrawLenderto withdraw assets.
An automated Chainlink Automation contract named
DebtCovenantmonitors the net asset values of both the borrower and lender deposits. This contract can calldeactivateto deactivate theLiquidityWarehouseif the liquidation threshold has been breached as well asactivateto reactivate theLiquidityWarehouseif the liquidation threshold is fulfilled. A full description on how the liquidation threshold is calculated is outlined here.Assets in the
LiquidityWarehouseare periodically deployed to the borrower’s smart contracts. These assets are withdrawn to theLiquidityWarehousein two cases namelyWhen a lender calls
withdrawLenderand there is insufficient liquidity in theLiquidityWarehouseWhen
deactivateis called to automatically withdraw all deployed assets from the borrower’s smart contracts.
Terms
The exact terms of the LiquidityWarehouse are stored in the Terms struct.
struct Terms {
/// @notice The asset the loans in the liquidity
/// warehouse is denominated in
IERC20 asset;
/// @notice The address that will receive fees
address feeRecipient;
/// @notice The liquidation threshold of the liquidity warehouse
uint64 liquidationThreshold;
/// @notice The capacity threshold of the liquidity warehouse
uint64 capacityThreshold;
/// @notice The interest rate of the liquidity warehouse
uint64 interestRate;
/// @notice The fee taken from compounded interest of the liquidity warehouse
uint64 interestFee;
/// @notice The fee taken from withdrawals
uint64 withdrawalFee;
}These parameters can be updated by the DEFAULT_ADMIN function by calling the relevant functions.
Net Asset Value
The total NAV (Net Asset Value) of a LiquidityWarehouse comprises of the NAV of reserve liquidity on the Liquidity Warehouse as well as NAV from funds deployed to Whitelisted Target pools.
For accounting purpose, total NAV can also be expressed as summation of lenders NAV and borrowers NAV.
To ensure that lenders have seniority over borrower for the fixed interest, lender’s NAV is whichever is larger between total NAV and lenders balance, where lenders balance is the amount theoretically owed to the lenders compounded by fixed interest.
This also means, to be consistent with the above formula, borrowers NAV is the leftover of total NAV subtracted by the lenders balance with 0 as the floor.
Both lenders NAV and borrowers NAV have corresponding LP tokens representing their share of the NAV. These LP tokens are minted and burned as depositors deposit/withdraw from the LiquidityWarehouse. The value of each of these LP tokens is given by dividing their respective total NAV values and the total LP token supply.
Calculating Deployed NAV
The deployed net asset value is calculated by iterating through the list of s_withdrawTargets and determining the NAV for that target. The logic to calculate the NAV for each target is implemented in the _getDeployedAssetValue function of the borrower specific LiquidityWarehouse implementation.
uint256 deployedNAV;
/// We acknowledge that this may be gas intensive but we do not foresee there
/// being a high number of withdraw targets
for (uint256 i; i < withdrawTargets.length; ++i) {
deployedNAV += _getDeployedAssetValue(withdrawTargets[i]);
}The mechanism for updating the list of s_withdrawTargets is outlined in the Access Control section below.
Thresholds
There are two thresholds that relates to the ratio between borrowers NAV and total NAV:
Capacity threshold: prevents deposits by lender and withdrawal by borrower if the resulting ratio between borrowers NAV and total NAV would be below the threshold
Liquidation threshold: allows for liquidation if the ratio between borrowers NAV and total NAV is below the threshold
LiquidationThreshold is enforced to always be lower then capacityThreshold
These thresholds can be configured by the contract admin by calling setLiquidationThreshold and setCapacityThreshold
Liquidation
A LiquidityWarehouse can be deactivated anytime the liquidationThreshold is breached by calling deactivate and reactivated by calling activate. Both of these functions are not access controlled, which means that any address is allowed to call it whenever the correct conditions are met.
The liquidationThreshold is considered to be breached whenever the following formula is true
What CAN be done when the LiquidationWarehouse is ACTIVE
Borrowers can deposit and withdraw by calling
depositBorrowerandwithdrawBorrowerLenders can deposit and withdraw by calling
depositLenderandwithdrawLenderInterest owed to the lenders continues to compound
What CANNOT be done when the LiquidationWarehouse is ACTIVE
Borrowers cannot withdraw an amount that will breach the
capacityThresholdAddresses cannot call
liquidateto withdraw all deployed assets from whitelisted targets
What CAN be done when the LiquidationWarehouse is ACTIVE
The contract admin can continue to call
executeto perform operational tasks
What CANNOT be done when the LiquidationWarehouse is INACTIVE
Borrowers CANNOT deposit by calling
depositBorrowerInterest stops accruing
Permissioned actors CANNOT execute actions by calling
execute
Withdrawing Liquidity
Liquidity deployed to the whitelisted targets can be withdrawn by either
Passing in an array of whitelisted targets in the
deactivatefunction to deactivate and withdraw in the same transaction.Passing in an array of whitelisted targets in the
liquidatefunction. This function exists in case the call todeactivatecould not withdraw the full deployed amount from all the whitelisted targets.
Internally both of these functions will loop through the list of whitelisted targets and try to withdraw as much liquidity as it can from each of the addresses passed in. The exact mechanism to withdraw funds will be implemented by the borrower specific implementation of the LiquidityWarehouse in the _withdrawFromTarget function.
DebtCovenant
DebtCovenantIn addition to the LiquidationWarehouse, Copra will also implement a DebtCovenant contract using Chainlink Automation to monitor the LiquidationWarehouse's liquidation ratio and automatically call activate or deactivate when the liquidation threshold is breached.
Chainlink Automation compatible contracts require that the implementation contract implements a checkUpkeep function to check whether or not an action needs to be performed and a performUpkeep function to execute the required transactions.
The checkUpkeep function will check to see if the liquidation threshold has been breached and determine whether or not a LiquidityWarehouse needs to be activated/deactivated. In addition to this it will also determine a list of whitelisted target addresses that the LiquidityWarehouse needs to withdraw liquidity from when deactivating.
The performUpkeep function will take in the data returned from checkUpkeep and call the relevant functions on the LiquidityWarehouse.
Deposits
Lenders and borrowers can both deposit into the LiquidityWarehouse to earn a fixed yield on their assets. In order to start the LiquidityWarehouse borrowers must first deposit an amount by calling depositBorrower to deposit some assets into the LiquidityWarehouse. Lenders can then call depositLender to deposit assets as long as the ratio between the borrower’s NAV and lender’s NAV is not greater than the capacityThreshold.
Example
LiquidityWarehousehas acapacityThresholdof 10%Borrower has deposited 10 USDC
Lender’s can deposit up to a maximum of 90 USDC

In return for depositing assets into the LiquidityWarehouse, lenders and borrowers are both minted LP tokens that represent their share of their respective balances. These LP tokens both exist within the LiquidityWarehouse contract, which implements ERC1155 in order to support multiple tokens. The lender’s LP token is reserved at ID 0 and the borrower’s LP token is reserved at ID 1. The amount of LP tokens minted for both lenders and borrowers are shown below.
Withdrawals
Depositors can withdraw from the LiquidityWarehouse by either calling withdrawLender or withdrawBorrower depending on whether they had deposited into the lender or borrower pools. Both of these functions takes in an amount parameter, which represents the amount of shares that the depositor wishes to burn in exchange for receiving withdrawn assets. The amount of withdrawable assets are given below.
In addition to taking in an amount parameter, the withdrawer may also specify a list of whitelistedTargets to withdraw assets from whitelisted targets from if the amount of assets in the LiquidityWarehouse is not enough to cover the withdrawable amount. The exact mechanism to facilitate withdrawals is left to the borrower specific implementation contract to implement in the _withdrawFromTarget function. This function
Access Control and Operations
The LiquidityWarehouse contract can be externally controlled to call functions on another contract particularly for the purpose of deployments (and withdrawals) of funds from the LiquidityWarehouse to whitelisted targets.
Whitelisting Callers
Callers can be whitelisted to perform certain function calls on specific target addresses by calling the toggleWhitelist function, which takes in an array of LiquidityWarehouseAccessControl.Action structs.
struct Action {
/// @notice The caller of the action
address caller;
/// @notice The target of the action
address target;
/// @notice The function that the action
/// will call
bytes4 fnSelector;
/// @notice True if the action is whitelisted
bool isWhitelisted;
/// @notice True if this is the withdraw function
bool isWithdraw;
}The whitelisted function calls, targets and callers are hashed together using keccak256 to generate an action ID, which are then mapped to a boolean value to represent whether or not the call is whitelisted.
bytes32 actionId = keccak256(abi.encode(target, caller, fnSelector));
s_whitelistedActions[actionId] = action.isWhitelisted;Withdraw Targets
The LiquidtyWarehouse needs to keep track of the list of whitelisted target addresses to calculate NAV and to withdraw liquidity from when the liquidity warehouse is deactivated. This list is stored as an EnumerablAddressSet in the s_withdrawTargets storage variable. Addresses can be added to this set by passing in an Action struct with isWithdraw and isWhitelisted set to true. Conversely the address can be removed from s_withdrawTargets by setting isWithdraw to true and isWhitelisted to false.
Execution
Whitelisted callers and the contract’s DEFAULT_ADMIN can call execute to perform any operational transactions as the LiquidityWarehouse. Example operational transactions include
Calling
approveon an ERC20 token contract to approve an address to spend the assets in theLiquidityWarehouseCalling functions on the borrower’s protocol to deploy assets from the
LiquidityWarehouse
The execute function takes in an array of ExecuteAction structs
struct ExecuteAction {
/// @notice The target address the action will call
address target;
/// @notice The action's calldata
bytes data;
/// @notice The amount of wei to send when executing
/// the action
uint256 value;
}For each ExecuteAction, the execute function will first regenerate an actionId using msg.sender, [executeAction.target](<http://executeAction.target>) and the function signature being called derived from bytes4(executeAction.data). The generated actionId will then be compared with the boolean entry in s_whitelistedActions to determine whether or not the caller has been whitelisted to perform the given action. This step is skipped if the caller is the DEFAULT_ADMIN.
Fees
There are two protocol fees
Interest fees: a percentage of interest amount added to lenders balance, practically implemented by minting appropriate amount of lender LP token to protocol treasury address
Withdrawal fees: a percentage of amount withdrawn by lender or borrower, practically implemented by transferring appropriate amount of lender or borrower LP token to protocol treasury address and subtracting withdrawal fees from total withdrawal amount
Roles
There are two privileged roles
PAUSER_ROLE: this will be owned by a 2 out of 3 multisig contract, and has the ability to pause (and unpause) the Liquidity Warehouse contractwhen the Liquidity Warehouse is paused, both lender and borrower deposit functions i.e
depositLenderanddepositBorrowercannot be called
DEFAULT_ADMIN_ROLE: TheDEFAULT_ADMINwill be able to update the terms of theLiquidityWarehouseand interact with the underlying borrower protocol to deploy and withdraw assets. This introduces a degree of centralization risk as a maliciousDEFAULT_ADMINwill be able to steal user funds. In order to mitigate this risk, allLiquidityWarehouseswill be owned by a Timelock contract so that there is a delay period before sensitive transactions are executed. A Gnosis Multisig requiring 2 out of 3 signer addresses will be granted thePROPOSER,EXECUTORandCANCELLERroles on the Timelock contract so that the the multisig can propose, execute or cancel operations
List of functions callable by the DEFAULT_ADMIN
Updating the fixed interest rate
setInterestRate
Updating the liquidation threshold
setLiquidationThreshold
Updating the capacity threshold
setCapacityThreshold
Updating the interest fee
setInterestFee
Updating the withdrawal fee
setWithdrawalFee
Updating the fee recipient address
setFeeRecipient
Updating the list of whitelisted functions
toggleWhitelist
Call functions on behalf of the LiquidityWarehouse. Examples of this include deploying assets to borrower protocols and withdrawing. This action is centralized to the DEFAULT_ADMIN but is mitigated by the fact that the Liquidity Warehouses will be owned by a Timelock contract so that lenders and borrowers have sufficient time to react to malicious activity
execute
Last updated