Theoretical-Practical: Lybra Finance Part 1

KamiWar
Coinmonks
8 min readJun 22, 2023

--

In my search for yield, I have recently stumbled onto Lybra Protocol. Lybra Protocol is a DeFi staking yield protocol that leverages Lido issued LSDs (Liquid Staking Derivatives) to allow users to mint and borrow eUSD by collateralizing their ETH and stETH.

Lybra’s value proposition from other traditional stablecoins is eUSD’s interest-bearing nature of APY of ~8.1% along with the fact that borrowing eUSD on their platform incurs zero interest fees. eUSD joins its peers such as Dai under the umbrella of crypto-collateralized stablecoins. The token itself is hard-pegged to the US dollar and maintains said peg via a collateral ratio of 150% exclusively through ETH & stETH. The protocol defines the collateral ratio as the ratio between the dollar value of a user’s collateral within the Lybra ecosystem and their respective loan in eUSD. The minimum collateral ratio is 150% and any user whose ratio falls below this threshold will be at risk of liquidation.

Lastly, the protocol itself contains a native governance token in the form of an ERC-20 token called LBR. LBR holders participate in governance and voting initiatives while also enjoying fees and rewards.

In this new series of articles, we will begin a deep dive into Lybra’s smart contracts with two main objectives: a critical examination into the control flow of the protocol itself in a typical end user interaction, and more importantly, how Lybra’s purported interest of 8% APY is actually generated.

The primary contract we will be examining will be the Lybra contract located at 0x97de57eC338AB5d51557DA3434828C5DbFaDA371.

The first call that is invoked when this contract was created is the constructor which sets the gov variable to be msg.sender. We can see that this particular variable is inherited from the Governable contract within the same directory. Crucially, the Governable contract provides an onlyGov modifier to prevent other third parties from calling certain protocol critical functions within Lybra which will be discussed at a later juncture.

From the perspective of the end user however, the first function that will be of actual relevance will likely be depositEtherToMint.

function depositStETHToMint(
address onBehalfOf,
uint256 stETHamount,
uint256 mintAmount
) external {
require(onBehalfOf != address(0), "DEPOSIT_TO_THE_ZERO_ADDRESS");
require(stETHamount >= 1 ether, "Deposit should not be less than 1 stETH.");
lido.transferFrom(msg.sender, address(this), stETHamount);

totalDepositedEther += stETHamount;
depositedEther[onBehalfOf] += stETHamount;
if (mintAmount > 0) {
_mintEUSD(onBehalfOf, onBehalfOf, mintAmount);
}
emit DepositEther(msg.sender, onBehalfOf, stETHamount, block.timestamp);
}

As we can see from above function signature, any address can invoke this function to deposit ETH on behalf of another address(onBehalfOf). The function begins by imposing certain requirements; depositEtherToMint cannot be called on behalf of an invalid address and the caller must supply at least 1 ETH when calling this function.

Once these requirements are passed, the function will call the submit method on the lido state variable that is initialized towards the beginning of Lybra. The address in the code block below points lido to Lido’s official Staked ETH Token contract so that the ETH sent by the caller is converted into stETH and encapsulated within the sharesAmount variable. There is also a further check here to ensure that sharesAmount is greater than 0 before any further code execution.

Ilido lido = Ilido(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);

The contract increments the totalDepositedEther state variable by the amount of ETH supplied. onBehalfOf’s key within the depositedEther mapping (which maps an address to an uint256 ) is also incremented by the same amount.

One small observation to note is that all deposits of ETH into Lybra will always be converted into stETH. Therefore, the assertion that eUSD is collateralized by ETH and stETH is somewhat murky since the protocol does not actually seem to hold any ETH. For this same reason, the variable names totalDepositedEther and depositedEther are both misnomers given the lack of actual held ETH. Finally, the function calls the _mintEUSD function supplying onBehalfOf and mintAmount variables as parameters with onBehalfOf being supplied twice.

Before we move on to examine _mintEUSD, we also need to take a quick look at depositStETHToMint since an end user can also deposit stETH instead of ETH to mint eUSD.

  function depositStETHToMint(
address onBehalfOf,
uint256 stETHamount,
uint256 mintAmount
) external {
require(onBehalfOf != address(0), "DEPOSIT_TO_THE_ZERO_ADDRESS");
require(stETHamount >= 1 ether, "Deposit should not be less than 1 stETH.");
lido.transferFrom(msg.sender, address(this), stETHamount);

totalDepositedEther += stETHamount;
depositedEther[onBehalfOf] += stETHamount;
if (mintAmount > 0) {
_mintEUSD(onBehalfOf, onBehalfOf, mintAmount);
}
emit DepositEther(msg.sender, onBehalfOf, stETHamount, block.timestamp);
}

