⚙️Mechanics

Technical Specifications

Overview

  1. Borrowers and lenders can deposit and withdraw into the LiquidityWarehouse contract by calling the relevant functions.

    1. Borrowers can call depositBorrower to deposit assets and withdrawBorrower to withdraw assets

    2. Lenders can call depositLender to deposit assets and withdrawLender to withdraw assets.

  2. An automated Chainlink Automation contract named DebtCovenant monitors the net asset values of both the borrower and lender deposits. This contract can call deactivate to deactivate the LiquidityWarehouse if the liquidation threshold has been breached as well as activate to reactivate the LiquidityWarehouse if the liquidation threshold is fulfilled. A full description on how the liquidation threshold is calculated is outlined here.

  3. Assets in the LiquidityWarehouse are periodically deployed to the borrower’s smart contracts. These assets are withdrawn to the LiquidityWarehouse in two cases namely

    1. When a lender calls withdrawLender and there is insufficient liquidity in the LiquidityWarehouse

    2. When deactivate is 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 below.

ActionFunction Name

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

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.

totalNAV=reserveNAV+deployedNAVtotalNAV = reserveNAV + deployedNAV

For accounting purpose, total NAV can also be expressed as summation of lenders NAV and borrowers NAV.

totalNAV=lenderNAV+borrowerNAVtotalNAV = lenderNAV + borrower 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.

lenderNAV=min(totalNAV,lenderBalance)lenderNAV = min(totalNAV, lenderBalance)

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.

borrowerNAV=max(totalNAVlenderBalance,0)borrowerNAV = max(totalNAV - lenderBalance, 0)

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.

lenderLPTokenValue=lenderNAVlenderLPTokenTotalSupplylenderLPTokenValue = \frac{lenderNAV}{lenderLPTokenTotalSupply}
borrowerLPTokenValue=borrowerNAVborrowerLPTokenTotalSupplyborrowerLPTokenValue = \frac{borrowerNAV}{borrowerLPTokenTotalSupply}

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

borrowerNAVtotalNAV<capacityThreshold\frac{borrowerNAV}{totalNAV} < capacityThreshold
  • Liquidation threshold: allows for liquidation if the ratio between borrowers NAV and total NAV is below the threshold

borrowerNAVtotalNAV<liquidationThreshold\frac{borrowerNAV}{totalNAV} < liquidationThreshold

LiquidationThreshold is enforced to always be lower then capacityThreshold

liquidationThreshold<capacityThresholdliquidationThreshold < 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

borrowerNAVtotalNAV<liquidationThreshold\frac{borrowerNAV}{totalNAV}<liquidationThreshold

What CAN be done when the LiquidationWarehouse is ACTIVE

  • Borrowers can deposit and withdraw by calling depositBorrower and withdrawBorrower

  • Lenders can deposit and withdraw by calling depositLender and withdrawLender

  • Interest 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 capacityThreshold

  • Addresses cannot call liquidate to withdraw all deployed assets from whitelisted targets

What CAN be done when the LiquidationWarehouse is ACTIVE

  • The contract admin can continue to call execute to perform operational tasks

What CANNOT be done when the LiquidationWarehouse is INACTIVE

  • Borrowers CANNOT deposit by calling depositBorrower

  • Interest stops accruing

  • Permissioned actors CANNOT execute actions by calling execute

Withdrawing Liquidity

Liquidity deployed to the whitelisted targets can be withdrawn by either

  1. Passing in an array of whitelisted targets in the deactivate function to deactivate and withdraw in the same transaction.

  2. Passing in an array of whitelisted targets in the liquidate function. This function exists in case the call to deactivate could 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

In 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

  • LiquidityWarehouse has a capacityThreshold of 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.

mintedLenderLPTokenAmount=lenderDepositAmountlenderLPTokenValuemintedLenderLPTokenAmount = \frac{lenderDepositAmount}{lenderLPTokenValue}
mintedBorrowerLPTokenAmount=borrowerDepositAmountborrowerLPTokenValuemintedBorrowerLPTokenAmount = \frac{borrowerDepositAmount}{borrowerLPTokenValue}

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.

lenderWithdrawalAmount=amountLenderLPTokenlenderLPTokenValuelenderWithdrawalAmount= amountLenderLPToken *lenderLPTokenValue
borrowerWithdrawalAmount=amountBorrowerLPTokenborrowerLPTokenValueborrowerWithdrawalAmount= amountBorrowerLPToken *borrowerLPTokenValue

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 approve on an ERC20 token contract to approve an address to spend the assets in the LiquidityWarehouse

  • Calling 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

interestFees=interestFees%(lenderBalanceAfterInterestlenderBalanceBeforeInterest)interestFees=interestFees\%*(lenderBalanceAfterInterest - lenderBalanceBeforeInterest)
lenderLPTokenMintToTreasury=lenderLPTokenTotalSupplyinterestFees/(lenderBalanceAfterInterestinterestFees)lenderLPTokenMintToTreasury=lenderLPTokenTotalSupply*interestFees/(lenderBalanceAfterInterest-interestFees)
  • 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

LPTokenTransferToTreasury=withdrawalFees%LPTokenRedeemedLPTokenTransferToTreasury = withdrawalFees\%*LPTokenRedeemed
withdrawalAmountAfterFees=withdrawalAmount(1withdrawalFees%)withdrawalAmountAfterFees = withdrawalAmount*(1-withdrawalFees\%)

Roles

There are two privileged roles

  1. PAUSER_ROLE: this will be owned by a 2/2 multisig contract, and has the ability to pause (and unpause) the Liquidity Warehouse contract

    1. when the Liquidity Warehouse is paused, both lender and borrower deposit functions cannot be called

  2. DEFAULT_ADMIN_ROLE: this will be owned by a timelock contract that is owned by itself so that configuration changes to the timelock are also subject to a delay period. The 2/2 multisig will be granted the PROPOSER, EXECUTOR and CANCELLER roles on the Timelock contract so that the the multisig can propose, execute or cancel operations. This setup indirectly puts a minimum delay between proposing and executing operations to update the terms of the liquidity warehouse.

Last updated