GithubHelp home page GithubHelp logo

2022-04-xtribe-findings's Introduction

xTRIBE Contest

Unless otherwise discussed, this repo will be made public after contest completion, sponsor review, judging, and two-week issue mitigation window.


Contest findings are submitted to this repo

Typically most findings come in on the last day of the contest, so don't be alarmed at all if there's nothing here but crickets until the end of the contest.

As a sponsor, you have four critical tasks in the contest process:

  1. Handle duplicate issues.
  2. Weigh in on severity.
  3. Respond to issues.
  4. Share your mitigation of findings.

Let's walk through each of these.

Handle duplicates

Because the wardens are submitting issues without seeing each others' submissions, there will always be findings that are clear duplicates. Other findings may use different language which ultimately describes the same issue but from different angles. Use your best judgement in identifying duplicates, and don't hesitate to reach out (in your private contest channel) to ask C4 for advice.

  1. Determine the best and most thorough description of the finding among the set of duplicates. (At least a portion of the content of the most useful description will be used in the audit report.)
  2. Close the other duplicate issues and label them with duplicate
  3. Mention the primary issue # when closing the issue so that duplicate issues get linked.

Weigh in on severity

Judges have the ultimate discretion in determining severity of issues as well as whether/how issues are considered duplicates. However, sponsor input is a significant criteria.

For a detailed breakdown of severity criteria and how to estimate risk, please refer to the judging criteria in our documentation.

If you disagree with a finding's severity, leave the original severity label set by the warden and add the label disagree with severity along with comment indicating your opinion for the judges to review. It is possible for issues to be considered 0 (Non-critical).

Feel free to use the question label to anything you would like additional C4 input on.

Respond to issues

Label each finding as one of these:

  • sponsor confirmed, meaning: "Yes, this is a problem and we intend to fix it."
  • sponsor disputed, meaning either: "We cannot duplicate this issue" or "We disagree that this is an issue at all."
  • sponsor acknowledged, meaning: "Yes, technically the issue is correct, but we are not going to resolve it for xyz reasons."

