Lybra Finance - Sathish9098's results

A protocol building the first interest-bearing omnichain stablecoin backed by LSD.

General Information

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

Lybra Finance

Findings Distribution

Researcher Performance

Rank: 14/132

Findings: 3

Award: $777.04

QA:
grade-a
Gas:
grade-a
Analysis:
grade-a

🌟 Selected for report: 1

🚀 Solo Findings: 0

Awards

57.9031 USDC - $57.90

Labels

bug
grade-a
high quality report
QA (Quality Assurance)
sponsor acknowledged
edited-by-warden
Q-40

External Links

LOW FINDINGS

LOW COUNTISSUESINSTANCES
[L-1]assetAmount * 2 <= depositedAsset[onBehalfOf] check is conflict with docs1
[L-2]Lack of check-effects-interactions (CEI) pattern is vulnerable to reentrancy attacks3
[L-3]User could deposit a small amount of ether and then mint a large amount of PEUSD, which could create a security risk1
[L-4]Use the safeAllowance() function instead of the normal approve() function1
[L-5]Use call instead of transfer to send ethers3
[L-6]Timelocks not implemented as per documentation-
[L-7]depositAssetToMint() does not check for a maximum limit on asset deposits1
[L-8]Project Upgrade and Stop Scenario should be-
[L-9]Lack of nonReentrant modifiers for critical external functions1
[L-10]Divide by zero should be avoided2
[L-11]````name maskingshould be avoided toprevent errors``2
[L-12]Division before multiplication can lead to precision errors2
[L-13]Hardcoding addresses in smart contracts can make them more vulnerable to attack1
[L-14]NATSPEC comments should be increased in contracts-
[L-15]Negative values may return the unexpected values when using uint256(int)conversion1
[L-16]Array does not have a pop function1
[L-17]Setters should have initial value check1
[L-18]External calls in an un-bounded for-loop may result in a DOS2

[L-1] assetAmount * 2 <= depositedAsset[onBehalfOf] check is conflict with docs

* - collateralAmount should be less than 50% of collateral

Impact

The comment for the function states that the collateral amount should be less than 50% of the deposited asset. However, the implementation of the function actually allows for the collateral amount to be equal to 50% of the deposited asset

This is a potential security vulnerability, as it could allow a user to liquidate more collateral than they are entitled to.

POC

FILE: 2023-06-lybra/contracts/lybra/pools/base/LybraEUSDVaultBase.sol

* - collateralAmount should be less than 50% of collateral

159:require(assetAmount * 2 <= depositedAsset[onBehalfOf], "a max of 50% collateral can be liquidated");

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L159

it would be advisable to change the implementation of the function to require that the collateral amount be less than 50% of the deposited asset, as stated in the comment

[L-2] Lack of check-effects-interactions (CEI) pattern is vulnerable to reentrancy attacks

Impact

In the function depositEtherToMint, the state change of minting PEUSD is performed after the external call to the IWBETH contract. This means that an attacker could call the IWBETH contract and then call the _mintPeUSD function before the state change of minting PEUSD has been completed. This could allow the attacker to mint more PEUSD than they are entitled to, which could create a security risk

POC

FILE: Breadcrumbs2023-06-lybra/contracts/lybra/pools/LybraWbETHVault.sol

23: IWBETH(address(collateralAsset)).deposit{value: msg.value}(address(configurator));
24:        uint256 balance = collateralAsset.balanceOf(address(this));
25:        depositedAsset[msg.sender] += balance - preBalance;
26:
27:        if (mintAmount > 0) {
28:            _mintPeUSD(msg.sender, msg.sender, mintAmount, getAssetPrice());
29:        }

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraWbETHVault.sol#L23-L29

FILE: 2023-06-lybra/contracts/lybra/pools/LybraRETHVault.sol

 rkPool.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());
        }

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraRETHVault.sol#L30-L36

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L58-L70

Use check-effects-interactions (CEI) pattern

[L-3] User could deposit a small amount of ether and then mint a large amount of PEUSD, which could create a security risk

Impact

if a user deposits 1 ether and mints 100,000 PEUSD, they would have a large amount of PEUSD that they could then use to buy up a large amount of esLBR. This could then give them control of a large portion of the ProtocolRewardsPoolcontract, which could allow them to manipulate the rewards distribution.

POC

FILE: 2023-06-lybra/contracts/lybra/pools/LybraWbETHVault.sol

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());
        }

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraWbETHVault.sol#L20-L32

The depositEtherToMint() function could be modified to limit the amount of PEUSD that a user can mint. The function could be modified to only allow a user to mint a certain percentage of the amount of ether that they deposit.

[L-4] Use the safeAllowance() function instead of the normal approve() function

Impact

  • It does not check if the spender has already transferred the requested amount of tokens
  • It is not as secure as the safeAllowance() function

The safeAllowance() function first checks if the spender has already been approved for the requested amount of tokens. If they have not, the function approves the spender for the requested amount of tokens and then checks if the spender has already transferred the requested amount of tokens. If they have, the function reverts. This helps to prevent malicious spenders from transferring tokens that they have not been approved to transfer.

  • It is more complex than the safeAllowance() function

