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: 73/132
Findings: 1
Award: $80.43
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: JCN
Also found by: 0xAnah, DavidGiladi, MohammedRizwan, Rageur, Raihan, ReyAdmirado, Rolezn, SAAJ, SAQ, SM3_SS, Sathish9098, ayo_dev, dharma09, fatherOfBlocks, hunter_w3b, mgf15, mrudenko, naman1778, shamsulhaq123, souilos, turvy_fuzz
80.434 USDC - $80.43
no | Issue | Instances | |
---|---|---|---|
[G-01] | Using fixed bytes is cheaper than using string | 1 | - |
[G-02] | Can make the variable outside the loop to save gas | 1 | - |
[G-03] | Using calldata instead of memory for read-only arguments in external functions saves gas | 2 | - |
[G-04] | Use assembly to check for address(0) | 5 | - |
[G-05] | Before transfer of some functions, we should check some variables for possible gas save | 5 | - |
[G-06] | State variables can be packed to use fewer storage slots | 1 | - |
[G-07] | Use constants instead of type(uintx).max | 1 | - |
[G-08] | Use nested if and, avoid multiple check combinations | 3 | - |
[G-09] | Use assembly to write address storage values | 9 | - |
[G-10] | Sort Solidity operations using short-circuit mode | 3 | - |
[G-11] | Use hardcode address instead address(this) | 8 | - |
[G-12] | Use assembly for math (add, sub, mul, div) | 2 | - |
[G-13] | Don't apply the same value to state variables | 4 | - |
[G-14] | Minimize external calls if possible | more files | - |
As a rule of thumb, use bytes for arbitrary-length raw byte data and string for arbitrary-length string (UTF-8) data. If you can limit the length to a certain number of bytes, always use one of bytes1 to bytes32 because they are much cheaper
file: /contracts/lybra/token/EUSD.sol 107 function symbol() public pure returns (string memory) { 108 return "eUSD";
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/token/EUSD.sol#L107
Consider making the stack variables before the loop which gonna save gas
file: /contracts/lybra/miner/EUSDMiningIncentives.sol 140 uint borrowed = pool.getBorrowedOf(user);
calldata must be used when declaring an external function's dynamic parameters
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.
file: /contracts/lybra/miner/esLBRBoost.sol 33 function addLockSetting(esLBRLockSetting memory setting) external onlyOwner {
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/esLBRBoost.sol#L33
file: /contracts/lybra/miner/EUSDMiningIncentives.sol 93 function setPools(address[] memory _pools) external onlyOwner {
Save 6 gas per instance.
file: /contracts/lybra/miner/stakerewardV2pool.sol 60 if (_account != address(0)) {
file: /contracts/lybra/token/PeUSDMainnetStableVision.sol 64 require(to != address(0), "TZA");
file: /contracts/lybra/configuration/LybraConfigurator.sol 99 if (address(EUSD) == address(0)) EUSD = IEUSD(_eusd); 100 if (address(peUSD) == address(0)) peUSD = IEUSD(_peusd);
file: /contracts/lybra/miner/EUSDMiningIncentives.sol 76 if (_account != address(0)) {
Before transfer, we should check for amount being 0 so the function doesn't run when its not gonna do anything.
file: /contracts/lybra/token/PeUSDMainnetStableVision.sol /// @audit before of tranfering, should check by if()/require(). 133 bool success = EUSD.transferFrom(address(receiver), address(this), EUSD.getMintedEUSDByShares(shareAmount));
file: /contracts/lybra/miner/ProtocolRewardsPool.sol# 210 token.transfer(msg.sender, tokenAmount);
file: /contracts/lybra/token/EUSD.sol 339 emit Transfer(owner, _recipient, tokensAmount); 351 emit Transfer(_sender, _recipient, _amount);
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/token/EUSD.sol#L339
The EVM works with 32 byte words. Variables less than 32 bytes can be declared next to eachother in storage and this will pack the values together into a single 32 byte storage slot (if the values combined are <= 32 bytes). If the variables packed together are retrieved together in functions we will effectively save ~2000 gas with every subsequent SLOAD for that storage slot. This is due to us incurring a Gwarmaccess (100 gas) versus a Gcoldsload (2100 gas). 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 are also cheaper.
file: /contracts/lybra/configuration/LybraConfigurator.sol 38 mapping(address => bool) public mintVault; 39 mapping(address => uint256) public mintVaultMaxSupply; 40 mapping(address => bool) public vaultMintPaused; 41 mapping(address => bool) public vaultBurnPaused; 42 mapping(address => uint256) vaultSafeCollateralRatio; 43 mapping(address => uint256) vaultBadCollateralRatio; 44 mapping(address => uint256) public vaultMintFeeApy; 45 mapping(address => uint256) public vaultKeeperRatio; 46 mapping(address => bool) redemptionProvider; 47 mapping(address => bool) public tokenMiner;
Sort of Variables to use fewer storage slots.
38 mapping(address => bool) public mintVault; 39 mapping(address => bool) public vaultMintPaused; 40 mapping(address => bool) public vaultBurnPaused; 41 mapping(address => bool) redemptionProvider; 42 mapping(address => bool) public tokenMiner; 43 mapping(address => uint256) public mintVaultMaxSupply; 44 mapping(address => uint256) vaultSafeCollateralRatio; 45 mapping(address => uint256) vaultBadCollateralRatio; 46 mapping(address => uint256) public vaultMintFeeApy; 47 mapping(address => uint256) public vaultKeeperRatio;
type(uint120).max or type(uint112).max, etc. it uses more gas in the distribution process and also for each transaction than constant usage.
file: /contracts/lybra/token/EUSD.sol 179 if (currentAllowance != type(uint256).max) {
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/token/EUSD.sol#L179
Using nested if is cheaper than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports.
file: /contracts/lybra/token/PeUSD.sol 46 if (_from != address(this) && _from != spender)
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/token/PeUSD.sol#L46
file: /contracts/lybra/token/LBR.sol 64 if (_from != address(this) && _from != spender)
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/token/LBR.sol#L64
file: /contracts/lybra/token/PeUSDMainnetStableVision.sol 199 if (_from != address(this) && _from != spender)
- 46: if (_from != address(this) && _from != spender) + if ( from != address(this)){ + if( _from != spender){ + + } + }
By using assembly to write to address storage values, you can bypass some of these operations and lower the gas cost of writing to storage. Assembly code allows you to directly access the Ethereum Virtual Machine (EVM) and perform low-level operations that are not possible in Solidity.
example of using assembly to write to address storage values:
contract MyContract { address private myAddress; function setAddressUsingAssembly(address newAddress) public { assembly { sstore(0, newAddress) } } }
Instances:
file: /contracts/lybra/governance/GovernanceTimelock.sol 25 function checkRole(bytes32 role, address _sender) public view returns(bool){ 29 function checkOnlyRole(bytes32 role, address _sender) public view returns(bool){
file: /contracts/lybra/token/PeUSD.sol 31 function _debitFrom(address _from, uint16, bytes32, uint _amount) internal virtual override returns (uint) { 38 function _creditTo(uint16, address _toAddress, uint _amount) internal virtual override returns (uint) {
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/token/PeUSD.sol#L31
file: /contracts/lybra/token/LBR.sol 56 function _creditTo(uint16, address _toAddress, uint _amount) internal virtual override returns (uint) { 61 function _transferFrom(address _from, address _to, uint _amount) internal virtual override returns (uint) {
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/token/LBR.sol#L56
file: /contracts/lybra/miner/stakerewardV2pool.sol 101 function getBoost(address _account) public view returns (uint256) {
file: /contracts/lybra/token/PeUSDMainnetStableVision.sol 191 function _creditTo(uint16, address _toAddress, uint _amount) internal virtual override returns (uint) {
file: /contracts/lybra/token/EUSD.sol 200 function approve(address _spender, uint256 _amount) public returns (bool) {
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/token/EUSD.sol#L200
Short-circuiting is a solidity contract development model that uses OR/AND logic to sequence different cost operations. It puts low gas cost operations in the front and high gas cost operations in the back, so that if the front is low If the cost operation is feasible, you can skip (short-circuit) the subsequent high-cost Ethereum virtual machine operation.
//f(x) is a low gas cost operation //g(y) is a high gas cost operation //Sort operations with different gas costs as follows f(x) || g(y) f(x) && g(y)
Instaces:
file: /contracts/lybra/miner/esLBRBoost.sol 60 if (userUpdatedAt >= boostEndTime || userUpdatedAt >= finishAt) { 63 if (finishAt <= boostEndTime || block.timestamp <= boostEndTime) {
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/esLBRBoost.sol#L60
file: /contracts/lybra/token/PeUSDMainnetStableVision.sol 80 require(_msgSender() == user || _msgSender() == address(this), "MDM");
Instead of using address(this), it is more gas-efficient to pre-calculate and use the hardcoded address. Foundry’s script.sol and solmate’s LibRlp.sol contracts can help achieve this. References: https://book.getfoundry.sh/reference/forge-std/compute-create-address
Here's an example :
contract MyContract { address constant public CONTRACT_ADDRESS = 0x1234567890123456789012345678901234567890; function getContractAddress() public view returns (address) { return CONTRACT_ADDRESS; } }
file: /contracts/lybra/pools/LybraStETHVault.sol 63 uint256 excessAmount = collateralAsset.balanceOf(address(this)) - totalDepositedAsset;
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/LybraStETHVault.sol#L63
file: /contracts/lybra/miner/stakerewardV2pool.sol 85 bool success = stakingToken.transferFrom(msg.sender, address(this), _amount);
file: /contracts/lybra/token/PeUSDMainnetStableVision.sol 82 bool success = EUSD.transferFrom(user, address(this), eusdAmount); 133 bool success = EUSD.transferFrom(address(receiver), address(this), EUSD.getMintedEUSDByShares(shareAmount));
file: /contracts/lybra/miner/ProtocolRewardsPool.sol 195 uint256 balance = EUSD.sharesOf(address(this)); 200 uint256 peUSDBalance = peUSD.balanceOf(address(this));
file: /contracts/lybra/configuration/LybraConfigurator.sol 290 uint256 peUSDBalance = peUSD.balanceOf(address(this)); 295 uint256 balance = EUSD.balanceOf(address(this));
Use assembly for math instead of Solidity. You can check for overflow/underflow in assembly to ensure safety. If using Solidity versions < 0.8.0 and you are using Safemath, you can gain significant gas savings by using assembly to calculate values and checking for overflow/underflow.
Examples: Multiplication:
//multiplication in Solidity function mulTest(uint256 a, uint256 b) public pure { uint256 c = a * b; }
Gas: 325
//multiplication in assembly function mulAssemblyTest(uint256 a, uint256 b) public pure { assembly { let c := mul(a, b) if lt(c, a) { mstore(0x00, "overflow") revert(0x00, 0x20) } } }
Gas: 265
Division:
///division in Solidity function divTest(uint256 a, uint256 b) public pure { uint256 c = a / b; }
Gas: 325
///division in assembly function divAssemblyTest(uint256 a, uint256 b) public pure { assembly { let c := div(a, b) if gt(c, a) { mstore(0x00, "underflow") revert(0x00, 0x20) } } }
Instances
:
file: /contracts/lybra/pools/LybraStETHVault.sol 104 return 10000 - (time / 30 minutes - 1) * 100;
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/LybraStETHVault.sol#L104
file: /contracts/lybra/miner/EUSDMiningIncentives.sol 210 uint256 biddingFee = (reward * biddingFeeRatio) / 10000;
uint256 value; assembly { // Calculate time / 30 minutes let quotient := div(time, 1800) // Subtract 1 from quotient let subtracted := sub(quotient, 1) // Multiply subtracted by 100 let multiplied := mul(subtracted, 100) // Subtract multiplied from 10000 value := sub(10000, multiplied) } return value; }
Assigning the same value to a state variable repeatedly can cause unnecessary gas costs, because each assignment operation incurs a gas cost. Additionally, if the value being assigned is the same as the current value of the state variable, the assignment operation does not actually change anything, which can waste gas.
file: /contracts/lybra/configuration/LybraConfigurator.sol /// @audit the premiumTradingEnabled and vaultMintPaused[pool] are state variable, which have same value. 168 premiumTradingEnabled = isActive; 178 vaultMintPaused[pool] = isActive; 204 vaultSafeCollateralRatio[pool] = newRatio; 226 vaultKeeperRatio[pool] = newRatio;
file: /contracts/lybra/miner/ProtocolRewardsPool.sol 120 unstakeRatio[msg.sender] = 0; 121 time2fullRedemption[msg.sender] = 0; 144 unstakeRatio[msg.sender] = 0; 145 time2fullRedemption[msg.sender] = 0;
External calls to other contracts are expensive in terms of gas. Try to minimize external calls by using in-line code or caching data in memory.
file: more file of lybra Finance More files of lybra Finance have external functions and call.
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/
#0 - c4-pre-sort
2023-07-27T21:25:05Z
JeffCX marked the issue as high quality report
#1 - c4-judge
2023-07-27T23:42:14Z
0xean marked the issue as grade-a
#2 - c4-sponsor
2023-07-29T09:10:41Z
LybraFinance marked the issue as sponsor acknowledged