(Note: please don't use sponsor disputed for a finding if you think it should be considered of lower or higher severity. Instead, use the label disagree with severity and add comments to recommend a different severity level -- and include your reasoning.)

Add any necessary comments explaining your rationale for your evaluation of the issue. Note that when the repo is public, after all issues are mitigated, wardens will read these comments.

Share your mitigation of findings

For each non-duplicate finding which you have confirmed, you will want to mitigate the issue before the contest report is made public.

As you undertake that process, we ask that you create a pull request in your original repo for each finding, and link to the PR in the issue the PR resolves. This will allow for complete transparency in showing the work of mitigating the issues found in the contest. Do not close the issue; simply label it as resolved. If the issue in question has duplicates, please link to your PR from the issue you selected as the best and most thoroughly articulated one.

2022-04-xtribe-findings's People

Contributors

code423n4 avatar c4-staff avatar itsmetechjay avatar ninek9 avatar

Stargazers

Rass. avatar Fthar avatar

Watchers

Ashok avatar Tom Waite avatar  avatar

2022-04-xtribe-findings's Issues

`xERC4626` does not work with fee-on-transfer tokens

Lines of code

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L65-L74

Vulnerability details

Impact

When fee-on-transfer tokens are used as the asset, the contract miscalculates the storedTotalAssets variable, which is used as a part of totalAssets(), which is used to calculate share amounts for minting, redeeming, etc.

Proof of Concept

This is an extension of the same issue that was found to be of medium risk in the prior tribe contest. Because xERC4626 extends ERC4626 the issues that occurred there occur in this contract as well, since the functions in question are not overridden or update to handle the issue.

Even excluding the parts that only are in the base contract, this new contract continues the pattern of not understanding fee-on-transfer tokens in its new callbacks:

File: lib/ERC4626/src/xERC4626.sol

65       function beforeWithdraw(uint256 amount, uint256 shares) internal virtual override {
66            super.beforeWithdraw(amount, shares);
67            storedTotalAssets -= amount;
68        }
69    
70        // Update storedTotalAssets on deposit/mint
71        function afterDeposit(uint256 amount, uint256 shares) internal virtual override {
72            storedTotalAssets += amount;
73            super.afterDeposit(amount, shares);
74        }

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L65-L74

If the contract cannot handle fee-on-transfer tokens, it does not fully allow for DeFi composability:

File: lib/ERC4626/src/xERC4626.sol

14             It is fully compatible with [ERC4626](https://eips.ethereum.org/EIPS/eip-4626) allowing for DeFi composability.

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L14

Tools Used

Code inspection

Recommended Mitigation Steps

Modify ERC4626 to measure balances before/after changes in balances, and provide those values to the before/after callbacks rather than using stated amounts

User can steal rewards

Lines of code

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L238

Vulnerability details

Impact

Users can steal rewards which were not meant for them

Proof of Concept

  1. Assume state index has become 6.
  2. User just made deposit on this strategy.
  3. Once accrueUser method is called user supplierIndex/userIndex will be 0 and hence would be getting reward for delat index 6 when he should have got none.

Recommended Mitigation Steps

Ideally supplierIndex/userIndex should be updated to global index once deposit is made

QA Report

Title: Add a timelock
Severity: Low Risk

To give more trust to users: functions that set key/critical variables should be put behind a timelock.

    https://github.com/code-423n4/2022-04-xtribe/tree/main/LIB/flywheel-v2/FlywheelCore.sol#L165
    https://github.com/code-423n4/2022-04-xtribe/tree/main/LIB/flywheel-v2/FlywheelGaugeRewards.sol#L273
    https://github.com/code-423n4/2022-04-xtribe/tree/main/LIB/flywheel-v2/FlywheelCore.sol#L183

Title: Require with empty message
Severity: Low Risk

The following requires are with empty messages.
This is very important to add a message for any require. Such that the user has enough
information to know the reason of failure:

    Solidity file: FlywheelGaugeRewards.sol, In line 114 with Empty Require message.
    Solidity file: FlywheelGaugeRewards.sol, In line 195 with Empty Require message.
    Solidity file: FlywheelGaugeRewards.sol, In line 200 with Empty Require message.

Title: Solidity compiler versions mismatch
Severity: Low Risk

The project is compiled with different versions of solidity, which is not recommended because it can lead to undefined behaviors.

Title: Require with not comprehensive message
Severity: Low Risk

The following requires has a non comprehensive messages.
This is very important to add a comprehensive message for any require. Such that the user has enough
information to know the reason of failure:

    Solidity file: FlywheelCore.sol, In line 147 with Require message: strategy

Title: Named return issue
Severity: Low Risk

Users can mistakenly think that the return value is the named return, but it is actually the actualreturn statement that comes after. To know that the user needs to read the code and is confusing.
Furthermore, removing either the actual return or the named return will save gas.

    FlywheelGaugeRewards.sol, getAccruedRewards

Title: Duplicates in array
Severity: Low Risk

    You allow in some arrays to have duplicates. Sometimes you assumes there are no duplicates in the array.

FlywheelCore._addStrategyForRewards pushed (strategy)
{
require(strategyState[strategy].index == 0, "strategy");

    allStrategies.push(strategy);
    emit AddStrategy(address(strategy));
}

Title: Check transfer receiver is not 0 to avoid burned money
Severity: Low Risk

Transferring tokens to the zero address is usually prohibited to accidentally avoid "burning" tokens by sending them to an unrecoverable zero address.

   flywheel-v2/FlywheelCore.sol#L168
   flywheel-v2/FlywheelCore.sol#L125
   xTRIBE.sol#L134
   xTRIBE.sol#L149

Title: Assert instead require to validate user inputs
Severity: Low Risk

    From solidity docs: Properly functioning code should never reach a failing assert statement; if this happens there is a bug in your contract which you should fix.
    With assert the user pays the gas and with require it doesn't. The ETH network gas isn't cheap and users can see it as a scam.
    
    FlywheelGaugeRewards.sol : reachable assert in line 195
    FlywheelGaugeRewards.sol : reachable assert in line 234

Title: Not verified input
Severity: Low Risk

external / public functions parameters should be validated to make sure the address is not 0.
Otherwise if not given the right input it can mistakenly lead to loss of user funds.

    
    FlywheelCore.sol.accrue user
    FlywheelCore.sol.claimRewards user
    xTRIBE.sol.transferFrom to
    xTRIBE.sol.transferFrom from
    xTRIBE.sol.transfer to

Title: transfer return value of a general ERC20 is ignored
Severity: High Risk

Need to use safeTransfer instead of transfer. As there are popular tokens, such as USDT that transfer/trasnferFrom method doesn’t return anything. The transfer return value has to be checked (as there are some other tokens that returns false instead revert), that means you must

  1. Check the transfer return value
    Another popular possibility is to add a whiteList.
    Those are the appearances (solidity file, line number, actual line):

    xTRIBE.sol, 134 (transfer), return ERC20.transfer(to, amount);
    xTRIBE.sol, 149 (transferFrom), return ERC20.transferFrom(from, to, amount);
    

QA Report

1. Assert is used instead of require in Flywheel*Rewards contracts (non-critical)

Impact

Assert will consume all the available gas, providing no additional benefits when being used instead of require, which both returns gas and allows for error message.

Proof of Concept

assert is used in FlywheelGaugeRewards:

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L196

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L235

And in FlywheelDynamicRewards:

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelDynamicRewards.sol#L54

Recommended Mitigation Steps

Using assert in production isn't recommended, consider substituting it with require in all the cases.

2. User facing functions miss emergency lever (non-critical)

Impact

If there be any emergency with system contracts or outside infrastructure, there is no way to temporary stop the operations.

Proof of Concept

Most core user-facing contracts do not have pausing functionality for new operation initiation.

For example, FlywheelCore user-facing accrue and claim logic cannot be temporary paused:

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L84-L129

Recommended Mitigation Steps

Consider making all new actions linked user facing functions pausable, first of all ones without access controls.

For example, by using OpenZeppelin's approach:

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/Pausable.sol

3. At least two kinds of floating pragma is used in the parts of the system (non-critical)

Impact

As different compiler versions have critical behavior specifics if the contracts get accidentally deployed using another compiler version compared to the one they were tested with, various types of undesired behavior can be introduced.

Proof of Concept

pragma solidity ^0.8.0 is used in xTRIBE, ERC20Gauges, xERC4626 contracts, for example:

https://github.com/fei-protocol/xTRIBE/blob/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L4

pragma solidity >=0.8.0 is used in ERC4626:

https://github.com/Rari-Capital/solmate/blob/e7deddffd5206cdec1d554c5361f529c2a525846/src/mixins/ERC4626.sol#L2

Recommended Mitigation Steps

Consider fixing the version to 0.8.10 across all the codebase, as Flywheel contracts has it fixed to that one.

4. Ownership is transferred with one step procedure (non-critical)

Impact

One step process offers no protection for the cases when ownership transfer is performed mistakenly or with any malicious intent.

Adding a modest complexity of an additional step and a delay is a low price to pay for having time to evaluate the ownership change.

Proof of Concept

authority and owner are set immediately in one step in the Auth contract being used for governance management:

https://github.com/Rari-Capital/solmate/blob/9f16db2144cc9a7e2ffc5588d4bf0b66784283bd/src/auth/Auth.sol#L38-L52

Recommended Mitigation Steps

Consider utilizing two-step ownership transferring process (proposition and acceptance in the separate actions) with a noticeable delay between the steps to enforce the transparency and stability of the system.

5. Core configuration variables aren't checked in constructor (non-critical)

Impact

A range of malfunctions is possible if the core configuration parameters are set by mistake to any values that make little sense. It is also not always right away evident that such a mistake took place.

Proof of Concept

FlywheelCore doesn't check token, booster, rewards contracts to be valid:

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L43-L53

FlywheelGaugeRewards doesn't check input configuration variables either:

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L80-L95

BaseFlywheelRewards as well:

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/BaseFlywheelRewards.sol#L25-L28

Recommended Mitigation Steps

Consider introducing zero-address and range checks for all core configuration variables: token and reward system contracts, length of the reward cycle and so on.

Gas Optimizations

File : FlyWheelCore.sol

Gas1 : use != instead of > for unsigned int to save gas

l#167

if (oldRewardBalance > 0) {

l#218

 if (strategyRewardsAccrued > 0) {

Gas2 : flywheelBooster in flywheelcore.sol can be gas-golfed via mload

l@220-22

uint256 supplyTokens = address(flywheelBooster) != address(0)
                ? flywheelBooster.boostedTotalSupply(strategy)
                : strategy.totalSupply();@audit

l@258-259

uint256 supplierTokens = address(flywheelBooster) != address(0)
            ? flywheelBooster.boostedBalanceOf(strategy, user)
            : strategy.balanceOf(user);@audit

File : FlyWheelGaugerRewards.sol

Gas2 : rewardToken can gas-golfed via mload

l#112,114 (rewardToken)

        uint256 balanceBefore = rewardToken.balanceOf(address(this));@audit
        totalQueuedForCycle = rewardsStream.getRewards();
        require(rewardToken.balanceOf(address(this)) - balanceBefore >= totalQueuedForCycle);@audit

l#153,151 (rewradToken)

            uint256 balanceBefore = rewardToken.balanceOf(address(this));@audit
            uint256 newRewards = rewardsStream.getRewards();
            require(rewardToken.balanceOf(address(this)) - balanceBefore >= newRewards);@audit

Gas3 : use prefix ++i in loops optimisation and no need to init i=0 and prefer for unchecked()

l#189

for (uint256 i = 0; i < size; i++) @audit 
{
            ERC20 gauge = ERC20(gauges[i]);

            QueuedRewards memory queuedRewards = gaugeQueuedRewards[gauge];
            ...

File : ERC20MultiVotes.sol

Gas3 : use prefix in loops ++i and no need to init i=0 and prefer for unchecked()

l#346

for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; i++) @audit
{
            address delegatee = delegateList[i];
            uint256 delegateVotes = _delegatesVotesCount[user][delegatee];
            ...

Gas2 : _delegateVotesCount[user][delegatee] can be gas-golfed via mload

l#348,l#354

            uint256 delegateVotes = _delegatesVotesCount[user][delegatee];@audit
            if (delegateVotes != 0) {
                totalFreed += delegateVotes;

                require(_delegates[user].remove(delegatee)); // Remove from set. Should never fail.

                _delegatesVotesCount[user][delegatee] = 0;@audit
                _writeCheckpoint(delegatee, _subtract, delegateVotes);   
                ...             

Gas Optimizations

1. Cheaper checks should be moved up

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #1

241        // Require freeVotes exceed the delegation size
242        uint256 free = freeVotes(delegator);
243        if (delegatee == address(0) || free < amount) revert DelegationError();

The address check should be split out and moved to before the call to freeVotes()
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L241-L243

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #2

393        require(signer != address(0));

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L393

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #3

259            if (cycle - block.timestamp <= incrementFreezeWindow) revert IncrementFreezeError();

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L259

2. Multiple address mappings can be combined into a single mapping of an address to a struct, where appropriate

Saves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas). Reads and subsequent writes can also be cheaper

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #1

59     mapping(address => mapping(address => uint112)) public getUserGaugeWeight;
60 
61     /// @notice a mapping from a user to their total allocated weight across all gauges
62     /// @dev NOTE this may contain weights for deprecated gauges
63     mapping(address => uint112) public getUserWeight;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L59-L63

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #2

151     mapping(address => mapping(address => uint256)) private _delegatesVotesCount;
152 
153     /// @notice mapping from a delegator to the total number of delegated votes.
154     mapping(address => uint256) public userDelegatedVotes;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L151-L154

3. State variables should be cached in stack variables rather than re-reading them from storage

The instances below point to the second access of a state variable within a function. Caching will replace each Gwarmaccess (100 gas) with a much cheaper stack read.
Less obvious fixes/optimizations include having local storage variables of mappings within state variable mappings or mappings within state variable structs, having local storage variables of structs within mappings, or having local caches of state variable contracts/addresses.

File: lib/ERC4626/src/xERC4626.sol   #1

40         rewardsCycleEnd = (block.timestamp.safeCastTo32() / rewardsCycleLength) * rewardsCycleLength;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L40

File: lib/ERC4626/src/xERC4626.sol   #2

89         uint32 end = ((timestamp + rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L89

File: lib/flywheel-v2/src/FlywheelCore.sol   #3

168             rewardToken.safeTransferFrom(address(flywheelRewards), address(newFlywheelRewards), oldRewardBalance);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L168

File: lib/flywheel-v2/src/FlywheelCore.sol   #4

168             rewardToken.safeTransferFrom(address(flywheelRewards), address(newFlywheelRewards), oldRewardBalance);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L168

File: lib/flywheel-v2/src/FlywheelCore.sol   #5

221                 ? flywheelBooster.boostedTotalSupply(strategy)

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L221

File: lib/flywheel-v2/src/FlywheelCore.sol   #6

259             ? flywheelBooster.boostedBalanceOf(strategy, user)

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L259

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #7

90         gaugeCycle = (block.timestamp.safeCastTo32() / gaugeCycleLength) * gaugeCycleLength;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L90

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #8

103         uint32 currentCycle = (block.timestamp.safeCastTo32() / gaugeCycleLength) * gaugeCycleLength;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L103

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #9

135         uint32 currentCycle = (block.timestamp.safeCastTo32() / gaugeCycleLength) * gaugeCycleLength;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L135

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #10

158         uint112 queued = nextCycleQueuedRewards;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L158

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #11

146         uint32 offset = paginationOffset;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L146

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #12

174         address[] memory gauges = gaugeToken.gauges(offset, numRewards);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L174

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #13

92             return (nowPlusOneCycle / gaugeCycleLength) * gaugeCycleLength; // cannot divide by zero and always <= nowPlusOneCycle so no overflow

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L92

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #14

263         if (added && _userGauges[user].length() > maxGauges && !canContractExceedMaxGauges[user])

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L263

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #15

61         return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L61

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #16

269         _delegatesVotesCount[delegator][delegatee] = newDelegates;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L269

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #17

354                 _delegatesVotesCount[user][delegatee] = 0;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L354

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #18

352                 require(_delegates[user].remove(delegatee)); // Remove from set. Should never fail.

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L352

4. <x> += <y> costs more gas than <x> = <x> + <y> for state variables

File: lib/ERC4626/src/xERC4626.sol   #1

67         storedTotalAssets -= amount;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L67

File: lib/ERC4626/src/xERC4626.sol   #2

72         storedTotalAssets += amount;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L72

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #3

155             nextCycleQueuedRewards += uint112(newRewards); // in case a previous incomplete cycle had rewards, add on

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L155

5. ++i/i++ should be unchecked{++i}/unchecked{++i} when it is not possible for them to overflow, as is the case when used in for- and while-loops

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #1

189         for (uint256 i = 0; i < size; i++) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L189

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #2

346         for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; i++) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L346

6. require()/revert() strings longer than 32 bytes cost extra gas

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #1

379         require(block.timestamp <= expiry, "ERC20MultiVotes: signature expired");

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L379

7. Not using the named return variables when a function returns, wastes deployment gas

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #1

231             return 0;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L231

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #2

248         return _incrementUserAndGlobalWeights(msg.sender, weight, currentCycle);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L248

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #3

317         return _incrementUserAndGlobalWeights(msg.sender, weightsSum, currentCycle);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L317

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #4

331         return _decrementUserAndGlobalWeights(msg.sender, weight, currentCycle);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L331

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #5

394         return _decrementUserAndGlobalWeights(msg.sender, weightsSum, currentCycle);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L394

8. Using bools for storage incurs overhead

    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/58f635312aa21f947cae5f8578638a85aa2519f5/contracts/security/ReentrancyGuard.sol#L23-L27
Use uint256(1) and uint256(2) for true/false

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #1

450     mapping(address => bool) public canContractExceedMaxGauges;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L450

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #2

111     mapping(address => bool) public canContractExceedMaxDelegates;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L111

9. Use a more recent version of solidity

Use a solidity version of at least 0.8.2 to get compiler automatic inlining
Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads
Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require() strings
Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value

File: lib/ERC4626/src/xERC4626.sol   #1

4 pragma solidity ^0.8.0;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L4

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #2

3 pragma solidity ^0.8.0;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L3

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #3

4 pragma solidity ^0.8.0;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L4

File: lib/xTRIBE/src/xTRIBE.sol   #4

4 pragma solidity ^0.8.0;

https://github.com/fei-protocol/xTRIBE/tree/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L4

10. It costs more gas to initialize variables to zero than to let the default of zero be applied

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #1

189         for (uint256 i = 0; i < size; i++) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L189

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #2

134         for (uint256 i = 0; i < num; ) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L134

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #3

184         for (uint256 i = 0; i < num; ) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L184

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #4

307         for (uint256 i = 0; i < size; ) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L307

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #5

384         for (uint256 i = 0; i < size; ) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L384

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #6

564         for (uint256 i = 0; i < size && (userFreeWeight + totalFreed) < weight; ) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L564

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #7

79         uint256 low = 0;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L79

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #8

346         for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; i++) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L346

File: lib/xTRIBE/src/xTRIBE.sol   #9

95         for (uint256 i = 0; i < size; ) {

https://github.com/fei-protocol/xTRIBE/tree/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L95

11. internal functions only called once can be inlined to save gas

File: lib/flywheel-v2/src/FlywheelCore.sol   #1

146     function _addStrategyForRewards(ERC20 strategy) internal {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L146

12. ++i costs less gas than ++i, especially when it's used in for-loops (--i/i-- too)

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #1

189         for (uint256 i = 0; i < size; i++) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L189

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #2

346         for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; i++) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L346

13. Usage of uints/ints smaller than 32 bytes (256 bits) incurs overhead

When using elements that are smaller than 32 bytes, your contract’s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.

https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html
Use a larger size then downcast where needed

File: lib/ERC4626/src/xERC4626.sol   #1

24     uint32 public immutable rewardsCycleLength;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L24

File: lib/ERC4626/src/xERC4626.sol   #2

27     uint32 public lastSync;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L27

File: lib/ERC4626/src/xERC4626.sol   #3

30     uint32 public rewardsCycleEnd;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L30

File: lib/ERC4626/src/xERC4626.sol   #4

33     uint192 public lastRewardAmount;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L33

File: lib/ERC4626/src/xERC4626.sol   #5

37     constructor(uint32 _rewardsCycleLength) {

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L37

File: lib/ERC4626/src/xERC4626.sol   #6

48         uint192 lastRewardAmount_ = lastRewardAmount;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L48

File: lib/ERC4626/src/xERC4626.sol   #7

49         uint32 rewardsCycleEnd_ = rewardsCycleEnd;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L49

File: lib/ERC4626/src/xERC4626.sol   #8

50         uint32 lastSync_ = lastSync;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L50

File: lib/ERC4626/src/xERC4626.sol   #9

79         uint192 lastRewardAmount_ = lastRewardAmount;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L79

File: lib/ERC4626/src/xERC4626.sol   #10

80         uint32 timestamp = block.timestamp.safeCastTo32();

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L80

File: lib/ERC4626/src/xERC4626.sol   #11

89         uint32 end = ((timestamp + rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L89

File: lib/flywheel-v2/src/FlywheelCore.sol   #12

195         uint224 index;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L195

File: lib/flywheel-v2/src/FlywheelCore.sol   #13

197         uint32 lastUpdatedTimestamp;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L197

File: lib/flywheel-v2/src/FlywheelCore.sol   #14

201     uint224 public constant ONE = 1e18;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L201

File: lib/flywheel-v2/src/FlywheelCore.sol   #15

224             uint224 deltaIndex;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L224

File: lib/flywheel-v2/src/FlywheelCore.sol   #16

244         uint224 strategyIndex = state.index;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L244

File: lib/flywheel-v2/src/FlywheelCore.sol   #17

245         uint224 supplierIndex = userIndex[strategy][user];

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L245

File: lib/flywheel-v2/src/FlywheelCore.sol   #18

256         uint224 deltaIndex = strategyIndex - supplierIndex;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L256

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #19

44     event CycleStart(uint32 indexed cycleStart, uint256 rewardAmount);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L44

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #20

47     event QueueRewards(address indexed gauge, uint32 indexed cycleStart, uint256 rewardAmount);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L47

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #21

50     uint32 public gaugeCycle;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L50

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #22

53     uint32 public immutable gaugeCycleLength;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L53

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #23

56     uint32 internal nextCycle;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L56

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #24

59     uint112 internal nextCycleQueuedRewards;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L59

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #25

62     uint32 internal paginationOffset;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L62

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #26

66         uint112 priorCycleRewards;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L66

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #27

67         uint112 cycleRewards;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L67

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #28

68         uint32 storedCycle;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L68

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #29

103         uint32 currentCycle = (block.timestamp.safeCastTo32() / gaugeCycleLength) * gaugeCycleLength;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L103

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #30

104         uint32 lastCycle = gaugeCycle;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L104

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #31

135         uint32 currentCycle = (block.timestamp.safeCastTo32() / gaugeCycleLength) * gaugeCycleLength;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L135

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #32

136         uint32 lastCycle = gaugeCycle;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L136

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #33

146         uint32 offset = paginationOffset;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L146

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #34

158         uint112 queued = nextCycleQueuedRewards;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L158

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #35

181         uint32 currentCycle,

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L181

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #36

182         uint32 lastCycle,

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L182

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #37

198             uint112 completedRewards = queuedRewards.storedCycle == lastCycle ? queuedRewards.cycleRewards : 0;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L198

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #38

218     function getAccruedRewards(ERC20 gauge, uint32 lastUpdatedTimestamp)

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L218

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #39

226         uint32 cycle = gaugeCycle;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L226

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #40

237         uint32 cycleEnd = cycle + gaugeCycleLength;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L237

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #41

241         uint112 cycleRewardsNext = queuedRewards.cycleRewards;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L241

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #42

250             uint32 beginning = lastUpdatedTimestamp > cycle ? lastUpdatedTimestamp : cycle;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L250

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #43

253             uint32 elapsed = block.timestamp.safeCastTo32() - beginning;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L253

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #44

254             uint32 remaining = cycleEnd - beginning;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L254

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #45

36     constructor(uint32 _gaugeCycleLength, uint32 _incrementFreezeWindow) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L36

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #46

36     constructor(uint32 _gaugeCycleLength, uint32 _incrementFreezeWindow) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L36

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #47

47     uint32 public immutable gaugeCycleLength;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L47

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #48

50     uint32 public immutable incrementFreezeWindow;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L50

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #49

53         uint112 storedWeight;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L53

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #50

54         uint112 currentWeight;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L54

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #51

55         uint32 currentCycle;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L55

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #52

84     function getGaugeCycleEnd() public view returns (uint32) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L84

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #53

89     function _getGaugeCycleEnd() internal view returns (uint32) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L89

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #54

90         uint32 nowPlusOneCycle = block.timestamp.safeCastTo32() + gaugeCycleLength;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L90

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #55

97     function getGaugeWeight(address gauge) public view returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L97

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #56

102     function getStoredGaugeWeight(address gauge) public view returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L102

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #57

108     function _getStoredWeight(Weight storage gaugeWeight, uint32 currentCycle) internal view returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L108

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #58

108     function _getStoredWeight(Weight storage gaugeWeight, uint32 currentCycle) internal view returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L108

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #59

113     function totalWeight() external view returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L113

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #60

118     function storedTotalWeight() external view returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L118

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #61

210         uint32 currentCycle = _getGaugeCycleEnd();

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L210

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #62

212         uint112 total = _getStoredWeight(_totalWeight, currentCycle);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L212

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #63

213         uint112 weight = _getStoredWeight(_getGaugeWeight[gauge], currentCycle);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L213

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #64

234     event IncrementGaugeWeight(address indexed user, address indexed gauge, uint256 weight, uint32 cycleEnd);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L234

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #65

237     event DecrementGaugeWeight(address indexed user, address indexed gauge, uint256 weight, uint32 cycleEnd);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L237

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #66

245     function incrementGauge(address gauge, uint112 weight) external returns (uint112 newUserWeight) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L245

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #67

245     function incrementGauge(address gauge, uint112 weight) external returns (uint112 newUserWeight) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L245

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #68

246         uint32 currentCycle = _getGaugeCycleEnd();

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L246

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #69

254         uint112 weight,

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L254

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #70

255         uint32 cycle

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L255

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #71

275         uint112 weight,

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L275

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #72

276         uint32 cycle

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L276

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #73

277     ) internal returns (uint112 newUserWeight) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L277

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #74

302         uint112 weightsSum;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L302

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #75

304         uint32 currentCycle = _getGaugeCycleEnd();

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L304

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #76

309             uint112 weight = weights[i];

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L309

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #77

326     function decrementGauge(address gauge, uint112 weight) external returns (uint112 newUserWeight) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L326

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #78

326     function decrementGauge(address gauge, uint112 weight) external returns (uint112 newUserWeight) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L326

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #79

327         uint32 currentCycle = _getGaugeCycleEnd();

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L327

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #80

337         uint112 weight,

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L337

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #81

338         uint32 cycle

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L338

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #82

340         uint112 oldWeight = getUserGaugeWeight[user][gauge];

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L340

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #83

355         uint112 weight,

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L355

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #84

356         uint32 cycle

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L356

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #85

357     ) internal returns (uint112 newUserWeight) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L357

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #86

372         returns (uint112 newUserWeight)

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L372

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #87

378         uint112 weightsSum;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L378

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #88

380         uint32 currentCycle = _getGaugeCycleEnd();

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L380

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #89

386             uint112 weight = weights[i];

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L386

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #90

404         function(uint112, uint112) view returns (uint112) op,

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L404

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #91

404         function(uint112, uint112) view returns (uint112) op,

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L404

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #92

404         function(uint112, uint112) view returns (uint112) op,

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L404

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #93

405         uint112 delta,

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L405

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #94

406         uint32 cycle

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L406

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #95

408         uint112 currentWeight = weight.currentWeight;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L408

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #96

410         uint112 stored = weight.currentCycle < cycle ? currentWeight : weight.storedWeight;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L410

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #97

411         uint112 newWeight = op(currentWeight, delta);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L411

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #98

418     function _add(uint112 a, uint112 b) private pure returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L418

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #99

418     function _add(uint112 a, uint112 b) private pure returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L418

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #100

418     function _add(uint112 a, uint112 b) private pure returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L418

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #101

422     function _subtract(uint112 a, uint112 b) private pure returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L422

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #102

422     function _subtract(uint112 a, uint112 b) private pure returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L422

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #103

422     function _subtract(uint112 a, uint112 b) private pure returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L422

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #104

453     function addGauge(address gauge) external requiresAuth returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L453

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #105

457     function _addGauge(address gauge) internal returns (uint112 weight) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L457

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #106

463         uint32 currentCycle = _getGaugeCycleEnd();

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L463

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #107

483         uint32 currentCycle = _getGaugeCycleEnd();

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L483

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #108

486         uint112 weight = _getGaugeWeight[gauge].currentWeight;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L486

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #109

553         uint32 currentCycle = _getGaugeCycleEnd();

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L553

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #110

556         uint112 userFreed;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L556

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #111

557         uint112 totalFreed;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L557

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #112

566             uint112 userGaugeWeight = getUserGaugeWeight[user][gauge];

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L566

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #113

28         uint32 fromBlock;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L28

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #114

29         uint224 votes;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L29

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #115

36     function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L36

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #116

41     function numCheckpoints(address account) public view virtual returns (uint32) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L41

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #117

375         uint8 v,

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L375

File: lib/xTRIBE/src/xTRIBE.sol   #118

18         returns (uint96);

https://github.com/fei-protocol/xTRIBE/tree/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L18

File: lib/xTRIBE/src/xTRIBE.sol   #119

20     function getCurrentVotes(address account) external view returns (uint96);

https://github.com/fei-protocol/xTRIBE/tree/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L20

File: lib/xTRIBE/src/xTRIBE.sol   #120

37         uint32 _rewardsCycleLength,

https://github.com/fei-protocol/xTRIBE/tree/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L37

File: lib/xTRIBE/src/xTRIBE.sol   #121

38         uint32 _incrementFreezeWindow

https://github.com/fei-protocol/xTRIBE/tree/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L38

14. Expressions for constant values such as a call to keccak256(), should use immutable rather than constant

See this issue for a detail description of the issue

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #1

368     bytes32 public constant DELEGATION_TYPEHASH =
369         keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L368-L369

15. Using private rather than public for constants, saves gas

If needed, the value can be read from the verified contract source code

File: lib/ERC4626/src/xERC4626.sol   #1

24     uint32 public immutable rewardsCycleLength;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L24

File: lib/flywheel-v2/src/FlywheelCore.sol   #2

201     uint224 public constant ONE = 1e18;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L201

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #3

53     uint32 public immutable gaugeCycleLength;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L53

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #4

47     uint32 public immutable gaugeCycleLength;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L47

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #5

50     uint32 public immutable incrementFreezeWindow;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L50

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #6

368     bytes32 public constant DELEGATION_TYPEHASH =
369         keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L368-L369

16. Multiplication/division by two should use bit shifting

<x> * 2 is equivalent to <x> << 1 and <x> / 2 is the same as <x> >> 1

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #1

94         return (a & b) + (a ^ b) / 2;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L94

17. require() or revert() statements that check input arguments should be at the top of the function

Checks that involve constants should come before checks that involve state variables

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #1

392         require(nonce == nonces[signer]++, "ERC20MultiVotes: invalid nonce");

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L392

18. Empty blocks should be removed

The code should be refactored such that they no longer exist, or the block should do something useful, such as emitting an event or reverting. If the block is an empty if-statement block to avoid doing subsequent checks in the else-if/else conditions, the else-if/else conditions should be nested under the negation of the if-statement, because they involve different classes of checks, which may lead to the introduction of errors when the code is later modified (if(x){}else if(y){...}else{...} => if(!x){if(y){...}else{...}})

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #1

243         if (incompleteCycle) {
244             // If current cycle queue incomplete, do nothing to current cycle rewards or accrued
245         } else if (block.timestamp >= cycleEnd) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L243-L245

19. Use custom errors rather than revert()/require() strings to save deployment gas

File: lib/flywheel-v2/src/FlywheelCore.sol (various lines)   #1

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol (various lines)   #2

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol

20. Functions guaranteed to revert when called by normal users can be marked payable

If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided.

File: lib/flywheel-v2/src/FlywheelCore.sol   #1

142     function addStrategyForRewards(ERC20 strategy) external requiresAuth {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L142

File: lib/flywheel-v2/src/FlywheelCore.sol   #2

165     function setFlywheelRewards(IFlywheelRewards newFlywheelRewards) external requiresAuth {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L165

File: lib/flywheel-v2/src/FlywheelCore.sol   #3

183     function setBooster(IFlywheelBooster newBooster) external requiresAuth {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L183

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #4

101     function queueRewardsForCycle() external requiresAuth returns (uint256 totalQueuedForCycle) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L101

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #5

133     function queueRewardsForCyclePaginated(uint256 numRewards) external requiresAuth {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L133

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #6

218     function getAccruedRewards(ERC20 gauge, uint32 lastUpdatedTimestamp)
219         external
220         override
221         onlyFlywheel
222         returns (uint256 accruedRewards)

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L218-L222

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #7

273     function setRewardsStream(IRewardsStream newRewardsStream) external requiresAuth {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L273

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #8

453     function addGauge(address gauge) external requiresAuth returns (uint112) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L453

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #9

475     function removeGauge(address gauge) external requiresAuth {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L475

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #10

495     function replaceGauge(address oldGauge, address newGauge) external requiresAuth {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L495

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #11

502     function setMaxGauges(uint256 newMax) external requiresAuth {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L502

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #12

510     function setContractExceedMaxGauges(address account, bool canExceedMax) external requiresAuth {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L510

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #13

114     function setMaxDelegates(uint256 newMax) external requiresAuth {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L114

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #14

122     function setContractExceedMaxDelegates(address account, bool canExceedMax) external requiresAuth {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L122

File: lib/xTRIBE/src/xTRIBE.sol   #15

89     function emitVotingBalances(address[] calldata accounts)
90         external
91         requiresAuth

https://github.com/fei-protocol/xTRIBE/tree/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L89-L91

File: lib/xTRIBE/src/xTRIBE.sol   #16

108     function syncRewards() public override requiresAuth {

https://github.com/fei-protocol/xTRIBE/tree/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L108

Gas Optimizations

Issues found

Don't Initialize Variables with Default Value

Impact

Issue Information: G001 - variables with default value

Findings:

ERC20Gauges.sol::134 => for (uint256 i = 0; i < num; ) {
ERC20Gauges.sol::184 => for (uint256 i = 0; i < num; ) {
ERC20Gauges.sol::307 => for (uint256 i = 0; i < size; ) {
ERC20Gauges.sol::384 => for (uint256 i = 0; i < size; ) {
ERC20Gauges.sol::564 => for (uint256 i = 0; i < size && (userFreeWeight + totalFreed) < weight; ) {
ERC20MultiVotes.sol::79 => uint256 low = 0;
ERC20MultiVotes.sol::284 => uint256 oldWeight = pos == 0 ? 0 : ckpts[pos - 1].votes;
ERC20MultiVotes.sol::346 => for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; i++) {
xTRIBE.sol::95 => for (uint256 i = 0; i < size; ) {

Use != 0 instead of > 0 for Unsigned Integer Comparison

Impact

Issue Information: G003 - use !=0 for unsigned int comparison

Findings:

ERC20Gauges.sol::467 => if (weight > 0) {
ERC20Gauges.sol::487 => if (weight > 0) {
ERC20MultiVotes.sol::287 => if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) {
FlywheelCore.sol::167 => if (oldRewardBalance > 0) {
FlywheelCore.sol::218 => if (strategyRewardsAccrued > 0) {

Long Revert Strings

Impact

Issue Information: G007 - long (revert) strings

Findings:

ERC20MultiVotes.sol::379 => require(block.timestamp <= expiry, "ERC20MultiVotes: signature expired");

Prefix increments are cheaper than postfix increments

Impact

Issue Information: G009 - Prefix increments are cheaper than postfix increments

Findings:

ERC20Gauges.sol::137 => i++;
ERC20Gauges.sol::187 => i++;
ERC20Gauges.sol::314 => i++;
ERC20Gauges.sol::391 => i++;
ERC20Gauges.sol::576 => i++;
ERC20MultiVotes.sol::346 => for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; i++) {
xTRIBE.sol::99 => i++;

Bypass rewardsCycleEnd duration

Lines of code

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L78

Vulnerability details

Impact

User can sync reward to bypass rewardsCycleEnd duration

Proof of Concept

  1. Assume currently storedTotalAssets_ is 100, lastRewardAmount_ is 50 and unlockedRewards is 10 while calling totalAssets function. rewardsCycleEnd_ has remaining 2 days when the remaining 40 rewards would be unlocked

  2. This can be simply bypassed by calling syncRewards function which will add lastRewardAmount into storedTotalAssets and make lastRewardAmount to 0

  3. Now if totalAssets function is called storedTotalAssets_ will become 150 which means all assets+reward has now been converted into storedTotalAssets_ without needing to wait for rewardsCycleEnd_ duration

Recommended Mitigation Steps

syncRewards should not be public and

  1. only owner should be allowed to call this
    or
  2. should be internal

NOT IN SCOPE, BUT BRINGING UP FOR VISIBILITY: Issue with setRewardsInfo and properly accruing rewards in FlywheelStaticRewards

Lines of code

https://github.com/fei-protocol/flywheel-v2/blob/main/src/rewards/FlywheelStaticRewards.sol#L36-L39

Vulnerability details

Impact

In FlywheelStaticRewards, when setRewardsInfo is updated without calling accrue, the amount of reward token accrued to the use can be overcalculated. The markdown does not document updating accrue when rewards are changed, only when the composition of the strategy (transfers, burns, mints) is updated.

Proof of Concept

Private Gist: https://gist.github.com/ypatil12/02498e7c3b968e7116a1bb993757ed0e

Tools Used

Foundry

Recommended Mitigation Steps

  1. Document updating accrue on every transaction from authority when calling setRewardsInfo
  2. Enforce an accrual on the contract. eg. call flywheel.accrue(strategy, ZERO_ADDRESS) before rewardsInfo[strategy] = rewards. The same paradigm is in masterchef, when we call updatePool in setPool.

QA Report

Unspecific Compiler Version Pragma

Impact

Issue Information: L003 - Unspecific Compiler Version Pragma

Findings:

ERC20Gauges.sol::3 => pragma solidity ^0.8.0;
ERC20MultiVotes.sol::4 => pragma solidity ^0.8.0;
xERC4626.sol::4 => pragma solidity ^0.8.0;
xTRIBE.sol::4 => pragma solidity ^0.8.0;

Gas Optimizations

Gas optimization issue in the for loop of xtribe.sol L95:

[1] Un-necessary initialisation of loop invariant :

Statement uint256 i=0 is not required, because all uninitialised uint variable, by default, are set to 0

[2] ++i is cheaper than i++ for unsigned integer
[3] Value of accounts[i] is loaded twice, so instead load the value of accounts[i] once, store it in a local variable and then use it.

So the final optimised for loop is as follows:

for (uint256 i; i < size; ) {
            uint256 memory acc = accounts[i];
            emit DelegateVotesChanged(acc, 0, getVotes(acc));

            unchecked {
                ++i;
            }
        }

rewardsCycleEnd is not set properly

Lines of code

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L40

Vulnerability details

Impact

rewardsCycleEnd is pointing to timestamp when contract was initialized which is incorrect

Proof of Concept

  1. Observe the constructor
constructor(uint32 _rewardsCycleLength) {
        rewardsCycleLength = _rewardsCycleLength;
        // seed initial rewardsCycleEnd
        rewardsCycleEnd = (block.timestamp.safeCastTo32() / rewardsCycleLength) * rewardsCycleLength;
    }
  1. Observe that rewardsCycleEnd is pointing to current timestamp and not even considering rewardsCycleLength which is incorrect

Recommended Mitigation Steps

Change the constructor like below:

constructor(uint32 _rewardsCycleLength) {
        rewardsCycleLength = _rewardsCycleLength;
        // seed initial rewardsCycleEnd
        rewardsCycleEnd = ((block.timestamp.safeCastTo32() + _rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength;
    }

Gas Optimizations

Bad Comparison with Zero

Impact

In If conditionals, prefer to use !=0 than > 0 when possible to save Gas

Proof of Concept

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L167

167if (oldRewardBalance > 0) {
168┆     rewardToken.safeTransferFrom(address(flywheelRewards), address(newFlywheelRewards), oldRewardBalance);
169┆ }

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L218

218if (strategyRewardsAccrued > 0) {

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L467

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L487

467if (weight > 0) {
⋮┆----------------------------------------
487if (weight > 0) {

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L287

287if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) {

Tools Used

Static code analysis

Recommended Mitigation steps

Replace the > 0 to != 0

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L167

167if (oldRewardBalance != 0) {

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L218

218if (strategyRewardsAccrued != 0) {

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L467

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L487

467if (weight != 0) {
⋮┆----------------------------------------
487if (weight != 0) {

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L287

287if (pos != 0 && ckpts[pos - 1].fromBlock == block.number) {

Postfix to Prefix

Impact

There is no risk of overflow caused by increasing the iteration index in for loops (the ++i in for (uint256 i = 0; i < numIterations; ++i)), but increments perform overflow checks that are not needed in this case.

So, to save gas, prefer ++i to i++ in loops.

Proof of Concept

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L189

189for (uint256 i = 0; i < size; i++) {

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L346

346for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; i++) {

Tools Used

Static code analysis

Recommended Mitigation steps

To save gas, prefer ++i to i++ in loops.

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L189

189for (uint256 i = 0; i < size; ++i) {

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L346

346for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; ++i) {

`xERC4626` can lose funds if `asset` is ever destructed

Lines of code

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L64-L74

Vulnerability details

Unlike OpenZeppelin's safeTransfer(), solmate's version of the function does not check for contract existence before its low level calls are made. The foot-gun associated with this difference is that it's up to the caller to verify that the contract exists before every call and to verify that the contract has not been destructed. A low level call to a non-existent contract results in a return value of success rather than reverting.

Impact

xERC4626 can lose funds if asset ever calls selfdestruct(), because it relies on ERC4626 which does not itself check that asset still exists

Proof of Concept

File: solmate/src/utils/SafeTransferLib.sol

/// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller.

https://github.com/Rari-Capital/solmate/blob/9f16db2144cc9a7e2ffc5588d4bf0b66784283bd/src/utils/SafeTransferLib.sol#L9

For example, in ERC4262.deposit() a transfer is done, and then the callback is triggered:

File: solmate/src/mixins/ERC4626.sol

46    function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) {
47        // Check for rounding error since we round down in previewDeposit.
48        require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES");
49
50        // Need to transfer before minting or ERC777s could reenter.
51        asset.safeTransferFrom(msg.sender, address(this), assets);
52
53        _mint(receiver, shares);
54
55        emit Deposit(msg.sender, receiver, assets, shares);
56
57        afterDeposit(assets, shares);
58    }

https://github.com/Rari-Capital/solmate/blob/9f16db2144cc9a7e2ffc5588d4bf0b66784283bd/src/mixins/ERC4626.sol#L46-L58

The callback is the one defined in xERC4626, which means the accounting is broken, leading to the mispricing of assets:

File: lib/ERC4626/src/xERC4626.sol

64        // Update storedTotalAssets on withdraw/redeem
65        function beforeWithdraw(uint256 amount, uint256 shares) internal virtual override {
66            super.beforeWithdraw(amount, shares);
67            storedTotalAssets -= amount;
68        }
69
70        // Update storedTotalAssets on deposit/mint
71        function afterDeposit(uint256 amount, uint256 shares) internal virtual override {
72            storedTotalAssets += amount;
73            super.afterDeposit(amount, shares);
74        }

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L64-L74

The affected callbacks are triggered by deposit(), mint(), withdraw(), and redeem(), so all of these are broken. xERC4626 is meant to be used in other contexts besides those of the xTRIBE token, so the asset cannot be guaranteed to never be destructed, especially if used by other projects, as other projects do with the base ERC4626 contract.

Tools Used

Code inspection

Recommended Mitigation Steps

Use override ERC4626's functions that interact with asset and use OpenZeppelin's safeTransfer() for them instead

Not checking proper conditions in assert as mentioned in comment

Lines of code

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L234-L235

Vulnerability details

Impact

The comment above assert says-
// if stored cycle != 0 it must be >= the last queued cycle
but the assert condition only checks for >= condition not for ==0 condition

Proof of Concept

Tools Used

Manual review

Recommended Mitigation Steps

change the line

assert(queuedRewards.storedCycle ==0 || queuedRewards.storedCycle >= cycle);

QA Report

Impact

By default, state variables/constants are internal, so the internal keyword can be omitted.
Affected code: https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L35

Proof of Concept

https://docs.soliditylang.org/en/v0.8.12/types.html#:~:text=to%20be%20omitted.-,By,-default%2C%20function%20types

Tools Used

Recommended Mitigation Steps

Recommended code:
uint256 storedTotalAssets;


Impact

By default, state variables/constants are internal, so the internal keyword can be omitted.
Affected code: https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L70

Proof of Concept

https://docs.soliditylang.org/en/v0.8.12/types.html#:~:text=to%20be%20omitted.-,By,-default%2C%20function%20types

Tools Used

Recommended Mitigation Steps

Recommended code:
Weight _totalWeight;


Impact

By default, state variables/constants are internal, so the internal keyword can be omitted.
Affected code: https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L74

Proof of Concept

https://docs.soliditylang.org/en/v0.8.12/types.html#:~:text=to%20be%20omitted.-,By,-default%2C%20function%20types

Tools Used

Recommended Mitigation Steps

Recommended code:
EnumerableSet.AddressSet _gauges;


Impact

By default, state variables/constants are internal, so the internal keyword can be omitted.
Affected code: https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L77

Proof of Concept

https://docs.soliditylang.org/en/v0.8.12/types.html#:~:text=to%20be%20omitted.-,By,-default%2C%20function%20types

Tools Used

Recommended Mitigation Steps

Recommended code:
EnumerableSet.AddressSet _deprecatedGauges;


Impact

By default, state variables/constants are internal, so the internal keyword can be omitted.
Affected code: https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L56

Proof of Concept

https://docs.soliditylang.org/en/v0.8.12/types.html#:~:text=to%20be%20omitted.-,By,-default%2C%20function%20types

Tools Used

Recommended Mitigation Steps

Recommended code:
uint32 nextCycle;


Impact

By default, state variables/constants are internal, so the internal keyword can be omitted.
Affected code: https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L59

Proof of Concept

https://docs.soliditylang.org/en/v0.8.12/types.html#:~:text=to%20be%20omitted.-,By,-default%2C%20function%20types

Tools Used

Recommended Mitigation Steps

Recommended code:
uint112 nextCycleQueuedRewards;


Impact

By default, state variables/constants are internal, so the internal keyword can be omitted.
Affected code: https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L62

Proof of Concept

https://docs.soliditylang.org/en/v0.8.12/types.html#:~:text=to%20be%20omitted.-,By,-default%2C%20function%20types

Tools Used

Recommended Mitigation Steps

Recommended code:
uint32 paginationOffset;

QA Report

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L119

claimRewards function: In case contract has less reward then no reward is transferred to user.

An example is user is pending 100 reward and contract has only 99 reward left. if this user claim reward, no reward will be transfered due to 100-99=1 lesser reward.

Recommendation: Ideally if contract balance is lesser than genuine claimed amount then atleast contract balance must be transferred to user

[WP-H0] `xERC4626.sol` Some users may not be able to withdraw until `rewardsCycleEnd` the due to underflow in `beforeWithdraw()`

Lines of code

https://github.com/fei-protocol/ERC4626/blob/2b2baba0fc480326a89251716f52d2cfa8b09230/src/xERC4626.sol#L65-L68

Vulnerability details

https://github.com/fei-protocol/ERC4626/blob/2b2baba0fc480326a89251716f52d2cfa8b09230/src/xERC4626.sol#L65-L68

function beforeWithdraw(uint256 amount, uint256 shares) internal virtual override {
    super.beforeWithdraw(amount, shares);
    storedTotalAssets -= amount;
}

https://github.com/fei-protocol/ERC4626/blob/2b2baba0fc480326a89251716f52d2cfa8b09230/src/xERC4626.sol#L78-L87

function syncRewards() public virtual {
    uint192 lastRewardAmount_ = lastRewardAmount;
    uint32 timestamp = block.timestamp.safeCastTo32();

    if (timestamp < rewardsCycleEnd) revert SyncError();

    uint256 storedTotalAssets_ = storedTotalAssets;
    uint256 nextRewards = asset.balanceOf(address(this)) - storedTotalAssets_ - lastRewardAmount_;

    storedTotalAssets = storedTotalAssets_ + lastRewardAmount_; // SSTORE
    ...

storedTotalAssets is a cached value of total assets which will only include the unlockedRewards when the whole cycle ends.

This makes it possible for storedTotalAssets -= amount to revert when the withdrawal amount exceeds storedTotalAssets, as the withdrawal amount may include part of the unlockedRewards in the current cycle.

PoC

Given:

  • rewardsCycleLength = 100 days
  1. Alice deposit() 100 TRIBE tokens;
  2. The owner transferred 100 TRIBE tokens as rewards and called syncRewards();
  3. 1 day later, Alice redeem() with all shares, the transaction will revert at xERC4626.beforeWithdraw().

Alice's shares worth 101 TRIBE at this moment, but storedTotalAssets = 100, making storedTotalAssets -= amount reverts due to underflow.

  1. Bob deposit() 1 TRIBE tokens;
  2. Alice withdraw() 101 TRIBE tokens, storedTotalAssets becomes 0;
  3. Bob can't even withdraw 1 wei of TRIBE token, as storedTotalAssets is now 0.

If there are no new deposits, both Alice and Bob won't be able to withdraw any of their funds until rewardsCycleEnd.

Recommendation

Consider changing to:

function beforeWithdraw(uint256 amount, uint256 shares) internal virtual override {
    super.beforeWithdraw(amount, shares);
    uint256 _storedTotalAssets = storedTotalAssets;
    if (amount >= _storedTotalAssets) {
        uint256 _totalAssets = totalAssets();
        // _totalAssets - _storedTotalAssets == unlockedRewards
        lastRewardAmount -= _totalAssets - _storedTotalAssets;
        lastSync = block.timestamp;
        storedTotalAssets = _totalAssets - amount;
    } else {
        storedTotalAssets = _storedTotalAssets - amount;
    }
}

FlywheelCore's setFlywheelRewards can remove access to reward funds from current users

Lines of code

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L164-L171

Vulnerability details

Impact

FlywheelCore.setFlywheelRewards can remove current reward funds from the current users' reach as it doesn't check that newFlywheelRewards' FlywheelCore is this contract.

If it's not, by mistake or with a malicious intent, the users will lose the access to reward funds as this FlywheelCore will not be approved for any fund access to the new flywheelRewards, while all the reward funds be moved there.

Setting severity to medium as on one hand that's system breaking issue (no rewards can be claimed after that, users are rugged reward-wise), on the other hand setFlywheelRewards function is requiresAuth. Also, a room for operational mistake isn't too small here as new flywheelRewards contract can be correctly configured and not malicious in all other regards.

Proof of Concept

FlywheelCore.setFlywheelRewards doesn't check that newFlywheelRewards' FlywheelCore is this FlywheelCore instance:

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L164-L171

FlywheelCore is immutable within flywheelRewards and its access to the flywheelRewards' funds is set on construction:

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/BaseFlywheelRewards.sol#L30

This way if new flywheelRewards contract have any different FlywheelCore then current users' access to reward funds will be irrevocably lost as both claiming functionality and next run of setFlywheelRewards will revert, not being able to transfer any funds from flywheelRewards with rewardToken.safeTransferFrom(address(flywheelRewards), ...):

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L125

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L168

As FlywheelCore holds user funds accounting via rewardsAccrued mapping, all these accounts became non-operational, as all the unclaimed rewards will be lost for the users.

Recommended Mitigation Steps

Consider adding the require for address(newFlywheelRewards.flywheel) == address(flywheelRewards.flywheel) in setFlywheelRewards so that users always retain funds access.

QA Report

Unspecific Compiler Version Pragma

Impact

Issue Information: L003 - Unspecific Compiler Version Pragma

Findings:

ERC20Gauges.sol::3 => pragma solidity ^0.8.0;
ERC20MultiVotes.sol::4 => pragma solidity ^0.8.0;
xERC4626.sol::4 => pragma solidity ^0.8.0;
xTRIBE.sol::4 => pragma solidity ^0.8.0;

Gas Optimizations

Table of Contents:

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.

The effect can be quite significant.

As an example, here, I suggest replacing the existing :

File: FlywheelCore.sol
146:     function _addStrategyForRewards(ERC20 strategy) internal {
147:         require(strategyState[strategy].index == 0, "strategy"); //@audit gas: should declare RewardsState storage _strategy = strategyState[strategy] and use _strategy.index
148:         strategyState[strategy] = RewardsState({index: ONE, lastUpdatedTimestamp: block.timestamp.safeCastTo32()});//@audit gas: should use "_strategy = "
149: 
150:         allStrategies.push(strategy);
151:         emit AddStrategy(address(strategy));
152:     }

With the following :

File: FlywheelCore.sol
    function _addStrategyForRewards(ERC20 strategy) internal {
        RewardsState storage _strategy = strategyState[strategy];

        require(_strategy.index == 0, "strategy");

        _strategy.index = ONE;
        _strategy.lastUpdatedTimestamp = block.timestamp.safeCastTo32();

        allStrategies.push(strategy);
        emit AddStrategy(address(strategy));
    }

To further prove this gas optimization, I suggest trying the following test snippet in Remix IDE with the Optimizer enabled at 10k. You'll notice that the gas consumption goes from 24748 to 24589, effectively saving 159 gas:

// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.10;

contract Test {
  
    mapping(address => RewardsState) public strategyState;

    struct RewardsState {
        uint224 index;
        uint32 lastUpdatedTimestamp;
    }

    function testStorageOptimization(address strategy) external { //gas: 24748
        require(strategyState[strategy].index == 0, "strategy");
        strategyState[strategy] = RewardsState({index: 1e18, lastUpdatedTimestamp: uint32(block.timestamp)});
    }

    function testStorageOptimization2(address strategy) external { //gas: 24589
        RewardsState storage _strategy = strategyState[strategy];
        require(_strategy.index == 0, "strategy");
        _strategy.index = 1e18;
        _strategy.lastUpdatedTimestamp = uint32(block.timestamp);
    }
}

Caching storage values in memory

The code can be optimized by minimising the number of SLOADs. SLOADs are expensive (100 gas) compared to MLOADs/MSTOREs (3 gas). Here, storage values should get cached in memory (see the @audit tags for further details):

lib/flywheel-v2/src/FlywheelCore.sol:
  166:         uint256 oldRewardBalance = rewardToken.balanceOf(address(flywheelRewards)); //@audit gas: should cache flywheelRewards
  168:             rewardToken.safeTransferFrom(address(flywheelRewards), address(newFlywheelRewards), oldRewardBalance); //@audit gas: should use cached flywheelRewards

lib/flywheel-v2/src/token/ERC20Gauges.sol:
  548:         uint256 userFreeWeight = balanceOf[user] - getUserWeight[user]; //@audit gas: should cache getUserWeight[user]
  581:         getUserWeight[user] -= userFreed; //@audit gas: should use cached getUserWeight[user] L548 in this operation (getUserWeight[user] = cacheGetUserWeightL548 - userFreed)

>= is cheaper than >

Strict inequalities (>) are more expensive than non-strict ones (>=). This is due to some supplementary checks (ISZERO, 3 gas)

I suggest using >= (or <=) instead of > (or <) to avoid some opcodes here:

flywheel-v2/src/rewards/FlywheelGaugeRewards.sol:250:            uint32 beginning = lastUpdatedTimestamp > cycle ? lastUpdatedTimestamp : cycle;
flywheel-v2/src/token/ERC20Gauges.sol:109:        return gaugeWeight.currentCycle < currentCycle ? gaugeWeight.currentWeight : gaugeWeight.storedWeight;
flywheel-v2/src/token/ERC20Gauges.sol:410:        uint112 stored = weight.currentCycle < cycle ? currentWeight : weight.storedWeight;

Shift Right instead of Dividing by 2

A division by 2 can be calculated by shifting one to the right.

While the DIV opcode uses 5 gas, the SHR opcode only uses 3 gas. Furthermore, Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting.

I suggest replacing / 2 with >> 1 here:

flywheel-v2/src/token/ERC20MultiVotes.sol:94:        return (a & b) + (a ^ b) / 2;

Amounts should be checked for 0 before calling a transfer

Checking non-zero transfer values can avoid an expensive external call and save gas.

Places I suggest adding a non-zero-value check:

flywheel-v2/src/token/ERC20Gauges.sol:531:    function transfer(address to, uint256 amount) public virtual override returns (bool) {
flywheel-v2/src/token/ERC20Gauges.sol:533:        return super.transfer(to, amount);
flywheel-v2/src/token/ERC20Gauges.sol:542:        return super.transferFrom(from, to, amount);
flywheel-v2/src/token/ERC20MultiVotes.sol:316:    function transfer(address to, uint256 amount) public virtual override returns (bool) {
flywheel-v2/src/token/ERC20MultiVotes.sol:318:        return super.transfer(to, amount);
flywheel-v2/src/token/ERC20MultiVotes.sol:321:    function transferFrom(
flywheel-v2/src/token/ERC20MultiVotes.sol:327:        return super.transferFrom(from, to, amount);
xTRIBE/src/xTRIBE.sol:126:    function transfer(address to, uint256 amount)
xTRIBE/src/xTRIBE.sol:134:        return ERC20.transfer(to, amount);
xTRIBE/src/xTRIBE.sol:137:    function transferFrom(
xTRIBE/src/xTRIBE.sol:149:        return ERC20.transferFrom(from, to, amount);

This is already applied here:

flywheel-v2/src/FlywheelCore.sol:125:            rewardToken.safeTransferFrom(address(flywheelRewards), user, accrued);
flywheel-v2/src/FlywheelCore.sol:168:            rewardToken.safeTransferFrom(address(flywheelRewards), address(newFlywheelRewards), oldRewardBalance);

++i costs less gas compared to i++ or i += 1

++i costs less gas compared to i++ or i += 1 for unsigned integer, as pre-increment is cheaper (about 5 gas per iteration). This statement is true even with the optimizer enabled.

i++ increments i and returns the initial value of i. Which means:

uint i = 1;  
i++; // == 1 but i == 2  

But ++i returns the actual incremented value:

uint i = 1;  
++i; // == 2 and i == 2 too, so no need for a temporary variable  

In the first case, the compiler has to create a temporary variable (when used) for returning 1 instead of 2

Instances include:

flywheel-v2/src/rewards/FlywheelGaugeRewards.sol:189:        for (uint256 i = 0; i < size; i++) {
flywheel-v2/src/token/ERC20Gauges.sol:137:                i++;
flywheel-v2/src/token/ERC20Gauges.sol:187:                i++;
flywheel-v2/src/token/ERC20Gauges.sol:314:                i++;
flywheel-v2/src/token/ERC20Gauges.sol:391:                i++;
flywheel-v2/src/token/ERC20Gauges.sol:576:                    i++;
flywheel-v2/src/token/ERC20MultiVotes.sol:346:        for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; i++) {
xTRIBE/src/xTRIBE.sol:99:                i++;

I suggest using ++i instead of i++ to increment the value of an uint variable.

Increments can be unchecked

In Solidity 0.8+, there's a default overflow check on unsigned integers. It's possible to uncheck this in for-loops and save some gas at each iteration, but at the cost of some code readability, as this uncheck cannot be made inline.

ethereum/solidity#10695

Instances include:

flywheel-v2/src/rewards/FlywheelGaugeRewards.sol:189:        for (uint256 i = 0; i < size; i++) {
flywheel-v2/src/token/ERC20MultiVotes.sol:346:        for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; i++) {

The code would go from:

for (uint256 i; i < numIterations; i++) {  
 // ...  
}  

to:

for (uint256 i; i < numIterations;) {  
 // ...  
 unchecked { ++i; }  
}  

The risk of overflow is inexistant for a uint256 here.

This is already applied in ERC20Gauges.sol.

No need to explicitly initialize variables with default values

If a variable is not set/initialized, it is assumed to have the default value (0 for uint, false for bool, address(0) for address...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.

As an example: for (uint256 i = 0; i < numIterations; ++i) { should be replaced with for (uint256 i; i < numIterations; ++i) {

Instances include:

flywheel-v2/src/rewards/FlywheelGaugeRewards.sol:189:        for (uint256 i = 0; i < size; i++) {
flywheel-v2/src/token/ERC20Gauges.sol:134:        for (uint256 i = 0; i < num; ) {
flywheel-v2/src/token/ERC20Gauges.sol:184:        for (uint256 i = 0; i < num; ) {
flywheel-v2/src/token/ERC20Gauges.sol:307:        for (uint256 i = 0; i < size; ) {
flywheel-v2/src/token/ERC20Gauges.sol:384:        for (uint256 i = 0; i < size; ) {
flywheel-v2/src/token/ERC20Gauges.sol:564:        for (uint256 i = 0; i < size && (userFreeWeight + totalFreed) < weight; ) {
flywheel-v2/src/token/ERC20MultiVotes.sol:79:        uint256 low = 0;
flywheel-v2/src/token/ERC20MultiVotes.sol:346:        for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; i++) {
xTRIBE/src/xTRIBE.sol:95:        for (uint256 i = 0; i < size; ) {

I suggest removing explicit initializations for default values.

Upgrade pragma to at least 0.8.4

Using newer compiler versions and the optimizer give gas optimizations. Also, additional safety checks are available for free.

The advantages here are:

  • Low level inliner (>= 0.8.2): Cheaper runtime gas (especially relevant when the contract has small functions).
  • Optimizer improvements in packed structs (>= 0.8.3)
  • Custom errors (>= 0.8.4): cheaper deployment cost and runtime cost. Note: the runtime cost is only relevant when the revert condition is met. In short, replace revert strings by custom errors.

Consider upgrading pragma to at least 0.8.4:

xTRIBE/src/xTRIBE.sol:4:pragma solidity ^0.8.0;

Reduce the size of error messages (Long revert Strings)

Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met.

Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.

Revert string > 32 bytes:

flywheel-v2/src/token/ERC20MultiVotes.sol:379:        require(block.timestamp <= expiry, "ERC20MultiVotes: signature expired"); //@audit gas: length == 34

I suggest shortening the revert strings to fit in 32 bytes, or using custom errors as described next.

Use Custom Errors instead of Revert Strings to save Gas

Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)

Source: https://blog.soliditylang.org/2021/04/21/custom-errors/:

Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.

Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).

Instances include:

flywheel-v2/src/rewards/FlywheelGaugeRewards.sol:114:        require(rewardToken.balanceOf(address(this)) - balanceBefore >= totalQueuedForCycle);
flywheel-v2/src/rewards/FlywheelGaugeRewards.sol:153:            require(rewardToken.balanceOf(address(this)) - balanceBefore >= newRewards);
flywheel-v2/src/rewards/FlywheelGaugeRewards.sol:154:            require(newRewards <= type(uint112).max); // safe cast
flywheel-v2/src/rewards/FlywheelGaugeRewards.sol:195:            require(queuedRewards.storedCycle < currentCycle);
flywheel-v2/src/rewards/FlywheelGaugeRewards.sol:200:            require(nextRewards <= type(uint112).max); // safe cast
flywheel-v2/src/token/ERC20Gauges.sol:345:            require(_userGauges[user].remove(gauge));
flywheel-v2/src/token/ERC20MultiVotes.sol:266:            require(_delegates[delegator].remove(delegatee));
flywheel-v2/src/token/ERC20MultiVotes.sol:352:                require(_delegates[user].remove(delegatee)); // Remove from set. Should never fail.
flywheel-v2/src/token/ERC20MultiVotes.sol:379:        require(block.timestamp <= expiry, "ERC20MultiVotes: signature expired");
flywheel-v2/src/token/ERC20MultiVotes.sol:392:        require(nonce == nonces[signer]++, "ERC20MultiVotes: invalid nonce");
flywheel-v2/src/token/ERC20MultiVotes.sol:393:        require(signer != address(0));
flywheel-v2/src/FlywheelCore.sol:147:        require(strategyState[strategy].index == 0, "strategy");

I suggest replacing revert strings with custom errors.

QA Report

Low Risk Issues

1. Nonce used for multiple purposes

The nonce mapping used for permit() calls is the same as the one used for delegateBySig(). This should at the very least be documented so signers know that the order of operations between the two functions matters, and so that multicall()s can be organized appropriately

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #1

392        require(nonce == nonces[signer]++, "ERC20MultiVotes: invalid nonce");

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L392

2. multicall()s involving permit() and delegateBySig() can be DOSed

Attackers monitoring the blockchain for multicalls can front-run by calling permit() and delegateBySig() before the multicall(), causing it to revert. Have separate flavors of the functions where the multicall() data is included in the hash

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #1

392        require(nonce == nonces[signer]++, "ERC20MultiVotes: invalid nonce");

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L392

3. Misleading comments

File: lib/flywheel-v2/src/FlywheelCore.sol   #1

82      @return the cumulative amount of rewards accrued to user (including prior)

the cumulative amount of rewards accrued to the user since the last claim
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L82

4. require() should be used instead of assert()

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #1

196             assert(queuedRewards.storedCycle == 0 || queuedRewards.storedCycle >= lastCycle);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L196

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #2

235         assert(queuedRewards.storedCycle >= cycle);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L235

Non-critical Issues

1. require()/revert() statements should have descriptive reason strings

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #1

114         require(rewardToken.balanceOf(address(this)) - balanceBefore >= totalQueuedForCycle);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L114

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #2

153             require(rewardToken.balanceOf(address(this)) - balanceBefore >= newRewards);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L153

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #3

154             require(newRewards <= type(uint112).max); // safe cast

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L154

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #4

195             require(queuedRewards.storedCycle < currentCycle);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L195

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #5

200             require(nextRewards <= type(uint112).max); // safe cast

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L200

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #6

345             require(_userGauges[user].remove(gauge));

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L345

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #7

266             require(_delegates[delegator].remove(delegatee));

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L266

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #8

352                 require(_delegates[user].remove(delegatee)); // Remove from set. Should never fail.

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L352

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #9

393         require(signer != address(0));

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L393

2. public functions not called by the contract should be declared external instead

Contracts are allowed to override their parents' functions and change the visibility from external to public.

File: lib/flywheel-v2/src/FlywheelCore.sol   #1

84     function accrue(ERC20 strategy, address user) public returns (uint256) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L84

File: lib/flywheel-v2/src/FlywheelCore.sol   #2

101     function accrue(
102         ERC20 strategy,
103         address user,
104         address secondUser
105     ) public returns (uint256, uint256) {

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L101-L105

3. Use a more recent version of solidity

Use a solidity version of at least 0.8.4 to get bytes.concat() instead of abi.encodePacked(<bytes>,<bytes>)
Use a solidity version of at least 0.8.12 to get string.concat() instead of abi.encodePacked(<str>,<str>)

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #1

4 pragma solidity ^0.8.0;

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L4

4. Constant redefined elsewhere

Consider defining in only one contract so that values cannot become out of sync when only one location is updated. If the variable is a local cache of another contract's value, consider making the cache variable internal or private, which will require external users to query the contract with the source of truth, so that callers don't get out of sync

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #1

47     uint32 public immutable gaugeCycleLength;

seen in lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol
https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L47

5. Non-library/interface files should use fixed compiler versions, not floating ones

File: lib/xTRIBE/src/xTRIBE.sol   #1

4 pragma solidity ^0.8.0;

https://github.com/fei-protocol/xTRIBE/tree/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L4

6. Typos

File: lib/flywheel-v2/src/FlywheelCore.sol   #1

17          The Core contract maintaings three important pieces of state:

maintaings
https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L17

File: lib/flywheel-v2/src/FlywheelCore.sol   #2

262         // accumulate rewards by multiplying user tokens by rewardsPerToken index and adding on unclaimed

rewardsPerToken
https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L262

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #3

230     /// @notice thrown when incremending during the freeze window.

incremending
https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L230

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #4

143     /// @notice An event thats emitted when an account changes its delegate

thats
https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L143

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #5

189      * @param delegatee the receivier of votes.

receivier
https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L189

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #6

364    /*///////////////////////////////////////////////////////////////
365                             EIP-712 LOGIC
366    //////////////////////////////////////////////////////////////*/

did you mean EIP-2612?
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L364-L366

7. NatSpec is incomplete

File: lib/flywheel-v2/src/FlywheelCore.sol   #1

93     /** 
94       @notice accrue rewards for a two users on a strategy
95       @param strategy the strategy to accrue a user's rewards on
96       @param user the first user to be accrued
97       @param user the second user to be accrued
98       @return the cumulative amount of rewards accrued to the first user (including prior)
99       @return the cumulative amount of rewards accrued to the second user (including prior)
100     */
101     function accrue(
102         ERC20 strategy,
103         address user,
104         address secondUser
105     ) public returns (uint256, uint256) {

Missing: @param secondUser
https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L93-L105

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #2

130       @param num the number of gauges to return
131     */
132     function gauges(uint256 offset, uint256 num) external view returns (address[] memory values) {

Missing: @return
https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L130-L132

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #3

176       @param num the number of gauges to return.
177     */
178     function userGauges(
179         address user,
180         uint256 offset,
181         uint256 num
182     ) external view returns (address[] memory values) {

Missing: @return
https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L176-L182

8. Event is missing indexed fields

Each event should use three indexed fields if there are three or more fields

File: lib/flywheel-v2/src/FlywheelCore.sol   #1

66     event AccrueRewards(ERC20 indexed strategy, address indexed user, uint256 rewardsDelta, uint256 rewardsIndex);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L66

File: lib/flywheel-v2/src/FlywheelCore.sol   #2

73     event ClaimRewards(address indexed user, uint256 amount);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L73

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #3

44     event CycleStart(uint32 indexed cycleStart, uint256 rewardAmount);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L44

File: lib/flywheel-v2/src/rewards/FlywheelGaugeRewards.sol   #4

47     event QueueRewards(address indexed gauge, uint32 indexed cycleStart, uint256 rewardAmount);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L47

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #5

234     event IncrementGaugeWeight(address indexed user, address indexed gauge, uint256 weight, uint32 cycleEnd);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L234

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #6

237     event DecrementGaugeWeight(address indexed user, address indexed gauge, uint256 weight, uint32 cycleEnd);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L237

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #7

440     event MaxGaugesUpdate(uint256 oldMaxGauges, uint256 newMaxGauges);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L440

File: lib/flywheel-v2/src/token/ERC20Gauges.sol   #8

443     event CanContractExceedMaxGaugesUpdate(address indexed account, bool canContractExceedMaxGauges);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L443

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #9

102     event MaxDelegatesUpdate(uint256 oldMaxDelegates, uint256 newMaxDelegates);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L102

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #10

105     event CanContractExceedMaxDelegatesUpdate(address indexed account, bool canContractExceedMaxDelegates);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L105

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #11

135     event Delegation(address indexed delegator, address indexed delegate, uint256 amount);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L135

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #12

138     event Undelegation(address indexed delegator, address indexed delegate, uint256 amount);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L138

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #13

141     event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance);

https://github.com/fei-protocol/flywheel-v2/tree/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L141

9. Consider addings checks for signature malleability

File: lib/flywheel-v2/src/token/ERC20MultiVotes.sol   #1

380        address signer = ecrecover(
381            keccak256(
382                abi.encodePacked(
383                    "\x19\x01",
384                    DOMAIN_SEPARATOR(),
385                    keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry))
386                )
387            ),
388            v,
389            r,
390            s
391        );
392        require(nonce == nonces[signer]++, "ERC20MultiVotes: invalid nonce");
393        require(signer != address(0));
394        _delegate(signer, delegatee);

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L380-L394

QA Report

1)- unclear comments- (Not Critical)
it is mentioned here that we are casting it to avoid overflow and then on the next line, it is explained why there can not be an overflow.
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L256-L258

then again we are recasting the variable to uint112 again here. https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L262

2)- No checks on lastUpdatedTimeStamp in function getAccruedRewards. I know this function is called by onlyFlywheel role. but it's good to check if it is not greater than or equal to current timestamp. cause here we are using this variable https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L250

incase lastUpdatedTimeStamp is equal to current timestamp, here the variable elapsed would be zero
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L253

which means all the further calculation is not needed so we could save gas by just checking.

Incase lastUpdatedTimeStamp is more than current timestamp the variable elapsed will be calculated negative resulting in overflow.

Gas Optimizations

[G-01] Redundant zero initialization

Solidity does not recognize null as a value, so uint variables are initialized to zero. Setting a uint variable to zero is redundant and can waste gas.

There are several places where an int is initialized to zero, which looks like

uint256 amount = 0;

Instances in code:
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L134
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L184
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L307
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L384
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L564
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L346
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L79
https://github.com/fei-protocol/xTRIBE/blob/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L95

Recommended Mitigation Steps

Remove the redundant zero initialization
uint256 amount;

[G-02] Use prefix not postfix in loops

Using a prefix increment (++i) instead of a postfix increment (i++) saves gas for each loop cycle and so can have a big gas impact when the loop executes on a large number of elements.

There are several examples of this
https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/external/Multicall.sol#L14
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L189
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L346
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L137
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L187
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L314
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L391
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L576
https://github.com/fei-protocol/xTRIBE/blob/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L99

Recommended Mitigation Steps

Use prefix not postfix to increment in a loop

[G-03] Short require strings save gas

Strings in solidity are handled in 32 byte chunks. A require string longer than 32 bytes uses more gas. Shortening these strings will save gas.

One cases of this gas optimization was found
34 chars https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L379

Recommended Mitigation Steps

Shorten all require strings to less than 32 characters

[G-04] Use != 0 instead of > 0

Using > 0 uses slightly more gas than using != 0. Use != 0 when comparing uint variables to zero, which cannot hold values below zero

Locations where this was found include
https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/external/PeripheryPayments.sol#L38
https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/external/PeripheryPayments.sol#L45
https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/external/PeripheryPayments.sol#L60
https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/external/PeripheryPayments.sol#L66
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L167
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L218
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L467
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L487
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L287

Recommended Mitigation Steps

Replace > 0 with != 0 to save gas

[G-05] Cache array length before loop

Caching the array length outside a loop saves reading it on each iteration, as long as the array's length is not changed during the loop. This saves gas.

This optimization is already used in some places, but is not used in this place
https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/external/Multicall.sol#L14

Recommended Mitigation Steps

Cache the array length before the for loop

[G-06] Bitshift for divide by 2

When multiply or dividing by a power of two, it is cheaper to bitshift than to use standard math operations.

There is a divide by 2 operation on this line
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L94

Recommended Mitigation Steps

Bitshift right by one bit instead of dividing by 2 to save gas

[G-07] Use simple comparison in trinary logic

The comparison operators >= and <= use more gas than >, <, or ==. Replacing the >= and ≤ operators with a comparison operator that has an opcode in the EVM saves gas

The existing code is
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelDynamicRewards.sol#L50

uint32 latest = timestamp >= cycle.end ? cycle.end : timestamp;

A simple comparison can be used for gas savings by reversing the logic

uint32 latest = timestamp < cycle.end ? timestamp : cycle.end;

Recommended Mitigation Steps

Replace the comparison operator and reverse the logic to save gas using the suggestions above

[G-08] Use simple comparison in if statement

The comparison operators >= and <= use more gas than >, <, or ==. Replacing the >= and ≤ operators with a comparison operator that has an opcode in the EVM saves gas

The existing code is
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L37-L39

if (_incrementFreezeWindow >= _gaugeCycleLength) revert IncrementFreezeError();
gaugeCycleLength = _gaugeCycleLength;
incrementFreezeWindow = _incrementFreezeWindow;

A simple comparison can be used for gas savings by reversing the logic

if (_incrementFreezeWindow < _gaugeCycleLength) {
gaugeCycleLength = _gaugeCycleLength;
incrementFreezeWindow = _incrementFreezeWindow;
} else {
revert IncrementFreezeError();
}

Recommended Mitigation Steps

Replace the comparison operator and reverse the logic to save gas using the suggestions above

[G-09] Use calldata instead of memory for function parameters

Use calldata instead of memory for function parameters. Having function arguments use calldata instead of memory can save gas.

There are several cases of function arguments using memory instead of calldata
https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/utils/ENSReverseRecord.sol#L22
https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/utils/ENSReverseRecord.sol#L26
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L210

Recommended Mitigation Steps

Change function arguments from memory to calldata

Gas Optimizations

1)- function emitVotingBalances can be view as no storage data is changed inside it.

https://github.com/fei-protocol/xTRIBE/blob/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L89-L102

2)- No need to initialize loop variable to zero. default value of uint is zero.
https://github.com/fei-protocol/xTRIBE/blob/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L95

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L189

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L134

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L184

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L307

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L384

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L564

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L346

3)- reading from local variable instead of storage.-

The value of queuedRewards.cycleRewards is already stored in local variable here - https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L241

The value of same variable is used again here
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L258 .
and
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L230

we can use local variable at these places instead of reading from storage again. as reading from memory is cheaper than storage.

4)- The value of queuedRewards.storedCycle is read 4 times in this function. we can store this value in local memory variable so that we don't have to read from storage 4 times.
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L195-L198

5)- In getAccruedRewards function also the value of queuedRewards.storedCycle is read 3 times in this function. we can store this value in local memory variable so that we don't have to read from storage 3 times.
attaching link for all 3 places. - https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L227
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L235
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L268

QA Report

Redundant arithmetic calculation

Impact

The Contract is performing redundant arithmetic calculations, making it possible to reduce code size and make it cleaner.

Proof of Concept

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L90

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L103

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L135

90┆ gaugeCycle = (block.timestamp.safeCastTo32() / gaugeCycleLength) * gaugeCycleLength;
  ⋮┆----------------------------------------
103uint32 currentCycle = (block.timestamp.safeCastTo32() / gaugeCycleLength) * gaugeCycleLength;
  ⋮┆----------------------------------------
135uint32 currentCycle = (block.timestamp.safeCastTo32() / gaugeCycleLength) * gaugeCycleLength;

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L40

40┆     rewardsCycleEnd = (block.timestamp.safeCastTo32() / rewardsCycleLength) * rewardsCycleLength;

Tools Used

Static code analysis

Recommended Mitigation steps

(A * B) / C will always be A, so remove unnecessary calculation.

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L90

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L103

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L135

90┆ gaugeCycle = block.timestamp.safeCastTo32();
  ⋮┆----------------------------------------
103uint32 currentCycle = block.timestamp.safeCastTo32();
  ⋮┆----------------------------------------
135uint32 currentCycle = block.timestamp.safeCastTo32();

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L40

40┆     rewardsCycleEnd = block.timestamp.safeCastTo32();

Gas Optimizations

GAS OPTIMIZATION

  • At FlywheelCore.sol, accrue() method, there is no adress(0) check for the user and secondUser. This can save gas prior trying to accrue rewards for address(0)

  • At FlywheelCore.sol, accrue() method, there is no require(user != secondUser, "err") check for the params. This can save gas for a single user for double accruing.

  • Instead of using block.timestamp for cycles, constants can be used.

  • Choosing either named return or explicit instead of specifying both may reduce gas due to unnecessary bytecode introduced. There is inconsistent use of implicit named return variables across the entire codebase which makes readability and maintainability hard.

In ERC20Gauges, contribution to total weight is double-counted when incrementGauge is called before addGauge for a given gauge.

Lines of code

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L214
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L257
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L248
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L465-L469

Vulnerability details

Impact

The impact depends really on how gauges are used by other contracts.

The most obvious consequence I can imagine is that some other contract distributes rewards based on calculateGaugeAllocation. However, because _getStoredWeight(_totalWeight, currentCycle) is now larger than the real total sum of weights, all rewards are smaller than they should be (because of larger denominator total).

There can be also (potentially large) leftover amount of rewards that is never distributed because now sum of calculateGaugeAllocation(gauge, quantity) over all gauges with constant quantity is less than quantity. So value might be lost.

Proof of Concept

I added this test (modified testCalculateGaugeAllocation ) to ERC20GaugesTest.t.sol and it passes.

    function testExploit() public {
        token.mint(address(this), 100e18);

        token.setMaxGauges(2);
        token.addGauge(gauge1);

        require(token.incrementGauge(gauge1, 1e18) == 1e18);
        require(token.incrementGauge(gauge2, 1e18) == 2e18);
        
        // gauge added after incrementing...
        token.addGauge(gauge2);

        hevm.warp(3600); // warp 1 hour to store changes
        require(token.calculateGaugeAllocation(gauge1, 150e18) == 50e18);
        require(token.calculateGaugeAllocation(gauge2, 150e18) == 50e18);

        // expected value would be 2e18
        require(token.totalWeight() == 3e18);

        require(token.incrementGauge(gauge2, 2e18) == 4e18);

        // ensure updates don't propagate until stored
        require(token.calculateGaugeAllocation(gauge1, 150e18) == 50e18);
        require(token.calculateGaugeAllocation(gauge2, 150e18) == 50e18);

        hevm.warp(7200); // warp another hour to store changes again
        require(token.calculateGaugeAllocation(gauge1, 125e18) == 25e18);
        require(token.calculateGaugeAllocation(gauge2, 125e18) == 75e18);
        
        // expected value would be 4e18
        require(token.totalWeight() == 5e18);
    }

As we can see, we can call token.incrementGauge(gauge2, 1e18) before token.addGauge(gauge2). This is because this check doesn't revert for gauges that were never added in the first place.

First time the total weight is incremented in _incrementUserAndGlobalWeights and 2nd time here.

If corrupting state like this is adventurous for someone, he can frontrun token.addGauge called by the admin with a call to incrementGauge which is permissionless.

Tools Used

Foundry

Recommended Mitigation Steps

Use condition _gauges.contains(gauge) && !_deprecatedGauges.contains(gauge) to check if a gauge can be incremented instead of just !_deprecatedGauges.contains(gauge) . There's a function isGauge in the contract that does exactly this.

Gas Optimizations

Gas Optimizations

++i use less gas than i++:

++i costs less gas compared to i++. about 5 gas per iteration.

FlywheelGaugeRewards.sol:189  for (uint256 i = 0; i < size; i++) {
ERC20MultiVotes.sol:346  for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; i++) {
ERC20Gauges.sol:137  i++;
ERC20Gauges.sol:187  i++;
ERC20Gauges.sol:314  i++;
ERC20Gauges.sol:391  i++;
ERC20Gauges.sol:576  i++;
xTRIBE.sol:99  i++;

!= 0 use less gas than > 0 for unsigned ints:

FlywheelCore.sol:167  if (oldRewardBalance > 0) {
FlywheelCore.sol:218  if (strategyRewardsAccrued > 0) { 
ERC20Gauges.sol:467  if (weight > 0) {
ERC20Gauges.sol:487  if (weight > 0) {
ERC20MultiVotes.sol:287  if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) {

Assign 0 to uint256:

Default value of uint256 is 0. you can delete = 0 for saving some gas.

FlywheelGaugeRewards.sol:189  for (uint256 i = 0; i < size; i++) {
ERC20Gauges.sol:134  for (uint256 i = 0; i < num; ) {
ERC20Gauges.sol:184  for (uint256 i = 0; i < num; ) {
ERC20Gauges.sol:307  for (uint256 i = 0; i < size; ) {
ERC20Gauges.sol:384  for (uint256 i = 0; i < size; ) {
ERC20Gauges.sol:564  for (uint256 i = 0; i < size && (userFreeWeight + totalFreed) < weight; ) {
ERC20MultiVotes.sol:346  for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; i++) {
ERC20MultiVotes.sol:79  uint256 low = 0;
xTRIBE.sol:95  for (uint256 i = 0; i < size; ) {

uint256 is the most gas efficient type:

In source code there are lots of uint112, uint32. using uint256 instead of them using less gas and remove the need of casting types.

ERC20Gauges: The _incrementGaugeWeight function does not check the gauge parameter enough, so the user may lose rewards.

Lines of code

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L257

Vulnerability details

Impact

The _incrementGaugeWeight function is used to increase the user's weight on the gauge. However, in the _incrementGaugeWeight function, it is only checked that the gauge parameter is not in _deprecatedGauges, but not checked that the gauge parameter is in _gauges. If the user accidentally uses the wrong gauge parameter, the function will be executed smoothly without any warning, which will cause user loss reward.

    function _incrementGaugeWeight(
        address user,
        address gauge,
        uint112 weight,
        uint32 cycle
    ) internal {
        if (_deprecatedGauges.contains(gauge)) revert InvalidGaugeError();
        unchecked {
            if (cycle - block.timestamp <= incrementFreezeWindow) revert IncrementFreezeError();
        }

        bool added = _userGauges[user].add(gauge); // idempotent add
        if (added && _userGauges[user].length() > maxGauges && !canContractExceedMaxGauges[user])
            revert MaxGaugeError();

        getUserGaugeWeight[user][gauge] += weight;

        _writeGaugeWeight(_getGaugeWeight[gauge], _add, weight, cycle);

        emit IncrementGaugeWeight(user, gauge, weight, cycle);
    }
    ...
    function _writeGaugeWeight(
        Weight storage weight,
        function(uint112, uint112) view returns (uint112) op,
        uint112 delta,
        uint32 cycle
    ) private {
        uint112 currentWeight = weight.currentWeight; // @audit  currentWeight = 0
        // If the last cycle of the weight is before the current cycle, use the current weight as the stored.
        uint112 stored = weight.currentCycle < cycle ? currentWeight : weight.storedWeight; // @audit  stored = 0 < cycle ? 0 : 0
        uint112 newWeight = op(currentWeight, delta); // @audit newWeight = 0 + delta

        weight.storedWeight = stored;
        weight.currentWeight = newWeight;
        weight.currentCycle = cycle;
    }

Proof of Concept

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L257

Tools Used

None

Recommended Mitigation Steps

    function _incrementGaugeWeight(
        address user,
        address gauge,
        uint112 weight,
        uint32 cycle
    ) internal {
-       if (_deprecatedGauges.contains(gauge)) revert InvalidGaugeError();
+       if (_deprecatedGauges.contains(gauge) || !_gauges.contains(gauge)) revert InvalidGaugeError();
        unchecked {
            if (cycle - block.timestamp <= incrementFreezeWindow) revert IncrementFreezeError();
        }

        bool added = _userGauges[user].add(gauge); // idempotent add
        if (added && _userGauges[user].length() > maxGauges && !canContractExceedMaxGauges[user])
            revert MaxGaugeError();

        getUserGaugeWeight[user][gauge] += weight;

        _writeGaugeWeight(_getGaugeWeight[gauge], _add, weight, cycle);

 }

`xERC4626` gives old rewards to new holders too

Lines of code

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L43-L62

Vulnerability details

xERC4626 distributes rewards by linearly changing the exchange rate based on the excess funds from the last known amounts. When a user mints or redeems, they get the new exchange rate, and thus get a portion of the rewards.

Impact

If a project uses xERC4626 and decides to give a large airdrop to its current holders via a deposit to the asset contract for the xERC4626's address, a whale can monitor the blockchain for such a large deposit, and mint() a large amount of the xAsset, wait the full cycle length, then redeem(), claiming a large portion of rewards that were meant for prior holders.

Proof of Concept

totalAssets() increases its amount based on storedTotalAssets:

File: lib/ERC4626/src/xERC4626.sol

43        /// @notice Compute the amount of tokens available to share holders.
44        ///         Increases linearly during a reward distribution period from the sync call, not the cycle start.
45        function totalAssets() public view override returns (uint256) {
46            // cache global vars
47            uint256 storedTotalAssets_ = storedTotalAssets;
48            uint192 lastRewardAmount_ = lastRewardAmount;
49            uint32 rewardsCycleEnd_ = rewardsCycleEnd;
50            uint32 lastSync_ = lastSync;
51    
52            if (block.timestamp >= rewardsCycleEnd_) {
53                // no rewards or rewards fully unlocked
54                // entire reward amount is available
55                return storedTotalAssets_ + lastRewardAmount_;
56            }
57    
58            // rewards not fully unlocked
59            // add unlocked rewards to stored total
60            uint256 unlockedRewards = (lastRewardAmount_ * (block.timestamp - lastSync_)) / (rewardsCycleEnd_ - lastSync_);
61            return storedTotalAssets_ + unlockedRewards;
62        }

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L43-L62

which comes from the last sync:

File: lib/ERC4626/src/xERC4626.sol

76        /// @notice Distributes rewards to xERC4626 holders.
77        /// All surplus `asset` balance of the contract over the internal balance becomes queued for the next cycle.
78        function syncRewards() public virtual {
79            uint192 lastRewardAmount_ = lastRewardAmount;
80            uint32 timestamp = block.timestamp.safeCastTo32();
81    
82            if (timestamp < rewardsCycleEnd) revert SyncError();
83    
84            uint256 storedTotalAssets_ = storedTotalAssets;
85            uint256 nextRewards = asset.balanceOf(address(this)) - storedTotalAssets_ - lastRewardAmount_;
86    
87            storedTotalAssets = storedTotalAssets_ + lastRewardAmount_; // SSTORE

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L76-L87

There is no code that tracks old user balances in ERC4626 - every user's exchange rate is the same based on totalAssets():

File: solmate/src/mixins/ERC4626.sol   #X

124    function convertToShares(uint256 assets) public view virtual returns (uint256) {
125        uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero.
126
127        return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets());
128    }
129
130    function convertToAssets(uint256 shares) public view virtual returns (uint256) {
131        uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero.
132
133        return supply == 0 ? shares : shares.mulDivDown(totalAssets(), supply);
134    }

https://github.com/Rari-Capital/solmate/blob/main/src/mixins/ERC4626.sol#L124-L134

Tools Used

Code inspection

Recommended Mitigation Steps

It seems as though EIP-4626's before/after callbacks and other internal functions should include the owner as a parameter, so that child contracts can track specific balances of each user, and when they change.

QA Report

https://github.com/fei-protocol/xTRIBE/blob/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L134
https://github.com/fei-protocol/xTRIBE/blob/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L149
Consider using safeTransferFrom instead of transferForm. This ensure validity of the receiver.

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L50
The parameter incrementFreezeWindow, is not used for any other business logic besides checking for current time at line 259. It can be removed and the check can e replaced with another business logic.

None

Lines of code

None

Vulnerability details

Impact

Detailed description of the impact of this finding.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Tools Used

Recommended Mitigation Steps

Gas Optimizations

QA Report

Impact

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L45-L62
Move lastSync_ closer to usage.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
function totalAssets() public view override returns (uint256) {
// cache global vars
uint256 storedTotalAssets_ = storedTotalAssets;
uint192 lastRewardAmount_ = lastRewardAmount;
uint32 rewardsCycleEnd_ = rewardsCycleEnd;

if (block.timestamp >= rewardsCycleEnd_) {
    // no rewards or rewards fully unlocked
    // entire reward amount is available
    return storedTotalAssets_ + lastRewardAmount_;
}

// rewards not fully unlocked
// add unlocked rewards to stored total
uint32 lastSync_ = lastSync;
uint256 unlockedRewards = (lastRewardAmount_ * (block.timestamp - lastSync_)) / (rewardsCycleEnd_ - lastSync_);
return storedTotalAssets_ + unlockedRewards;

}


Impact

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L4
Solidity version >=0.8.4 is required since the code is using custom errors.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
pragma solidity ^0.8.4;


Impact

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L4
Solidity version >=0.8.4 is required since the code is using custom errors.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
pragma solidity ^0.8.4;


Impact

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L3
Solidity version >=0.8.4 is required since the code is using custom errors.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
pragma solidity ^0.8.4;


Impact

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L134
Uint256 by default is set to zero.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
uint256 i;


Impact

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L184
Uint256 by default is set to zero.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
uint256 i;


Impact

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L307
Uint256 by default is set to zero.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
uint256 i;


Impact

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L384
Uint256 by default is set to zero.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
uint256 i;


Impact

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L564
Uint256 by default is set to zero.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
uint256 i;


Impact

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/FlywheelGaugeRewards.sol#L189
Uint256 by default is set to zero.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
uint256 i;


Impact

https://github.com/fei-protocol/xTRIBE/blob/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L95
Uint256 by default is set to zero.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
uint256 i;


Impact

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L79
Uint256 by default is set to zero.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
uint256 i;


Impact

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L346
Uint256 by default is set to zero.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
uint256 i;

Gas Optimizations

general considerations

  • there are multiple instances of block.timestamp.safeCastTo32(), which could be changed to uint32(block.timestamp) with no practical risk, and saves the require() check
  • uint256 should be used in many scenarios inside of the functions or as function parameters instead, and only cast it into a lower size uint, when storing it instead. reference. for example timestamp is being casted way too early, and then being used, resulting in higher gas.
  • snapshots below were run with forge snapshot --optimize --optimize-runs 1000000

use storage pointers where possible [ERC20MultiVotes]

diff --git a/.gas-snapshot b/.gas-snapshot
index c5e3f62..c439cf6 100644
--- a/.gas-snapshot
+++ b/.gas-snapshot
@@ -53,13 +53,13 @@ ERC20MultiVotesTest:testCanContractExceedMax() (gas: 35031)
 ERC20MultiVotesTest:testCanContractExceedMaxNonContractFails() (gas: 13535)
 ERC20MultiVotesTest:testCanContractExceedMaxNonOwnerFails() (gas: 13760)
 ERC20MultiVotesTest:testDecrementOverWeightFails() (gas: 247398)
-ERC20MultiVotesTest:testDecrementUntilFreeDouble() (gas: 350624)
-ERC20MultiVotesTest:testDecrementUntilFreeSingle() (gas: 327524)
+ERC20MultiVotesTest:testDecrementUntilFreeDouble() (gas: 350352)
+ERC20MultiVotesTest:testDecrementUntilFreeSingle() (gas: 327354)
 ERC20MultiVotesTest:testDecrementUntilFreeWhenFree() (gas: 375129)
 ERC20MultiVotesTest:testDelegateOverMaxDelegatesApproved() (gas: 509277)
 ERC20MultiVotesTest:testDelegateOverMaxDelegatesFails() (gas: 413581)
 ERC20MultiVotesTest:testDelegateOverVotesFails() (gas: 247599)
 ERC20MultiVotesTest:testDelegateToAddressZeroFails() (gas: 87370)
-ERC20MultiVotesTest:testPastVotes() (gas: 351022)
+ERC20MultiVotesTest:testPastVotes() (gas: 350952)
 ERC20MultiVotesTest:testSetMaxDelegatesNonOwnerFails() (gas: 13647)
-ERC20MultiVotesTest:testUndelegate() (gas: 211796)
+ERC20MultiVotesTest:testUndelegate() (gas: 211684)
diff --git a/src/token/ERC20MultiVotes.sol b/src/token/ERC20MultiVotes.sol
index 08e2242..bb75374 100644
--- a/src/token/ERC20MultiVotes.sol
+++ b/src/token/ERC20MultiVotes.sol
@@ -260,13 +260,14 @@ abstract contract ERC20MultiVotes is ERC20, Auth {
         address delegatee,
         uint256 amount
     ) internal virtual {
-        uint256 newDelegates = _delegatesVotesCount[delegator][delegatee] - amount;
+        mapping(address => uint256) storage ptr = _delegatesVotesCount[delegator];
+        uint256 newDelegates = ptr[delegatee] - amount;
 
         if (newDelegates == 0) {
             require(_delegates[delegator].remove(delegatee));
         }
 
-        _delegatesVotesCount[delegator][delegatee] = newDelegates;
+        ptr[delegatee] = newDelegates;
         userDelegatedVotes[delegator] -= amount;
 
         emit Undelegation(delegator, delegatee, amount);
@@ -339,19 +340,22 @@ abstract contract ERC20MultiVotes is ERC20, Auth {
         uint256 totalFreed;
 
         // Loop through all delegates
-        address[] memory delegateList = _delegates[user].values();
+        EnumerableSet.AddressSet storage _delegatesUser = _delegates[user];
+        address[] memory delegateList = _delegatesUser.values();
 
         // Free delegates until through entire list or under votes amount
         uint256 size = delegateList.length;
         for (uint256 i = 0; i < size && (userFreeVotes + totalFreed) < votes; i++) {
             address delegatee = delegateList[i];
-            uint256 delegateVotes = _delegatesVotesCount[user][delegatee];
+
+            mapping(address => uint256) storage _delegatesVotesCountUser = _delegatesVotesCount[user];
+            uint256 delegateVotes = _delegatesVotesCountUser[delegatee];
             if (delegateVotes != 0) {
                 totalFreed += delegateVotes;
 
-                require(_delegates[user].remove(delegatee)); // Remove from set. Should never fail.
+                require(_delegatesUser.remove(delegatee)); // Remove from set. Should never fail.
 
-                _delegatesVotesCount[user][delegatee] = 0;
+                _delegatesVotesCountUser[delegatee] = 0;
 
                 _writeCheckpoint(delegatee, _subtract, delegateVotes);
                 emit Undelegation(user, delegatee, delegateVotes);

use storage pointers where possible [ERC20Gauges]

diff --git a/.gas-snapshot b/.gas-snapshot
index c439cf6..ddddaf1 100644
--- a/.gas-snapshot
+++ b/.gas-snapshot
@@ -4,17 +4,17 @@ FlywheelTest:testAccrueTwoUsersSeparately() (gas: 330979)
 FlywheelTest:testFailAddStrategy() (gas: 15092)
 FlywheelTest:testSetFlywheelBoosterUnauthorized() (gas: 13594)
 FlywheelTest:testSetFlywheelRewardsUnauthorized() (gas: 13615)
-FlywheelGaugeRewardsTest:testGetPriorRewards() (gas: 566359)
-FlywheelGaugeRewardsTest:testGetRewards() (gas: 546077)
+FlywheelGaugeRewardsTest:testGetPriorRewards() (gas: 566122)
+FlywheelGaugeRewardsTest:testGetRewards() (gas: 545907)
 FlywheelGaugeRewardsTest:testGetRewardsUninitialized() (gas: 12512)
-FlywheelGaugeRewardsTest:testIncompletePagination() (gas: 972359)
-FlywheelGaugeRewardsTest:testPagination() (gas: 900730)
-FlywheelGaugeRewardsTest:testQueue() (gas: 524891)
-FlywheelGaugeRewardsTest:testQueueFullPagination() (gas: 527970)
-FlywheelGaugeRewardsTest:testQueueSkipCycle() (gas: 345785)
-FlywheelGaugeRewardsTest:testQueueSkipCycleFullPagination() (gas: 348443)
-FlywheelGaugeRewardsTest:testQueueTwoCycles() (gas: 560780)
-FlywheelGaugeRewardsTest:testQueueTwoCyclesFullPagination() (gas: 566759)
+FlywheelGaugeRewardsTest:testIncompletePagination() (gas: 972019)
+FlywheelGaugeRewardsTest:testPagination() (gas: 900390)
+FlywheelGaugeRewardsTest:testQueue() (gas: 524721)
+FlywheelGaugeRewardsTest:testQueueFullPagination() (gas: 527800)
+FlywheelGaugeRewardsTest:testQueueSkipCycle() (gas: 345700)
+FlywheelGaugeRewardsTest:testQueueSkipCycleFullPagination() (gas: 348358)
+FlywheelGaugeRewardsTest:testQueueTwoCycles() (gas: 560543)
+FlywheelGaugeRewardsTest:testQueueTwoCyclesFullPagination() (gas: 566522)
 FlywheelGaugeRewardsTest:testQueueWithoutGaugesBeforeCycle() (gas: 13468)
 FlywheelGaugeRewardsTest:testQueueWithoutGaugesNoGauges() (gas: 69137)
 FlywheelStaticRewardsTest:testGetAccruedRewards() (gas: 97429)
@@ -23,27 +23,27 @@ FlywheelStaticRewardsTest:testGetAccruedRewardsCappedAfterEnd() (gas: 97873)
 FlywheelStaticRewardsTest:testSetRewardsInfo() (gas: 39879)
 ERC20GaugesTest:testAddGaugeNonOwner() (gas: 38452)
 ERC20GaugesTest:testAddGaugeTwice() (gas: 113090)
-ERC20GaugesTest:testCalculateGaugeAllocation() (gas: 488805)
+ERC20GaugesTest:testCalculateGaugeAllocation() (gas: 488640)
 ERC20GaugesTest:testCanContractExceedMax() (gas: 35078)
 ERC20GaugesTest:testCanContractExceedMaxNonContract() (gas: 13603)
 ERC20GaugesTest:testCanContractExceedMaxNonOwner() (gas: 13917)
-ERC20GaugesTest:testDecrement() (gas: 310594)
-ERC20GaugesTest:testDecrementAllRemovesGauge() (gas: 300684)
-ERC20GaugesTest:testDecrementGauges() (gas: 432470)
-ERC20GaugesTest:testDecrementGaugesOver() (gas: 415437)
-ERC20GaugesTest:testDecrementGaugesSizeMismatch() (gas: 466673)
-ERC20GaugesTest:testDecrementUntilFreeDeprecated() (gas: 447041)
-ERC20GaugesTest:testDecrementUntilFreeDouble() (gas: 431097)
-ERC20GaugesTest:testDecrementUntilFreeSingle() (gas: 449392)
-ERC20GaugesTest:testDecrementUntilFreeWhenFree() (gas: 477079)
-ERC20GaugesTest:testDecrementWithStorage() (gas: 324403)
-ERC20GaugesTest:testIncrementAtMax() (gas: 380777)
-ERC20GaugesTest:testIncrementGauges() (gas: 477454)
+ERC20GaugesTest:testDecrement() (gas: 310419)
+ERC20GaugesTest:testDecrementAllRemovesGauge() (gas: 300562)
+ERC20GaugesTest:testDecrementGauges() (gas: 432171)
+ERC20GaugesTest:testDecrementGaugesOver() (gas: 415206)
+ERC20GaugesTest:testDecrementGaugesSizeMismatch() (gas: 466503)
+ERC20GaugesTest:testDecrementUntilFreeDeprecated() (gas: 446756)
+ERC20GaugesTest:testDecrementUntilFreeDouble() (gas: 430812)
+ERC20GaugesTest:testDecrementUntilFreeSingle() (gas: 449178)
+ERC20GaugesTest:testDecrementUntilFreeWhenFree() (gas: 476909)
+ERC20GaugesTest:testDecrementWithStorage() (gas: 324228)
+ERC20GaugesTest:testIncrementAtMax() (gas: 380697)
+ERC20GaugesTest:testIncrementGauges() (gas: 477289)
 ERC20GaugesTest:testIncrementGaugesDeprecated() (gas: 282189)
-ERC20GaugesTest:testIncrementGaugesOver() (gas: 421785)
+ERC20GaugesTest:testIncrementGaugesOver() (gas: 421615)
 ERC20GaugesTest:testIncrementGaugesSizeMismatch() (gas: 281154)
-ERC20GaugesTest:testIncrementOverMax() (gas: 420342)
-ERC20GaugesTest:testIncrementWithStorage() (gas: 514952)
+ERC20GaugesTest:testIncrementOverMax() (gas: 420170)
+ERC20GaugesTest:testIncrementWithStorage() (gas: 514787)
 ERC20GaugesTest:testRemoveGauge() (gas: 179755)
 ERC20GaugesTest:testRemoveGaugeNonOwner() (gas: 112716)
 ERC20GaugesTest:testRemoveGaugeTwice() (gas: 180455)
diff --git a/src/token/ERC20Gauges.sol b/src/token/ERC20Gauges.sol
index ad8fabe..2b59338 100644
--- a/src/token/ERC20Gauges.sol
+++ b/src/token/ERC20Gauges.sol
@@ -259,9 +259,10 @@ abstract contract ERC20Gauges is ERC20, Auth {
             if (cycle - block.timestamp <= incrementFreezeWindow) revert IncrementFreezeError();
         }
 
-        bool added = _userGauges[user].add(gauge); // idempotent add
-        if (added && _userGauges[user].length() > maxGauges && !canContractExceedMaxGauges[user])
-            revert MaxGaugeError();
+        EnumerableSet.AddressSet storage pUserGauges = _userGauges[user];
+
+        bool added = pUserGauges.add(gauge); // idempotent add
+        if (added && pUserGauges.length() > maxGauges && !canContractExceedMaxGauges[user]) revert MaxGaugeError();
 
         getUserGaugeWeight[user][gauge] += weight;
 
@@ -337,9 +338,10 @@ abstract contract ERC20Gauges is ERC20, Auth {
         uint112 weight,
         uint32 cycle
     ) internal {
-        uint112 oldWeight = getUserGaugeWeight[user][gauge];
+        mapping(address => uint112) storage pUserGaugeWeight = getUserGaugeWeight[user];
+        uint112 oldWeight = pUserGaugeWeight[gauge];
 
-        getUserGaugeWeight[user][gauge] = oldWeight - weight;
+        pUserGaugeWeight[gauge] = oldWeight - weight;
         if (oldWeight == weight) {
             // If removing all weight, remove gauge from user list.
             require(_userGauges[user].remove(gauge));
@@ -561,9 +563,11 @@ abstract contract ERC20Gauges is ERC20, Auth {
 
         // Free gauges until through entire list or under weight
         uint256 size = gaugeList.length;
+        mapping(address => uint112) storage pUserGaugeWeight = getUserGaugeWeight[user];
+
         for (uint256 i = 0; i < size && (userFreeWeight + totalFreed) < weight; ) {
             address gauge = gaugeList[i];
-            uint112 userGaugeWeight = getUserGaugeWeight[user][gauge];
+            uint112 userGaugeWeight = pUserGaugeWeight[gauge];
             if (userGaugeWeight != 0) {
                 // If the gauge is live (not deprecated), include its weight in the total to remove
                 if (!_deprecatedGauges.contains(gauge)) {

unchecked i++ [FlywheelGaugeRewards]

diff --git a/.gas-snapshot b/.gas-snapshot
index ddddaf1..14733f7 100644
--- a/.gas-snapshot
+++ b/.gas-snapshot
@@ -4,17 +4,17 @@ FlywheelTest:testAccrueTwoUsersSeparately() (gas: 330979)
 FlywheelTest:testFailAddStrategy() (gas: 15092)
 FlywheelTest:testSetFlywheelBoosterUnauthorized() (gas: 13594)
 FlywheelTest:testSetFlywheelRewardsUnauthorized() (gas: 13615)
-FlywheelGaugeRewardsTest:testGetPriorRewards() (gas: 566122)
-FlywheelGaugeRewardsTest:testGetRewards() (gas: 545907)
+FlywheelGaugeRewardsTest:testGetPriorRewards() (gas: 565862)
+FlywheelGaugeRewardsTest:testGetRewards() (gas: 545777)
 FlywheelGaugeRewardsTest:testGetRewardsUninitialized() (gas: 12512)
-FlywheelGaugeRewardsTest:testIncompletePagination() (gas: 972019)
-FlywheelGaugeRewardsTest:testPagination() (gas: 900390)
-FlywheelGaugeRewardsTest:testQueue() (gas: 524721)
-FlywheelGaugeRewardsTest:testQueueFullPagination() (gas: 527800)
-FlywheelGaugeRewardsTest:testQueueSkipCycle() (gas: 345700)
-FlywheelGaugeRewardsTest:testQueueSkipCycleFullPagination() (gas: 348358)
-FlywheelGaugeRewardsTest:testQueueTwoCycles() (gas: 560543)
-FlywheelGaugeRewardsTest:testQueueTwoCyclesFullPagination() (gas: 566522)
+FlywheelGaugeRewardsTest:testIncompletePagination() (gas: 971499)
+FlywheelGaugeRewardsTest:testPagination() (gas: 900130)
+FlywheelGaugeRewardsTest:testQueue() (gas: 524591)
+FlywheelGaugeRewardsTest:testQueueFullPagination() (gas: 527670)
+FlywheelGaugeRewardsTest:testQueueSkipCycle() (gas: 345635)
+FlywheelGaugeRewardsTest:testQueueSkipCycleFullPagination() (gas: 348293)
+FlywheelGaugeRewardsTest:testQueueTwoCycles() (gas: 560283)
+FlywheelGaugeRewardsTest:testQueueTwoCyclesFullPagination() (gas: 566262)
 FlywheelGaugeRewardsTest:testQueueWithoutGaugesBeforeCycle() (gas: 13468)
 FlywheelGaugeRewardsTest:testQueueWithoutGaugesNoGauges() (gas: 69137)
 FlywheelStaticRewardsTest:testGetAccruedRewards() (gas: 97429)
diff --git a/src/rewards/FlywheelGaugeRewards.sol b/src/rewards/FlywheelGaugeRewards.sol
index 85a77c0..f099b07 100644
--- a/src/rewards/FlywheelGaugeRewards.sol
+++ b/src/rewards/FlywheelGaugeRewards.sol
@@ -186,7 +186,7 @@ contract FlywheelGaugeRewards is Auth, BaseFlywheelRewards {
 
         if (size == 0) revert EmptyGaugesError();
 
-        for (uint256 i = 0; i < size; i++) {
+        for (uint256 i = 0; i < size; ) {
             ERC20 gauge = ERC20(gauges[i]);
 
             QueuedRewards memory queuedRewards = gaugeQueuedRewards[gauge];
@@ -206,6 +206,10 @@ contract FlywheelGaugeRewards is Auth, BaseFlywheelRewards {
             });
 
             emit QueueRewards(address(gauge), currentCycle, nextRewards);
+
+            unchecked {
+                i++;
+            }
         }
     }

Gas Optimizations

variable used only once - do not cache

Description

for variables only used once, calculating it inline saves gas

Example

ECR20Gauges.sol

90: uint32 nowPlusOneCycle = block.timestamp.safeCastTo32() + gaugeCycleLength;
212: uint112 total = _getStoredWeight(_totalWeight, currentCycle);
213: uint112 weight = _getStoredWeight(_getGaugeWeight[gauge], currentCycle);
410: uint112 stored = weight.currentCycle < cycle ? currentWeight : weight.storedWeight;
411: uint112 newWeight = op(currentWeight, delta);
458: bool newAdd = _gauges.add(gauge);
459: bool previouslyDeprecated = _deprecatedGauges.remove(gauge);
464: uint32 currentCycle = _getGaugeCycleEnd();
485: uint32 currentCycle = _getGaugeCycleEnd();
505: uint256 oldMax = maxGauges;

ERC20MultiVotes.sol

242: uint256 free = freeVotes(delegator);

FlywheelCore.sol

257:uint224 deltaIndex = strategyIndex - supplierIndex;

FlywheelGaugeRewards.sol

113: uint256 balanceBefore = rewardToken.balanceOf(address(this));
259: uint32 elapsed = block.timestamp.safeCastTo32() - beginning;
260: uint32 remaining = cycleEnd - beginning;

xERC4626.sol

60: uint256 unlockedRewards = (lastRewardAmount_ * (block.timestamp - lastSync_)) / (rewardsCycleEnd_ - lastSync_);

named returns and a return statement isn’t necessary

Description

Removing unused named returns variables can reduce gas usage (MSTOREs/MLOADs) and improve code clarity. To save gas and improve code quality: consider using only one of those.

Example

ERC20Gauges.sol

    function incrementGauges(address[] calldata gaugeList, uint112[] calldata weights)
        external
        returns (uint256 newUserWeight)

ERC20Gauges.sol

function decrementGauge(address gauge, uint112 weight) external returns (uint112 newUserWeight) {

ERC20Gauges.sol

    function decrementGauges(address[] calldata gaugeList, uint112[] calldata weights)
        external
        returns (uint112 newUserWeight)

require statements should be checked first

Description

require statements can be placed earlier to reduce gas usage on revert

Example

ERC20Gauges.sol

        if (oldWeight == weight) {
            // If removing all weight, remove gauge from user list.
            require(_userGauges[user].remove(gauge));
        }

use shift operator (divide by 2)

Description

Gas can be saved by using >> operator instead of dividing by 2

Example

ERC20MultiVotes.sol

94: return (a & b) + (a ^ b) / 2;

mark state variables immutable/private

Description

There are variables that do not change so they can be marked as immutable to greatly improve the gast costs.

Example

FlywheelCore.sol

201: uint224 public constant ONE = 1e18;

cache in variables instead of loading

Description

The code can be optimized by minimising the number of SLOADs. SLOADs are expensive (100 gas) compared to MLOADs/MSTOREs (3 gas). Here, storage values should get cached in memory

Example

queuedRewards.storedCycle used multiple times
FlywheelGaugeRewards.sol

198: require(queuedRewards.storedCycle < currentCycle);
199: assert(queuedRewards.storedCycle == 0 || queuedRewards.storedCycle >= lastCycle);
201: uint112 completedRewards = queuedRewards.storedCycle == lastCycle ? queuedRewards.cycleRewards : 0;
230: bool incompleteCycle = queuedRewards.storedCycle > cycle;
241: assert(queuedRewards.storedCycle >= cycle);

queuedRewards.priorCycleRewards used multiple times
FlywheelGaugeRewards.sol

234: if (queuedRewards.priorCycleRewards == 0 && (queuedRewards.cycleRewards == 0 || incompleteCycle)) {
246: accruedRewards = queuedRewards.priorCycleRewards;

queuedRewards.cycleRewards used multiple times

FlywheelGaugeRewards.sol

234: if (queuedRewards.priorCycleRewards == 0 && (queuedRewards.cycleRewards == 0 || incompleteCycle)) {
247: uint112 cycleRewardsNext = queuedRewards.cycleRewards;

QA Report

Report

ERC20MultiVotes can't fully enforce the max delegate count

Currently, the ERC20MultiVotes contract has a state variable maxDelegates. It specifies the number of delegates each delegator is allowed to have. This is not really an enforceable rule tho. People can distribute their tokens to multiple addresses and delegate through that. While it's annoying to circumvent that limitation it's possible.

I don't see the reasoning behind even having a limitation. It doesn't seem to affect the goal of the contract in any form. You're supposed to delegate your tokens to multiple people. Why limit the number of delegates?

You could argue that the decrementVotesUntilFree() function loops over all the delegates. Having a large number of them might result in the function running out of gas. But, burning tokens, which calls the decrementVotesUntilFree() function, uses only ~42.000 gas. So you should be able to have more than 350 delegates before the function runs into any gas problems. People having that many delegates seems unrealistic to me.

Here's the test I used for that:

contract QuickTest is DSTestPlus {
    MockERC20MultiVotes token;
    address constant delegate1 = address(0xDEAD);
    function setUp() public {
        token = new MockERC20MultiVotes(address(this));
        token.mint(address(this), 100e18);
        token.setMaxDelegates(2);
        token.incrementDelegation(delegate1, 100e18);
        require(token.freeVotes(address(this)) == 0);
    }

    function testBurn() public {
        token.burn(address(this), 100e18);
    }
}

I just want to emphasize that this is not a valid limitation. It is possible to get around it. You should expect people to bypass it.

Also, if people had x amount of delegates and the new limit is y, where y < x, the contract doesn't force those people to reduce their number of delegates. They are even allowed to increase the overall amount of tokens they delegate to those people. They just can't delegate to any new people unless they go below the new limit. If you want to keep the maxDelegates it might make sense to block people from increasing the number of tokens for existing delegates unless they fulfill the new limit.

Relevant code:

QA Report

Floating pragma

Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively.
https://swcregistry.io/docs/SWC-103

Lines of code

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L3
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20MultiVotes.sol#L4
https://github.com/fei-protocol/ERC4626/blob/main/src/xERC4626.sol#L4
https://github.com/fei-protocol/xTRIBE/blob/989e47d176facbb0c38bc1e1ca58672f179159e1/src/xTRIBE.sol#L4

Recommended Mitigation Steps

Lock the pragma

Potentially early unlock of reward amount

Lines of code

https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L89
https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L40

Vulnerability details

Impact

The precision loss when calculating the rewardsCycleEnd can cause the reward amount to be unlocked before rewardsCycleLength lapses.

Proof of Concept

Next rewardsCycleEnd is calculated by the line:
uint32 end = ((timestamp + rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength; //@Audit precision loss
Since the division is executed before multiplication, any amount smaller than rewardsCycleLength would be truncated.

For the sake of simplicity, let's calculate everything in months terms.
block.timestamp = As of 1 May 2022 --> 52 years and 4 months = 52*12 + 4 = 628 months
Let's say rewardsCycleLength = 12 months and we start a new cycle at 1 May 2022, so expect the cycle to end at 1 May 2023.
Hence the calculation would be;
uint32 end = ((628 months + 12) / 12) * 12 = (640 / 12) * 12 = 53 * 12
So rewardsCycleEnd would point to 1 Jan 2023, whereas it should be 1 May 2023.

Tools Used

Manual analysis

Recommended Mitigation Steps

I suggest to execute the multiplication before division. When doing so uint32 may overflow, so timestamp should be casted to a greater data type first.

_incrementGaugeWeight allows user to add weight to nonexistent gauges

Lines of code

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L257

Vulnerability details

Impact

User adds weight to a gauge that hasn't been added

In addition to adding to a nonexistent gauge it also increments _totalWeight which only contains weight for live gauges. This value then results in https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L208 returning values for reward distribution that account for the nonexistent gauge but never send tokens to it resulting in reward tokens being permanently stuck in FlywheelGaugeRewards.sol

Proof of Concept

For a mapping all keys exist in solidity so using a key that has not been added will instead returns an empty weight. This means that nothing would throw an error allowing the user to add weight to a nonexistent gauge

Tools Used

Recommended Mitigation Steps

Use isGauge() instead of _deprecatedGauges.contains(gauge)

xTRIBE reward syncing is not permissionless allowing the owner to grieve users

Lines of code

https://github.com/fei-protocol/xTRIBE/blob/master/src/xTRIBE.sol#L108
https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L76-L97
https://github.com/fei-protocol/xTRIBE/blob/master/src/xTRIBE.sol#L25-L30

Vulnerability details

Impact

According to the comments in the xTRIBE contract, it is supposed to have:

  • a manipulation resistant reward distribution
  • an owner that only controls the maximum number and approved overrides of gauges and delegates, as well as the live gauge list.

But, it also controls the syncRewards() function which unlocks the queued rewards and starts the next cycle of rewards. Thus, the owner is able to grieve users by not synching rewards at the correct time (rewardsCycleEnd) for each cycle. If the function isn't called at all, the user won't receive their rewards. If the function is called at a later time than rewardsCycleEnd, they receive their rewards at a later time than they originally should.

The xERC4646 contract, which xTRIBE inherits, allows the function to be called by anyone as well.

Proof of Concept

xTRIBE's function only callable by the owner:

    function syncRewards() public override requiresAuth {
        super.syncRewards();
    }

xERC4626's function callable by anyone:

    /// @notice Distributes rewards to xERC4626 holders.
    /// All surplus `asset` balance of the contract over the internal balance becomes queued for the next cycle.
    function syncRewards() public virtual {
        uint192 lastRewardAmount_ = lastRewardAmount;
        uint32 timestamp = block.timestamp.safeCastTo32();

        if (timestamp < rewardsCycleEnd) revert SyncError();

        uint256 storedTotalAssets_ = storedTotalAssets;
        // @audit this will underflow unless there's enough balance.
        // So you can't sync unless you have enough.
        // Could not syncing have any effect on the rest of the protocol?
        uint256 nextRewards = asset.balanceOf(address(this)) - storedTotalAssets_ - lastRewardAmount_;

        storedTotalAssets = storedTotalAssets_ + lastRewardAmount_; // SSTORE

        uint32 end = ((timestamp + rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength;

        // Combined single SSTORE
        lastRewardAmount = nextRewards.safeCastTo192();
        lastSync = timestamp;
        rewardsCycleEnd = end;

        emit NewRewardsCycle(end, nextRewards);
    }

Tools Used

none

Recommended Mitigation Steps

Remove the requiresAuth modifier from xTRIBE.syncRewards()

QA Report

QA (LOW / NON-CRITICAL FINDINGS)

  • Floating Pragma used in xERC4626.sol, ERC20Gauges.sol, ERC20MutilVotes.sol, xTRIBE.sol . Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma (i.e. by not using ^) helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively.
    Reference

  • It is better to use one Solidity compiler version across all contracts instead of different versions with different bugs and security checks. Different versions of Solidity compilers are used (0.8.0. and 0.8.10)

  • Since safeTransfer method uses transfer with 2300 gas stipend which is not adjustable,it may likely to get broken in future in case of hardforks causing gas price changes or when calling a contract's fallback function.
    Reference Link -1, Reference Link -2

  • At ERC20MultiVotes.sol, Using abi.encodePacked() with multiple variable length arguments can, in certain situations, lead to a hash collision. Instead abi.encode() can be used. Reference Link

  • There is no address(0) or Zero Value check for the initiliazing state variables at the constructors for FlywheelCore.sol, FlywheelGaugeRewards.sol, ERC20Gauges.sol (no zero value check),xTRIBE.sol (no zero value and adress(0) check), xERC4626.sol (no zero value check for the initiliazing state vars),

  • Solidity integer division might truncate. As a result, performing multiplication before division can sometimes avoid loss of precision. Reference Below are examples:

xERC4626.sol#L40
https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L37-L41

xERC4626.sol#L89
https://github.com/fei-protocol/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L78-L97

ERC20Gauges.sol#L92
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L89-L94

  • At xERC4626.sol, lastSync variable was not initialized and remained at zero value untill syncRewards() method is called which will be after the rewardsCycleEnd.

  • block.timestamp is used on many places at the scoped contracts. Hoewever, this can be manipulated by malicious miners which may lead to early withdrawal of the rewards by calling syncRewards(), miscalculation of rewards by rewardstate manipulation, manipulation in gaugeCycle, and providing misinformation to the users on the interface.

  • At xERC4626.sol#L60, For the consistency, a require statement can be added as require(rewardsCycleEnd_ != lastSync_, "Err msg here").

  • At FlywheelCore.sol, accrue() method, there is no adress(0) check for the user and secondUser

  • At FlywheelCore.sol, accrue() method, one can truncate the user's rewards by calling same user adresses continously. A require statement can be added as require(user != secondUser, "err").

  • At ERC20MultiVotes.sol#L71 the logic should be;

if (blockNumber > block.number) revert BlockError(); 

instead of;

if (blockNumber >= block.number) revert BlockError(); 
  • The team can consider making the contracts Pausable so in case of an unexpected event, the owner/governance can pause transfers.
    Library can be utilized for this.

  • All require statements in FlywheelGaugeRewards.sol don't throw error. In case of any error pops up, it will not be possible to know the error source.

  • The contracts are lacking of nonReentrant modifiers. It's a good practice to include nonReentrant modifier to token distribution methods.

Gas Optimizations

Function Ordering via Method ID

Context: All Contracts

Description:
Contracts most called functions could simply save gas by function ordering via Method ID. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions. One could use This tool to help find alternative function names with lower Method IDs while keeping the original name intact.

Recommendation:
Find a lower method ID name for the most called functions for example mostCalled() vs. mostCalled_41q() is cheaper by 44 gas.

Use ++index instead of index++ to increment a loop counter

Context: FlywheelGaugeRewards.sol#L179-L210

Description:
Due to reduced stack operations, using ++index saves 5 gas per iteration.

Recommendation:
Use ++index to increment a loop counter.

Setting The Constructor To Payable

Context: xTRIBE.sol#L33-L44, FlywheelCore.sol#L43-L53, ERC20Gauges.sol#L36-L40, FlywheelGaugeRewards.sol#L80-L95, xERC4626.sol#L37-L41

Description:
You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. Making the constructor payable eliminates the need for an initial check of msg.value == 0 and saves 21 gas on deployment with no security risks.

Recommendation:
Set the constructor to payable.

For array elements, arr[i] = arr[i] + 1 is cheaper than arr[i] += 1

Context: ERC20Gauges.sol#L251-L271 (For L266), ERC20Gauges.sol#L547-L583 (For L581), ERC20MultiVotes.sol#L236-L256 (For both L251 and L252), ERC20MultiVotes.sol#L258-L274 (For L270), ERC20MultiVotes.sol#L332-L362 (For L261)

Description:
Due to stack operations this is 25 gas cheaper when dealing with arrays in storage, and 4 gas cheaper for memory arrays.

Recommendation:
Use arr[i] = arr[i] + 1 instead of arr[i] += 1 when dealing with arrays

Upgrade To At Least 0.8.4

Context: xTRIBE.sol, ERC20Gauges.sol, xERC4626.sol

Description:
Using newer compiler versions and the optimizer gives gas
optimizations and additional safety checks for free!

The advantages of versions =0.8.*= over =<0.8.0= are:

  • Safemath by default from =0.8.0= (can be more gas efficient than /some/
    library based safemath).
  • Low level inliner from =0.8.2=, leads to cheaper runtime gas.
    Especially relevant when the contract has small functions. For
    example, OpenZeppelin libraries typically have a lot of small
    helper functions and if they are not inlined, they cost an
    additional 20 to 40 gas because of 2 extra =jump= instructions and
    additional stack operations needed for function calls.
  • Optimizer improvements in packed structs: Before =0.8.3=, storing
    packed structs, in some cases used an additional storage read
    operation. After EIP-2929, if the slot was already cold, this
    means unnecessary stack operations and extra deploy time costs.
    However, if the slot was already warm, this means additional cost
    of =100= gas alongside the same unnecessary stack operations and
    extra deploy time costs.
  • Custom errors from =0.8.4=, leads to cheaper deploy time cost and
    run time cost. Note: the run time cost is only relevant when the
    revert condition is met. In short, replace revert strings by
    custom errors.

Recommendation:
Upgrade to at least 0.8.4 for the additional benefits.

QA Report

low1: zero-address checks

lack of zero-address checks below:

FlyWheelGaugeRewards.sol
L#92
L#94
FlyWheelCore.sol
l#50-52

`FlywheelCore.setBooster()` can be used to steal unclaimed rewards

Lines of code

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L258-L266

Vulnerability details

Impact

A malicious authorized user can steal all unclaimed rewards and break the reward accounting

Even if the authorized user is benevolent the fact that there is a rug vector available may negatively impact the protocol's reputation. Furthermore since this contract is meant to be used by other projects, the trustworthiness of every project cannot be vouched for.

Proof of Concept

By setting a booster that returns zero for all calls to boostedBalanceOf() where the user address is not under the attacker's control, and returning arbitrary values for those under his/her control, an attacker can choose specific amounts of rewardToken to assign to himself/herself. The attacker can then call claimRewards() to withdraw the funds. Any amounts that the attacker assigns to himself/herself over the amount that normally would have been assigned, upon claiming, is taken from other users' unclaimed balances, since tokens are custodied by the flywheelRewards address rather than per-user accounts.

File: flywheel-v2/src/FlywheelCore.sol

182       /// @notice swap out the flywheel booster contract
183       function setBooster(IFlywheelBooster newBooster) external requiresAuth {
184           flywheelBooster = newBooster;
185   
186           emit FlywheelBoosterUpdate(address(newBooster));
187       }

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L182-L187

File: flywheel-v2/src/FlywheelCore.sol

258           uint256 supplierTokens = address(flywheelBooster) != address(0)
259               ? flywheelBooster.boostedBalanceOf(strategy, user)
260               : strategy.balanceOf(user);
261   
262           // accumulate rewards by multiplying user tokens by rewardsPerToken index and adding on unclaimed
263           uint256 supplierDelta = (supplierTokens * deltaIndex) / ONE;
264           uint256 supplierAccrued = rewardsAccrued[user] + supplierDelta;
265   
266           rewardsAccrued[user] = supplierAccrued;

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L258-L266

File: flywheel-v2/src/FlywheelCore.sol

119       function claimRewards(address user) external {
120           uint256 accrued = rewardsAccrued[user];
121   
122           if (accrued != 0) {
123               rewardsAccrued[user] = 0;
124   
125               rewardToken.safeTransferFrom(address(flywheelRewards), user, accrued);

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/FlywheelCore.sol#L119-L125

Projects also using BaseFlywheelRewards or its child contrats, are implicitly approving infinite transfers by the core

File: flywheel-v2/src/rewards/BaseFlywheelRewards.sol

25        constructor(FlywheelCore _flywheel) {
26            flywheel = _flywheel;
27            ERC20 _rewardToken = _flywheel.rewardToken();
28            rewardToken = _rewardToken;
29    
30            _rewardToken.safeApprove(address(_flywheel), type(uint256).max);
31        }

https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/rewards/BaseFlywheelRewards.sol#L25-L31

The attacker need not keep the booster set this way - he/she can set it, call accrue() for his/her specific user, and unset it, all in the same block

Tools Used

Code inspection

Recommended Mitigation Steps

Make flywheelRewards immutable, or only allow it to change if there are no current users

Gas Optimizations

Title: Unnecessary array boundaries check when loading an array element twice
Severity: GAS

There are places in the code (especially in for-each loops) that loads the same array element more than once. 
In such cases, only one array boundaries check should take place, and the rest are unnecessary.
Therefore, this array element should be cached in a local variable and then be loaded
again using this local variable, skipping the redundant second array boundaries check: 

    xTRIBE.sol.emitVotingBalances - double load of accounts[i]

Title: Prefix increments are cheaper than postfix increments
Severity: GAS

Prefix increments are cheaper than postfix increments.
Further more, using unchecked {++x} is even more gas efficient, and the gas saving accumulates every iteration and can make a real change
There is no risk of overflow caused by increamenting the iteration index in for loops (the ++i in for (uint256 i = 0; i < numIterations; ++i)).
But increments perform overflow checks that are not necessary in this case.
These functions use not using prefix increments (++x) or not using the unchecked keyword:

    change to prefix increment and unchecked: FlywheelGaugeRewards.sol, i, 189

Title: Use != 0 instead of > 0
Severity: GAS

Using != 0 is slightly cheaper than > 0. (see code-423n4/2021-12-maple-findings#75 for similar issue)

    FlywheelGaugeRewards.sol, 153: change 'balance > 0' to 'balance != 0'
    FlywheelGaugeRewards.sol, 114: change 'balance > 0' to 'balance != 0'

Title: Unnecessary index init
Severity: GAS

In for loops you initialize the index to start from 0, but it already initialized to 0 in default and this assignment cost gas.
It is more clear and gas efficient to declare without assigning 0 and will have the same meaning:

    xTRIBE.sol, 95
    FlywheelGaugeRewards.sol, 189

Title: Public functions to external
Severity: GAS

The following functions could be set external to save gas and improve code quality.
External call cost is less expensive than of public functions.

    xTRIBE.sol, tribe
    xTRIBE.sol, transferFrom
    xTRIBE.sol, syncRewards
    xTRIBE.sol, transfer
    xTRIBE.sol, getPastVotes

Title: Internal functions to private
Severity: GAS

The following functions could be set private to save gas and improve code quality:

    FlywheelGaugeRewards.sol, _queueRewards
    FlywheelCore.sol, _addStrategyForRewards
    xTRIBE.sol, _burn

Title: Use unchecked to save gas for certain additive calculations that cannot overflow
Severity: GAS

You can use unchecked in the following calculations since there is no risk to overflow:

    FlywheelCore.sol (L#230) - index: state.index + deltaIndex, lastUpdatedTimestamp: block.timestamp.safeCastTo32()

Title: Inline one time use functions
Severity: GAS

The following functions are used exactly once. Therefore you can inline them and save gas and improve code clearness.

    xTRIBE.sol, _burn
    FlywheelCore.sol, _addStrategyForRewards

Title: Unnecessary constructor
Severity: GAS

The following constructors are empty.
(A similar issue code-423n4/2021-11-fei-findings#12)

    xTRIBE.sol.constructor

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.