System Overview
Last updated
Last updated
LombardFi is a permissionless DeFi protocol that provides functionality for custom, permissionless reputation-based undercollateralized loans. The smart contracts are meant to allow anyone to become a borrower, whereas external teams are encouraged to develop custom frontends that can filter borrowers, provide reputation-based metadata and verify identities.
There are 3 types of actors in the LombardFi protocol: pool borrower, pool lender and governance. The LombardFi protocol is designed in such a way that every action can only be performed by a single role. There are two exceptions to this rule: deploying a pool and repaying debt, which can be performed by anyone. The rationale behind not restricting debt repayment to the borrower is outlined below.
The borrower role is pool-scoped: when we speak of the borrower in the context of the protocol, we mean a specific pool's borrower. A user becomes the borrower when they create a pool through the factory. The borrower can perform the following actions in the protocol:
Router.sol
Borrow from their pool
Repay the borrow from the pool
Withdraw leftover rewards from their pool
Pool.sol
Set a whitelisted lender for their pool
The lender role is pool-scoped: when we speak of a lender in the context of the protocol, we mean a specific pool's lender. A user becomes a lender in a pool when they deposit assets.
LombardFi pools are public by default, thus every address except the borrower can be a lender. The borrower can optionally take their pool private by whitelisting a certain address to be the sole lender in their pool. The lender can perform the following actions in the protocol:
Router.sol
Deposit into a pool
Redeem their deposit with interest
The governor role is protocol-scoped: when we speak of the governor the context of the protocol, we mean the protocol-wide governing role. By default the governor is the contract deployer.
LombardFi contracts use OpenZeppelin's Ownable module in the Router, PoolFactory and OracleManager. Therefore each contract has an owner
state variable set initially to the deployer.
The onlyOwner
modifier provided by Ownable
is used to restrict access to administrative functions.
Router.sol
Set the PoolFactory address
Set the OracleManager address
Set the treasury address
Pause the contract
Unpause the contract
PoolFactory.sol
Set the maximum number of collateral address
Set the protocol-wide origination fee
OracleManager.sol
Set the oracle implementations
The lender role is pool-scoped: when we speak of a lender in the context of the protocol, we mean a specific pool's lender. A user becomes a lender in a pool when they deposit assets.
LombardFi pools are public by default, thus every address except the borrower can be a lender. The borrower can optionally take their pool private by whitelisting a certain address to be the sole lender in their pool. The lender can perform the following actions in the protocol:
Deposit into a pool
Redeem their deposit with interest
The PoolFactory
is the contract that handles the creation of pools. When the contract is constructed, the immutable Pool
implementation contract is created. Every pool is a clone of this contract. The PoolFactory
also acts as a registry for deployed pools.
The Router
is the entry point for all interactions with deployed pools.
The OracleManager
is an oracle aggregator that can get prices from multiple oracle adapters. Every adapter must conform to the IBasePriceOracle
interface. The oracle manager's strategy is not to combine prices but to have multiple oracle implementations in order of robustness. For example, first it tries Chainlink, then UniV3. Only Chainlink is supported at this moment with the Uniswap V3 TWAP oracle in development.
The Treasury
is the address where the origination fee is forwarded to. This is intended to become a DAO treasury.
The core functionality of the Lombard protocol is the pool. Anyone can become a borrower by deploying a Pool
via PoolFactory.createPool()
.
Each pool has a set of parameters, collectively called a term sheet due to their equivalence with term sheets in traditional finance. Part of the term sheet is set by the borrower at pool creation, whereas the rest is set by the protocol. These parameters will remain immutable throughout the lifecycle of the Pool with the exception of the whitelisted lender addressed later.
lentAsset
, the address of an ERC20 token that the borrower wants to borrow. Lender will deposit and earn on this asset.
collateralAssets
, an array of addresses corresponding to the ERC20 tokens that the borrower can supply as collateral at the time of borrow.
minSupply
, the minimum amount of the lent asset that must be achieved in order to activate the pool. If this amount is achieved, the borrower can borrow the collected funds. At maturity the lenders can redeem their notional with yield.
maxSupply
, the maximum amount of the lent asset that the pool can accept.
startsAt
, the timestamp of the block in which the pool was created.
activeAt
, the timestamp after which the pool becomes active.
maturesAt
, the timestamp after which the pool matures.
coupon
, the yield paid by the borrower to the lenders for the duration in which the pool is active. The coupon is different from the APR.
ltv
, the loan-to-value ratio which says the minimum value of the collateral that must be supplied by the borrower for the pool.
originationFee
, A pool has an origination fee, deducted from the borrowed amount at the point of borrow, and deposited into the protocol treasury.
whitelistedLender
, set to the zero address by default, which means that pool is public by default. The borrower can choose to whitelist a certain address as a lender at any time, mimicking a private OTC deal.
Parameters in the term sheet are chosen by the borrower at pool creation with the exception of the origination fee (defined at protocol level by the governance), the start timestamp which is automatically set to the block timestamp at pool creation and the whitelisted lender which can be changed by the borrower at any time.
There are 3 timestamps in a pool: startsAt
, activeAt
and maturesAt
that collectively delineate 3 pool periods: the bonding, active and mature periods.
The start timestamp is set to the time at which the pool is created. Up until the active timestamp the pool is said to be bonding. During the bonding period lenders can deposit the lent asset.
Between the active timestamp and the maturity timestamp the pool is said to be active. Once a pool is active lenders can no longer deposit.
If the minimum supply is achieved the borrower can borrow all collected funds (all at once, partial borrows are not allowed). They must repay the loan by the end of the active period.
If the minimum supply is not achieved the borrower cannot borrow. Lenders can instead redeem their deposits without the coupon. The borrowers can withdraw their upfront.
After the maturity timestamp the pool is said to be mature.
If all debt was repaid lenders can redeem their deposit + the coupon on their deposit.
If debt was partially repaid lenders can redeem a pro-rata distribution on the partial repayment + the coupon on their deposit + a pro-rata distribution of the collateral.
If debt was not repaid lenders can redeem the coupon on their deposit + a pro-rata distribution of the collateral.
Lenders that default are not punished by the system. Instead, the reputation problem is to be solved on the user interface level, where any number of on-chain or off-chain reputation/curation systems can be used.
Anyone can repay the borrower's loan on behalf of the borrower. This is done for the greater flexibility of institutional borrowers, which will usually deploy the borrowed capital on centralized exchanges, and would prefer to repay with a hot wallet.
When creating a pool the borrower must supply the coupon for the maximum supply upfront. For example if the maximum is 1M DAI and the coupon is 6% then they must supply 60000 DAI in order to create the pool. This is done to guarantee yield to the lenders and to encourage the creation of high-quality pools by reputable actors. If the maximum supply is not achieved borrowers can withdraw the coupon for the unfilled amount.
When borrowing the collected funds the borrower must supply collateral of any subset of the collateral assets whose value satisfies the loan-to-value ratio.
Pools are OpenZeppelin Clones of an immutable pool implementation created in the constructor of PoolFactory
.
Native Ether is not accepted. All assets must be ERC20 tokens.
Tokens with more than 18 decimals are not accepted due to concerns about mathematical precision and token sanity.
Boris spots a profitable market-neutral opportunity for USDC on a centralized exchange. He wants to borrow USDC and provide collateral with WETH and/or WBTC. He wants a minimum of 1M DAI and a maximum of 20M DAI. He chooses a 500% LTV, and gives a 1.5% coupon for a 3-month term, amounting to a 6% APR. Deposits are open for 2 weeks.
Boris creates a pool by calling PoolFactory.createPool()
with the desired termsheet. To do that he has to supply 0.3M DAI as an upfront (1.5% * 20M DAI).
Lena spots the pool and deposits 3M DAI by calling Router.deposit()
.
The initial 2 weeks are over. The pool is now active.
Boris borrows all 3M DAI by calling Router.borrow()
. To do that he has to supply collateral of value equal to 20% of the borrowed amount (0.6M DAI) to achieve a 500% LTV. He decides to give 10% of that in WETH and 90% of that in WBTC as he believes that WBTC is overvalued and WETH is undervalued.
Boris withdraws the extra coupon for the unfilled 17M DAI by calling Router.withdrawLeftovers()
. This amounts to 45K DAI.
Boris deploys the funds off-chain. After 2 1/2 months the off-chain opportunity has dried up.
Boris repays the borrowed 3M in time by calling Router.repay()
. He gets back all collateral.
The pool matures.
Lena redeems her deposit (3M) + coupon (0.3M) by calling Router.redeem()
.
The codebase has unit tests for all contracts for 100% branch and line coverage, as well as end-to-end tests for scenarios and integration.
There is a number of deployment scripts under scripts/
that allow for mainnet and testnet deployments.