While depositEtherToMint and depositStETHToMint share some similarities, the primary difference is that there is a call to lido’s transferFrom which transfers the input amount of stETHamount from msg.sender(caller) to Lybra. Similar to above, totalDepositedEther and depositedEther are both incremented by stETHamount to reflect the new infusion of stETH which is followed by invocation of _mintEUSD.

    function _mintEUSD(
address _provider,
address _onBehalfOf,
uint256 _amount
) internal {
uint256 sharesAmount = getSharesByMintedEUSD(_amount);
if (sharesAmount == 0) {
//EUSD totalSupply is 0: assume that shares correspond to EUSD 1-to-1
sharesAmount = _amount;
}
eslbrMinter.refreshReward(_provider);
borrowed[_provider] += _amount;

_mintShares(_onBehalfOf, sharesAmount);

_saveReport();
totalEUSDCirculation += _amount;
_checkHealth(_provider);
emit Mint(msg.sender, _onBehalfOf, _amount, block.timestamp);
}

The first line of the function calls getSharesByMintedEUSD (inherited from the EUSD contract)with _amount(derived from mintAmount in Lybra) to calculate the sharesAmount variable. sharesAmount represents the amount of shares that corresponds to _amount(getSharesByMintedEUSD interprets this as _EUSDAmount as seen below) that should be minted for the end user.

Before we proceed any further, we will summarize how each end user’s stake within Lybra is represented. The total supply of eUSD is dynamic; it increases or decreases as end users mint or repay eUSD. Because of this, an end user’s stake within the whole protocol is represented by shares which, similar to the supply of eUSD, are subject to change as the end user mints or repays eUSD. Each end user’s stake within the protocol is represented within EUSD by the shares state variable (a private mapping that maps uint256 ⇒ address).

An end user’s token balance within Lybra Protocol is calculated by multiplying the end user’s shares by the ratio between the total supply of eUSD and the total amount of shares:

shares[account] * _getTotalMintedEUSD() / _getTotalShares()
function getSharesByMintedEUSD(
uint256 _EUSDAmount
) public view returns (uint256) {
uint256 totalMintedEUSD = _getTotalMintedEUSD();
if (totalMintedEUSD == 0) {
return 0;
} else {
return _EUSDAmount.mul(_getTotalShares()).div(totalMintedEUSD);
}
}

We calculate the total amount of minted eUSD (totalMintedEUSD) by calling _getTotalMintedEUSD which is inherited and defined by Lybra. This is a view function that returns the state variable totalEUSDCirculation.

If totalMintedEUSD is 0, the function return 0. Otherwise, we return the product between _EUSDAmount and the quotient of _getTotalShares (a view function that returns totalShares) and totalMintedEUSD.

The calculation above seems to be an inverse of the token balance calculation above. In this case, the total shares is divided by the total supply of eUSD which makes sense as we are not calculating the end user’s token balance but rather calculating how many additional shares in the protocol should be minted for the end user given _*EUSDAmount.

After this, there is a call to the refreshReward function on the eslbrMinter state variable passing in _provider. We will not be doing a substantive review of this function and LBR in this article as this will be reserved for a future section. However, the utility of this function seems to be to update the reward when the end user’s borrowed eUSD amount is altered.

Prior to minting the shares, the end user’s debt obligation is recorded within the borrowed mapping (address ⇒ uint256) and incremented by _amount.

We call the _mintShares function from EUSD passing in _onBehalfOf and sharesAmount as _recipient and _sharesAmount respectively. Once the require statement is passed, we create the newTotalShares variable which encapsulates the total amount of current shares in EUSD plus the additional amount being minted in _sharesAmount.

    function _mintShares(
address _recipient,
uint256 _sharesAmount
) internal returns (uint256 newTotalShares) {
require(_recipient != address(0), "MINT_TO_THE_ZERO_ADDRESS");

newTotalShares = _getTotalShares().add(_sharesAmount);
totalShares = newTotalShares;

shares[_recipient] = shares[_recipient].add(_sharesAmount);
}

