Lybra Finance - dharma09'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: 79/132

Findings: 1

Award: $80.43

Gas:
grade-a

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

80.434 USDC - $80.43

Labels

bug
G (Gas Optimization)
grade-a
high quality report
edited-by-warden
G-20

External Links

Table of contents

The result of a function call should be cached rather than re-calling the function


External calls are expensive. Consider caching the following:

ProtocolRewardsPool.sol.notifyRewardAmount() : Results of totalStaked() should be cached


In Solidity, caching repeated function calls can be an effective way to optimize gas usage, especially when the function is called frequently with the same arguments

File: contracts/lybra/miner/ProtocolRewardsPool.sol
	227: function notifyRewardAmount(uint amount, uint tokenType) external {
	228:        require(msg.sender == address(configurator));
	229:        if (totalStaked() == 0) return; //@audit: Initial call
	230:        require(amount > 0, "amount = 0");
	231:        if(tokenType == 0) {
	232:            uint256 share = IEUSD(configurator.getEUSDAddress()).getSharesByMintedEUSD(amount);
	233:            rewardPerTokenStored = rewardPerTokenStored + (share * 1e18) / totalStaked(); //@audit: 2nd call
	234        } else if(tokenType == 1) {
	235:            ERC20 token = ERC20(configurator.stableToken());
	236:            rewardPerTokenStored = rewardPerTokenStored + (amount * 1e36 / token.decimals()) / totalStaked(); //@audit  2nd call
	234        } else if(tokenType == 1) {
	237:        } else {
	238:            rewardPerTokenStored = rewardPerTokenStored + (amount * 1e18) / totalStaked(); //@audit  2nd call
	234        } else if(tokenType == 1) {
	239:        }
	240:    }
diff --git a/contracts/lybra/miner/ProtocolRewardsPool.sol b/contracts/lybra/miner/ProtocolRewardsPool.sol
index 8fc83d6..7c70c15 100644
--- a/contracts/lybra/miner/ProtocolRewardsPool.sol
+++ b/contracts/lybra/miner/ProtocolRewardsPool.sol
@@ -225,17 +225,18 @@ contract ProtocolRewardsPool is Ownable {
      * @dev When receiving stablecoin tokens other than eUSD, the decimals of the token are converted to 18 for consistent calculations.
      */
     function notifyRewardAmount(uint amount, uint tokenType) external {
-        require(msg.sender == address(configurator));
-        if (totalStaked() == 0) return;
+        require(msg.sender == address(configurator)); //@audit cache function
+        uint256 _totalStaked = totalStaked();
+        if (_totalStaked == 0) return;
         require(amount > 0, "amount = 0");
         if(tokenType == 0) {
             uint256 share = IEUSD(configurator.getEUSDAddress()).getSharesByMintedEUSD(amount);
-            rewardPerTokenStored = rewardPerTokenStored + (share * 1e18) / totalStaked();
+            rewardPerTokenStored = rewardPerTokenStored + (share * 1e18) / _totalStaked;
         } else if(tokenType == 1) {
             ERC20 token = ERC20(configurator.stableToken());
-            rewardPerTokenStored = rewardPerTokenStored + (amount * 1e36 / token.decimals()) / totalStaked();
+            rewardPerTokenStored = rewardPerTokenStored + (amount * 1e36 / token.decimals()) / _totalStaked;
         } else {
-            rewardPerTokenStored = rewardPerTokenStored + (amount * 1e18) / totalStaked();
+            rewardPerTokenStored = rewardPerTokenStored + (amount * 1e18) / _totalStaked;
         }

EUSDMiningIncentives.sol.rewardPerToken() : Result of totalStaled() should be cached


File: /contracts/lybra/miner/EUSDMiningIncentives.sol
163: function rewardPerToken() public view returns (uint256) {
164:        if (totalStaked() == 0) {
165:            return rewardPerTokenStored;
166:        }
167:
168:        return rewardPerTokenStored + (rewardRatio * (lastTimeRewardApplicable() - updatedAt) * 1e18) / totalStaked();
169:    }
diff --git a/contracts/lybra/miner/EUSDMiningIncentives.sol b/contracts/lybra/miner/EUSDMiningIncentives.sol
index e6c57c8..450e02a 100644
--- a/contracts/lybra/miner/EUSDMiningIncentives.sol
+++ b/contracts/lybra/miner/EUSDMiningIncentives.sol
@@ -161,11 +161,12 @@ contract EUSDMiningIncentives is Ownable {
     }
 
     function rewardPerToken() public view returns (uint256) {
-        if (totalStaked() == 0) {
+         uint256 totalStaked = totalStaked();
+        if (totalStaked == 0) {
             return rewardPerTokenStored;
         }
 
-        return rewardPerTokenStored + (rewardRatio * (lastTimeRewardApplicable() - updatedAt) * 1e18) / totalStaked(
);
+        return rewardPerTokenStored + (rewardRatio * (lastTimeRewardApplicable() - updatedAt) * 1e18) / totalStaked;
     }

PeUSDMainnetStableVision.sol.convertToPeUSDAndCrossChain() : Results of _msgsender() should be cached


File: /contracts/lybra/token/PeUSDMainnetStableVision.sol
	97: function convertToPeUSDAndCrossChain(
	98:        uint256 eusdAmount,
	99:        uint16 dstChainId,
	100:        bytes32 toAddress,
	101:        LzCallParams calldata callParams
	102:    ) external payable {
	103:        convertToPeUSD(_msgSender(), eusdAmount); //@audit initial call
	104:        sendFrom(_msgSender(), dstChainId, toAddress, eusdAmount, callParams); //@audit 2nd call
	105:    }
diff --git a/contracts/lybra/token/PeUSDMainnetStableVision.sol b/contracts/lybra/token/PeUSDMainnetStableVision.sol
index a1b2c72..ef7f474 100644
--- a/contracts/lybra/token/PeUSDMainnetStableVision.sol
+++ b/contracts/lybra/token/PeUSDMainnetStableVision.sol
@@ -100,8 +100,9 @@ contract PeUSDMainnet is BaseOFTV2, ERC20 {
         bytes32 toAddress,
         LzCallParams calldata callParams
     ) external payable {
-        convertToPeUSD(_msgSender(), eusdAmount);
-        sendFrom(_msgSender(), dstChainId, toAddress, eusdAmount, callParams);
+         address spender = _msgSender();
+        convertToPeUSD(spender, eusdAmount);
+        sendFrom(spender, dstChainId, toAddress, eusdAmount, callParams);
     }

PeUSDMainnetStableVision.sol.convertToPeUSD : Results of _msgsender() should be cached


File: /contracts/lybra/token/PeUSDMainnetStableVision.sol
79: function convertToPeUSD(address user, uint256 eusdAmount) public {
80:        require(_msgSender() == user || _msgSender() == address(this), "MDM");
81:        require(EUSD.balanceOf(address(this)) + eusdAmount <= configurator.getEUSDMaxLocked(),"ESL");
82:        bool success = EUSD.transferFrom(user, address(this), eusdAmount);83:
84:        require(success, "TF");
85:        uint256 share = EUSD.getSharesByMintedEUSD(eusdAmount);
86:        userConvertInfo[user].depositedEUSDShares += share;
87:        userConvertInfo[user].mintedPeUSD += eusdAmount;
88:        _mint(user, eusdAmount);
89:    }
diff --git a/contracts/lybra/token/PeUSDMainnetStableVision.sol b/contracts/lybra/token/PeUSDMainnetStableVision.sol
index a1b2c72..ebd4f65 100644
--- a/contracts/lybra/token/PeUSDMainnetStableVision.sol
+++ b/contracts/lybra/token/PeUSDMainnetStableVision.sol
@@ -77,7 +77,8 @@ contract PeUSDMainnet is BaseOFTV2, ERC20 {
      * @param eusdAmount The amount of eUSD to deposit and mint PeUSD tokens.
      */
     function convertToPeUSD(address user, uint256 eusdAmount) public {
-        require(_msgSender() == user || _msgSender() == address(this), "MDM");
+      address spender = _msgSender();
+        require(spender == user || spender == address(this), "MDM");
         require(EUSD.balanceOf(address(this)) + eusdAmount <= configurator.getEUSDMaxLocked(),"ESL");
         bool success = EUSD.transferFrom(user, address(this), eusdAmount);
         require(success, "TF");
          uint256 share = EUSD.getSharesByMintedEUSD(eusdAmount);
      userConvertInfo[user].depositedEUSDShares += share;
        userConvertInfo[user].mintedPeUSD += eusdAmount;
        _mint(user, eusdAmount);
    }

State variables only set in the constructor should be declared immutable


Avoids a Gsset (20000 gas) in the constructor, and replaces the first access in each transaction (Gcoldsload - 2100 gas) and each access thereafter (Gwarmacces - 100 gas) with a PUSH32 (3 gas).

While strings are not value types, and therefore cannot be immutable/constant if not hard-coded outside of the constructor, the same behavior can be achieved by making the current contract abstract with virtual functions for the string accessors, and having a child contract override the functions with the hard-coded implementation-specific values.

2.1k per var

File: contracts/lybra/miner/stakerewardV2pool.sol
	52: esLBRBoost = IesLBRBoost(_boost);  //@audit esLRBoost (constructor)

Multiple accesses of a mapping/array should use a local variable cache


Caching a mapping's value in a local storage or calldata variable when the value is accessed multiple times saves ~42 gas per access due to not having to perform the same offset calculation every time. Help the Optimizer by saving a storage variable's reference instead of repeatedly fetching it

To help the optimizer,declare a storage type variable and use it instead of repeatedly fetching the reference in a map or an array. As an example, instead of repeatedly calling someMap[someIndex], save its reference like this: SomeStruct storage someStruct = someMap[someIndex] and use it.

LybraGovernance.sol.proposals() :proposalData[proposalId] should be cached in local storage


File: /contracts/lybra/governance/LybraGovernance.sol
	112: function proposals(uint256 proposalId) external view returns (uint256 id, address proposer, uint256 eta, uint256 startBlock, uint256 endBlock, uint256 forVotes, uint256 againstVotes, uint256 abstainVotes, bool canceled, bool executed) {
	113:        id = proposalId;
	114:        eta = proposalEta(proposalId);
	115:        startBlock = proposalSnapshot(proposalId);
	116:        endBlock = proposalDeadline(proposalId);
	117:
	118:        proposer = proposalProposer(proposalId);
	119:        
	120:        forVotes =  proposalData[proposalId].supportVotes[0];
	121:        againstVotes =  proposalData[proposalId].supportVotes[1];
	122:        abstainVotes =  proposalData[proposalId].supportVotes[2];
	123:
	124:        ProposalState currentState = state(proposalId);
	125:        canceled = currentState == ProposalState.Canceled;
	126:        executed = currentState == ProposalState.Executed;
	127:    }
diff --git a/contracts/lybra/governance/LybraGovernance.sol b/contracts/lybra/governance/LybraGovernance.sol
index 7b2d4ad..2230c45 100644
--- a/contracts/lybra/governance/LybraGovernance.sol
+++ b/contracts/lybra/governance/LybraGovernance.sol
@@ -57,6 +57,7 @@ contract LybraGovernance is GovernorTimelockControl {
      * @dev Amount of votes already cast passes the threshold limit.
      */
     function _quorumReached(uint256 proposalId) internal view override returns (bool){
+        
         return proposalData[proposalId].supportVotes[1] + proposalData[proposalId].supportVotes[2] >= quorum(proposalSnapshot(proposalId));
     }
 
@@ -116,10 +117,10 @@ contract LybraGovernance is GovernorTimelockControl {
         endBlock = proposalDeadline(proposalId);
 
         proposer = proposalProposer(proposalId);
-        
-        forVotes =  proposalData[proposalId].supportVotes[0];
-        againstVotes =  proposalData[proposalId].supportVotes[1];
-        abstainVotes =  proposalData[proposalId].supportVotes[2];
+        ProposalExtraData storage proposalExtraData = proposalData[proposalId];
+        forVotes =  proposalExtraData.supportVotes[0];
+        againstVotes =  proposalExtraData.supportVotes[1];
+        abstainVotes =  proposalExtraData.supportVotes[2];
 
         ProposalState currentState = state(proposalId);
         canceled = currentState == ProposalState.Canceled;

LybraGovernance.sol.getReceipt() :proposalData[proposalId] should be cached in local storage


File: /contracts/lybra/governance/LybraGovernance.sol
	129: function getReceipt(uint256 proposalId, address account) external view returns (bool voted, uint8 support, uint256 votes){  
	130:        voted = proposalData[proposalId].receipts[account].hasVoted;
	131:        support = proposalData[proposalId].receipts[account].support;
	132:        votes = proposalData[proposalId].receipts[account].votes;
	133:	    }
@@ -127,9 +128,10 @@ contract LybraGovernance is GovernorTimelockControl {
     }
 
     function getReceipt(uint256 proposalId, address account) external view returns (bool voted, uint8 support, uint2
56 votes){  
-        voted = proposalData[proposalId].receipts[account].hasVoted;
-        support = proposalData[proposalId].receipts[account].support;
-        votes = proposalData[proposalId].receipts[account].votes;
+       ProposalExtraData storage proposalExtraData = proposalData[proposalId];
+        voted = proposalExtraData.receipts[account].hasVoted;
+        support = proposalExtraData.receipts[account].support;
+        votes = proposalExtraData.receipts[account].votes;
     }

Internal/Private functions only called once can be inlined to save gas Gas saved: 20 * 20= 400


Not inlining costs 20 to 40 gas because of two extra JUMP instructions and additional stack operations needed for function calls.

File: contracts/lybra/pools/base/LybraEUSDVaultBase.sol
300: function _newFee() internal view returns (uint256) {
301:        return (poolTotalEUSDCirculation * configurator.vaultMintFeeApy(address(this)) * (block.timestamp - lastReportTime)) / (86400 * 365) / 10000;
302:    }

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

 Save 4k gas

File: /contracts/lybra/miner/esLBRBoost.sol
	39: esLBRLockSetting memory _setting = esLBRLockSettings[id];
-  esLBRLockSetting memory _setting = esLBRLockSettings[id];
+  esLBRLockSetting storage _setting = esLBRLockSettings[id];

A modifier used only once and not being inherited should be inlined to save gas

File: /contracts/lybra/token/PeUSDMainnetStableVision.sol
50: modifier BurnPaused() {
51:        require(!configurator.vaultBurnPaused(msg.sender), "BPP");
52:        _;
53:    }

The above modifier is only being called on L69

File: /contracts/lybra/token/EUSD.sol
83: modifier MintPaused() {
84:        require(!configurator.vaultMintPaused(msg.sender), "MPP");
85:        _;
86:    }

The above modifier is only being called on L411

#0 - c4-pre-sort

2023-07-27T21:36:17Z

JeffCX marked the issue as high quality report

#1 - c4-judge

2023-07-27T23:42:27Z

0xean marked the issue as grade-a

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