Platform: Code4rena
Start Date: 23/06/2023
Pot Size: $60,500 USDC
Total HM: 31
Participants: 132
Period: 10 days
Judge: 0xean
Total Solo HM: 10
Id: 254
League: ETH
Rank: 20/132
Findings: 3
Award: $550.53
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: bytes032
Also found by: 0xMAKEOUTHILL, 0xgrbr, 0xkazim, 0xnacho, Arz, Co0nan, CrypticShepherd, Cryptor, HE1M, Iurii3, LaScaloneta, LokiThe5th, LuchoLeonel1, MrPotatoMagic, Musaka, Qeew, RedTiger, SovaSlava, Toshii, Vagner, a3yip6, azhar, bart1e, devival, hl_, jnrlouis, kutugu, peanuts, pep7siup, qpzm, smaul
1.3247 USDC - $1.32
Judge has assessed an item in Issue #729 as 2 risk. The relevant finding follows:
[L-02]
#0 - c4-judge
2023-07-27T23:55:57Z
0xean marked the issue as duplicate of #27
#1 - c4-judge
2023-07-28T17:15:02Z
0xean marked the issue as satisfactory
🌟 Selected for report: 0xnev
Also found by: 0xRobocop, 0xbrett8571, 0xkazim, 0xnacho, 3agle, 8olidity, ABAIKUNANBAEV, Bauchibred, Co0nan, CrypticShepherd, D_Auditor, DelerRH, HE1M, Iurii3, Kaysoft, MrPotatoMagic, RedOneN, RedTiger, Rolezn, SanketKogekar, Sathish9098, Timenov, Toshii, Vagner, bart1e, bytes032, codetilda, devival, halden, hals, kutugu, m_Rassska, naman1778, nonseodion, seth_lawson, solsaver, squeaky_cactus, totomanov, y51r, yudan, zaevlad
57.9031 USDC - $57.90
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/EUSDMiningIncentives.sol#L84 All functions from Line 84-130 do not have events and their emissions even though they change state variables.
File: contracts/lybra/miner/EUSDMiningIncentives.sol 84: function setToken(address _lbr, address _eslbr) external onlyOwner { 85: LBR = _lbr; 86: esLBR = _eslbr; 87: } 88: 89: function setLBROracle(address _lbrOracle) external onlyOwner { 90: lbrPriceFeed = AggregatorV3Interface(_lbrOracle); 91: } 92: 93: function setPools(address[] memory _pools) external onlyOwner { 94: for (uint i = 0; i < _pools.length; i++) { 95: require(configurator.mintVault(_pools[i]), "NOT_VAULT"); 96: } 97: pools = _pools; 98: } 99: 100: function setBiddingCost(uint256 _biddingRatio) external onlyOwner { 101: require(_biddingRatio <= 8000, "BCE"); 102: biddingFeeRatio = _biddingRatio; 103: } 104: 105: function setExtraRatio(uint256 ratio) external onlyOwner { 106: require(ratio <= 1e20, "BCE"); 107: extraRatio = ratio; 108: } 109: 110: function setPeUSDExtraRatio(uint256 ratio) external onlyOwner { 111: require(ratio <= 1e20, "BCE"); 112: peUSDExtraRatio = ratio; 113: } 114: 115: function setBoost(address _boost) external onlyOwner { 116: esLBRBoost = IesLBRBoost(_boost); 117: } 118: 119: function setRewardsDuration(uint256 _duration) external onlyOwner { 120: require(finishAt < block.timestamp, "reward duration not finished"); 121: duration = _duration; 122: } 123: 124: function setEthlbrStakeInfo(address _pool, address _lp) external onlyOwner { 125: ethlbrStakePool = _pool; 126: ethlbrLpToken = _lp; 127: } 128: function setEUSDBuyoutAllowed(bool _bool) external onlyOwner { 129: isEUSDBuyoutAllowed = _bool; 130: }
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/configuration/LybraConfigurator.sol#L98 Event emission missing for these three functions.
File: contracts/lybra/configuration/LybraConfigurator.sol 98: function initToken(address _eusd, address _peusd) external onlyRole(DAO) { 99: if (address(EUSD) == address(0)) EUSD = IEUSD(_eusd); 100: if (address(peUSD) == address(0)) peUSD = IEUSD(_peusd); 101: } 109: function setMintVault(address pool, bool isActive) external onlyRole(DAO) { 110: mintVault[pool] = isActive; 111: } 119: function setMintVaultMaxSupply(address pool, uint256 maxSupply) external onlyRole(DAO) { 120: mintVaultMaxSupply[pool] = maxSupply; 121: }
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/configuration/LybraConfigurator.sol#L158C5-L178C42 Event emission missing for these three functions.
File: contracts/lybra/configuration/LybraConfigurator.sol 158: function setvaultBurnPaused(address pool, bool isActive) external checkRole(TIMELOCK) { 159: vaultBurnPaused[pool] = isActive; 160: } 167: function setPremiumTradingEnabled(bool isActive) external checkRole(TIMELOCK) { 168: premiumTradingEnabled = isActive; 169: } 177: function setvaultMintPaused(address pool, bool isActive) external checkRole(ADMIN) { 178: vaultMintPaused[pool] = isActive; 179: }
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/configuration/LybraConfigurator.sol#L246 Event emission missing for these two functions. Note: function on Line 253 already has an event emission but it should be placed after Line 256.
File: contracts/lybra/configuration/LybraConfigurator.sol 246: function setMaxStableRatio(uint256 _ratio) external checkRole(TIMELOCK) { 247: require(_ratio <= 10_000, "The maximum value is 10000"); 248: maxStableRatio = _ratio; 249: } 253: function setFlashloanFee(uint256 fee) external checkRole(TIMELOCK) { 254: if (fee > 10_000) revert('EL'); 255: emit FlashloanFeeUpdated(fee); 256: flashloanFee = fee; 257: } 261: function setProtocolRewardsToken(address _token) external checkRole(TIMELOCK) { 262: stableToken = _token; 263: }
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraWbETHVault.sol#L16 The contract address is of the rETH contract and not WBETH.
File: contracts/lybra/pools/LybraWbETHVault.sol 16: //WBETH = 0xae78736Cd615f374D3085123A210448E74Fc6393
The contracts below copy paste functions from the OFTV2.sol contract. It is a better design choice to inherit from an already existing contract (OFTV2.sol) and pass in the values to its constructor directly. Additionally, we do not need to import the BaseOFTV2.sol contract as well since the OFTV2.sol already inherits it.
LBR.sol PeUSD.sol PeUSDMainnetStableVision.sol
For example, the content of the peUSD contract can now only have:
contract PeUSD is OFTV2 { constructor(uint8 _sharedDecimals, address _lzEndpoint) OFTV2("peg-eUSD", "peUSD", _sharedDecimals, _lzEndpoint) { } }
If token contracts are upgraded to another address, it might make our contracts incompatible. To prevent this from happening, pool setter functions should be implemented in case a contract is upgraded in the future (like it is implemented in LybraRETHVault.sol)
Upgradeable contracts: WBETH.sol stETH.sol
Contracts requiring setter function: LybraWbETHVault.sol LybraWstETHVault.sol LybraStETHVault.sol
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/EUSD.sol#L248 It is better for functions with opposite functionalities to use similar patterns in it's code.
Instead of this:
File: contracts/lybra/token/EUSD.sol 248: function increaseAllowance(address _spender, uint256 _addedValue) public returns (bool) { 249: address owner = _msgSender(); 250: _approve(owner, _spender, allowances[owner][_spender].add(_addedValue)); 251: return true; 252: }
Use this:
File: contracts/lybra/token/EUSD.sol 248: function inreaseAllowance(address spender, uint256 _addedValue) public returns (bool) { 249: address owner = _msgSender(); 250: uint256 currentAllowance = allowance(owner, spender); 251: require(currentAllowance + _addedValue <= type(uint256).max, "ERC20: increases allowance above max limit"); 252: unchecked { 253: _approve(owner, spender, currentAllowance + _addedValue); 254: } 255: 256: return true; 257: }
If amount > 0 condition returns false, the if block is not executed, which means no LBR withdrawal occurred. But Line 105 still updates the lastWithdrawTime.
File: contracts/lybra/miner/ProtocolRewardsPool.sol 100: function withdraw(address user) public { 101: uint256 amount = getClaimAbleLBR(user); 102: if (amount > 0) { 103: LBR.mint(user, amount); 104: } 105: lastWithdrawTime[user] = block.timestamp; 106: emit WithdrawLBR(user, amount, block.timestamp); 107: }
Recommended Mitigation Steps: Line 105 should be moved inside the if block after the mint statement on Line 103 to ensure that lastWithdrawTime is only updated if mint/withdrawal occurs.
There are 2 instances of this issue:
When depositEtherToMint() function calls getAssetPrice(), it reverts since an invalid external call is made. This prevents a user from depositing ether to mint PeUSD. This can be a form of DOS. https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraRETHVault.sol#L47 Correct function in RETH contract is getExchangeRate() not getExchangeRatio()
File: contracts/lybra/pools/LybraRETHVault.sol 46: function getAssetPrice() public override returns (uint256) { 47: return (_etherPrice() * IRETH(address(collateralAsset)).getExchangeRatio()) / 1e18; 48: }
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraWbETHVault.sol#L35 Correct function in WBETH contract is exchangeRate() and not exchangeRatio().
File: contracts/lybra/pools/LybraWbETHVault.sol 34: function getAssetPrice() public override returns (uint256) { 35: return (_etherPrice() * IWBETH(address(collateralAsset)).exchangeRatio()) / 1e18; 36: }
There are 3 instances of this issue, where modifier updateReward() makes state changes to the contract:
Make updateReward() into an internal function instead of a modifier.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L181 There are 4 instances of this in LybraPeUSDVaultBase.sol:
File: contracts/lybra/pools/base/LybraPeUSDVaultBase.sol 181: PeUSD.mint(_onBehalfOf, _mintAmount); 199: PeUSD.transferFrom(_provider, address(configurator), totalFee); 200: PeUSD.burn(_provider, amount - totalFee); 203: PeUSD.transferFrom(_provider, address(configurator), amount);
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L264 There are 2 instances of this in LybraEUSDVaultBase.sol
File: contracts/lybra/pools/base/LybraEUSDVaultBase.sol 264: EUSD.mint(_onBehalfOf, _mintAmount); 279: EUSD.burn(_provider, amount);
For all instances below, if a require check is not added and either the etherPrice or lbrPrice comes to be negative (highly unlikely), the typecast from int to uint will result in an underflow. https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/EUSDMiningIncentives.sol#L153 Instead of this:
File: contracts/lybra/miner/EUSDMiningIncentives.sol 151: (, int etherPrice, , , ) = etherPriceFeed.latestRoundData(); 152: (, int lbrPrice, , , ) = lbrPriceFeed.latestRoundData(); 153: uint256 etherInLp = (IEUSD(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2).balanceOf(ethlbrLpToken) * uint(etherPrice)) / 1e8; 154: uint256 lbrInLp = (IEUSD(LBR).balanceOf(ethlbrLpToken) * uint(lbrPrice)) / 1e8;
Use this:
File: contracts/lybra/miner/EUSDMiningIncentives.sol 151: (, int etherPrice, , , ) = etherPriceFeed.latestRoundData(); 152: (, int lbrPrice, , , ) = lbrPriceFeed.latestRoundData(); 153: require(etherPrice >= 0, "etherPrice is below zero"); 154: require(lbrPrice >= 0, "lbrPrice is below zero"); 155: uint256 etherInLp = (IEUSD(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2).balanceOf(ethlbrLpToken) * uint(etherPrice)) / 1e8; 156: uint256 lbrInLp = (IEUSD(LBR).balanceOf(ethlbrLpToken) * uint(lbrPrice)) / 1e8;
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/EUSDMiningIncentives.sol#L212 Instead of this:
File: contracts/lybra/miner/EUSDMiningIncentives.sol 212: (, int lbrPrice, , , ) = lbrPriceFeed.latestRoundData(); 213: biddingFee = biddingFee * uint256(lbrPrice) / 1e8;
Use this:
File: contracts/lybra/miner/EUSDMiningIncentives.sol 212: (, int lbrPrice, , , ) = lbrPriceFeed.latestRoundData(); 213: require(lbrPrice >= 0, "lbrPrice is below zero"); 214: biddingFee = biddingFee * uint256(lbrPrice) / 1e8;
These require checks ensure that there cannot be an underflow if price is negative.
Performing multiplication before division is generally better to avoid loss of precision because Solidity integer division might truncate.
File: contracts/lybra/pools/base/LybraEUSDVaultBase.sol 239: uint256 collateralAmount = (((eusdAmount * 1e18) / assetPrice) * (10000 - configurator.redemptionFee())) / 10000;
#0 - c4-pre-sort
2023-07-27T20:43:46Z
JeffCX marked the issue as high quality report
#1 - JeffCX
2023-07-27T20:44:44Z
[N-02] Incorrect contract address in comment
duplicate of #18
[L-02] Incorrect external function calls prevent users from depositing asset to mint PeUSD
duplicate of #27
#2 - c4-judge
2023-07-27T23:55:04Z
0xean marked the issue as grade-a
#3 - c4-sponsor
2023-07-29T09:20:21Z
LybraFinance marked the issue as sponsor acknowledged
🌟 Selected for report: Sathish9098
Also found by: 0x3b, 0xbrett8571, ABAIKUNANBAEV, K42, MrPotatoMagic, hl_, ktg, peanuts, solsaver
491.3125 USDC - $491.31
This was the order in which I audited the contracts. In places where I could not understand a certain functionality, I dmed the sponsors and referred to the documentation to seek additional context.
The protocol is designed in an easy to understand way. There are certain contracts that can improve the way they are designed. For example, in one of my findings, I've reported three (LBR, PeUSD, PeUSDMainnetStableVision) token contracts that could directly inherit from the LayerZero OFTV2.sol contract instead of copy pasting its functions (keeping the endpoints simple through which cross-chain communication occurs is recommended). Another one where a setter function was not implemented for upgradeable token contracts in specific LST vaults. These minute architecture recommendations have been included in the QA report I submitted. Personally, architecture-wise I would rate it on a Medium level.
The use of the LayerZero OFT contracts, which enable cross chain communication is unique. I did not delve deeper on the OFT contracts side but just checked the return values and some of the internal functions utilized.
With the absence of NatSpec comments and a fully complete documentation, it was difficult to understand the assumptions being made on the developer side. For example, how stake, unstake and withdraw mechanism would work. The errors related to staking and unstaking have been submitted as HMs. Other than that, there were several low issues and non-critical ones I've included. These include the inconsistent patterns used in the codebase such as inconsistent event emissions, using unchecked in decreaseAllowance but not in increaseAllowance and the require checks being used in depositAssetAmount() being different in both LybraPeUSDVaultBase.sol and LybraEUSDVaultBase.sol contracts. Overall, from my point of view the codebase was not ready for an audit due to the presence of barely any tests, absence of in-depth documentation and typo errors in external function calls.
Some degree of centralization is introduced through the Governance contracts but the presence of timelock contracts on them tilt it towards some degree of decentralisation. Mainly the access control exists in the LybraConfigurator, which I think does not pose much risk. I think this is a common pattern used in most protocols. On the other hand, the owner in the reward contracts poses a centralization risk since it has control over most functions which could destabilize the system. Additionally not using a two-step change for critical addresses like the owner can lead to the contract ownership being transferred to an unwanted address.
From the issues I've found the only systemic risks existing are those in the ProtocolRewardsPool.sol contract and PeUSDMainnetStableVision.sol. In the former, one can tamper with the stake/unstake functionality while in the latter the system is at risk due to the absence of rate limiting when borrowing a flash loan. There are several forms of DOS existing in the contract logic itself, which prevent users from utilizing a service in the protocol (included in findings). In addition to that, the centralization risks above pose a systemic risk as well.
Here is a mechanical mindmap to understand the architecture and some interactions of the protocol: https://drive.google.com/file/d/1EwQTqNQWdRx1zAMkVbvmGg1ySGncJwRe/view?usp=sharing I have provided edit access to draw on top of the image if necessary.
70 hours
#0 - c4-pre-sort
2023-07-04T15:11:39Z
JeffCX marked the issue as high quality report
#1 - c4-sponsor
2023-07-27T06:23:12Z
LybraFinance marked the issue as sponsor confirmed
#2 - c4-judge
2023-07-28T17:07:23Z
0xean marked the issue as grade-a
#3 - c4-judge
2023-07-28T19:38:34Z
0xean marked the issue as satisfactory