Liquity Protocol — DeFi protocol explained from its Smart Contracts. Part 1
Considering how much we have to learn about DeFi protocols in order to improve our auditing skills, and as the next article in the series of DeFi Protocols by code newsletter. Here is the first part, of two, about Liquity protocol.
I finally decided to divide it in two as otherwise it might have been a massive article and I felt it could be separated to understand different concepts.
What will we go through in this article?
Introduction
Tokens
What is a Trove?
Borrowing
PriceFeed and Oracle
Introduction
Liquity is a fully automated and governance-free decentralized borrowing protocol. It’s designed to allow permissionless loaning of its native token and to draw interest-free loans against Ether used as collateral.
Liquity does not run its own Frontend. To interact with the protocol, users have to use third-party frontends. This is done to achieve maximum capital efficiency and user-friendliness.
Loans are paid out in LUSD and need to maintain a minimum collateral ratio of 110%.
In addition to the collateral, the loans are secured by a Stability Pool containing LUSD and by fellow borrowers collectively acting as guarantors of last resort.
Tokens
What is LUSD?
LUSD is a USD-pegged stablecoin that serves as the primary network token for the Liquity protocol. When users place their Ethereum into their account, or Trove, as it’s known, they receive a loan in the form of LUSD.
The smart contracts involved:
LUSDToken.sol
— :
It is the stablecoin token contract, which implements the ERC20 fungible token standard in conjunction with EIP-2612 and a mechanism that blocks (accidental) transfers to addresses like the StabilityPool and address(0) that are not supposed to receive funds through direct transfers.
The contract mints, burns and transfers LUSD tokens.
In order to validate who is minting the tokens, we can notice in the mint
function that it’s only going to be allowed to mint from the BorrowOperations
contract:
function mint(address _account, uint256 _amount) external override {
_requireCallerIsBorrowerOperations();
_mint(_account, _amount);
}
And the main situation where new LUSD tokens will be minted is through openTrove()
function that is used to borrow.
What is LQTY?
LQTY is the secondary network token of the Liquity borrowing system, and is given out as a reward and incentive to those who make the system work. These include the frontends that complete the transactions, contributors to the stability pool, and liquidity providers. These are the only ways to earn LQTY.
The LQTY contracts consist of:
LQTYStaking.sol
— :
the staking contract, containing stake and unstake functionality for LQTY holders.
This contract receives ETH fees from redemptions and LUSD fees from new debt issuance.
CommunityIssuance.sol
— :
This contract handles the issuance of LQTY tokens to Stability Providers as a function of time.
function issueLQTY() external override returns (uint) {
_requireCallerIsStabilityPool();
uint latestTotalLQTYIssued = LQTYSupplyCap.mul(_getCumulativeIssuanceFraction()).div(DECIMAL_PRECISION);
uint issuance = latestTotalLQTYIssued.sub(totalLQTYIssued);
totalLQTYIssued = latestTotalLQTYIssued;
emit TotalLQTYIssuedUpdated(latestTotalLQTYIssued);
return issuance;
}
It is controlled by the
StabilityPool
. You can see that because of the use of
_requireCallerIsStabilityPool();
The contract also issues these LQTY tokens to the Stability Providers over time. And is transferring them with
function sendLQTY(address _account, uint _LQTYamount) external override {
_requireCallerIsStabilityPool();
lqtyToken.transfer(_account, _LQTYamount);
}
LQTYToken.sol
—:
This is the LQTY ERC20 contract.
It has a hard cap supply of 100 million. It is interesting how it is minting them:
// Allocate 2 million for bounties/hackathons
_mint(_bountyAddress, bountyEntitlement);
uint bountyEntitlement = _1_MILLION.mul(2);
// Allocate 32 million to the algorithmic issuance schedule
uint depositorsAndFrontEndsEntitlement = _1_MILLION.mul(32);
_mint(_communityIssuanceAddress, depositorsAndFrontEndsEntitlement);
// Allocate 1.33 million for LP rewards
uint _lpRewardsEntitlement = _1_MILLION.mul(4).div(3);
lpRewardsEntitlement = _lpRewardsEntitlement;
_mint(_lpRewardsAddress, _lpRewardsEntitlement);
// Allocate the remainder to the LQTY Multisig: (100 - 2 - 32 - 1.33) million = 64.66 million
uint multisigEntitlement = _1_MILLION.mul(100)
.sub(bountyEntitlement)
.sub(depositorsAndFrontEndsEntitlement)
.sub(_lpRewardsEntitlement);
_mint(_multisigAddress, multisigEntitlement);
What is a Trove?
A Trove is where you take out and maintain your loan. Each Trove is linked to an Ethereum address and each address can have just one Trove.
If you are familiar with Vaults or CDPs from other platforms, Troves are similar in concept.
struct Trove {
uint debt;
uint coll;
uint stake;
Status status;
uint128 arrayIndex;
}
Troves maintain two balances: one is an asset (ETH) acting as collateral, and the other is a debt denominated in LUSD.
You can change the amount of each by adding collateral or repaying debt.
You can close your Trove at any time by fully paying off your debt.
The smart contracts involved:
TroveManager.sol
Contains functionality for liquidations and redemptions.
In this enum
, which is used in the events of the respective functions, we can see the names of some of the main functions.
enum TroveManagerOperation {
applyPendingRewards,
liquidateInNormalMode,
liquidateInRecoveryMode,
redeemCollateral
}
There are different “Trove Liquidation functions”:
// Single liquidation function.
// Closes the trove if its ICR is lower than the minimum collateral ratio.
function liquidate(address _borrower) external
// Liquidate one trove, in Normal Mode.
function _liquidateNormalMode(
IActivePool _activePool,
IDefaultPool _defaultPool,
address _borrower,
uint _LUSDInStabPool
)
internal
returns (LiquidationValues memory singleLiquidation)
// Liquidate one trove, in Recovery Mode.
function _liquidateRecoveryMode(
IActivePool _activePool,
IDefaultPool _defaultPool,
address _borrower,
uint _ICR,
uint _LUSDInStabPool,
uint _TCR,
uint _price
)
internal
returns (LiquidationValues memory singleLiquidation)
/*
* Liquidate a sequence of troves. Closes a maximum number of
* n under-collateralized Troves,
* starting from the one with the lowest collateral ratio in the system,
* and moving upwards
*/
function liquidateTroves(uint _n) external
It sends redemption fees to the LQTYStaking
contract in the function redeemCollateral
function redeemCollateral(
uint _LUSDamount,
address _firstRedemptionHint,
address _upperPartialRedemptionHint,
address _lowerPartialRedemptionHint,
uint _partialRedemptionHintNICR,
uint _maxIterations,
uint _maxFeePercentage
)
external
TroveManager
contract stores its data in ContractsCache
ContractsCache memory contractsCache = ContractsCache(
activePool,
defaultPool,
lusdToken,
lqtyStaking,
sortedTroves,
collSurplusPool,
gasPoolAddress
);
and while redeeming the collateral it will use them for the following actions:
// To confirm redeemer's balance is less than total LUSD supply
assert(contractsCache.lusdToken.balanceOf(msg.sender) <= totals.totalLUSDSupplyAtStart);
// To add the borrowers's coll and debt rewards earned from redistributions,
// to their Trove
_applyPendingRewards(
contractsCache.activePool,
contractsCache.defaultPool,
currentBorrower
);
// To send the ETH fee to the LQTY staking contract
contractsCache.activePool.sendETH(address(contractsCache.lqtyStaking), totals.ETHFee);
contractsCache.lqtyStaking.increaseF_ETH(totals.ETHFee);
Also contains the state of each Trove - i.e., a record of the Trove’s collateral and debt.
And the status is defined in an enum, which is passed as a parameter to internal functions like
function _closeTrove(address _borrower, Status closedStatus)
or also in the struct Trove
mentioned above.
enum Status {
nonExistent,
active,
closedByOwner,
closedByLiquidation,
closedByRedemption
}
TroveManager does not hold value (i.e. Ether / other tokens).
TroveManager functions call in to the various Pools to tell them to move Ether/tokens between Pools, where necessary.
Keep reading with a 7-day free trial
Subscribe to DeFi Protocol by code to keep reading this post and get 7 days of free access to the full post archives.