The totalShares variable is override by newTotalShares and the shares are created by incrementing the recipient’s value within shares by _sharesAmount.

The _saveReport() function is invoked next which increments the fees owed to the protocol (feeStored) by the return value of _newFee while updating the lastReportTime to reflect the current block timestamp.

function _saveReport() internal {
feeStored += _newFee();
lastReportTime = block.timestamp;
}

The new fee is as calculated as follows:

function _newFee() internal view returns (uint256) {
return
(totalEUSDCirculation *
mintFeeApy *
(block.timestamp - lastReportTime)) /
year /
10000;
}

We derive the product of the total supply of eUSD (totalEUSDCirculation), the mint fee APY (mintFeeApy) and the duration between the current time minus the last time a report was saved (block.timestamp - lastReportTime).

The result is then divided by year to convert the duration from seconds to years and finally divided by 10000 to convert the interest rate to a decimal format.

One last note regarding mintFeeApy, this rate can be amended by gov so long as it does not exceed 150 or 1.5%.

We increment the totalEUSDCirculation state variable by _amount which keeps a tally of the total supply of eUSD that has been minted.

Lastly, there is a _checkHealth function that assess whether collateral ratio of the provider is actually in line with protocol rules.

   function _checkHealth(address user) internal {
if (
((depositedEther[user] * _etherPrice() * 100) / borrowed[user]) <
safeCollateralRate
) revert("collateralRate is Below safeCollateralRate");
}

If the end user’s collateral ratio is less than safeCollateralRate, that is less than 160%, the transaction will revert and no eUSD will be minted. Without this function, an end user could simply call Lybra to mint an arbitrarily large amount of eUSD while providing 1 ETH which would have the effect of threatening the stability of eUSD and its peg.

There is a mint function which allows for the end user to mint eUSD without the provision of any additional collateral. An astute reader may realize that this function is not entirely risk free as minting eUSD will lower the end user’s collateral ratio leading to a greater chance of liquidation.

function mint(address onBehalfOf, uint256 amount) public {
require(onBehalfOf != address(0), "MINT_TO_THE_ZERO_ADDRESS");
require(amount > 0, "ZERO_MINT");
_mintEUSD(msg.sender, onBehalfOf, amount);
if (
(borrowed[msg.sender] * 100) / totalSupply() > 10 &&
totalSupply() > 10_000_000 * 1e18
) revert("Mint Amount cannot be more than 10% of total circulation");
}

The other side of mint is the withdraw function which allows an end user to withdraw their stETH from the protocol at the expense of a riskier collateral ratio.

    function withdraw(address onBehalfOf, uint256 amount) external {
require(onBehalfOf != address(0), "WITHDRAW_TO_THE_ZERO_ADDRESS");
require(amount > 0, "ZERO_WITHDRAW");
require(depositedEther[msg.sender] >= amount, "Insufficient Balance");
totalDepositedEther -= amount;
depositedEther[msg.sender] -= amount;

lido.transfer(onBehalfOf, amount);
if (borrowed[msg.sender] > 0) {
_checkHealth(msg.sender);
}
emit WithdrawEther(msg.sender, onBehalfOf, amount, block.timestamp);
}

The code is rather simple; once we pass the require statements, totalDepositedEther and depositedEther are both decremented by the amount specified by the caller. There is a call on lido to transfer the amount to onBehalfOf and the protocol examines the collateral ratio of the caller via _checkHealth if the end user still has a debt obligation to the protocol within borrowed.

Finally, we have the burn function where the end user fulfils their debt obligation to the protocol and eUSD is burned. As one can see below, burn holds little actual logic and is more or less a wrapper function that calls the internal _repay function to implement the actual burn of eUSD. Due to the length of this article, we will not be exploring the logic of _repay right now.

    function burn(address onBehalfOf, uint256 amount) external {
require(onBehalfOf != address(0), "BURN_TO_THE_ZERO_ADDRESS");
_repay(msg.sender, onBehalfOf, amount);
}

In sum, we have established a succinct but hopefully helpful guide that lays out the control flow when an end user initiates a typical transaction to mint eUSD with Lybra Protocol. In Part 2 of this series, we will begin a thorough examination into the finances of the protocol, namely how does Lybra make money and where does this purpoted 8% APY actually come from?

Stay tuned!

New to trading? Try crypto trading bots or copy trading on the best crypto exchanges

Also Read

Join Coinmonks Telegram Channel and Youtube Channel to get daily Crypto News

--

--