POC

FILE: 2023-06-lybra/contracts/lybra/pools/LybraWstETHVault.sol

35: lido.approve(address(collateralAsset), msg.value);

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraWstETHVault.sol#L35C59-L35C59

Use safeAllowance() for better security

[L-5] Use call instead of transfer to send ethers

Impact

.transfer will relay 2300 gas and .call will relay all the gas. If the receive/fallback function from the recipient proxy contract has complex logic, using .transfer will fail, causing integration issues

POC

FILE: 2023-06-lybra/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol

139: collateralAsset.transfer(msg.sender, reducedAsset);
142: collateralAsset.transfer(provider, reducedAsset - reward2keeper);
143: collateralAsset.transfer(msg.sender, reward2keeper);

Replace .transfer with .call. Note that the result of .call need to be checked

[L-6] Timelocks not implemented as per documentation

Does it use a timelock function?: True

Impact

  • If a user accidentally transfers their tokens to the wrong address, or if a malicious actor gains access to a user's wallet, the tokens can be transferred to the attacker's address without any delay.

Implement time locks as per documentations

[L-7] depositAssetToMint() does not check for a maximum limit on asset deposits

This means that a user could theoretically deposit an unlimited amount of collateral asset into the contract. This could potentially lead to a number of problems,

  • The contract could become too centralized if a single user or group of users deposits a large enough amount of collateral asset
  • The contract could run out of gas if a user deposits a very large amount of collateral asset
  • The contract could be used to launder money or finance illegal activities

POC

FILE: 2023-06-lybra/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol

function depositAssetToMint(uint256 assetAmount, uint256 mintAmount) external virtual {
        require(assetAmount >= 1 ether, "Deposit should not be less than 1 collateral asset.");
        uint256 preBalance = collateralAsset.balanceOf(address(this));
        collateralAsset.transferFrom(msg.sender, address(this), assetAmount);
        require(collateralAsset.balanceOf(address(this)) >= preBalance + assetAmount, "");

        depositedAsset[msg.sender] += assetAmount;
        if (mintAmount > 0) {
            uint256 assetPrice = getAssetPrice();
            _mintPeUSD(msg.sender, msg.sender, mintAmount, assetPrice);
        }
        emit DepositAsset(msg.sender, address(collateralAsset), assetAmount, block.timestamp);
    }

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L58-L70

It would be advisable to add a maximum limit on asset deposits to the depositAssetToMint function

[L-8] Project Upgrade and Stop Scenario should be

At the start of the project, the system may need to be stopped or upgraded, I suggest you have a script beforehand and add it to the documentation. This can also be called an ” EMERGENCY STOP (CIRCUIT BREAKER) PATTERN “.

https://github.com/maxwoe/solidity_patterns/blob/master/security/EmergencyStop.sol

[L-9] Lack of nonReentrant modifiers for critical external functions

Impact

Apply the nonReentrant modifier to critical external functions to ensure they are not susceptible to reentrancy attacks

nonReentrant modifier, you can help protect your contract from reentrancy vulnerabilities. It's important to apply this modifier to functions that involve state changes or interact with external contracts, especially if they involve transfers of Ether or tokens

POC

FILE: Breadcrumbs2023-06-lybra/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol

82: function withdraw(address onBehalfOf, uint256 amount) external virtual {

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L82C77-L82C77

It would be advisable to add the nonReentrant modifier

[L-10] Divide by zero should be avoided

Impact

Division by zero should be avoided in Solidity and in any programming language. Division by zero is an undefined operation, and it can lead to unexpected and unpredictable results. In Solidity, division by zero will cause the contract to revert. This means that the transaction will be canceled, and the caller will not receive any funds or tokens

POC

FILE: 2023-06-lybra/contracts/lybra/miner/esLBRBoost.sol

67: return ((boostEndTime - userUpdatedAt) * maxBoost) / (time - userUpdatedAt);

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/esLBRBoost.sol#L67C89-L67C89

FILE: 2023-06-lybra/contracts/lybra/pools/base/LybraEUSDVaultBase.sol

156: uint256 onBehalfOfCollateralRatio = (depositedAsset[onBehalfOf] * assetPrice * 100) / borrowed[onBehalfOf];

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L190

  • Use the SafeMath library
  • Check the divisor before dividing

[L-11] name masking should be avoided to prevent errors

Impact

Name masking can be a source of errors, so it is important to be aware of the potential for name masking when writing Solidity contracts. By giving variables unique names and avoiding name masking, you can help to prevent errors in your contracts.

If a variable is declared with the same name as a function, the variable will shadow the function. This means that the variable will be used instead of the function when the name is referenced.

POC

FILE: 2023-06-lybra/contracts/lybra/token/PeUSD.sol

12: uint8 decimals = decimals();

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/PeUSD.sol#L12

FILE: 2023-06-lybra/contracts/lybra/token/LBR.sol

20: uint8 decimals = decimals();

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/LBR.sol#L20

Avoid using the same name for a variable and a function

[L-12] Division before multiplication can lead to precision errors

Impact

Because Solidity integer division may truncate, it is often preferable to do multiplication before division to prevent precision loss.

POC

FILE: Breadcrumbs2023-06-lybra/contracts/lybra/miner/EUSDMiningIncentives.sol

142: borrowed = borrowed * (1e20 + peUSDExtraRatio) / 1e20;

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/EUSDMiningIncentives.sol#L142


142: borrowed = (borrowed * (1e20 + peUSDExtraRatio)) / 1e20;

[L-13] Hardcoding addresses in smart contracts can make them more vulnerable to attack

Impact

  • It makes the contract more difficult to audit. This is because it is more difficult to understand how the contract works if the addresses are hardcoded.

  • It makes the contract more vulnerable to attack. This is because it is easier for an attacker to find a way to manipulate the values of the variables and get an incorrect result.

  • It makes the contract more difficult to update. This is because it is necessary to update the hardcoded addresses if the addresses of the contracts change.

POC

FILE: Breadcrumbs2023-06-lybra/contracts/lybra/miner/EUSDMiningIncentives.sol

153: uint256 etherInLp = (IEUSD(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2).balanceOf(ethlbrLpToken) * uint(etherPrice)) / 1e8;

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/EUSDMiningIncentives.sol#L153

Use dynamic addresses. Dynamic addresses are addresses that are not hardcoded into the contract. Instead, they are generated at runtime and stored in a state variable. This makes it more difficult for an attacker to manipulate the values of the variables and get an incorrect result

[L-14] NATSPEC comments should be increased in contracts

It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability. https://docs.soliditylang.org/en/v0.8.15/natspec-format.html

Recommendation NatSpec comments should be increased in Contracts

[L-15] Negative values may return the unexpected values when using uint256(int) conversion

Impact

When converting a negative int256 value to a uint256 using uint256(int256), the result will be unexpected. if you try to convert a negative int256 value to a uint256, the result will be interpreted as the two's complement representation of the negative value. This means that the resulting uint256 value will be a large positive number.

PROOF: We attempt to convert the negative value -10 to a uint256. However, the resulting value will not be the expected value of -10 but rather a large positive number.

If you execute the function, the returned value will be 115792089237316195423570985008687907853269984665640564039457584007913129639934. This result corresponds to the two's complement representation of -10 when interpreted as an unsigned uint256.

POC

FILE: Breadcrumbs2023-06-lybra/contracts/lybra/miner/EUSDMiningIncentives.sol

212: (, int lbrPrice, , , ) = lbrPriceFeed.latestRoundData();
213: biddingFee = biddingFee * uint256(lbrPrice) / 1e8;

Carefully handle conversions between signed and unsigned integer types and ensure that the values are within the valid ranges of the target type.

[L-16] Array does not have a pop function

Impact

Arrays without the pop operation in Solidity can lead to inefficient memory management and increase the likelihood of out-of-gas errors.

POC

FILE: 2023-06-lybra/contracts/lybra/miner/esLBRBoost.sol

26: esLBRLockSettings.push(esLBRLockSetting(30 days, 20 * 1e18));
27:        esLBRLockSettings.push(esLBRLockSetting(90 days, 30 * 1e18));
28:        esLBRLockSettings.push(esLBRLockSetting(180 days, 50 * 1e18));
29:        esLBRLockSettings.push(esLBRLockSetting(365 days, 100 * 1e18));

34:   esLBRLockSettings.push(setting);

Add pop function to avoid out-of-gas errors

[L-17] Setters should have initial value check

Impact

Setters should have initial value check to prevent assigning wrong value to the variable. Assginment of wrong value can lead to unexpected behavior of the contract.

POC

FILE: Breadcrumbs2023-06-lybra/contracts/lybra/configuration/LybraConfigurator.sol

function setMintVaultMaxSupply(address pool, uint256 maxSupply) external onlyRole(DAO) {
        mintVaultMaxSupply[pool] = maxSupply;
    }

maxSupply add initial value check then assign to mintVaultMaxSupply[pool]

[L-18] External calls in an un-bounded for-loop may result in a DOS

Impact

Consider limiting the number of iterations in for-loops that make external calls

POC

FILE: Breadcrumbs2023-06-lybra/contracts/lybra/miner/EUSDMiningIncentives.sol

for (uint i = 0; i < _pools.length; i++) {
            require(configurator.mintVault(_pools[i]), "NOT_VAULT"); //@audit external call
        }

for (uint i = 0; i < pools.length; i++) {
            ILybra pool = ILybra(pools[i]);
            uint borrowed = pool.getBorrowedOf(user); //@audit external call
            if (pool.getVaultType() == 1) {
                borrowed = borrowed * (1e20 + peUSDExtraRatio) / 1e20;
            }
            amount += borrowed;

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/EUSDMiningIncentives.sol#L94-L96

Ensure that loops are bounded and their iterations are reasonable and predictable

#0 - c4-pre-sort

2023-07-27T19:32:57Z

JeffCX marked the issue as high quality report

#1 - c4-judge

2023-07-27T23:58:12Z

0xean marked the issue as grade-a

#2 - c4-sponsor

2023-07-29T11:20:20Z

LybraFinance marked the issue as sponsor acknowledged

Awards

80.434 USDC - $80.43

Labels

bug
G (Gas Optimization)
grade-a
high quality report
sponsor acknowledged
edited-by-warden
G-17

External Links

GAS OPTIMIZATION

TABLE OF CONTENTS

Total Gas saved from all findings - 68800 GAS

FINDINGS

[G-1] Optimizing gas consumption with tight variable packing

If variables occupying the same slot are both written the same function or by the constructor, avoids a separate Gsset (20000 gas). Reads of the variables can also be cheaper

EUSDMiningIncentives.sol: duration ,finishAt, updatedAt, rewardRatio, extraRatio ,peUSDExtraRatio Variables can be uint128 instead of uint256.biddingFeeRatio can be uint94 instead of uint256 : Saves 8000 GAS , 4 SLOT

  • duration,finishAt,updatedAt,rewardRatio can be changed from uint256 to uint128. The maximum value that a uint128 variable can store is 18,446,744,073,709,551,615 (2^128 - 1). The block timestamp will not reach this value for over 280 billion years. Therefore, a uint128 variable is enough to store the block timestamp for the foreseeable future. Saves 4000 gas , 2 SLOT

  • extraRatio, peUSDExtraRatio can be changed from uint256 to uint128. The values of extraRatio, peUSDExtraRatio not exceeds 1e20 as per require condition checks require(ratio <= 1e20, "BCE"), require(ratio <= 1e20, "BCE"). So uint128 alone enough instead of uin256. Saves 2000 gas , 1 SLOT

  • biddingFeeRatio can be uint94 instead of uitn256. biddingFeeRatio value not exceeds 8000 as per require(_biddingRatio <= 8000, "BCE") this check. Saves 2000 gas , 1 SLOT

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/EUSDMiningIncentives.sol#L35-L54

FILE: Breadcrumbs2023-06-lybra/contracts/lybra/miner/EUSDMiningIncentives.sol

35: // Duration of rewards to be paid out (in seconds)
- 36:    uint256 public duration = 2_592_000;
+ 36:    uint128 public duration = 2_592_000;
37:    // Timestamp of when the rewards finish
- 38:    uint256 public finishAt;
+ 38:    uint128 public finishAt;
39:    // Minimum of last updated time and reward finish time
- 40:    uint256 public updatedAt;
+ 40:    uint128 public updatedAt;
41:    // Reward to be paid out per second
- 42:    uint256 public rewardRatio;
+ 42:    uint128 public rewardRatio;
43:    // Sum of (reward ratio * dt * 1e18 / total supply)
44:    uint256 public rewardPerTokenStored;
45:    // User address => rewardPerTokenStored
46:    mapping(address => uint256) public userRewardPerTokenPaid;
47:    // User address => rewards to be claimed
48:    mapping(address => uint256) public rewards;
49:    mapping(address => uint256) public userUpdatedAt;
- 50:    uint256 public extraRatio = 50 * 1e18;
+ 50:    uint128 public extraRatio = 50 * 1e18;
- 51:    uint256 public peUSDExtraRatio = 10 * 1e18;
+ 51:    uint128 public peUSDExtraRatio = 10 * 1e18;
- 52:    uint256 public biddingFeeRatio = 3000;
+ 52:    uint94 public biddingFeeRatio = 3000;
53:    address public ethlbrStakePool;
54:    address public ethlbrLpToken;

LybraConfigurator.sol: redemptionFee ,flashloanFee ,maxStableRatio can be uint94 instead of uint256. Saves 6000 GAS, 3 SLOT

  • redemptionFee is not exceeds 500 because of this check require(newFee <= 500, "Max Redemption Fee is 5%");

  • flashloanFee is not exceeds 10_000 because of this check if (fee > 10_000) revert('EL');

  • maxStableRatio is not exceeds 10_000 because of this check require(_ratio <= 10_000, "The maximum value is 10000");

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/configuration/LybraConfigurator.sol#L49-L61

FILE: 2023-06-lybra/contracts/lybra/configuration/LybraConfigurator.sol

- 49:  uint256 public redemptionFee = 50;
+ 49:  uint94 public redemptionFee = 50;
50:    IGovernanceTimelock public GovernanceTimelock;
51:
52:    IeUSDMiningIncentives public eUSDMiningIncentives;
53:    IProtocolRewardsPool public lybraProtocolRewardsPool;
54:    IEUSD public EUSD;
55:    IEUSD public peUSD;
- 56:    uint256 public flashloanFee = 500;
- 56:    uint94 public flashloanFee = 500;
57:    // Limiting the maximum percentage of eUSD that can be cross-chain transferred to L2 in relation to the total supply.
- 58:    uint256 maxStableRatio = 5_000;
+ 58:    uint94 maxStableRatio = 5_000;
59:    address public stableToken;

ProtocolRewardsPool.sol: grabFeeRatio can be uint94 instead of uint256 since the value not exceeds 8000 as per this check require(_ratio <= 8000, "BCE"); : Saves 2000 GAS, 1 SLOD

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L27-L41

FILE: 2023-06-lybra/contracts/lybra/miner/ProtocolRewardsPool.sol

26: IesLBR public esLBR;
27:    IesLBR public LBR;
+ 41:    uint94 public grabFeeRatio = 3000;
28:    IesLBRBoost public esLBRBoost;
29:
30:    // Sum of (reward ratio * dt * 1e18 / total supply)
31:    uint public rewardPerTokenStored;

39: uint256 immutable exitCycle = 90 days;
40:    uint256 public grabableAmount;
- 41:    uint256 public grabFeeRatio = 3000;
42:    event Restake(address indexed user, uint256 amount, uint256 time);

stakerewardV2pool.sol: duration ,finishAt, updatedAt, rewardRatio Variables can be uint128 instead of uint256: Saves 4000 Gas , 2 SLOT

  • duration,finishAt,updatedAt,rewardRatio can be changed from uint256 to uint128. The maximum value that a uint128 variable can store is 18,446,744,073,709,551,615 (2^128 - 1). The block timestamp will not reach this value for over 280 billion years. Therefore, a uint128 variable is enough to store the block timestamp for the foreseeable future. Saves 4000 gas , 2 SLOT
FILE: 2023-06-lybra/contracts/lybra/miner/stakerewardV2pool.sol

- 23:  uint256 public duration = 2_592_000;
+ 23:  uint128 public duration = 2_592_000;
24:    // Timestamp of when the rewards finish
- 25:    uint256 public finishAt;
+ 25:    uint128 public finishAt;
26:    // Minimum of last updated time and reward finish time
- 27:    uint256 public updatedAt;
+ 27:    uint128 public updatedAt;
28:    // Reward to be paid out per second
- 29:    uint256 public rewardRatio;
+ 29:    uint128 public rewardRatio;
30:    // Sum of (reward ratio * dt * 1e18 / total supply)
31:    uint256 public rewardPerTokenStored;

[G-2] Structs can be packed into fewer storage slots

Each slot saved can avoid an extra Gsset (20000 gas) for the first setting of the struct. Subsequent reads as well as writes have smaller gas savings.

esLBRBoost.sol: unlockTime,duration can be uint128 instead of uint256 : Saves 2000 GAS, 1 SLOT

The maximum value that a uint128 variable can store is 18,446,744,073,709,551,615 (2^128 - 1). The block timestamp will not reach this value for over 280 billion years. Therefore, a uint128 variable is enough to store the block timestamp for the foreseeable future

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/esLBRBoost.sol#L18-L22

FILE: 2023-06-lybra/contracts/lybra/miner/esLBRBoost.sol

18: struct LockStatus {
- 19:        uint256 unlockTime;
+ 19:        uint128 unlockTime;
- 20:        uint256 duration;
+ 20:        uint128 duration;
21:        uint256 miningBoost;
22:    }

[G-3] maxSupply can be declared as constant to save large volume of gas

The maxSupply value not changed any where inside the contract. This value only used for checking totalSupply() + amount <= maxSupply

The maxSupply variable can be declared as a constant to save gas. This is because constants are stored in memory, which is a much cheaper operation than storing variables in storage.

The expression 100_000_000 * 1e18 evaluates to a very large number, which would require a lot of gas to store in storage. By declaring maxSupply as a constant, we can avoid this gas cost.

ContractGas Cost
maxSupply as a variable21,000 gas
maxSupply as a constant1,500 gas

esLBR.sol,LBR.sol: maxSupply should be declared as constant instead of state variable : Saves 42000 GAS, 200 GAS for each call

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/esLBR.sol#L20

FILE: 2023-06-lybra/contracts/lybra/token/esLBR.sol

- 20: uint256 maxSupply = 100_000_000 * 1e18;
+ 20: uint256 public constant maxSupply = 100_000_000 * 1e18;

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/LBR.sol#L15

FILE: 2023-06-lybra/contracts/lybra/token/LBR.sol

- 15: uint256 maxSupply = 100_000_000 * 1e18;
+ 20: uint256 public constant maxSupply = 100_000_000 * 1e18;

[G-4] Avoiding Unnecessary Storage of rkPool Address

The constructor of the LybraRETHVault contract takes the address of the rkPool contract as a parameter. This means that the value of rkPool is already known to the contract when it is deployed. There is no need to store the value of rkPool in the state of the contract, as it can be retrieved from the parameter of the constructor

Storing the value of rkPool in the state of the contract would consume unnecessary gas. The gas cost of storing a 20-byte address in the state of a contract is 200 gas. This means that storing the value of rkPool would consume 200 gas, even though the value of rkPool is already known to the contract

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraRETHVault.sol#L18

FILE: 2023-06-lybra/contracts/lybra/pools/LybraRETHVault.sol

18: IRkPool rkPool = IRkPool(0xDD3f50F8A6CafbE9b31a427582963f465E745AF8);

[G-5] Using storage instead of memory for structs/arrays saves gas

When fetching data from a storage location, assigning the data to a memory variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declearing the variable with the memory keyword, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory, or if the array/struct is being read from another memory array/struct

NOTE: These instance is not found by bot race

esLBRBoost.sol: storage should be used instead of ```memory: Saves 4100 GAS

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/esLBRBoost.sol#L39

FILE: 2023-06-lybra/contracts/lybra/miner/esLBRBoost.sol

- 39: esLBRLockSetting memory _setting = esLBRLockSettings[id];
+ 39: esLBRLockSetting strorage _setting = esLBRLockSettings[id];

[G-6] Using calldata instead of memory for read-only arguments in external functions saves gas

When a function with a memory array is called externally, the abi.decode () step has to use a for-loop to copy each index of the calldata to the memory index. Each iteration of this for-loop costs at least 60 gas (i.e. 60 * <mem_array>.length). Using calldata directly, obliviates the need for such a loop in the contract code and runtime execution

EUSDMiningIncentives.sol: Use calldata instead of memory: Saves 312 GAS per iteration

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/EUSDMiningIncentives.sol#L93

FILE: 2023-06-lybra/contracts/lybra/miner/EUSDMiningIncentives.sol

- 93: function setPools(address[] memory _pools) external onlyOwner {
+ 93: function setPools(address[] calldata _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:    }

esLBRBoost.sol: Use calldata instead of memory: Saves 312 GAS

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/esLBRBoost.sol#L33

FILE: 2023-06-lybra/contracts/lybra/miner/esLBRBoost.sol

- 33: function addLockSetting(esLBRLockSetting memory setting) external onlyOwner {
+ 33: function addLockSetting(esLBRLockSetting calldata setting) external onlyOwner {
34:        esLBRLockSettings.push(setting);
35:    }

[G-7] State variables should be cached outside the loop to save gas

State variables should be cached outside the loop to save gas. This is because the gas cost of accessing a state variable inside a loop is higher than the gas cost of accessing a state variable outside a loop

peUSDExtraRatio should be cached outside the loop: Saves 100 GAS per iterations

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/EUSDMiningIncentives.sol#L138

FILE: 2023-06-lybra/contracts/lybra/miner/EUSDMiningIncentives.sol

+ uint256 _peUSDExtraRatio= peUSDExtraRatio;
138:for (uint i = 0; i < pools.length; i++) {
139:            ILybra pool = ILybra(pools[i]);
140:            uint borrowed = pool.getBorrowedOf(user);
141:            if (pool.getVaultType() == 1) {
- 142:                borrowed = borrowed * (1e20 + peUSDExtraRatio) / 1e20;
+ 142:                borrowed = borrowed * (1e20 + _peUSDExtraRatio) / 1e20;
143:            }

#0 - c4-pre-sort

2023-07-13T21:00:41Z

JeffCX marked the issue as high quality report

#1 - c4-sponsor

2023-07-27T07:25:50Z

LybraFinance marked the issue as sponsor acknowledged

#2 - c4-judge

2023-07-27T23:42:22Z

0xean marked the issue as grade-a

Findings Information

🌟 Selected for report: Sathish9098

Also found by: 0x3b, 0xbrett8571, ABAIKUNANBAEV, K42, MrPotatoMagic, hl_, ktg, peanuts, solsaver

Labels

analysis-advanced
grade-a
selected for report
sponsor confirmed
A-06

Awards

638.7063 USDC - $638.71

External Links

Lybra Finance - Analysis

HeadDetails
Approach taken in evaluating the codebaseWhat is unique? How are the existing patterns used?
Codebase quality analysisits structure, readability, maintainability, and adherence to best practices
Centralization riskspower, control, or decision-making authority is concentrated in a single entity
Bug Fixprocess of identifying and resolving issues or errors
Gas Optimizationprocess of reducing the amount of gas consumed by a smart contract or transaction on a blockchain network
Other recommendationsRecommendations for improving the quality of your codebase
Time spent on analysisTime detail of individual stages in my code review and analysis

Approach taken in evaluating the codebase

Steps:

  • Use a static code analysis tool: Static code analysis tools can scan the code for potential bugs and vulnerabilities. These tools can be used to identify a wide range of issues, including:

    • Insecure coding practices
    • Common vulnerabilities
    • Code that is not compliant with security standards
  • Read the documentation: The documentation for Lybra Finance should provide a detailed overview of the protocol and its codebase. This documentation can be used to understand the purpose of the code and to identify potential areas of concern.

  • Scope the analysis: Once you have a basic understanding of the protocol and its codebase, you can start to scope the analysis. This involves identifying the specific areas of code that you want to focus on. For example, you may want to focus on the code that handles user input, the code that interacts with external APIs, or the code that stores sensitive data.

  • Manually review the code: Once you have scoped the analysis, you can start to manually review the code. This involves reading the code line-by-line and looking for potential problems. Some of the things you should look for include:

    • Unvalidated user input
    • Hardcoded credentials
    • Insecure cryptographic functions
    • Unsafe deserialization
  • Mark vulnerable code parts with @audit tags: Once you have identified any potential vulnerabilities, you should mark them with @audit tags. This will help you to identify the vulnerable code parts later on.

  • Dig deep into vulnerable code parts and compare with documentations: For each vulnerable code part, you should dig deep to understand how it works and why it is vulnerable. You should also compare the code with the documentation to see if there are any discrepancies.

  • Perform a series of tests: Once you have finished reviewing the code, you should perform a series of tests to ensure that it works as intended. These tests should cover a wide range of scenarios, including:

    • Valid and invalid user input
    • Different types of attacks
    • Different operating systems and hardware platforms
  • Report any problems: If you find any problems with the code, you should report them to the developers of Lybra Finance. The developers will then be able to fix the problems and release a new version of the protocol.

Codebase quality analysis

LybraPeUSDVaultBase.sol

  • The contract does not have explicit access control modifiers, such as "onlyOwner" or "onlyAuthorized," to restrict access to sensitive functions
  • The contract lacks comprehensive error handling mechanisms. It does not provide explicit error messages or revert reasons in many cases -The contract lacks detailed inline comments explaining the purpose and functionality of the code

LybraEUSDVaultBase.sol

  • The contract lacks sufficient comments to explain the purpose and functionality of the code
  • The contract uses a mix of different naming conventions for variables, functions, and events
  • Some functions and variables have no access modifiers specified, such as totalDepositedAsset, lastReportTime, and poolTotalEUSDCirculation (e.g., public, external, internal, private).
  • There are some missing or inadequate error handling mechanisms in the contract. For example, the require statements do not provide specific error messages, which could make it challenging to diagnose issues when a transaction fails
  • The contract's event names, such as LiquidationRecord and LSDValueCaptured, do not follow the typical event naming conventions
  • Some variables, such as vaultType, feeStored, and lastReportTime, are declared but not used in the contract. Removing these unused variables would enhance code clarity and reduce unnecessary storage costs
  • There is some code duplication in functions like liquidation and superLiquidation. Duplicated code increases the risk of errors and makes the contract harder to maintain

EUSDMiningIncentives.sol

  • Instead of initializing the configurator and esLBRBoost variables in the constructor, consider using constructor initialization syntax. This can improve readability and reduce the number of function calls needed during deployment
  • Add input validation checks to functions that require specific conditions to be met. For example, in the setBiddingCost function, ensure that the _biddingRatio parameter is within the allowed range
  • When calculating the stakedOf function, consider optimizing the loop by using a for loop with a fixed length instead of a dynamic for loop. This can improve gas efficiency.
  • Explicitly specify the visibility modifiers for all functions, including external and internal functions
  • Consider using enumerations to represent different states or types within the contract
  • Look for opportunities to refactor repetitive code sections into reusable functions or modifiers. For example, the reward calculation logic in the earned function can be extracted into a separate internal function to improve code readability and maintainability.
  • Implement appropriate error handling mechanisms, such as reverting transactions with informative error messages when specific conditions are not met

LybraConfigurator.sol

  • Consider adding visibility specifiers like public, external, or internal to functions and state variables. vaultBadCollateralRatio,vaultSafeCollateralRatio,redemptionProvider don't have any visibility.
  • In the setBadCollateralRatio function, you can add additional checks to validate the newRatio value
  • You can combine similar mapping variables like vaultMintPaused and vaultBurnPaused into a single mapping that stores a struct with both flags

ProtocolRewardsPool.sol

  • The contract lacks sufficient inline comments to explain the purpose and functionality of the cod
  • Some functions, such as getPreUnlockableAmount and getReservedLBRForVesting, contain complex calculations and lack clear explanations or documentation
  • Ensuring consistent naming throughout the contract improves code readability and maintainability.
  • The contract combines various functionalities, such as staking, claiming rewards, and conversion between different tokens, in a single contract. Breaking down the contract into smaller, modular components can enhance code organization and reusability
  • The contract does not provide detailed error messages in some cases, making it challenging to identify the root cause of failures or exceptions

PeUSDMainnetStableVision.sol

  • The contract does not perform sufficient input validation for certain parameters, such as the eusdAmount in the convertToPeUSD function
  • The code uses require statements to check conditions and revert transactions when the conditions are not met. However, the error messages provided in the require statements are not descriptive
  • The contract's constructor initializes several variables and requires the input of _config, _sharedDecimals, and _lzEndpoint. It is crucial to ensure that these parameters are correctly set during contract deployment

Centralization risks

A single point of failure is not acceptable for this project Centrality risk is high in the project, the role of 'onlyOwner' detailed below has very critical and important powers

Project and funds may be compromised by a malicious or stolen private key onlyOwner msg.sender

FILE: 2023-06-lybra/contracts/lybra/miner/EUSDMiningIncentives.sol

84: function setToken(address _lbr, address _eslbr) external onlyOwner {
89: function setLBROracle(address _lbrOracle) external onlyOwner {
93: function setPools(address[] memory _pools) external onlyOwner {
100:function setBiddingCost(uint256 _biddingRatio) external onlyOwner {
105:function setExtraRatio(uint256 ratio) external onlyOwner {
110: function setPeUSDExtraRatio(uint256 ratio) external onlyOwner {
115: function setBoost(address _boost) external onlyOwner {
119: function setRewardsDuration(uint256 _duration) external onlyOwner {
124: function setEthlbrStakeInfo(address _pool, address _lp) external onlyOwner {
128: function setEUSDBuyoutAllowed(bool _bool) external onlyOwner {


FILE: 2023-06-lybra/contracts/lybra/miner/ProtocolRewardsPool.sol

52:  function setTokenAddress(address _eslbr, address _lbr, address _boost) external onlyOwner {
58:  function setGrabCost(uint256 _ratio) external onlyOwner {

FILE: Breadcrumbs2023-06-lybra/contracts/lybra/miner/stakerewardV2pool.sol

121: function setRewardsDuration(uint256 _duration) external onlyOwner {
127: function setBoost(address _boost) external onlyOwner {
132: function notifyRewardAmount(uint256 _amount) external onlyOwner updateReward(address(0)) {

Bug Fix

  1. Does it use a timelock function?: True timeclock functions not implemented as per documentations

  2. Potential reentrancy vulnerability the code includes external contract calls, such as EUSD.transfer and peUSD.transfer. If these contracts are not implemented securely and follow the checks-effects-interactions pattern, there may be a risk of reentrancy attacks.

  3. deposit and withdraw, are not properly checked for potential integer overflow or underflow.

  4. Should disable the renownceOwnerhip() when ever we use Ownable. Its possible onlyOwner can renounceOwner in unexpected way. So contracts become deep trouble without owner

  5. The withdraw function allows the sender to specify an arbitrary address to send the funds. This design could be vulnerable to denial-of-servicescenario by consumingexcessive gas`` during the withdrawal process.

  6. Consider adding event logging to important contract functions, such as deposit and withdraw important state changes

  7. The setBiddingCost function should include a check to ensure that the _biddingRatio is within a valid range.

  8. The contract includes some external function calls (e.g., EUSD.transferFrom, configurator.distributeRewards), but it does not handle potential exceptions that could arise from these calls.

  9. Some state variables, such as redemptionFee, flashloanFee, and maxStableRatio, are directly modifiable by privileged roles without any additional checks or restrictions.

  10. The code does not include mechanisms to prevent or mitigate DoS attacks, such as gas limit restrictions, rate limiting, or circuit breakers. Malicious actors could potentially exploit vulnerable areas in the code to consume excessive gas, leading to DoS attacks.

  11. In the notifyRewardAmount function, when calculating the rewardPerTokenStored, there could be precision loss when performing division operations. The code should handle the decimal places appropriately and ensure that precision loss does not occur, especially when dealing with token amounts or ratios

Gas Optimization

  • Some calculations, especially in the getPreUnlockableAmount function, involve multiple operations that may consume a significant amount of gas. Optimizing these calculations can improve the contract's gas efficiency and reduce transaction costs

  • Review data types: Analyze the data types used in your smart contracts and consider if they can be further optimized. For example, changing uint256 to uint128 or uint94 can save gas and storage slots

  • Struct packing : Look for opportunities to pack structs into fewer storage slots. By carefully selecting appropriate data types for struct members, you can reduce the overall storage usage.

  • Use constant values : If certain values in your contracts are constant and do not change, declare them as constants rather than storing them as state variables. This can significantly save gas costs.

  • Avoid unnecessary storage : Examine your code and eliminate any unnecessary storage of variables or addresses that are not required for contract functionality.

  • Storage vs. memory usage : When working with arrays or structs, consider whether using storage instead of memory can save gas. Using storage allows direct access to the state variables and avoids unnecessary copying of data.

  • replacing the use of memory with calldata for read-only arguments in external functions

Other recommendations

  • Regular code reviews and adherence to best practices
  • Conduct external audits by security experts
  • Consider open sourcing the contract for community review
  • Maintain comprehensive security documentation
  • Establish a responsible disclosure policy for vulnerabilities
  • Implement continuous monitoring for unusual activity
  • Educate users about risks and best practices

Time spent on analysis

15 Hours

Time spent:

15 hours

#0 - c4-sponsor

2023-07-27T08:39:17Z

LybraFinance marked the issue as sponsor confirmed

#1 - c4-judge

2023-07-28T17:09:04Z

0xean marked the issue as grade-a

#2 - c4-judge

2023-07-28T17:10:11Z

0xean marked the issue as selected for report

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter