1 Executive Summary
This report presents the results of our engagement with Lybra Finance Team to review the Lybra Protocol.
The review was conducted over four and a half weeks, from July 10, 2023 to Aug 9, 2023, by George Kobakhidze and Sergii Kravchenko. A total of 7 person-weeks were spent.
The Lybra Protocol is a DeFi system that focuses on collateral-based stablecoin lending and management. A particular feature of this protocol is its focus on the stablecoin holders and Liquid Staking Derivative tokens for Ethereum staking as collateral. The resulting product of the system is an interest-bearing rebasing stablecoin called EUSD
. With the V2 version of the protocol, which is what is in the scope of this audit, the system introduces more LSD tokens along with Lido’s stETH
from the first version such as those from RocketPool and Binance. Lybra Protocol V2 also introduces PeUSD
a non-rebasing USD stablecoin that features LayerZero’s Omnichain capabilities. PeUSD
may be minted with EUSD
or with non-rebasing LSD tokens that are introduced in the V2. PeUSD
may also accommodate easier integrations with other DeFi protocols as the rebasing EUSD
may cause accounting issues.
The repository is organized and the code is written well with plentiful comments and elegant design choices. As Lybra Protocol is a complex system, users should expect to understand and digest a multitude of parameters, many of which may be changed by governance and many of which are hardcoded and may not.
At the end of the audit, the engagement was extended to verify fixes and some other code changes made during the audit.
2 Scope
Our review focused on the commit hash 48c98f288c77f57fa17e87964394f98e1e2ee636. The list of files in scope can be found in the Appendix.
Following the audit, all commits between 48c98f288c77f57fa17e87964394f98e1e2ee636 and 90285107de8a6754954c303cd69d97b5fdb4e248 were reviewed as well.
2.1 Objectives
Together with the Lybra Finance team, we identified the following priorities for our review:
- Correctness of the implementation, consistent with the intended functionality and without unintended edge cases.
- Identify known vulnerabilities particular to smart contract systems, as outlined in our Smart Contract Best Practices, and the Smart Contract Weakness Classification Registry.
- Review the logic of all vault types and the implementation of their core functions such as depositing, borrowing, withdrawing, liquidating, redeeming, and distributing excess income through interest realization for rebase assets.
- The Governance checks for proposals and voting are not bypassable by a malicious actor.
- Permission checks in the
LybraConfigurator
contract work as intended. - The rewards staking, mining, and distribution logic work as intended in contracts such as
ProtocolRewardsPool
andEUSDMiningIncentives
.
3 System Overview
Lybra is an interest-bearing stablecoin protocol backed by different LSD (liquid staking derivatives).
3.1 Stablecoins
EUSD
- anERC-20
rebase token representing a yield-bearing stablecoin.peUSD
- a regularERC-20
stablecoin that can be converted fromEUSD
. ThepeUSD
token doesn’t earn any interest, but users that convertEUSD
will still be getting interest from their depositedEUSD
tokens. Also has an LayerZero Omnichain component making it a OFTV2 token as well for multi-chain capabilities.
3.2 Vaults
There are two types of vaults in the protocol. One of them takes a LSD as collateral and mints EUSD
(LybraEUSDVaultBase
), and the other type mints peUSD
(LybraPeUSDVaultBase
). The following vaults are implemented:
LybraStETHVault
- takesstETH
(stakedETH
by Lido) as collateral and mintsEUSD
.LybraWstETHVault
- takesWstETH
(wrappedstETH
) as collateral and mintspeUSD
.LybraRETHVault
- takesRETH
(Rocket PoolETH
) as collateral and mintspeUSD
.LybraWbETHVault
- takesWBETH
(Wrapped BinanceETH
) as collateral and mintspeUSD
.
The main difference between peUSD
and EUSD
vaults is that the collateral of the EUSD
vault is a rebase token. So for peUSD
vaults, every depositor’s collateral value is supposed to grow over time due to ETH staking rewards. Unlike that, every individual stETH
collateral deposit isn’t directly growing due to its rebase nature. So the excess of stETH
tokens in the vault contract is sold for EUSD
to anyone willing to buy it. The shares of the profit are burnt in favor of every EUSD
holder, which increases their balances due to the token’s rebase nature.
3.3 Mining
The system has two reward pools that are forked from Synthetix StakingRewards with some changes made:
EUSDMiningIncentives
- a staking contract that rewards borrowers ofEUSD
andpeUSD
. The users are getting rewards in the form ofesLBR
(escrowedLBR
tokens).esLBRBoost
- a contract allowing users to lock theirLBR
in exchange for boosted rewards inEUSDMiningIncentives
.ProtocolRewardsPool
- a staking contract that rewards holders ofesLBR
withpeUSD
or other external stablecoins.
3.4 System Diagram
Please find below a non-exhaustive diagram of the Lybra Protocol system that focuses on calls and data flow within the system.
- Configurator - Miner.
- Reward logic such as
notifyRewardAmount
andrefreshReward
.
- Reward logic such as
- Configurator - Governance.
- Access control checks like
checkRole
andcheckOnlyRole
.
- Access control checks like
- Configurator - Vaults.
- Vault information such as
getVaultType
.
- Vault information such as
- Configurator - Tokens.
- Token transfer logic that happens during reward distribution such as
approve
andsafeTransfer
, as well asconvertToPeUSD
.
- Token transfer logic that happens during reward distribution such as
- Governance - Tokens.
- Voting logic, specifically
getPastVotes
fromesLBR
tokens.
- Voting logic, specifically
- Vaults - Configurator.
- Parameter retrievals such as
getEUSDAddress
,getPeUSDAddress
,isRedemptionProvider
,mintVaultMaxSupply
, and others for successful vault management. - Reward distribution logic through calls like
refreshMintReward
anddistributeReward
.
- Parameter retrievals such as
- Vaults - Tokens.
- Token administration logic such as
transfer
andallowance
calls but also highly privileged methods such asburn
andmint
.
- Token administration logic such as
- Tokens - Configurator.
- Vault parameter retrieval such as
vaultMintPaused
,mintVault[]
,getProtocolRewardsPool
,tokenMiner
.
- Vault parameter retrieval such as
- Tokens - Miner.
- Reward logic through calls such as
refreshReward
.
- Reward logic through calls such as
- Miner - Configurator.
- Information retrieval such as
getEUSDAddress
,isRedemptionProvider
,mintVault[]
.
- Information retrieval such as
- Miner - Vaults.
- Token and vault information retrieval such as
getPoolTotalCirculation
andgetBorrowedOf
.
- Token and vault information retrieval such as
- Miner - Tokens.
- Token information retrieval such as
balanceOf
,totalSupply
, andgetUserBoost
. - Token administration such as
transfer
and highly privileged calls such asmint
andburn
.
- Token information retrieval such as
- Miner - Miner.
- External calls between miner contracts such as
totalSupply
,totalStaked
,stakedOf
, anduserLockStatus
.
- External calls between miner contracts such as
4 Security Specification
This section describes, from a security perspective, the expected behavior of the system under audit. It is not a substitute for documentation. The purpose of this section is to identify specific security properties that were validated by the audit team.
4.1 Actors
The relevant actors are listed below with their respective abilities:
- Lybra Finance team - contract owners and deployers. The contract team currently manages the deployment and initial configuration of the contracts.
- Lybra Governance.
LBR
(andesLBR
) token holders who can to vote, pass, and execute proposals. - Vault owners. The users that supply the collateral and leverage it for minting the Lybra Protocol’s stablecoins.
- Token holders. The end-user holders of the
EUSD
andPeUSD
tokens. - Keepers. Users and/or bots that call functions such as excess income distribution and vault liquidations of those vaults that are below the necessary collateral ratios.
- Price Oracles. Third-party contracts that provide price feeds for various tokens for the Lybra Protocol.
4.2 Trust Model
In any system, it’s important to identify what trust is expected/required between various actors. For this audit, we established the following trust model:
- Lybra Finance team (contract owners and deployers). In the current scope of this protocol, there is in fact a large amount of trust put into the deployer of the contracts to initialize them properly. As seen in the System Overview section, the
LybraConfigurator
contract, for example, contains numerous mission-critical variables that are used by other contracts in the system. Setting those up correctly in the beginning is the responsibility of the deployer. However, as this is all on a public blockchain, it would be possible to identify if the Lybra deployer has made a mistake in the configurations, so this is not a significant issue vector. That being said, it is important to point out that upon deployment of theGovernanceTimelock
contract, themsg.sender
explicitly gets aDAO
and aGOV
role, the former of which also allows it to bypass any checks that are protected by thecheckRole
function as it specifically allows aDAO
role to go through. As a result, there is significant trust involved with the Lybra deployer for him not to get compromised as it could be disastrous for the system. - Lybra Governance. As with most DAOs, the holders of governance tokens (
LBR
andesLBR
) are trusted with voting, passing, and executing proposals as they see fit. As a result, the Governance is trusted not to pass malicious proposals. - Vault owners. No particular trust assumptions are made about the vault owners. They are expected to maintain a healthy collateral-to-borrow ratio. However, if they fail to do so, there are incentive mechanisms to enable other users, such as keepers, to liquidate such vaults. Vault owners can also become redemption providers to earn additional fees and receive a boost in their rewards, so other users can use their vaults to redeem tokens directly. However, if the vault owner is marked as a redemption provider but in fact doesn’t have enough liquidity to support a redemption, it will simply revert and not stop the overall system from working.
- Token holders. Similarly, no specific trust assumptions are made for token holders. The interactions they can make with the protocol are all trustless.
- Keepers. While they are not explicitly trusted to maintain the Lybra Protocol system, keepers are crucial for the overall protocol health as they run liquidations and assist in staking reward distribution through purchases of excess amounts of Liquid Staking Tokens for
EUSD
. Industry-standard mechanisms are employed to get the keepers to interact with the system, such as Dutch Auction discounts, additional keeper fees, and so on. As a result, the overall user base of the chain where the Lybra Protocol is deployed is assumed to have enough rational actors to execute the incentives and move the mechanisms forward.
4.3 Security Properties
The following is a non-exhaustive list of security properties that were identified in this audit:
- Hardcoded properties. The Lybra Protocol is complex and maintains many of properties necessary for its operations, such as collateral ratios, fee ratios, boosts, Dutch Auction settings, and so on. Some of these are set by the Governance through the
LybraConfigurator
contract but some of them are hardcoded in the system. It is important for users to identify if they are comfortable with both potentially changing parameters by the governance and hardcoded parameters that will not change throughout the lifecycle of the system. - Proxy. From the documentation, the comments, and the files in scope, it appears that a proxy deployment specifically for the
LybraConfigurator
contract might be possible. As mentioned throughout the report, this is a mission-critical contract and users should be aware of risks associated with its upgradeability as a faulty upgrade might critically damage the system. - Lybra Deployer. As mentioned above, the Lybra Deployer is explicitly given the
DAO
andGOV
roles upon deploying theGovernanceTimelock
contract. While theGOV
role is the admin of other roles, theDAO
role allows the owner of this private key to do almost anything they want with the system. For example, they can set a new contract to be aLBR
miner, mint as manyLBR
tokens as they want, and then take full control of the DAO’s governance. However unlikely, a malicious actor may compromise these keys, so this is a risk users need to take into account. It is understandable to approach the deployment of this system with some safeguards like this in the beginning that would allow the Lybra Finance team to surgically address issues such as those in configurations. However, it would be critical to then revoke theDAO
role from that private address as the system matures. Otherwise, this poses a significant risk.
5 Post Audit Verifications
During the verification week after the audit, the following points have been identified as potential improvements to the additional changes provided:
- Improve the logic of the rigidRedemption commit. The
checkWithdrawal
check is introduced to ensure collateral doesn’t leave the system within the 3 day period of having been deposited. However, the check is made on the user requested the rigid redemption whereas the collateral comes from the provider. Therefore, it seems more applicable to apply thecheckWithdrawal
check on theprovider
. - Improve the logic of the rigidRedemption commit. The emitted event
RigidRedemption
has an incorrect amount emitted. If the logic of thewithdraw
function in the same contract is to be followed, the event should emit the amount that is corrected by thecheckWithdrawal
function instead of what the user first requested to withdraw (or rigid redeem). - fix getVaultWeight logic commit. The vault is now checked to be active if there is no special weight assigned to it. However, if weight is assigned but it is disabled (so
mintVault
gives false), this will still return a value, which may be in conflict with the new change that appears to only want to give avaultWeight
back if it is enabled.
6 Findings
Each issue has an assigned severity:
- Minor issues are subjective in nature. They are typically suggestions around best practices or readability. Code maintainers should use their own judgment as to whether to address such issues.
- Medium issues are objective in nature but are not security vulnerabilities. These should be addressed unless there is a clear reason not to.
- Major issues are security vulnerabilities that may not be directly exploitable or may require certain conditions in order to be exploited. All major issues should be addressed.
- Critical issues are directly exploitable security vulnerabilities that need to be fixed.
6.1 Re-Entrancy Risks Associated With External Calls With Other Liquid Staking Systems. Major ✓ Fixed
Resolution
Description
As part of the strategy to integrate with Liquid Staking tokens for Ethereum staking, the Lybra Protocol vaults are required to make external calls to Liquid Staking systems.
For example, the depositEtherToMint
function in the vaults makes external calls to deposit Ether and receive the LSD tokens back. While external calls to untrusted third-party contracts may be dangerous, in this case, the Lybra Protocol already extends trust assumptions to these third parties simply through the act of accepting their tokens as collateral. Indeed, in some cases the contract addresses are even hardcoded into the contract and called directly instead of relying on some registry:
contracts/lybra/pools/LybraWstETHVault.sol:L21-L40
contract LybraWstETHVault is LybraPeUSDVaultBase {
Ilido immutable lido;
//WstETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;
//Lido = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
constructor(address _lido, address _asset, address _oracle, address _config) LybraPeUSDVaultBase(_asset, _oracle, _config) {
lido = Ilido(_lido);
}
function depositEtherToMint(uint256 mintAmount) external payable override {
require(msg.value >= 1 ether, "DNL");
uint256 sharesAmount = lido.submit{value: msg.value}(address(configurator));
require(sharesAmount != 0, "ZERO_DEPOSIT");
lido.approve(address(collateralAsset), msg.value);
uint256 wstETHAmount = IWstETH(address(collateralAsset)).wrap(msg.value);
depositedAsset[msg.sender] += wstETHAmount;
if (mintAmount > 0) {
_mintPeUSD(msg.sender, msg.sender, mintAmount, getAssetPrice());
}
emit DepositEther(msg.sender, address(collateralAsset), msg.value,wstETHAmount, block.timestamp);
}
In that case, depending on the contract, it may be known what contract is being called, and the risk may be assessed as far as what logic may be executed.
However, in the cases of BETH
and rETH
, the calls are being made into a proxy and a contract registry of a DAO (RocketPool’s DAO) respectively.
contracts/lybra/pools/LybraWbETHVault.sol:L15-L32
contract LybraWBETHVault is LybraPeUSDVaultBase {
//WBETH = 0xa2e3356610840701bdf5611a53974510ae27e2e1
constructor(address _asset, address _oracle, address _config)
LybraPeUSDVaultBase(_asset, _oracle, _config) {}
function depositEtherToMint(uint256 mintAmount) external payable override {
require(msg.value >= 1 ether, "DNL");
uint256 preBalance = collateralAsset.balanceOf(address(this));
IWBETH(address(collateralAsset)).deposit{value: msg.value}(address(configurator));
uint256 balance = collateralAsset.balanceOf(address(this));
depositedAsset[msg.sender] += balance - preBalance;
if (mintAmount > 0) {
_mintPeUSD(msg.sender, msg.sender, mintAmount, getAssetPrice());
}
emit DepositEther(msg.sender, address(collateralAsset), msg.value,balance - preBalance, block.timestamp);
}
contracts/lybra/pools/LybraRETHVault.sol:L25-L42
constructor(address _rocketStorageAddress, address _rETH, address _oracle, address _config)
LybraPeUSDVaultBase(_rETH, _oracle, _config) {
rocketStorage = IRocketStorageInterface(_rocketStorageAddress);
}
function depositEtherToMint(uint256 mintAmount) external payable override {
require(msg.value >= 1 ether, "DNL");
uint256 preBalance = collateralAsset.balanceOf(address(this));
IRocketDepositPool(rocketStorage.getAddress(keccak256(abi.encodePacked("contract.address", "rocketDepositPool")))).deposit{value: msg.value}();
uint256 balance = collateralAsset.balanceOf(address(this));
depositedAsset[msg.sender] += balance - preBalance;
if (mintAmount > 0) {
_mintPeUSD(msg.sender, msg.sender, mintAmount, getAssetPrice());
}
emit DepositEther(msg.sender, address(collateralAsset), msg.value,balance - preBalance, block.timestamp);
}
As a result, it is impossible to make any guarantees for what logic will be executed during the external calls. Namely, reentrancy risks can’t be ruled out, and the damage could be critical to the system. While the trust in these parties isn’t in question, it would be best practice to avoid any additional reentrancy risks by placing reentrancy guards. Indeed, in the LybraRETHVault
and LybraWbETHVault
contracts, one can see the possible damage as the calls are surrounded in a preBalance <-> balance
pattern.
The whole of third party Liquid Staking systems’ operations need not be compromised, only these particular parts would be enough to cause critical damage to the Lybra Protocol.
Recommendation
After conversations with the Lybra Finance team, it has been assessed that reentrancy guards are appropriate in this scenario to avoid any potential reentrancy risk, which is exactly the recommendation this audit team would provide.
6.2 The Deployer of GovernanceTimelock
Gets Privileged Access to the System. Major ✓ Fixed
Resolution
GovernanceTimelock
code in commit 77e8bc3664fb1b195fd718c2ce1d49af8530f981 to instead introduce a multisig address that will have the ADMIN
role whose only permission within the configurator contract is to pause the minting function in emergency situations.
Description
The GovernanceTimelock
contract is responsible for Roles Based Access Control management and checks in the Lybra Protocol. It offers two functions specifically that check if an address has the required role - checkRole
and checkOnlyRole
:
contracts/lybra/governance/GovernanceTimelock.sol:L24-L30
function checkRole(bytes32 role, address _sender) public view returns(bool){
return hasRole(role, _sender) || hasRole(DAO, _sender);
}
function checkOnlyRole(bytes32 role, address _sender) public view returns(bool){
return hasRole(role, _sender);
}
In checkRole
, the contract also lets an address with the role DAO
bypass the check altogether, making it a powerful role.
For initial role management, when the GovernanceTimelock
contract gets deployed, its constructor logic initializes a few roles, assigns relevant admin roles, and, notably, assigns the DAO
role to the contract, and the DAO
and the GOV
role to the deployer.
contracts/lybra/governance/GovernanceTimelock.sol:L14-L23
constructor(uint256 minDelay, address[] memory proposers, address[] memory executors, address admin) TimelockController(minDelay, proposers, executors, admin) {
_setRoleAdmin(DAO, GOV);
_setRoleAdmin(TIMELOCK, GOV);
_setRoleAdmin(ADMIN, GOV);
_grantRole(DAO, address(this));
_grantRole(DAO, msg.sender);
_grantRole(GOV, msg.sender);
}
The assignment of such powerful roles to a single private key with the deployer has inherent risks. Specifically in our case, the DAO
role alone as we saw may bypass many checks within the Lybra Protocol, and the GOV
role even has role management privileges.
However, it does make sense to assign such roles at the beginning of the deployment to finish initialization and assign the rest of the roles. One could argue that having access to the DAO
role in the early stages of the system’s life could allow for quick disaster recovery in the event of incidents as well. Though, it is still dangerous to hold privileges for such a system in a single address as we have seen over the last years in security incidents that have to do with compromised keys.
Recommendation
While redesigning the deployment process to account for a lesser-privileged deployer would be ideal, the Lybra Finance team should at least transfer ownership as soon as the deployment is complete to minimize compromised private key risk.
6.3 The configurator.getEUSDMaxLocked()
Condition Can Be Bypassed During a Flashloan Medium ✓ Fixed
Resolution
eUSD
amount after flash loan.
Description
When converting EUSD
tokens to peUSD
, there is a check that limits the total amount of EUSD
that can be converted:
contracts/lybra/token/PeUSDMainnet.sol:L74-L77
function convertToPeUSD(address user, uint256 eusdAmount) public {
require(_msgSender() == user || _msgSender() == address(this), "MDM");
require(eusdAmount != 0, "ZA");
require(EUSD.balanceOf(address(this)) + eusdAmount <= configurator.getEUSDMaxLocked(),"ESL");
The issue is that there is a way to bypass this restriction. An attacker can get a flash loan (in EUSD
) from this contract, essentially reducing the visible amount of locked tokens (EUSD.balanceOf(address(this))
).
Recommendation
Multiple approaches can solve this issue. One would be adding reentrancy protection. Another one could be keeping track of the borrowed amount for a flashloan.
6.4 Liquidation Keepers Automatically Become eUSD
Debt Providers for Other Liquidations. Medium ✓ Fixed
Resolution
msg.sender == provider
, thus removing the danger for solo liquidators that don’t use another provider.
Description
One of the most important mechanisms in the Lybra Protocol is the liquidation of poorly collateralized vaults. For example, if a vault is found to have a collateralization ratio that is too small, a liquidator may provide debt tokens to the protocol and retrieve the vault collateral at a discount:
contracts/lybra/pools/base/LybraEUSDVaultBase.sol:L148-L170
function liquidation(address provider, address onBehalfOf, uint256 assetAmount) external virtual {
uint256 assetPrice = getAssetPrice();
uint256 onBehalfOfCollateralRatio = (depositedAsset[onBehalfOf] * assetPrice * 100) / borrowed[onBehalfOf];
require(onBehalfOfCollateralRatio < badCollateralRatio, "Borrowers collateral ratio should below badCollateralRatio");
require(assetAmount * 2 <= depositedAsset[onBehalfOf], "a max of 50% collateral can be liquidated");
require(EUSD.allowance(provider, address(this)) != 0, "provider should authorize to provide liquidation EUSD");
uint256 eusdAmount = (assetAmount * assetPrice) / 1e18;
_repay(provider, onBehalfOf, eusdAmount);
uint256 reducedAsset = assetAmount * 11 / 10;
totalDepositedAsset -= reducedAsset;
depositedAsset[onBehalfOf] -= reducedAsset;
uint256 reward2keeper;
if (provider == msg.sender) {
collateralAsset.safeTransfer(msg.sender, reducedAsset);
} else {
reward2keeper = (reducedAsset * configurator.vaultKeeperRatio(address(this))) / 110;
collateralAsset.safeTransfer(provider, reducedAsset - reward2keeper);
collateralAsset.safeTransfer(msg.sender, reward2keeper);
}
emit LiquidationRecord(provider, msg.sender, onBehalfOf, eusdAmount, reducedAsset, reward2keeper, false, block.timestamp);
}
To liquidate the vault, the liquidator needs to transfer debt tokens from the provider address, which in turn needs to have had approved allowance of the token for the vault:
contracts/lybra/pools/base/LybraEUSDVaultBase.sol:L154
require(EUSD.allowance(provider, address(this)) != 0, "provider should authorize to provide liquidation EUSD");
The allowance doesn’t need to be large, it only needs to be non-zero. While it is true that in the superLiquidation
function the allowance check is for eusdAmount
, which is the amount associated with assetAmount
(the requested amount of collateral to be liquidated), the liquidator could simply call the maximum of the allowance the provider has given to the vault and then repeat the liquidation process. The allowance does not actually decrease throughout the liquidation process.
contracts/lybra/pools/base/LybraEUSDVaultBase.sol:L191
require(EUSD.allowance(provider, address(this)) >= eusdAmount, "provider should authorize to provide liquidation EUSD");
Notably, this address doesn’t have to be the same one as the liquidator. In fact, there are no checks on whether the liquidator has an agreement or allowance from the provider to use their tokens in this particular vault’s liquidation. The contract only checks to see if the provider has EUSD
allowance for the vault, and how to split the rewards if the provider is different from the liquidator:
contracts/lybra/pools/base/LybraEUSDVaultBase.sol:L162-L168
if (provider == msg.sender) {
collateralAsset.safeTransfer(msg.sender, reducedAsset);
} else {
reward2keeper = (reducedAsset * configurator.vaultKeeperRatio(address(this))) / 110;
collateralAsset.safeTransfer(provider, reducedAsset - reward2keeper);
collateralAsset.safeTransfer(msg.sender, reward2keeper);
}
In fact, this is a design choice of the system to treat the allowance to the vault as an agreement to become a public provider of debt tokens for the liquidation process. It is important to note that there are incentives associated with being a provider as they get the collateral asset at a discount.
However, it is not obvious from documentation at the time of the audit nor the code that an address having a non-zero
EUSD
allowance for the vault automatically allows other users to use that address as a provider. Indeed, many general-purpose liquidator bots use their tokens during liquidations, using the same address for both the liquidator and the provider. As a result, this would put that address at the behest of any other user who would want to utilize these tokens in liquidations. The user might not be comfortable doing this trade in any case, even at a discount.
In fact, due to this mechanism, even during consciously initiated liquidations MEV bots could spot this opportunity and front-run the liquidator’s transaction. A frontrunner could put themselves as the keeper and the original user as the provider, grabbing the reward2keeper
fee and leaving the original address with fewer rewards and failed gas after the liquidation.
Recommendation
While the mechanism is understood to be done for convenience and access to liquidity as a design decision, this could put unaware users in unfortunate situations of having performed a trade without explicit consent. Specifically, the MEV attack vector could be executed and repeated without fail by a capable actor monitoring the mempool. Consider having a separate, explicit flag for allowing others to use a user’s tokens during liquidation, thus also accommodating solo liquidators by removing the MEV attack vector. Consider explicitly mentioning these mechanisms in the documentation as well.
6.5 Use the Same Solidity Version Across Contracts. Minor ✓ Fixed
Resolution
Description
Most contracts use the same Solidity version with pragma solidity ^0.8.17
. The only exception is the StakingRewardsV2
contract which has pragma solidity ^0.8
.
contracts/lybra/miner/stakerewardV2pool.sol:L2
pragma solidity ^0.8;
Recommendation
If all contracts will be tested and utilized together, it would be best to utilize and document the same version within all contract code to avoid any issues and inconsistencies that may arise across Solidity versions.
6.6 Duplication of Bad Collateral Ratio Minor Acknowledged
Resolution
The Lybra Finance team has acknowledged this as a choice by design and provided the following note:
The liquidation ratio for each eUSD vault is fixed, and this has been stated in our docs. Therefore, we will keep it unchanged.
Description
It is possible to set a bad collateral ratio in the LybraConfigurator
contract for any vault:
contracts/lybra/configuration/LybraConfigurator.sol:L137-L141
function setBadCollateralRatio(address pool, uint256 newRatio) external onlyRole(DAO) {
require(newRatio >= 130 * 1e18 && newRatio <= 150 * 1e18 && newRatio <= vaultSafeCollateralRatio[pool] + 1e19, "LNA");
vaultBadCollateralRatio[pool] = newRatio;
emit SafeCollateralRatioChanged(pool, newRatio);
}
But in the LybraEUSDVaultBase
contract, this value is fixed and cannot be changed:
contracts/lybra/pools/base/LybraEUSDVaultBase.sol:L19
uint256 public immutable badCollateralRatio = 150 * 1e18;
This duplication of values can be misleading at some point. It’s better to make sure you cannot change the bad collateral ratio in the LybraConfigurator
contract for some types of vaults.
6.7 Missing Events. Minor ✓ Fixed
Resolution
Description
In a few cases in the Lybra Protocol system, there are contracts that are missing events in significant scenarios, such as important configuration changes like a price oracle change. Consider implementing more events in the below examples.
Examples
- No events in the contract:
contracts/lybra/miner/esLBRBoost.sol:L10-L30
contract esLBRBoost is Ownable {
esLBRLockSetting[] public esLBRLockSettings;
mapping(address => LockStatus) public userLockStatus;
IMiningIncentives public miningIncentives;
// Define a struct for the lock settings
struct esLBRLockSetting {
uint256 duration;
uint256 miningBoost;
}
// Define a struct for the user's lock status
struct LockStatus {
uint256 lockAmount;
uint256 unlockTime;
uint256 duration;
uint256 miningBoost;
}
// Constructor to initialize the default lock settings
constructor(address _miningIncentives) {
- Missing an event during a premature unlock:
contracts/lybra/miner/ProtocolRewardsPool.sol:L125-L135
function unlockPrematurely() external {
require(block.timestamp + exitCycle - 3 days > time2fullRedemption[msg.sender], "ENW");
uint256 burnAmount = getReservedLBRForVesting(msg.sender) - getPreUnlockableAmount(msg.sender);
uint256 amount = getPreUnlockableAmount(msg.sender) + getClaimAbleLBR(msg.sender);
if (amount > 0) {
LBR.mint(msg.sender, amount);
}
unstakeRatio[msg.sender] = 0;
time2fullRedemption[msg.sender] = 0;
grabableAmount += burnAmount;
}
- Missing events for setting important configurations such as
setToken
,setLBROracle
, andsetPools
:
contracts/lybra/miner/EUSDMiningIncentives.sol:L87-L102
function setToken(address _lbr, address _eslbr) external onlyOwner {
LBR = _lbr;
esLBR = _eslbr;
}
function setLBROracle(address _lbrOracle) external onlyOwner {
lbrPriceFeed = AggregatorV3Interface(_lbrOracle);
}
function setPools(address[] memory _vaults) external onlyOwner {
require(_vaults.length <= 10, "EL");
for (uint i = 0; i < _vaults.length; i++) {
require(configurator.mintVault(_vaults[i]), "NOT_VAULT");
}
vaults = _vaults;
}
- Missing events for setting important configurations such as
setRewardsDuration
andsetBoost
:
contracts/lybra/miner/stakerewardV2pool.sol:L121-L130
// Allows the owner to set the rewards duration
function setRewardsDuration(uint256 _duration) external onlyOwner {
require(finishAt < block.timestamp, "reward duration not finished");
duration = _duration;
}
// Allows the owner to set the boost contract address
function setBoost(address _boost) external onlyOwner {
esLBRBoost = IesLBRBoost(_boost);
}
- Missing event during what is essentially staking
LBR
intoesLBR
(such as inProtocolRewardsPool.stake()
). Consider an appropriate event here such asStakeLBR
:
contracts/lybra/miner/esLBRBoost.sol:L55-L58
if(useLBR) {
IesLBR(miningIncentives.LBR()).burn(msg.sender, lbrAmount);
IesLBR(miningIncentives.esLBR()).mint(msg.sender, lbrAmount);
}
Recommendation
Implement additional events as appropriate.
6.8 Incorrect Interfaces Minor ✓ Fixed
Resolution
Description
In a few cases, incorrect interfaces are used on top of contracts. Though the effect is the same as the contracts are just tokens and follow the same interfaces, it is best practice to implement correct interfaces.
IPeUSD
is used instead ofIEUSD
contracts/lybra/configuration/LybraConfigurator.sol:L60
IPeUSD public EUSD;
IPeUSD
is used instead ofIEUSD
contracts/lybra/configuration/LybraConfigurator.sol:L109
if (address(EUSD) == address(0)) EUSD = IPeUSD(_eusd);
IesLBR
instead ofILBR
contracts/lybra/miner/ProtocolRewardsPool.sol:L29
IesLBR public LBR;
IesLBR
instead ofILBR
contracts/lybra/miner/ProtocolRewardsPool.sol:L57
LBR = IesLBR(_lbr);
Recommendation
Implement correct interfaces for consistency.
6.9 The ETH Staking Rewards Distribution Tradeoff
Description
When users deposit stETH
to the LybraStETHDepositVault
, they give up their rewards from ETH
staking. In exchange, all the rewards from stETH
are going to the EUSD
holders proportionally. So every user is incentivized to borrow as much EUSD
as possible to get their fair share of rewards. Additionally, if any other LybraEUSDVaultBase
vault is added with a lower yield LSD, the depositors of that vault will receive an extra portion of stETH
rewards. While the depositors of LybraStETHDepositVault
will start getting less.
Appendix 1 - Files in Scope
This audit covered the following files:
File | SHA-1 hash |
---|---|
contracts/lybra/Proxy/LybraProxy.sol | ab1ed4e8e31a501c8d1db02086fdc6e883626948 |
contracts/lybra/Proxy/LybraProxyAdmin.sol | 8d745b8fb75d3a9f88db0b2253a2d5ef62e1ae0c |
contracts/lybra/configuration/LybraConfigurator.sol | 4f439afd7578a4f1f8eceae61ab41435d97d1988 |
contracts/lybra/governance/AdminTimelock.sol | 535affc02992b45c3584087d361e893cad4b7ec4 |
contracts/lybra/governance/GovernanceTimelock.sol | 91b1cd13d0a86ca27e94771b0e52725b98dcf730 |
contracts/lybra/governance/LybraGovernance.sol | a62a2c595ae33903375af7685f368a56100c957e |
contracts/lybra/miner/EUSDMiningIncentives.sol | 9b8b29038cc934a7d3f977495c1ffb3c4e709e4f |
contracts/lybra/miner/ProtocolRewardsPool.sol | 2c96638c9c570c8252aa3a37050b46a72385922b |
contracts/lybra/miner/esLBRBoost.sol | 9709aef5aba508a0fc2278066ff257504ebe35eb |
contracts/lybra/miner/stakerewardV2pool.sol | 305609a52c0e0d4f85da80c9d4eb2e24a1cbe97c |
contracts/lybra/pools/LybraRETHVault.sol | 8e91a0cb71f408ebedac495456a7b4c217ecb9d3 |
contracts/lybra/pools/LybraStETHVault.sol | baf5b8f6eede7e46f49b3df45744b808631a1c4a |
contracts/lybra/pools/LybraWbETHVault.sol | 07d96ce9074bfc8366572f33fd597b031f10390b |
contracts/lybra/pools/LybraWstETHVault.sol | f1b5181faf3ecdd517ff4bdb39782be5a8b43525 |
contracts/lybra/pools/base/LybraEUSDVaultBase.sol | 359dc0953fd1938834143e9573a39acfdbc18652 |
contracts/lybra/pools/base/LybraPeUSDVaultBase.sol | 94631f7b46b2788e08b91fbc5a046bd8c1aeac85 |
contracts/lybra/token/EUSD.sol | dfdf7a49d4b268cdb428cf8e90bbde075696e8e1 |
contracts/lybra/token/LBR.sol | 2f39fb851d45abff1dbc1ea34ab6ed0091fe1c0a |
contracts/lybra/token/PeUSD.sol | 61c63b361a82ae5eb78b73dceee3fd07c3e65a66 |
contracts/lybra/token/PeUSDMainnet.sol | 1e600938b49f04ce0c43a4261af87cb71d7a34d6 |
contracts/lybra/token/esLBR.sol | d722f7df6c40b82f5a55bec6c17dec173ef60e1c |
Appendix 2 - Disclosure
Consensys Diligence (“CD”) typically receives compensation from one or more clients (the “Clients”) for performing the analysis contained in these reports (the “Reports”). The Reports may be distributed through other means, including via Consensys publications and other distributions.
The Reports are not an endorsement or indictment of any particular project or team, and the Reports do not guarantee the security of any particular project. This Report does not consider, and should not be interpreted as considering or having any bearing on, the potential economics of a token, token sale or any other product, service or other asset. Cryptographic tokens are emergent technologies and carry with them high levels of technical risk and uncertainty. No Report provides any warranty or representation to any third party in any respect, including regarding the bug-free nature of code, the business model or proprietors of any such business model, and the legal compliance of any such business. No third party should rely on the Reports in any way, including for the purpose of making any decisions to buy or sell any token, product, service or other asset. Specifically, for the avoidance of doubt, this Report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. CD owes no duty to any third party by virtue of publishing these Reports.
A.2.1 Purpose of Reports
The Reports and the analysis described therein are created solely for Clients and published with their consent. The scope of our review is limited to a review of code and only the code we note as being within the scope of our review within this report. Any Solidity code itself presents unique and unquantifiable risks as the Solidity language itself remains under development and is subject to unknown risks and flaws. The review does not extend to the compiler layer, or any other areas beyond specified code that could present security risks. Cryptographic tokens are emergent technologies and carry with them high levels of technical risk and uncertainty. In some instances, we may perform penetration testing or infrastructure assessments depending on the scope of the particular engagement.
CD makes the Reports available to parties other than the Clients (i.e., “third parties”) on its website. CD hopes that by making these analyses publicly available, it can help the blockchain ecosystem develop technical best practices in this rapidly evolving area of innovation.
A.2.2 Links to Other Web Sites from This Web Site
You may, through hypertext or other computer links, gain access to web sites operated by persons other than Consensys and CD. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such web sites’ owners. You agree that Consensys and CD are not responsible for the content or operation of such Web sites, and that Consensys and CD shall have no liability to you or any other person or entity for the use of third party Web sites. Except as described below, a hyperlink from this web Site to another web site does not imply or mean that Consensys and CD endorses the content on that Web site or the operator or operations of that site. You are solely responsible for determining the extent to which you may use any content at any other web sites to which you link from the Reports. Consensys and CD assumes no responsibility for the use of third-party software on the Web Site and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.
A.2.3 Timeliness of Content
The content contained in the Reports is current as of the date appearing on the Report and is subject to change without notice unless indicated otherwise, by Consensys and CD.