GithubHelp home page GithubHelp logo

2022-10-juicebox-findings's Introduction

Juicebox Contest

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

Contributors to this repo: prior to report publication, please review the Agreements & Disclosures issue.


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.

High and Medium Risk Issues

Handle duplicates

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

  1. For all issues labeled 3 (High Risk) or 2 (Medium Risk), 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 (using the format Duplicate of #issueNumber), so that duplicate issues get linked.

Note: QA and Gas reports do not need to be de-duped. Please see the "QA and Gas reports" section below for more details.

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 a 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 for anything you would like additional C4 input on.

Please don't change the severity labels; that's up to the judge's discretion.

Respond to issues

Label each High or Medium risk 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.

QA and Gas Reports

For low and non-critical findings (AKA QA), as well as gas optimizations: all warden submissions in these three categories should now be submitted as bulk listings of issues and recommendations:

  • QA reports should include all low severity and non-critical findings, along with a summary statement.
  • Gas reports should include all gas optimization recommendations, along with a summary statement.

For QA and Gas reports, we ask that you:

  • Leave a comment for the judge on any reports you consider to be particularly high quality. (These reports will be awarded on a curve.)
  • Add the sponsor disputed label to any reports that you think should be completely disregarded by the judge, i.e. the report contains no valid findings at all.

Once de-duping and labelling is complete

When you have marked all duplicates and labelled all findings, drop the C4 team a note in your private Discord backroom channel and let us know you've completed the sponsor review process. At this point, we will pass the repo over to the judge, and they'll get to work while you work on mitigation.

Share your mitigation of findings

Note: this section does not need to be completed in order to move to the judging phase. You can continue work on mitigations while judging is occurring and even beyond that. Ultimately we won't publish the final audit report until you give us the ok.

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 request that you take the following steps:

  1. Within your own GitHub repo, create a pull request for each finding.
  2. Link the PR to the issue that it resolves within your contest findings repo.

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-10-juicebox-findings's People

Contributors

code423n4 avatar c4-judge avatar c4-staff avatar itsmetechjay avatar carlos-stackfive avatar

Stargazers

ペレス・クリス  avatar vincent avatar

Watchers

Ashok avatar  avatar

2022-10-juicebox-findings's Issues

QA Report

Impact

Issue Information: L001

Findings:

contracts/forge-test/E2E.t.sol::153 => IERC721(NFTRewardDataSource).transferFrom(_beneficiary, address(696969420), tokenId);
contracts/forge-test/E2E.t.sol::159 => IERC721(NFTRewardDataSource).transferFrom(address(696969420), address(123456789), tokenId);
contracts/forge-test/E2E.t.sol::233 => IERC721(NFTRewardDataSource).transferFrom(_beneficiary, address(696969420), tokenId);
contracts/forge-test/NFTReward_Unit.t.sol::3971 => IERC721(delegate).transferFrom(msg.sender, beneficiary, _tokenId);
contracts/forge-test/NFTReward_Unit.t.sol::4088 => IERC721(_delegate).transferFrom(msg.sender, beneficiary, _tokenId);
contracts/forge-test/governance/JB721GlobalGovernance.t.sol::70 => ERC721(address(_delegate)).transferFrom(_user, _userFren, _generateTokenId(_tier + 1, 1));
contracts/forge-test/governance/JB721TieredGovernance.t.sol::73 => ERC721(address(_delegate)).transferFrom(_user, _userFren, _generateTokenId(_tier + 1, 1));

Tools used

c4udit

Unspecific Compiler Version Pragma

Impact

Issue Information: L003

Findings:

contracts/JB721GlobalGovernance.sol::2 => pragma solidity ^0.8.16;
contracts/JB721TieredGovernance.sol::2 => pragma solidity ^0.8.16;
contracts/JBTiered721Delegate.sol::2 => pragma solidity ^0.8.16;
contracts/JBTiered721DelegateDeployer.sol::2 => pragma solidity ^0.8.16;
contracts/JBTiered721DelegateProjectDeployer.sol::2 => pragma solidity ^0.8.16;
contracts/JBTiered721DelegateStore.sol::2 => pragma solidity ^0.8.16;
contracts/abstract/ERC721.sol::4 => pragma solidity ^0.8.16;
contracts/abstract/JB721Delegate.sol::2 => pragma solidity ^0.8.16;
contracts/abstract/Votes.sol::3 => pragma solidity ^0.8.0;
contracts/enums/JB721GovernanceType.sol::2 => pragma solidity ^0.8.0;
contracts/forge-test/Deployer_Unit.t.sol::1 => pragma solidity ^0.8.16;
contracts/forge-test/E2E.t.sol::1 => pragma solidity ^0.8.16;
contracts/forge-test/NFTReward_Unit.t.sol::1 => pragma solidity ^0.8.16;
contracts/forge-test/governance/JB721GlobalGovernance.t.sol::1 => pragma solidity ^0.8.16;
contracts/forge-test/governance/JB721TieredGovernance.t.sol::1 => pragma solidity ^0.8.16;
contracts/forge-test/utils/AccessJBLib.sol::2 => pragma solidity ^0.8.16;
contracts/forge-test/utils/TestBaseWorkflow.sol::2 => pragma solidity ^0.8.16;
contracts/interfaces/IJB721Delegate.sol::2 => pragma solidity ^0.8.0;
contracts/interfaces/IJB721TieredGovernance.sol::2 => pragma solidity ^0.8.0;
contracts/interfaces/IJBTiered721Delegate.sol::2 => pragma solidity ^0.8.0;
contracts/interfaces/IJBTiered721DelegateDeployer.sol::2 => pragma solidity ^0.8.0;
contracts/interfaces/IJBTiered721DelegateProjectDeployer.sol::2 => pragma solidity ^0.8.0;
contracts/interfaces/IJBTiered721DelegateStore.sol::2 => pragma solidity ^0.8.0;
contracts/libraries/JBBitmap.sol::2 => pragma solidity ^0.8.16;
contracts/libraries/JBIpfsDecoder.sol::2 => pragma solidity ^0.8.16;
contracts/libraries/JBTiered721FundingCycleMetadataResolver.sol::2 => pragma solidity ^0.8.16;
contracts/scripts/Deploy.s.sol::1 => pragma solidity ^0.8.16;
contracts/scripts/LaunchProjectFor.s.sol::1 => pragma solidity ^0.8.16;
contracts/structs/JB721PricingParams.sol::2 => pragma solidity ^0.8.0;
contracts/structs/JB721Tier.sol::2 => pragma solidity ^0.8.16;
contracts/structs/JB721TierParams.sol::2 => pragma solidity ^0.8.0;
contracts/structs/JBBitmapWord.sol::2 => pragma solidity ^0.8.0;
contracts/structs/JBDeployTiered721DelegateData.sol::2 => pragma solidity ^0.8.0;
contracts/structs/JBLaunchFundingCyclesData.sol::2 => pragma solidity ^0.8.0;
contracts/structs/JBLaunchProjectData.sol::2 => pragma solidity ^0.8.0;
contracts/structs/JBReconfigureFundingCyclesData.sol::2 => pragma solidity ^0.8.0;
contracts/structs/JBStored721Tier.sol::2 => pragma solidity ^0.8.0;
contracts/structs/JBTiered721Flags.sol::2 => pragma solidity ^0.8.0;
contracts/structs/JBTiered721FundingCycleMetadata.sol::2 => pragma solidity ^0.8.0;
contracts/structs/JBTiered721MintForTiersData.sol::2 => pragma solidity ^0.8.0;
contracts/structs/JBTiered721MintReservesForTiersData.sol::2 => pragma solidity ^0.8.0;
contracts/structs/JBTiered721SetTierDelegatesData.sol::2 => pragma solidity ^0.8.0;

Tools used

c4udit

Gas Optimizations

i++ is less efficient than ++i

The gas cost can be reduced by using ++i or i += 1 instead of i++. This saves about 5 gas.

JBIpfsDecoder.sol, Line 59:

	digitlength++;

JBIpfsDecoder.sol, Line 68:

	for (uint256 i = 0; i < _length; i++) {

JBIpfsDecoder.sol, Line 76:

	for (uint256 i = 0; i < _input.length; i++) {

JBIpfsDecoder.sol, Line 84:

	for (uint256 i = 0; i < _indices.length; i++) {

JBTiered721DelegateStore.sol, Line 245:

	_tiers[_numberOfIncludedTiers++] = JB721Tier({

JBTiered721DelegateStore.sol, Line 1108:

	_storedTierOf[msg.sender][_tierId].remainingQuantity++;

Gas Optimizations

GAS ISSUES FOR JUICE-NFT-REWARDS

[G-01] Use ++i instead of i++

./contracts/JBTiered721DelegateStore.sol

L1108: _storedTierOf[msg.sender][_tierId].remainingQuantity++;

./contracts/libraries/JBIpfsDecoder.sol

L68:   for (uint256 i = 0; i < _length; i++) {

L76:   for (uint256 i = 0; i < _input.length; i++) {

L84:   for (uint256 i = 0; i < _indices.length; i++) {

[G-02] uncheck the i++/i-- in for loops since there's no way to overflow/underflow

./contracts/JBTiered721Delegate.sol

L341:  ++_i;

L355:  ++_i;

./contracts/libraries/JBIpfsDecoder.sol

L51:   for (uint256 j = 0; j < digitlength; ++j) {

L68:   for (uint256 i = 0; i < _length; i++) {

L76:   for (uint256 i = 0; i < _input.length; i++) {

L84:   for (uint256 i = 0; i < _indices.length; i++) {

[G-03] != 0 comparison is cheaper than > 0

./contracts/libraries/JBIpfsDecoder.sol

L57:   while (carry > 0) {

./contracts/JBTiered721DelegateStore.sol

L1254: if (_numerator - JBConstants.MAX_RESERVED_RATE * _numberReservedTokensMintable > 0)

[G-04] A=A+B costs less gas than A+=B if A and B are located in storage

./contracts/JBTiered721DelegateStore.sol

L827:  numberOfReservesMintedFor[msg.sender][_tierId] += _count;

[G-05] Use default values of variables types

Description: uint256 - 0;, string - "";, address - address(0);, etc.

./contracts/libraries/JBIpfsDecoder.sol

L49:   for (uint256 i = 0; i < _source.length; ++i) {

L68:   for (uint256 i = 0; i < _length; i++) {

L76:   for (uint256 i = 0; i < _input.length; i++) {

L84:   for (uint256 i = 0; i < _indices.length; i++) {

[G-06] Inline internal functions to save gas

./contracts/libraries/JBIpfsDecoder.sol

L66:   function _truncate(uint8[] memory _array, uint8 _length) private pure returns (uint8[] memory) {

L74:   function _reverse(uint8[] memory _input) private pure returns (uint8[] memory) {

L82:   function _toAlphabet(uint8[] memory _indices) private pure returns (bytes memory) {

[G-07] If a function is not open to any user, it can be marked as payable to save gas

Description: Under the hood the compiler checks if a function is payable. The check is skipped if it is marked as such by the developer

./contracts/JBTiered721Delegate.sol

L370:  function setDefaultReservedTokenBeneficiary(address _beneficiary) external override onlyOwner {

L386:  function setBaseUri(string memory _baseUri) external override onlyOwner {

L418:  function setTokenUriResolver(IJBTokenUriResolver _tokenUriResolver) external override onlyOwner {

[G-08] A < B + 1 is cheaper than A <= B

./contracts/JB721TieredGovernance.sol

L133:  if (_blockNumber >= block.number) revert BLOCK_NOT_YET_MINED();

./contracts/JBTiered721DelegateStore.sol

L903:  if (_storedTierOf[msg.sender][_tierId].lockedUntil >= block.timestamp) revert TIER_LOCKED();

[G-09] Skip copying data from calldata to memory for function parameters

Description: Use calldata location for reference-type input args

./contracts/JBTiered721DelegateProjectDeployer.sol

L72:   JBDeployTiered721DelegateData memory _deployTiered721DelegateData,

L109:  JBDeployTiered721DelegateData memory _deployTiered721DelegateData,

L152:  JBDeployTiered721DelegateData memory _deployTiered721DelegateData,

L153:  JBReconfigureFundingCyclesData memory _reconfigureFundingCyclesData

L191:  function _launchProjectFor(address _owner, JBLaunchProjectData memory _launchProjectData)

L218:  JBLaunchFundingCyclesData memory _launchFundingCyclesData

L244:  JBReconfigureFundingCyclesData memory _reconfigureFundingCyclesData

./contracts/abstract/JB721Delegate.sol

L207:  string memory _symbol

L311:  function _didBurn(uint256[] memory _tokenIds) internal virtual {

L323:  function _redemptionWeightOf(uint256[] memory _tokenIds) internal view virtual returns (uint256) {

./contracts/JBTiered721Delegate.sol

L205:  string memory _name,

L208:  string memory _baseUri,

L210:  string memory _contractUri,

L211:  JB721PricingParams memory _pricing,

L213:  JBTiered721Flags memory _flags

L290:  function mintFor(JBTiered721MintForTiersData[] memory _mintForTiersData)

L598:  function _didBurn(uint256[] memory _tokenIds) internal override {

L652:  uint16[] memory _mintTierIds,

L695:  function _redemptionWeightOf(uint256[] memory _tokenIds)

L789:  JB721Tier memory _tier

./contracts/JBTiered721DelegateStore.sol

L628:  function recordAddTiers(JB721TierParams[] memory _tiersToAdd)

L1091: function recordBurn(uint256[] memory _tokenIds) external override {

L1227: JBStored721Tier memory _storedTier

./contracts/JB721TieredGovernance.sol

L147:  function setTierDelegates(JBTiered721SetTierDelegatesData[] memory _setTierDelegatesData)

L313:  JB721Tier memory _tier

./contracts/JB721GlobalGovernance.sol

L55:   JB721Tier memory _tier

./contracts/JBTiered721DelegateDeployer.sol

L71:   JBDeployTiered721DelegateData memory _deployTiered721DelegateData

./contracts/libraries/JBBitmap.sol

L29:   function isTierIdRemoved(JBBitmapWord memory self, uint256 _index) internal pure returns (bool) {

L59:   function refreshBitmapNeeded(JBBitmapWord memory self, uint256 _index)

./contracts/libraries/JBIpfsDecoder.sol

L22:   function decode(string memory _baseUri, bytes32 _hexString)

       /// @audit Store `_source` in calldata.
L44:   function _toBase58(bytes memory _source) private pure returns (string memory) {

       /// @audit Store `_array` in calldata.
L66:   function _truncate(uint8[] memory _array, uint8 _length) private pure returns (uint8[] memory) {

       /// @audit Store `_input` in calldata.
L74:   function _reverse(uint8[] memory _input) private pure returns (uint8[] memory) {

Governance voting outcome can be manipulated

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/main/contracts/JB721TieredGovernance.sol#L177

Vulnerability details

Impact

The _totalTierCheckpoints does not get updated if user has delegated its votes to address(0). This becomes a problem for Governance since for a tier, the total votes would become more than existing vote. In case if a significant portion of vote has been delegated to address 0, then the voting system will get impacted

Proof of Concept

  1. User A delegates his voting units to address(0) using setTierDelegate function
function setTierDelegate(address _delegatee, uint256 _tierId) public virtual override {
    _delegateTier(msg.sender, _delegatee, _tierId);
  }
  1. This internally calls _delegateTier function which then calls _moveTierDelegateVotes function with voting units of User A
function _delegateTier(
    address _account,
    address _delegatee,
    uint256 _tierId
  ) internal virtual {
    // Get the current delegatee
    address _oldDelegate = _tierDelegation[_account][_tierId];

    // Store the new delegatee
    _tierDelegation[_account][_tierId] = _delegatee;

    emit DelegateChanged(_account, _oldDelegate, _delegatee);

    // Move the votes.
    _moveTierDelegateVotes(
      _oldDelegate,
      _delegatee,
      _tierId,
      _getTierVotingUnits(_account, _tierId)
    );
  }
  1. Observe there is no check to verify that new _delegatee is not address 0

  2. Now _moveTierDelegateVotes function is called which updates the _delegateTierCheckpoints for this user on the mentioned tier

function _moveTierDelegateVotes(
    address _from,
    address _to,
    uint256 _tierId,
    uint256 _amount
  ) internal {
    // Nothing to do if moving to the same account, or no amount is being moved.
    if (_from == _to || _amount == 0) return;

    // If not moving from the zero address, update the checkpoints to subtract the amount.
    if (_from != address(0)) {
      (uint256 _oldValue, uint256 _newValue) = _delegateTierCheckpoints[_from][_tierId].push(
        _subtract,
        _amount
      );
      emit TierDelegateVotesChanged(_from, _oldValue, _newValue, _tierId, msg.sender);
    }

    // If not moving to the zero address, update the checkpoints to add the amount.
    if (_to != address(0)) {
      (uint256 _oldValue, uint256 _newValue) = _delegateTierCheckpoints[_to][_tierId].push(
        _add,
        _amount
      );
      emit TierDelegateVotesChanged(_to, _tierId, _oldValue, _newValue, msg.sender);
    }
  }
  1. Since _to is address 0 (user is delegating to 0 address) so _delegateTierCheckpoints[_to][_tierId] will not be updated and only _delegateTierCheckpoints[_from][_tierId] will be updated which is correct

  2. The problem here is that now this tier has in a way burned all the voting units for User A which means this tier has now lesser number of voting units. This should have been updated in _totalTierCheckpoints (which tracks total voting units in tier) but it is not done within this flow

  3. This means after this transaction, total voting units in this checkpoint will be including the non existing votes (from user A delegation to 0 address)

  4. So lets say in tier id1

total votes : 1000
user A voting units: 600

User A delegates to address 0 

total votes : 1000
user A voting units: 0 (since _delegateTierCheckpoints[_from][_tierId].push( _subtract, _amount );)

Governance voting starts
Only 1000-600=400 votes can be placed
If governance required atleast 50% consensus from total votes or required 50% participation of overall votes then this would never reach

Recommended Mitigation Steps

Do not allow User to delegate to 0 address if they have some voting units. If required then kindly update _totalTierCheckpoints before delegating to 0 address

Gas Optimizations

c4udit Report

Files analyzed

  • contracts/JB721GlobalGovernance.sol
  • contracts/JB721TieredGovernance.sol
  • contracts/JBTiered721Delegate.sol
  • contracts/JBTiered721DelegateDeployer.sol
  • contracts/JBTiered721DelegateProjectDeployer.sol
  • contracts/JBTiered721DelegateStore.sol
  • contracts/abstract/ERC721.sol
  • contracts/abstract/JB721Delegate.sol
  • contracts/abstract/Votes.sol
  • contracts/enums/JB721GovernanceType.sol
  • contracts/forge-test/Deployer_Unit.t.sol
  • contracts/forge-test/E2E.t.sol
  • contracts/forge-test/NFTReward_Unit.t.sol
  • contracts/forge-test/governance/JB721GlobalGovernance.t.sol
  • contracts/forge-test/governance/JB721TieredGovernance.t.sol
  • contracts/forge-test/utils/AccessJBLib.sol
  • contracts/forge-test/utils/TestBaseWorkflow.sol
  • contracts/interfaces/IJB721Delegate.sol
  • contracts/interfaces/IJB721TieredGovernance.sol
  • contracts/interfaces/IJBTiered721Delegate.sol
  • contracts/interfaces/IJBTiered721DelegateDeployer.sol
  • contracts/interfaces/IJBTiered721DelegateProjectDeployer.sol
  • contracts/interfaces/IJBTiered721DelegateStore.sol
  • contracts/libraries/JBBitmap.sol
  • contracts/libraries/JBIpfsDecoder.sol
  • contracts/libraries/JBTiered721FundingCycleMetadataResolver.sol
  • contracts/scripts/Deploy.s.sol
  • contracts/scripts/LaunchProjectFor.s.sol
  • contracts/structs/JB721PricingParams.sol
  • contracts/structs/JB721Tier.sol
  • contracts/structs/JB721TierParams.sol
  • contracts/structs/JBBitmapWord.sol
  • contracts/structs/JBDeployTiered721DelegateData.sol
  • contracts/structs/JBLaunchFundingCyclesData.sol
  • contracts/structs/JBLaunchProjectData.sol
  • contracts/structs/JBReconfigureFundingCyclesData.sol
  • contracts/structs/JBStored721Tier.sol
  • contracts/structs/JBTiered721Flags.sol
  • contracts/structs/JBTiered721FundingCycleMetadata.sol
  • contracts/structs/JBTiered721MintForTiersData.sol
  • contracts/structs/JBTiered721MintReservesForTiersData.sol
  • contracts/structs/JBTiered721SetTierDelegatesData.sol

Issues found

Don't Initialize Variables with Default Value

Impact

Issue Information: G001

Findings:

contracts/forge-test/E2E.t.sol::184 => for (uint256 i = 0; i < 5; i++) {
contracts/libraries/JBIpfsDecoder.sol::49 => for (uint256 i = 0; i < _source.length; ++i) {
contracts/libraries/JBIpfsDecoder.sol::51 => for (uint256 j = 0; j < digitlength; ++j) {
contracts/libraries/JBIpfsDecoder.sol::68 => for (uint256 i = 0; i < _length; i++) {
contracts/libraries/JBIpfsDecoder.sol::76 => for (uint256 i = 0; i < _input.length; i++) {
contracts/libraries/JBIpfsDecoder.sol::84 => for (uint256 i = 0; i < _indices.length; i++) {

Tools used

c4udit

Cache Array Length Outside of Loop

Impact

Issue Information: G002

Findings:

contracts/JB721TieredGovernance.sol::153 => uint256 _numberOfTierDelegates = _setTierDelegatesData.length;
contracts/JBTiered721Delegate.sol::230 => if (bytes(_baseUri).length != 0) _store.recordSetBaseUri(_baseUri);
contracts/JBTiered721Delegate.sol::233 => if (bytes(_contractUri).length != 0) _store.recordSetContractUri(_contractUri);
contracts/JBTiered721Delegate.sol::240 => if (_pricing.tiers.length > 0) _store.recordAddTiers(_pricing.tiers);
contracts/JBTiered721Delegate.sol::269 => uint256 _numberOfTiers = _mintReservesForTiersData.length;
contracts/JBTiered721Delegate.sol::296 => uint256 _numberOfBeneficiaries = _mintForTiersData.length;
contracts/JBTiered721Delegate.sol::327 => uint256 _numberOfTiersToAdd = _tiersToAdd.length;
contracts/JBTiered721Delegate.sol::330 => uint256 _numberOfTiersToRemove = _tierIdsToRemove.length;
contracts/JBTiered721Delegate.sol::494 => uint256 _numberOfTokens = _tierIds.length;
contracts/JBTiered721Delegate.sol::551 => _data.metadata.length > 36 &&
contracts/JBTiered721Delegate.sol::570 => if (_tierIdsToMint.length != 0)
contracts/JBTiered721Delegate.sol::666 => uint256 _mintsLength = _tokenIds.length;
contracts/JBTiered721DelegateDeployer.sol::123 => // Shift the length to the length placeholder, in the constructor
contracts/JBTiered721DelegateDeployer.sol::126 => // Insert the length in the correct sport (after the PUSH3 / 0x62)
contracts/JBTiered721DelegateStore.sol::221 => // Initialize an array with the appropriate length.
contracts/JBTiered721DelegateStore.sol::530 => uint256 _numberOfTokenIds = _tokenIds.length;
contracts/JBTiered721DelegateStore.sol::634 => uint256 _numberOfNewTiers = _tiersToAdd.length;
contracts/JBTiered721DelegateStore.sol::642 => // Initialize an array with the appropriate length.
contracts/JBTiered721DelegateStore.sol::829 => // Initialize an array with the appropriate length.
contracts/JBTiered721DelegateStore.sol::893 => uint256 _numTiers = _tierIds.length;
contracts/JBTiered721DelegateStore.sol::1021 => uint256 _numberOfTiers = _tierIds.length;
contracts/JBTiered721DelegateStore.sol::1029 => // Initialize an array with the appropriate length.
contracts/JBTiered721DelegateStore.sol::1093 => uint256 _numberOfTokenIds = _tokenIds.length;
contracts/abstract/ERC721.sol::112 => return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : '';
contracts/abstract/ERC721.sol::175 => //solhint-disable-next-line max-line-length
contracts/abstract/ERC721.sol::397 => if (reason.length == 0) {
contracts/abstract/JB721Delegate.sol::120 => _data.metadata.length < 4 || bytes4(_data.metadata[0:4]) != type(IJB721Delegate).interfaceId
contracts/abstract/JB721Delegate.sol::259 => _data.metadata.length < 4 || bytes4(_data.metadata[0:4]) != type(IJB721Delegate).interfaceId
contracts/abstract/JB721Delegate.sol::266 => uint256 _numberOfTokenIds = _decodedTokenIds.length;
contracts/forge-test/E2E.t.sol::323 => uint256[] memory _tiersToRemove = new uint256[](_originalTiers.length);
contracts/forge-test/E2E.t.sol::326 => for (uint256 _i; _i < _originalTiers.length; _i++) {
contracts/forge-test/E2E.t.sol::603 => for (uint256 i; i < rawMetadata.length; i++) rawMetadata[i] = uint16(tier);
contracts/forge-test/E2E.t.sol::615 => _jbETHPaymentTerminal.pay{value: floor * rawMetadata.length}(
contracts/forge-test/E2E.t.sol::637 => assertEq(rawMetadata.length, tokenBalance);
contracts/forge-test/E2E.t.sol::642 => for (uint256 i; i < rawMetadata.length; i++) {
contracts/forge-test/E2E.t.sol::684 => _jbETHPaymentTerminal.pay{value: floor * rawMetadata.length}(
contracts/forge-test/E2E.t.sol::704 => assertEq(rawMetadata.length, tokenBalance);
contracts/forge-test/NFTReward_Unit.t.sol::345 => _delegate.test_store().tiers(address(_delegate), 0, numberOfTiers).length,
contracts/forge-test/NFTReward_Unit.t.sol::803 => for (uint256 i; i < _tiers.length; i++) {
contracts/forge-test/NFTReward_Unit.t.sol::839 => for (uint256 i = 1; i <= _tiers.length; i++) {
contracts/forge-test/NFTReward_Unit.t.sol::1716 => _tiersToMint[_tiersToMint.length - 1 - i] = uint16(i)+1;
contracts/forge-test/NFTReward_Unit.t.sol::1785 => _tiersToMintOne[_tiersToMintOne.length - 1 - i] = uint16(i)+1;
contracts/forge-test/NFTReward_Unit.t.sol::1788 => _tiersToMintTwo[_tiersToMintTwo.length - 1 - i] = uint16(i)+1;
contracts/forge-test/NFTReward_Unit.t.sol::1864 => _tiersToMint[_tiersToMint.length - 1 - i] = uint16(i)+1;
contracts/forge-test/NFTReward_Unit.t.sol::1952 => vm.assume(floorTiersToAdd.length > 0 && floorTiersToAdd.length < 15);
contracts/forge-test/NFTReward_Unit.t.sol::2015 => JB721TierParams[] memory _tierParamsToAdd = new JB721TierParams[](floorTiersToAdd.length);
contracts/forge-test/NFTReward_Unit.t.sol::2016 => JB721Tier[] memory _tiersAdded = new JB721Tier[](floorTiersToAdd.length);
contracts/forge-test/NFTReward_Unit.t.sol::2018 => for (uint256 i; i < floorTiersToAdd.length; i++) {
contracts/forge-test/NFTReward_Unit.t.sol::2031 => id: _tiers.length + (i + 1),
contracts/forge-test/NFTReward_Unit.t.sol::2052 => initialNumberOfTiers + floorTiersToAdd.length
contracts/forge-test/NFTReward_Unit.t.sol::2056 => assertEq(_storedTiers.length, _tiers.length + _tiersAdded.length);
contracts/forge-test/NFTReward_Unit.t.sol::2063 => for (uint256 i = 1; i < _storedTiers.length; i++) {
contracts/forge-test/NFTReward_Unit.t.sol::2173 => for (uint256 i; i < _tiers.length; i++) {
contracts/forge-test/NFTReward_Unit.t.sol::2178 => for (uint256 i; i < tiersRemaining.length; ) {
contracts/forge-test/NFTReward_Unit.t.sol::2181 => for (uint256 j; j < tiersToRemove.length; j++)
contracts/forge-test/NFTReward_Unit.t.sol::2184 => tiersRemaining[i] = tiersRemaining[tiersRemaining.length - 1];
contracts/forge-test/NFTReward_Unit.t.sol::2185 => tierParamsRemaining[i] = tierParamsRemaining[tierParamsRemaining.length - 1];
contracts/forge-test/NFTReward_Unit.t.sol::2187 => // Remove the last elelment / reduce array length by 1
contracts/forge-test/NFTReward_Unit.t.sol::2201 => for (uint256 i; i < tiersToRemove.length; i++) {
contracts/forge-test/NFTReward_Unit.t.sol::2210 => uint256 finalNumberOfTiers = initialNumberOfTiers - tiersToRemove.length;
contracts/forge-test/NFTReward_Unit.t.sol::2219 => assertEq(_storedTiers.length, finalNumberOfTiers);
contracts/forge-test/NFTReward_Unit.t.sol::2373 => id: _tiers.length + (i + 1),
contracts/forge-test/NFTReward_Unit.t.sol::2398 => assertEq(_storedTiers.length, 7);
contracts/forge-test/NFTReward_Unit.t.sol::2408 => for (uint256 j = 1; j < _storedTiers.length; j++) {
contracts/forge-test/NFTReward_Unit.t.sol::2490 => id: _tiers.length + (i + 1),
contracts/forge-test/NFTReward_Unit.t.sol::2593 => id: _tiers.length + (i + 1),
contracts/forge-test/NFTReward_Unit.t.sol::2696 => id: _tiers.length + (i + 1),
contracts/forge-test/NFTReward_Unit.t.sol::2799 => _delegate.store().tiers(address(_delegate), 0, initialNumberOfTiers).length,
contracts/forge-test/NFTReward_Unit.t.sol::2904 => for (uint256 i; i < _tiers.length; i++) {
contracts/forge-test/NFTReward_Unit.t.sol::2909 => for (uint256 i; i < tiersRemaining.length; ) {
contracts/forge-test/NFTReward_Unit.t.sol::2912 => for (uint256 j; j < tiersToRemove.length; j++)
contracts/forge-test/NFTReward_Unit.t.sol::2915 => tiersRemaining[i] = tiersRemaining[tiersRemaining.length - 1];
contracts/forge-test/NFTReward_Unit.t.sol::2916 => tierParamsRemaining[i] = tierParamsRemaining[tierParamsRemaining.length - 1];
contracts/forge-test/NFTReward_Unit.t.sol::2918 => // Remove the last elelment / reduce array length by 1
contracts/forge-test/NFTReward_Unit.t.sol::3580 => vm.assume(_invalidTier > tiers.length);
contracts/forge-test/NFTReward_Unit.t.sol::4873 => assertEq(first.length, second.length);
contracts/forge-test/NFTReward_Unit.t.sol::4875 => for (uint256 i; i < first.length; i++) {
contracts/forge-test/NFTReward_Unit.t.sol::4904 => if (smol.length > bigg.length) {
contracts/forge-test/NFTReward_Unit.t.sol::4909 => if (smol.length == 0) return true;
contracts/forge-test/NFTReward_Unit.t.sol::4914 => for (uint256 smolIter; smolIter < smol.length; smolIter++) {
contracts/forge-test/NFTReward_Unit.t.sol::4916 => for (uint256 biggIter; biggIter < bigg.length; biggIter++) {
contracts/forge-test/NFTReward_Unit.t.sol::4919 => count += smolIter + 1; // 1-indexed, as the length
contracts/forge-test/NFTReward_Unit.t.sol::4925 => // Insure all the smoll indexes have been iterated on (ie we've seen (smoll.length)! elements)
contracts/forge-test/NFTReward_Unit.t.sol::4926 => if (count == (smol.length * (smol.length + 1)) / 2) return true;
contracts/forge-test/NFTReward_Unit.t.sol::4950 => for (uint256 i; i < _in.length; i++) {
contracts/forge-test/NFTReward_Unit.t.sol::4954 => for (uint256 j = i; j < _in.length; j++) {
contracts/forge-test/NFTReward_Unit.t.sol::4968 => for (uint256 i; i < _in.length; i++) {
contracts/forge-test/NFTReward_Unit.t.sol::4972 => for (uint256 j = i; j < _in.length; j++) {
contracts/forge-test/NFTReward_Unit.t.sol::4986 => for (uint256 i; i < _in.length; i++) {
contracts/forge-test/NFTReward_Unit.t.sol::4990 => for (uint256 j = i; j < _in.length; j++) {
contracts/forge-test/NFTReward_Unit.t.sol::5095 => // Initialize an array with the appropriate length.
contracts/forge-test/NFTReward_Unit.t.sol::5129 => for (uint256 i = _tiers.length - 1; i >= 0; i--) {
contracts/forge-test/governance/JB721GlobalGovernance.t.sol::33 => vm.assume(_tier < NFTRewardDeployerData.pricing.tiers.length);
contracts/forge-test/governance/JB721GlobalGovernance.t.sol::102 => vm.assume(_tier < NFTRewardDeployerData.pricing.tiers.length);
contracts/forge-test/governance/JB721TieredGovernance.t.sol::33 => vm.assume(_tier < NFTRewardDeployerData.pricing.tiers.length);
contracts/forge-test/governance/JB721TieredGovernance.t.sol::107 => vm.assume(_tier < NFTRewardDeployerData.pricing.tiers.length);
contracts/libraries/JBIpfsDecoder.sol::45 => if (_source.length == 0) return new string(0);
contracts/libraries/JBIpfsDecoder.sol::48 => uint8 digitlength = 1;
contracts/libraries/JBIpfsDecoder.sol::49 => for (uint256 i = 0; i < _source.length; ++i) {
contracts/libraries/JBIpfsDecoder.sol::51 => for (uint256 j = 0; j < digitlength; ++j) {
contracts/libraries/JBIpfsDecoder.sol::58 => digits[digitlength] = uint8(carry % 58);
contracts/libraries/JBIpfsDecoder.sol::59 => digitlength++;
contracts/libraries/JBIpfsDecoder.sol::63 => return string(_toAlphabet(_reverse(_truncate(digits, digitlength))));
contracts/libraries/JBIpfsDecoder.sol::66 => function _truncate(uint8[] memory _array, uint8 _length) private pure returns (uint8[] memory) {
contracts/libraries/JBIpfsDecoder.sol::67 => uint8[] memory output = new uint8[](_length);
contracts/libraries/JBIpfsDecoder.sol::68 => for (uint256 i = 0; i < _length; i++) {
contracts/libraries/JBIpfsDecoder.sol::75 => uint8[] memory output = new uint8[](_input.length);
contracts/libraries/JBIpfsDecoder.sol::76 => for (uint256 i = 0; i < _input.length; i++) {
contracts/libraries/JBIpfsDecoder.sol::77 => output[i] = _input[_input.length - 1 - i];
contracts/libraries/JBIpfsDecoder.sol::83 => bytes memory output = new bytes(_indices.length);
contracts/libraries/JBIpfsDecoder.sol::84 => for (uint256 i = 0; i < _indices.length; i++) {

Tools used

c4udit

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

Impact

Issue Information: G003

Findings:

contracts/JBTiered721Delegate.sol::240 => if (_pricing.tiers.length > 0) _store.recordAddTiers(_pricing.tiers);
contracts/JBTiered721DelegateStore.sol::1254 => if (_numerator - JBConstants.MAX_RESERVED_RATE * _numberReservedTokensMintable > 0)
contracts/abstract/ERC721.sol::112 => return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : '';
contracts/abstract/JB721Delegate.sol::116 => if (_data.tokenCount > 0) revert UNEXPECTED_TOKEN_REDEEMED();
contracts/abstract/Votes.sol::154 => if (from != to && amount > 0) {
contracts/forge-test/NFTReward_Unit.t.sol::209 => vm.assume(numberOfTiers > 0 && numberOfTiers < 30);
contracts/forge-test/NFTReward_Unit.t.sol::276 => vm.assume(numberOfTiers > 0 && numberOfTiers < 30);
contracts/forge-test/NFTReward_Unit.t.sol::368 => vm.assume(numberOfTiers > 0 && numberOfTiers < 30);
contracts/forge-test/NFTReward_Unit.t.sol::469 => vm.assume(numberOfTiers > 0 && numberOfTiers < 30);
contracts/forge-test/NFTReward_Unit.t.sol::539 => vm.assume(numberOfTiers > 0 && numberOfTiers < 30);
contracts/forge-test/NFTReward_Unit.t.sol::671 => vm.assume(numberOfTiers > 0 && numberOfTiers < 30);
contracts/forge-test/NFTReward_Unit.t.sol::729 => vm.assume(_tierId > 0 && _tokenNumber > 0);
contracts/forge-test/NFTReward_Unit.t.sol::862 => vm.assume(numberOfTiers > 0 && numberOfTiers < 30);
contracts/forge-test/NFTReward_Unit.t.sol::932 => vm.assume(numberOfTiers > 0 && numberOfTiers < 30);
contracts/forge-test/NFTReward_Unit.t.sol::1952 => vm.assume(floorTiersToAdd.length > 0 && floorTiersToAdd.length < 15);
contracts/forge-test/NFTReward_Unit.t.sol::2083 => vm.assume(initialNumberOfTiers > 0 && initialNumberOfTiers < 15);
contracts/forge-test/NFTReward_Unit.t.sol::2084 => vm.assume(numberOfTiersToRemove > 0 && numberOfTiersToRemove < initialNumberOfTiers);
contracts/forge-test/NFTReward_Unit.t.sol::2419 => vm.assume(numberTiersToAdd > 0);
contracts/forge-test/NFTReward_Unit.t.sol::2522 => vm.assume(numberTiersToAdd > 0);
contracts/forge-test/NFTReward_Unit.t.sol::2625 => vm.assume(numberTiersToAdd > 0);
contracts/forge-test/NFTReward_Unit.t.sol::2722 => vm.assume(initialNumberOfTiers > 0 && initialNumberOfTiers < 15);
contracts/forge-test/NFTReward_Unit.t.sol::2814 => vm.assume(initialNumberOfTiers > 0 && initialNumberOfTiers < 15);
contracts/forge-test/NFTReward_Unit.t.sol::2815 => vm.assume(numberOfTiersToRemove > 0 && numberOfTiersToRemove < initialNumberOfTiers);
contracts/forge-test/NFTReward_Unit.t.sol::3260 => vm.assume(_amount > 0 && _amount < tiers[0].contributionFloor);
contracts/forge-test/NFTReward_Unit.t.sol::4561 => vm.assume(_tokenCount > 0);
contracts/libraries/JBIpfsDecoder.sol::57 => while (carry > 0) {

Tools used

c4udit

Use immutable for OpenZeppelin AccessControl's Roles Declarations

Impact

Issue Information: G006

Findings:

contracts/forge-test/Deployer_Unit.t.sol::16 => address owner = address(bytes20(keccak256('owner')));
contracts/forge-test/Deployer_Unit.t.sol::17 => address reserveBeneficiary = address(bytes20(keccak256('reserveBeneficiary')));
contracts/forge-test/Deployer_Unit.t.sol::18 => address mockJBDirectory = address(bytes20(keccak256('mockJBDirectory')));
contracts/forge-test/Deployer_Unit.t.sol::19 => address mockTokenUriResolver = address(bytes20(keccak256('mockTokenUriResolver')));
contracts/forge-test/Deployer_Unit.t.sol::20 => address mockTerminalAddress = address(bytes20(keccak256('mockTerminalAddress')));
contracts/forge-test/Deployer_Unit.t.sol::21 => address mockJBController = address(bytes20(keccak256('mockJBController')));
contracts/forge-test/Deployer_Unit.t.sol::22 => address mockJBFundingCycleStore = address(bytes20(keccak256('mockJBFundingCycleStore')));
contracts/forge-test/Deployer_Unit.t.sol::23 => address mockJBOperatorStore = address(bytes20(keccak256('mockJBOperatorStore')));
contracts/forge-test/Deployer_Unit.t.sol::24 => address mockJBProjects = address(bytes20(keccak256('mockJBProjects')));
contracts/forge-test/E2E.t.sol::16 => address reserveBeneficiary = address(bytes20(keccak256('reserveBeneficiary')));
contracts/forge-test/E2E.t.sol::287 => address _user = address(bytes20(keccak256('user')));
contracts/forge-test/NFTReward_Unit.t.sol::17 => address beneficiary = address(bytes20(keccak256('beneficiary')));
contracts/forge-test/NFTReward_Unit.t.sol::18 => address owner = address(bytes20(keccak256('owner')));
contracts/forge-test/NFTReward_Unit.t.sol::19 => address reserveBeneficiary = address(bytes20(keccak256('reserveBeneficiary')));
contracts/forge-test/NFTReward_Unit.t.sol::20 => address mockJBDirectory = address(bytes20(keccak256('mockJBDirectory')));
contracts/forge-test/NFTReward_Unit.t.sol::21 => address mockJBFundingCycleStore = address(bytes20(keccak256('mockJBFundingCycleStore')));
contracts/forge-test/NFTReward_Unit.t.sol::22 => address mockTokenUriResolver = address(bytes20(keccak256('mockTokenUriResolver')));
contracts/forge-test/NFTReward_Unit.t.sol::23 => address mockTerminalAddress = address(bytes20(keccak256('mockTerminalAddress')));
contracts/forge-test/NFTReward_Unit.t.sol::24 => address mockJBProjects = address(bytes20(keccak256('mockJBProjects')));
contracts/forge-test/NFTReward_Unit.t.sol::67 => address delegate_i = address(bytes20(keccak256('delegate_implementation')));
contracts/forge-test/NFTReward_Unit.t.sol::1080 => address(bytes20(keccak256('previousOne')))
contracts/forge-test/NFTReward_Unit.t.sol::1756 => address beneficiaryTwo = address(bytes20(keccak256('beneficiaryTwo')));
contracts/forge-test/NFTReward_Unit.t.sol::1920 => address(bytes20(keccak256('newReserveBeneficiary')))
contracts/forge-test/NFTReward_Unit.t.sol::1942 => address(bytes20(keccak256('newOne')))
contracts/forge-test/NFTReward_Unit.t.sol::2091 => uint256 _newTierCandidate = uint256(keccak256(abi.encode(seed))) % initialNumberOfTiers;
contracts/forge-test/NFTReward_Unit.t.sol::2822 => uint256 _newTierCandidate = uint256(keccak256(abi.encode(seed))) % initialNumberOfTiers;
contracts/forge-test/NFTReward_Unit.t.sol::3433 => address _jbPrice = address(bytes20(keccak256('MockJBPrice')));
contracts/forge-test/NFTReward_Unit.t.sol::4097 => address _holder = address(bytes20(keccak256('_holder')));
contracts/forge-test/NFTReward_Unit.t.sol::4593 => address _holder = address(bytes20(keccak256('_holder')));
contracts/forge-test/NFTReward_Unit.t.sol::4696 => address _holder = address(bytes20(keccak256('_holder')));
contracts/forge-test/NFTReward_Unit.t.sol::4742 => address _holder = address(bytes20(keccak256('_holder')));
contracts/forge-test/NFTReward_Unit.t.sol::4785 => address _holder = address(bytes20(keccak256('_holder')));
contracts/forge-test/governance/JB721GlobalGovernance.t.sol::11 => address _user = address(bytes20(keccak256('user')));
contracts/forge-test/governance/JB721GlobalGovernance.t.sol::12 => address _userFren = address(bytes20(keccak256('user_fren')));
contracts/forge-test/governance/JB721GlobalGovernance.t.sol::80 => address _user = address(bytes20(keccak256('user')));
contracts/forge-test/governance/JB721GlobalGovernance.t.sol::81 => address _userFren = address(bytes20(keccak256('user_fren')));
contracts/forge-test/governance/JB721TieredGovernance.t.sol::11 => address _user = address(bytes20(keccak256('user')));
contracts/forge-test/governance/JB721TieredGovernance.t.sol::12 => address _userFren = address(bytes20(keccak256('user_fren')));
contracts/forge-test/governance/JB721TieredGovernance.t.sol::85 => address _user = address(bytes20(keccak256('user')));
contracts/forge-test/governance/JB721TieredGovernance.t.sol::86 => address _userFren = address(bytes20(keccak256('user_fren')));
contracts/forge-test/utils/TestBaseWorkflow.sol::208 => bytes32 hash = keccak256(data);

Tools used

c4udit

Use Shift Right/Left instead of Division/Multiplication if possible

Impact

Issue Information: G008

Findings:

contracts/forge-test/NFTReward_Unit.t.sol::527 => ((numberOfTiers * (numberOfTiers + 1)) / 2)
contracts/forge-test/NFTReward_Unit.t.sol::583 => assertEq(_delegate.balanceOf(holder), 10 * ((numberOfTiers * (numberOfTiers + 1)) / 2));
contracts/forge-test/NFTReward_Unit.t.sol::717 => 10 * ((numberOfTiers * (numberOfTiers - 1)) / 2) // (numberOfTiers-1)! * 10
contracts/forge-test/NFTReward_Unit.t.sol::868 => uint256 _maxNumberOfTiers = (numberOfTiers * (numberOfTiers + 1)) / 2; // "tier amount" of token mintable per tier -> max == numberOfTiers!
contracts/forge-test/NFTReward_Unit.t.sol::1699 => uint16[] memory _tiersToMint = new uint16[](nbTiers * 2);
contracts/forge-test/NFTReward_Unit.t.sol::1766 => uint16[] memory _tiersToMintOne = new uint16[](nbTiers * 2);
contracts/forge-test/NFTReward_Unit.t.sol::1767 => uint16[] memory _tiersToMintTwo = new uint16[](nbTiers * 2);
contracts/forge-test/NFTReward_Unit.t.sol::1847 => uint16[] memory _tiersToMint = new uint16[](nbTiers * 2);
contracts/forge-test/NFTReward_Unit.t.sol::3087 => tiers[0].contributionFloor * 2 + tiers[1].contributionFloor,
contracts/forge-test/NFTReward_Unit.t.sol::3349 => uint256 _amount = tiers[0].contributionFloor * 2 + tiers[1].contributionFloor + _leftover / 2;
contracts/forge-test/NFTReward_Unit.t.sol::3468 => uint256 _amountInOtherCurrency = tiers[0].contributionFloor * 2 + tiers[1].contributionFloor;
contracts/forge-test/NFTReward_Unit.t.sol::3469 => uint256 _amountInEth = (tiers[0].contributionFloor * 2 + tiers[1].contributionFloor) * 2;
contracts/forge-test/NFTReward_Unit.t.sol::3560 => tiers[0].contributionFloor * 2 + tiers[1].contributionFloor,
contracts/forge-test/NFTReward_Unit.t.sol::3621 => tiers[0].contributionFloor * 2 + tiers[1].contributionFloor,
contracts/forge-test/NFTReward_Unit.t.sol::3679 => tiers[0].contributionFloor * 2 + tiers[1].contributionFloor - 1,
contracts/forge-test/NFTReward_Unit.t.sol::3955 => tiers[0].contributionFloor * 2 + tiers[1].contributionFloor,
contracts/forge-test/NFTReward_Unit.t.sol::4073 => tiers[0].contributionFloor * 2 + tiers[1].contributionFloor,
contracts/forge-test/NFTReward_Unit.t.sol::4227 => uint256 _redemptionRate = 4000; // 40%
contracts/forge-test/NFTReward_Unit.t.sol::4451 => uint256 _redemptionRate = _accessJBLib.MAX_RESERVED_RATE(); // 40%
contracts/forge-test/NFTReward_Unit.t.sol::4926 => if (count == (smol.length * (smol.length + 1)) / 2) return true;
contracts/forge-test/utils/TestBaseWorkflow.sol::195 => //https://ethereum.stackexchange.com/questions/24248/how-to-calculate-an-ethereum-contracts-address-during-its-creation-using-the-so
contracts/libraries/JBBitmap.sol::74 => return _index / 256;
contracts/libraries/JBIpfsDecoder.sol::52 => carry += uint256(digits[j]) * 256;

Tools used

c4udit

QA Report

See the markdown file with the details of this report here.

The tier setting parameter are unsafely downcasted from type uint256 to type uint80 / uint48 / uint40 / uint16

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L240
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L628
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L689

Vulnerability details

Impact

The tier setting parameter are unsafely downcasted from uint256 to uint80 / uint48 / uint16

the tier is setted by owner is crucial because the parameter affect how nft is minted.

the

the callstack is

JBTiered721Delegate.sol#initialize -> Store#recordAddTiers

function recordAddTiers(JB721TierParams[] memory _tiersToAdd)

what does the struct JB721TierParams look like? all parameter in JB721TierParams is uint256 type

struct JB721TierParams {
  uint256 contributionFloor;
  uint256 lockedUntil;
  uint256 initialQuantity;
  uint256 votingUnits;
  uint256 reservedRate;
  address reservedTokenBeneficiary;
  bytes32 encodedIPFSUri;
  bool allowManualMint;
  bool shouldUseBeneficiaryAsDefault;
}

however in side the function

// Record adding the provided tiers.
if (_pricing.tiers.length > 0) _store.recordAddTiers(_pricing.tiers);

all uint256 parameter are downcasted.

// Add the tier with the iterative ID.
_storedTierOf[msg.sender][_tierId] = JBStored721Tier({
contributionFloor: uint80(_tierToAdd.contributionFloor),
lockedUntil: uint48(_tierToAdd.lockedUntil),
remainingQuantity: uint40(_tierToAdd.initialQuantity),
initialQuantity: uint40(_tierToAdd.initialQuantity),
votingUnits: uint16(_tierToAdd.votingUnits),
reservedRate: uint16(_tierToAdd.reservedRate),
allowManualMint: _tierToAdd.allowManualMint
});

uint256 contributionFloor is downcasted to uint80,

uint256 lockedUntil is downcasted to uint48

uint256 initialQuantity and initialQuantity are downcasted to uint40

uint256 votingUnits and uint256 reservedRate are downcasted to uint16

this means the original setting is greatly trancated.

For example, the owner wants to set the initial supply to a number larger than uint40, but the supply is trancated to type(uint40).max

The owner wants to set the contribution floor price above uint80,but the contribution floor price is trancated to type(uint80).max, the user may underpay the price and get the NFT price at a discount.

Proof of Concept

We can add POC

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/forge-test/NFTReward_Unit.t.sol#L1689

 function testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTiers_downcast_POC() public {
    uint256 nbTiers = 1;

    vm.mockCall(
      mockJBProjects,
      abi.encodeWithSelector(IERC721.ownerOf.selector, projectId),
      abi.encode(owner)
    );

    JB721TierParams[] memory _tiers = new JB721TierParams[](nbTiers);
    uint16[] memory _tiersToMint = new uint16[](nbTiers);

    // Temp tiers, will get overwritten later (pass the constructor check)
    uint256 originalFloorPrice = 10000000000000000000000000 ether;
  
    for (uint256 i; i < nbTiers; i++) {
      _tiers[i] = JB721TierParams({
        contributionFloor: originalFloorPrice,
        lockedUntil: uint48(0),
        initialQuantity: 20,
        votingUnits: uint16(0),
        reservedRate: uint16(0),
        reservedTokenBeneficiary: reserveBeneficiary,
        encodedIPFSUri: tokenUris[i],
        allowManualMint: true, // Allow this type of mint
        shouldUseBeneficiaryAsDefault: false
      });

      _tiersToMint[i] = uint16(i)+1;
      _tiersToMint[_tiersToMint.length - 1 - i] = uint16(i)+1;
    }

    ForTest_JBTiered721DelegateStore _ForTest_store = new ForTest_JBTiered721DelegateStore();
    ForTest_JBTiered721Delegate _delegate = new ForTest_JBTiered721Delegate(
      projectId,
      IJBDirectory(mockJBDirectory),
      name,
      symbol,
      IJBFundingCycleStore(mockJBFundingCycleStore),
      baseUri,
      IJBTokenUriResolver(mockTokenUriResolver),
      contractUri,
      _tiers,
      IJBTiered721DelegateStore(address(_ForTest_store)),
      JBTiered721Flags({
        lockReservedTokenChanges: false,
        lockVotingUnitChanges: false,
        lockManualMintingChanges: true,
        pausable: true
      })
    );

    _delegate.transferOwnership(owner);

    uint256 floorPrice = _delegate.test_store().tier(address(_delegate), 1).contributionFloor;
    console.log("original floor price");
    console.log(originalFloorPrice);
    console.log("truncated floor price");
    console.log(floorPrice);

}

note, our initial contribution floor price setting is

uint256 originalFloorPrice = 10000000000000000000000000 ether;

for (uint256 i; i < nbTiers; i++) {
  _tiers[i] = JB721TierParams({
	contributionFloor: originalFloorPrice,

then we run our test

forge test -vv --match testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTiers_downcast_POC

the result is

[PASS] testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTiers_downcast_POC() (gas: 7601212)
Logs:
  original floor price
  10000000000000000000000000000000000000000000
  truncated floor price
  863278115882885135204352

Test result: ok. 1 passed; 0 failed; finished in 10.43ms

clearly the floor price is unsafed downcasted and trancated.

Tools Used

Foundry, Manual Review

Recommended Mitigation Steps

We recommend the project either change the data type in the struct

struct JB721TierParams {
  uint256 contributionFloor;
  uint256 lockedUntil;
  uint256 initialQuantity;
  uint256 votingUnits;
  uint256 reservedRate;
  address reservedTokenBeneficiary;
  bytes32 encodedIPFSUri;
  bool allowManualMint;
  bool shouldUseBeneficiaryAsDefault;
}

or safely downcast the number to make sure the number is not shortened unexpectedly.

https://docs.openzeppelin.com/contracts/3.x/api/utils#SafeCast

Too late checking of amount for zero

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol#L312

Vulnerability details

Impact

Detailed description of the impact of this finding.
The check for _amount == 0 comes too late (inside function * _moveTierDelegateVotes* call), by then, line 249 or line 252 might have been completed. Since there is no revert, this will become permanent.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol#L312

Tools Used

Reading source code

Recommended Mitigation Steps

All the arguments should be check right from the beginning and use custom error revert when they do not meet the requirement

QA Report

Juicebox

QA Report

L-01 _safeMint() should be used rather than _mint() wherever possible

There are 1 instances of this issue:

File: /contracts/JBTiered721Delegate.sol

677: _mint(_beneficiary, _tokenId);

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol

L-02 Use of floating pragma

There are 10 instances of this issue:

File: /contracts/JB721GlobalGovernance.sol

File: /contracts/JBTiered721DelegateDeployer.sol

File: /contracts/JBTiered721DelegateProjectDeployer.sol

File: /contracts/JB721TieredGovernance.sol

File: /contracts/JBTiered721Delegate.sol

File: /contracts/JBTiered721DelegateStore.sol

File: /contracts/abstract/JB721Delegate.sol

File: /contracts/libraries/JBTiered721FundingCycleMetadataResolver.sol

File: /contracts/libraries/JBBitmap.sol

File: /contracts/libraries/JBIpfsDecoder.sol

L-03 Zero-address checks are missing

There are 1 instances of this issue:

File: /contracts/JBTiered721DelegateProjectDeployer.sol

91: _launchProjectFor(_owner, _launchProjectData);

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateProjectDeployer.sol

N-01 Not using the named return variables anywhere in the function is confusing

There are 5 instances of this issue:

File: /contracts/JBTiered721DelegateProjectDeployer.sol

119: returns (uint256 configuration)

162: returns (uint256 configuration)

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateProjectDeployer.sol

File: /contracts/JBTiered721Delegate.sol

123: function balanceOf(address _owner) public view override returns (uint256 balance) {

617: ) internal returns (uint256 leftoverAmount) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol

File: /contracts/abstract/JB721Delegate.sol

105: function redeemParams(JBRedeemParamsData calldata _data)

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/abstract/JB721Delegate.sol

N-02 require() / revert() statements should have descriptive reason strings

There are 2 instances of this issue:

File: /contracts/JBTiered721Delegate.sol

216: require(address(this) != codeOrigin);

218: require(address(store) == address(0));

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol

N-03 constants should be defined rather than using magic numbers

There are 9 instances of this issue:

File: /contracts/libraries/JBBitmap.sol

30: return (self.currentWord >> (_index % 256)) & 1 == 1;

52: self[_depth] |= uint256(1 << (_index % 256));

74: return _index / 256;

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBBitmap.sol

File: /contracts/libraries/JBIpfsDecoder.sol

46: uint8[] memory digits = new uint8[](46); // hash size with the prefix

52: carry += uint256(digits[j]) * 256;

53: digits[j] = uint8(carry % 58);

54: carry = carry / 58;

58: digits[digitlength] = uint8(carry % 58);

60: carry = carry / 58;

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol

Gas Optimizations

Juicebox

Gas Optimizations Report

G-01 public functions not called by the contract should be declared external instead

There are 4 instances of this issue:

File: /contracts/JB721TieredGovernance.sol

147: function setTierDelegates(JBTiered721SetTierDelegatesData[] memory _setTierDelegatesData)

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol

File: /contracts/JBTiered721DelegateStore.sol

499: function balanceOf(address _nft, address _owner) public view override returns (uint256 balance) {

523: function redemptionWeightOf(address _nft, uint256[] calldata _tokenIds)

550: function totalRedemptionWeight(address _nft) public view override returns (uint256 weight) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol

G-02 Functions that are access-restricted from most users may be marked as payable

Marking a function as payable reduces gas cost since the compiler does not have to check whether a payment was provided or not. This change will save around 21 gas per function call.

There are 7 instances of this issue:

File: /contracts/JBTiered721Delegate.sol

290: function mintFor(JBTiered721MintForTiersData[] memory _mintForTiersData)

321: function adjustTiers(JB721TierParams[] calldata _tiersToAdd, uint256[] calldata _tierIdsToRemove)

370: function setDefaultReservedTokenBeneficiary(address _beneficiary) external override onlyOwner {

386: function setBaseUri(string memory _baseUri) external override onlyOwner {

402: function setContractUri(string calldata _contractUri) external override onlyOwner {

418: function setTokenUriResolver(IJBTokenUriResolver _tokenUriResolver) external override onlyOwner {

480: function mintFor(uint16[] memory _tierIds, address _beneficiary)

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol

G-03 The usage of ++i will cost less gas than i++. The same change can be applied to i-- as well.

This change would save up to 6 gas per instance/loop.

There are 6 instances of this issue:

File: /contracts/JBTiered721DelegateStore.sol

1106: numberOfBurnedFor[msg.sender][_tierId]++;

1108: _storedTierOf[msg.sender][_tierId].remainingQuantity++;

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol

File: /contracts/libraries/JBIpfsDecoder.sol

59: digitlength++;

68: for (uint256 i = 0; i < _length; i++) {

76: for (uint256 i = 0; i < _input.length; i++) {

84: for (uint256 i = 0; i < _indices.length; i++) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol

G-04 <x> += <y> costs more gas than <x> = <x> + <y> for state variables

There are 7 instances of this issue:

File: /contracts/JBTiered721DelegateStore.sol

354: supply += _storedTier.initialQuantity - _storedTier.remainingQuantity;

409: units += _balance * _storedTierOf[_nft][_i].votingUnits;

506: balance += tierBalanceOf[_nft][_owner][_i];

534: weight += _storedTierOf[_nft][tierIdOfToken(_tokenIds[_i])].contributionFloor;

563: weight +=

827: numberOfReservesMintedFor[msg.sender][_tierId] += _count;

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol

File: /contracts/libraries/JBIpfsDecoder.sol

52: carry += uint256(digits[j]) * 256;

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol

G-05 Replace x >= y with x > y - 1

In the EVM, there is no opcode for >= or <=. When using greater than or equal, two operations are performed: > and =. Using strict comparison operators hence saves gas

There are 1 instances of this issue:

File: /contracts/JBTiered721DelegateStore.sol

903: if (_storedTierOf[msg.sender][_tierId].lockedUntil >= block.timestamp) revert TIER_LOCKED();

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol

G-06 It costs more gas to initialize non-constant/non-immutable variables to zero than to let the default of zero be applied

Not overwriting the default for stack variables saves 8 gas. Storage and memory variables have larger savings

There are 5 instances of this issue:

File: /contracts/libraries/JBIpfsDecoder.sol

49: for (uint256 i = 0; i < _source.length; ++i) {

51: for (uint256 j = 0; j < digitlength; ++j) {

68: for (uint256 i = 0; i < _length; i++) {

76: for (uint256 i = 0; i < _input.length; i++) {

84: for (uint256 i = 0; i < _indices.length; i++) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol

G-07 Use calldata instead of memory for function parameters

If a reference type function parameter is read-only, it is cheaper in gas to use calldata instead of memory. Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory. Try to use calldata as a data location because it will avoid copies and also makes sure that the data cannot be modified.

There are 5 instances of this issue:

File: /contracts/libraries/JBIpfsDecoder.sol

22: function decode(string memory _baseUri, bytes32 _hexString)

44: function _toBase58(bytes memory _source) private pure returns (string memory) {

66: function _truncate(uint8[] memory _array, uint8 _length) private pure returns (uint8[] memory) {

74: function _reverse(uint8[] memory _input) private pure returns (uint8[] memory) {

82: function _toAlphabet(uint8[] memory _indices) private pure returns (bytes memory) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol

G-08 Not using the named return variables when a function returns wastes deployment gas

There are 5 instances of this issue:

File: /contracts/JBTiered721DelegateProjectDeployer.sol

119: returns (uint256 configuration)

162: returns (uint256 configuration)

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateProjectDeployer.sol

File: /contracts/JBTiered721Delegate.sol

123: function balanceOf(address _owner) public view override returns (uint256 balance) {

617: ) internal returns (uint256 leftoverAmount) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol

File: /contracts/abstract/JB721Delegate.sol

105: function redeemParams(JBRedeemParamsData calldata _data)

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/abstract/JB721Delegate.sol

Gas Optimizations

G01: COMPARISONS WITH ZERO FOR UNSIGNED INTEGERS

problem

0 is less gas efficient than !0 if you enable the optimizer at 10k AND you’re in a require statement. Detailed explanation with the opcodes https://twitter.com/gzeon/status/1485428085885640706

prof

libraries/JBIpfsDecoder.sol, 57, b' while (carry > 0) {'
JBTiered721Delegate.sol, 240, b' if (_pricing.tiers.length > 0) _store.recordAddTiers(_pricing.tiers);'
JBTiered721DelegateStore.sol, 1254, b' if (_numerator - JBConstants.MAX_RESERVED_RATE * _numberReservedTokensMintable > 0)'
abstract/JB721Delegate.sol, 116, b' if (_data.tokenCount > 0) revert UNEXPECTED_TOKEN_REDEEMED();'

G02: PREFIX INCREMENT SAVE MORE GAS

problem

prefix increment ++i is more cheaper than postfix i++

prof

libraries/JBIpfsDecoder.sol, 59, b' digitlength++;'
libraries/JBIpfsDecoder.sol, 68, b' for (uint256 i = 0; i < _length; i++) {'
libraries/JBIpfsDecoder.sol, 76, b' for (uint256 i = 0; i < _input.length; i++) {'
libraries/JBIpfsDecoder.sol, 84, b' for (uint256 i = 0; i < _indices.length; i++) {'
JBTiered721DelegateStore.sol, 1106, b' numberOfBurnedFor[msg.sender][_tierId]++;'
JBTiered721DelegateStore.sol, 1108, b' _storedTierOf[msg.sender][_tierId].remainingQuantity++;'

G03: X += Y COSTS MORE GAS THAN X = X + Y FOR STATE VARIABLES

prof

JBTiered721DelegateStore.sol, 827, b' numberOfReservesMintedFor[msg.sender][_tierId] += _count;'

G04: resign the default value to the variables.

problem

resign the default value to the variables will cost more gas.

prof

libraries/JBIpfsDecoder.sol, 51, b' for (uint256 j = 0; j < digitlength; ++j) {'
libraries/JBIpfsDecoder.sol, 49, b' for (uint256 i = 0; i < _source.length; ++i) {'
libraries/JBIpfsDecoder.sol, 68, b' for (uint256 i = 0; i < _length; i++) {'
libraries/JBIpfsDecoder.sol, 76, b' for (uint256 i = 0; i < _input.length; i++) {'
libraries/JBIpfsDecoder.sol, 84, b' for (uint256 i = 0; i < _indices.length; i++) {'

G05: ++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

problem

The unchecked keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are. This saves 30-40 gas per loop

prof

libraries/JBIpfsDecoder.sol, 51, b' for (uint256 j = 0; j < digitlength; ++j) {'
libraries/JBIpfsDecoder.sol, 59, b' digitlength++;'
libraries/JBIpfsDecoder.sol, 49, b' for (uint256 i = 0; i < _source.length; ++i) {'
libraries/JBIpfsDecoder.sol, 68, b' for (uint256 i = 0; i < _length; i++) {'
libraries/JBIpfsDecoder.sol, 76, b' for (uint256 i = 0; i < _input.length; i++) {'
libraries/JBIpfsDecoder.sol, 84, b' for (uint256 i = 0; i < _indices.length; i++) {'

G06: FUNCTIONS GUARANTEED TO REVERT WHEN CALLED BY NORMAL USERS CAN BE MARKED PAYABLE

problem

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. The extra opcodes avoided are CALLVALUE(2),DUP1(3),ISZERO(3),PUSH2(3),JUMPI(10),PUSH1(3),DUP1(3),REVERT(0),JUMPDEST(1),POP(2), which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost

prof

JBTiered721Delegate.sol, 109, b' function firstOwnerOf(uint256 _tokenId) external view override returns (address) '
JBTiered721Delegate.sol, 109, b' function firstOwnerOf(uint256 _tokenId) external view override returns (address) '
JBTiered721Delegate.sol, 125, b' function balanceOf(address _owner) public view override returns (uint256 balance) '
JBTiered721Delegate.sol, 125, b' function balanceOf(address _owner) public view override returns (uint256 balance) '
JBTiered721Delegate.sol, 154, b' function tokenURI(uint256 _tokenId) public view override returns (string memory) '
JBTiered721Delegate.sol, 252, b' function initialize(\n uint256 _projectId,\n IJBDirectory _directory,\n string memory _name,\n string memory _symbol,\n IJBFundingCycleStore _fundingCycleStore,\n string memory _baseUri,\n IJBTokenUriResolver _tokenUriResolver,\n string memory _contractUri,\n JB721PricingParams memory _pricing,\n IJBTiered721DelegateStore _store,\n JBTiered721Flags memory _flags\n ) public override '
JBTiered721Delegate.sol, 309, b' function mintFor(JBTiered721MintForTiersData[] memory _mintForTiersData)\n external\n override\n onlyOwner\n '
JBTiered721Delegate.sol, 359, b' function adjustTiers(JB721TierParams[] calldata _tiersToAdd, uint256[] calldata _tierIdsToRemove)\n external\n override\n onlyOwner\n '
JBTiered721Delegate.sol, 375, b' function setDefaultReservedTokenBeneficiary(address _beneficiary) external override onlyOwner '
JBTiered721Delegate.sol, 375, b' function setDefaultReservedTokenBeneficiary(address _beneficiary) external override onlyOwner '
JBTiered721Delegate.sol, 391, b' function setBaseUri(string memory _baseUri) external override onlyOwner '
JBTiered721Delegate.sol, 391, b' function setBaseUri(string memory _baseUri) external override onlyOwner '
JBTiered721Delegate.sol, 407, b' function setContractUri(string calldata _contractUri) external override onlyOwner '
JBTiered721Delegate.sol, 407, b' function setContractUri(string calldata _contractUri) external override onlyOwner '
JBTiered721Delegate.sol, 423, b' function setTokenUriResolver(IJBTokenUriResolver _tokenUriResolver) external override onlyOwner '
JBTiered721Delegate.sol, 423, b' function setTokenUriResolver(IJBTokenUriResolver _tokenUriResolver) external override onlyOwner '
JBTiered721Delegate.sol, 512, b' function mintFor(uint16[] memory _tierIds, address _beneficiary)\n public\n override\n onlyOwner\n returns (uint256[] memory tokenIds)\n '
JBTiered721Delegate.sol, 749, b' function _beforeTokenTransfer(\n address _from,\n address _to,\n uint256 _tokenId\n ) internal virtual override '
JBTiered721DelegateDeployer.sol, 105, b' function deployDelegateFor(\n uint256 _projectId,\n JBDeployTiered721DelegateData memory _deployTiered721DelegateData\n ) external override returns (IJBTiered721Delegate) '
JBTiered721DelegateStore.sol, 512, b' function balanceOf(address _nft, address _owner) public view override returns (uint256 balance) '
JBTiered721DelegateStore.sol, 512, b' function balanceOf(address _nft, address _owner) public view override returns (uint256 balance) '
JBTiered721DelegateStore.sol, 1125, b' function recordSetFirstOwnerOf(uint256 _tokenId, address _owner) external override '
JBTiered721DelegateStore.sol, 1125, b' function recordSetFirstOwnerOf(uint256 _tokenId, address _owner) external override '
abstract/JB721Delegate.sol, 289, b' function didRedeem(JBDidRedeemData calldata _data) external payable virtual override '
JBTiered721DelegateProjectDeployer.sol, 92, b' function launchProjectFor(\n address _owner,\n JBDeployTiered721DelegateData memory _deployTiered721DelegateData,\n JBLaunchProjectData memory _launchProjectData\n ) external override returns (uint256 projectId) '
JBTiered721DelegateProjectDeployer.sol, 92, b' function launchProjectFor(\n address _owner,\n JBDeployTiered721DelegateData memory _deployTiered721DelegateData,\n JBLaunchProjectData memory _launchProjectData\n ) external override returns (uint256 projectId) '
JBTiered721DelegateProjectDeployer.sol, 135, b' function launchFundingCyclesFor(\n uint256 _projectId,\n JBDeployTiered721DelegateData memory _deployTiered721DelegateData,\n JBLaunchFundingCyclesData memory _launchFundingCyclesData\n )\n external\n override\n requirePermission(\n controller.projects().ownerOf(_projectId),\n _projectId,\n JBOperations.RECONFIGURE\n )\n returns (uint256 configuration)\n '
JBTiered721DelegateProjectDeployer.sol, 178, b' function reconfigureFundingCyclesOf(\n uint256 _projectId,\n JBDeployTiered721DelegateData memory _deployTiered721DelegateData,\n JBReconfigureFundingCyclesData memory _reconfigureFundingCyclesData\n )\n external\n override\n requirePermission(\n controller.projects().ownerOf(_projectId),\n _projectId,\n JBOperations.RECONFIGURE\n )\n returns (uint256 configuration)\n '
JBTiered721DelegateProjectDeployer.sol, 205, b' function _launchProjectFor(address _owner, JBLaunchProjectData memory _launchProjectData)\n internal\n '
JBTiered721DelegateProjectDeployer.sol, 205, b' function _launchProjectFor(address _owner, JBLaunchProjectData memory _launchProjectData)\n internal\n '

G07: USING PRIVATE RATHER THAN PUBLIC FOR CONSTANTS, SAVES GAS

problem:

We can save getter function of public constants.

prof:

JBTiered721DelegateDeployer.sol, 28, b' JB721GlobalGovernance public immutable globalGovernance;'
JBTiered721DelegateDeployer.sol, 34, b' JB721TieredGovernance public immutable tieredGovernance;'
JBTiered721DelegateDeployer.sol, 40, b' JBTiered721Delegate public immutable noGovernance;'
JBTiered721DelegateProjectDeployer.sol, 30, b' IJBController public immutable override controller;'
JBTiered721DelegateProjectDeployer.sol, 36, b' IJBTiered721DelegateDeployer public immutable override delegateDeployer;'

QA Report

  1. USE A MORE RECENT VERSION OF SOLIDITY

^0.8.16 is used.
Should use 0.8.17
New features and bugs fixed are introduced in the last version

Centralization risks in adjustTiers()

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L321-L359

Vulnerability details

Impact

In JBTiered721DelegateStore, _flagsOf is used to restrict the types of tier the owner can create, including not allowing the owner to mint NFTs for free, etc. But there is no restriction on the parameters when creating a tier.
Consider a JB721GlobalGovernance contract where the owner is restricted from minting NFTs for free, but the owner can create a tier in the adjustTiers function using parameters such as contributionFloor == 0, lockedUntil = block.timestamp-1, votingUnits == 1e50, etc., and the owner can mint NFT for free and delete the tier in the same transaction (since lockedUntil < block.timestamp, the deletion will succeed).
Since the NFTs in the deleted tier still have votes, the owner can have a large number of votes

Proof of Concept

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L321-L359
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L688-L696

Tools Used

None

Recommended Mitigation Steps

Consider restricting the parameters when the owner creates a tier, in particular requiring lockedUntil > block.timestamp

Owner can set contribution floor to 0 , meaning the user lose the fund / fee for 0 contribution power NFT and NFT have no redemption weight

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L568
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L523
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L550

Vulnerability details

Impact

Owner can set contribution floor to 0, meaning the user lose the fund / fee for 0 contribution power NFT and NFT have no redemption weight.

the normal payment flow for user is

Payment Terminal -> pay -> didPay -> processPayment ->

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L568

// Mint rewards if they were specified.
if (_tierIdsToMint.length != 0)
_leftoverAmount = _mintAll(_leftoverAmount, _tierIdsToMint, _data.beneficiary);

then later, the NFT can be redeemed. the weight is calculated via redemption weight derived from each NFT's contribution power.

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L523

 function redemptionWeightOf(address _nft, uint256[] calldata _tokenIds)

and

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L550

function totalRedemptionWeight(address _nft) public view override returns (uint256 weight) {

the weight is sum of the (contributionFloor * number of nft)

// Add the tier's contribution floor multiplied by the quantity minted.
weight +=
(_storedTier.contributionFloor *
  (_storedTier.initialQuantity - _storedTier.remainingQuantity)) +
_numberOfReservedTokensOutstandingFor(_nft, _i, _storedTier);

however, the contributionFloor can be set to 0, meaning the user pay mint fee or gas fee but get a NFT that have no redemption number, as shown in POC

Proof of Concept

We add the POC

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/forge-test/NFTReward_Unit.t.sol#L1689

 function testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTiers_Zero_floor_POC() public {
    uint256 nbTiers = 1;

    vm.mockCall(
      mockJBProjects,
      abi.encodeWithSelector(IERC721.ownerOf.selector, projectId),
      abi.encode(owner)
    );

    JB721TierParams[] memory _tiers = new JB721TierParams[](nbTiers);
    uint16[] memory _tiersToMint = new uint16[](nbTiers);

    // Temp tiers, will get overwritten later (pass the constructor check)
    for (uint256 i; i < nbTiers; i++) {
      _tiers[i] = JB721TierParams({
        contributionFloor: 0,
        lockedUntil: uint48(0),
        initialQuantity: uint40(5),
        votingUnits: uint16(0),
        reservedRate: uint16(0),
        reservedTokenBeneficiary: reserveBeneficiary,
        encodedIPFSUri: tokenUris[i],
        allowManualMint: true, // Allow this type of mint
        shouldUseBeneficiaryAsDefault: false
      });

      _tiersToMint[i] = uint16(i)+1;
      _tiersToMint[_tiersToMint.length - 1 - i] = uint16(i)+1;
    }

    ForTest_JBTiered721DelegateStore _ForTest_store = new ForTest_JBTiered721DelegateStore();
    ForTest_JBTiered721Delegate _delegate = new ForTest_JBTiered721Delegate(
      projectId,
      IJBDirectory(mockJBDirectory),
      name,
      symbol,
      IJBFundingCycleStore(mockJBFundingCycleStore),
      baseUri,
      IJBTokenUriResolver(mockTokenUriResolver),
      contractUri,
      _tiers,
      IJBTiered721DelegateStore(address(_ForTest_store)),
      JBTiered721Flags({
        lockReservedTokenChanges: false,
        lockVotingUnitChanges: false,
        lockManualMintingChanges: true,
        pausable: true
      })
    );

    _delegate.transferOwnership(owner);

    vm.startPrank(owner);

    for(uint256 i = 0; i < 4; i++) {
        _delegate.mintFor(_tiersToMint, beneficiary);
    }

    vm.stopPrank();

    console.log("_delegate.balanceOf(beneficiary)");
    console.log(_delegate.balanceOf(beneficiary));

    uint256 weight = _delegate.test_store().totalRedemptionWeight(address(_delegate));
    console.log("nft weight");
    console.log(weight);
 
}

note, the setting below compiles when the contribution floor is set to 0

_tiers[i] = JB721TierParams({
	contributionFloor: 0,
	lockedUntil: uint48(0),
	initialQuantity: uint40(5),
	votingUnits: uint16(0),
	reservedRate: uint16(0),
	reservedTokenBeneficiary: reserveBeneficiary,
	encodedIPFSUri: tokenUris[i],
	allowManualMint: true, // Allow this type of mint
	shouldUseBeneficiaryAsDefault: false
  });

then we mint 4 NFT and check the NFT redemption weight.

We run the test

forge test -vv --match testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTiers_Zero_floor_POC

and the result is

[PASS] testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTiers_Zero_floor_POC() (gas: 7801413)
Logs:
  _delegate.balanceOf(beneficiary)
  4
  nft weight
  0

Test result: ok. 1 passed; 0 failed; finished in 17.05ms

if the user that mint via Payment terminal will also get nft with no redemption weight.

Tools Used

Hardhat, Foundry

Recommended Mitigation Steps

We recommend make sure the contribution floor cannot be set to 0.

JB NFT may be minted to non ERC721 receivers

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L461
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L504

Vulnerability details

Impact

the safeTransfer function for ERC721 tokens is inside the codebase,

However, the nft mint in mintFor and in mintReservesFor uses _mint rather than _safeMint and does not check that the receiver accepts ERC721 token transfers

// Mint the token.
_mint(_reservedTokenBeneficiary, _tokenId);

emit MintReservedToken(_tokenId, _tierId, _reservedTokenBeneficiary, msg.sender);

and

// Mint the token.
_mint(_beneficiary, _tokenId);

emit Mint(_tokenId, _tierIds[_i], _beneficiary, 0, msg.sender);

Proof of Concept

In mintReservesFor, the _reservedTokenBeneficiary can be a ERC20 token, which is not designed to receive NFT, then the NFT sent to that address will be locked ans lost forever.

_mint(_reservedTokenBeneficiary, _tokenId);

Tools Used

Manual Review

Recommended Mitigation Steps

we recommend the project implement and use _safeMint to make sure the NFT recipient is capable of handling the received NFT.

function _safeMint(address to, uint256 id) internal virtual {
	_mint(to, id);

	require(
		to.code.length == 0 ||
			ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") ==
			ERC721TokenReceiver.onERC721Received.selector,
		"UNSAFE_RECIPIENT"
	);
}

the safeMint introduce re-entrancy risk, please handling the state change properly if using safeMint.

QA Report

for non library contracts use specific version of solidity

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721GlobalGovernance.sol#L2)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateDeployer.sol#L2)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateProjectDeployer.sol#L2)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L2)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol#L2)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L2)

check for 0 address

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateDeployer.sol#L46-L54)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateProjectDeployer.sol#L47-L54)

there is no return statement or returns a different data type

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateDeployer.sol#L69-L105)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L213-L268)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L342-L360)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L389-L415)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L467-L469)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L499-L512)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L523-L540)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L550-L572)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L628-L794)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L808-L846)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L924-L999)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1012-L1083)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1270-L1280)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1314-L1318)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1328-L1332)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateDeployer.sol#L115-L137)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateProjectDeployer.sol#L70-L92)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L480-L512)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L613-L638)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L650-L685)

return true or false

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L175-L179)

put constructor before functions

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L185-L187)

error message is missing

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L216)

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L218)

avoid nest if

(https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1195)

Owner can set lockUntil to a very large timestamp to create not-removeable tier and not-pause-able tier

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L335
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L903

Vulnerability details

Impact

Owner can set lockUntil to a very large timestamp to create not-removeable tier.

when a tier is created, the owner can set lockUntil parameter

  @member lockedUntil The time up to which this tier cannot be removed or paused.

when the owner call adjustTier, the transaction would revert if lockedUntil timestamp is still in-effect.

// Record the removed tiers.
store.recordRemoveTierIds(_tierIdsToRemove);

then we call

// If the tier is locked throw an error.
if (_storedTierOf[msg.sender][_tierId].lockedUntil >= block.timestamp) revert TIER_LOCKED();

however, there is no upper limit for the lockedUntil setting,

Owner can set lockUntil to a very large timestamp to create not-removeable tier and not-pause-able tier.

Proof of Concept

We add the POC below

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/forge-test/NFTReward_Unit.t.sol#L1689

 function testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTiers_non_removeable_tier_POC() public {
    uint256 nbTiers = 1;

    vm.mockCall(
      mockJBProjects,
      abi.encodeWithSelector(IERC721.ownerOf.selector, projectId),
      abi.encode(owner)
    );

    JB721TierParams[] memory _tiers = new JB721TierParams[](nbTiers);
    uint16[] memory _tiersToMint = new uint16[](nbTiers);

    // Temp tiers, will get overwritten later (pass the constructor check)

    for (uint256 i; i < nbTiers; i++) {
      _tiers[i] = JB721TierParams({
        contributionFloor: 5 ether,
        lockedUntil: type(uint48).max,
        initialQuantity: 20,
        votingUnits: uint16(0),
        reservedRate: uint16(0),
        reservedTokenBeneficiary: reserveBeneficiary,
        encodedIPFSUri: tokenUris[i],
        allowManualMint: true, // Allow this type of mint
        shouldUseBeneficiaryAsDefault: false
      });

      _tiersToMint[i] = uint16(i)+1;
      _tiersToMint[_tiersToMint.length - 1 - i] = uint16(i)+1;
    }

    ForTest_JBTiered721DelegateStore _ForTest_store = new ForTest_JBTiered721DelegateStore();
    ForTest_JBTiered721Delegate _delegate = new ForTest_JBTiered721Delegate(
      projectId,
      IJBDirectory(mockJBDirectory),
      name,
      symbol,
      IJBFundingCycleStore(mockJBFundingCycleStore),
      baseUri,
      IJBTokenUriResolver(mockTokenUriResolver),
      contractUri,
      _tiers,
      IJBTiered721DelegateStore(address(_ForTest_store)),
      JBTiered721Flags({
        lockReservedTokenChanges: false,
        lockVotingUnitChanges: false,
        lockManualMintingChanges: true,
        pausable: true
      })
    );

    _delegate.transferOwnership(owner);

     vm.prank(owner);

     uint256[] memory tiersToRemove = new uint256[](_tiersToMint.length);
     tiersToRemove[0] = _tiersToMint[0];
    _delegate.adjustTiers(new JB721TierParams[](0), tiersToRemove);
 
}

we set the lockedUntil to

lockedUntil: type(uint48).max,

type(uint48).max is 281474976710655
current timestamp in ethereum block is 15789894, basically, the tier become not-removeable tier and not-pause-able.

Tools Used

Hardhat, foundry

Recommended Mitigation Steps

We recommend the project add a upper limit for the parameter lockedUntil to make sure not-removeable tier and not-pause-able tier cannot be created.

Gas Optimizations

Contracts include unused custom errors

Locations:
1
2

Description

Unused errors simply drive up the cost of deployment for the contracts involved. These unused errors may also represent holes in logic that were intended to be tested but were missed.

Ensure that the errors are unnecessary before removing them.

Use calldata when possible over memory for function arguments

Locations:
1
2
3
4
5
6
7
8
9
10 <-- This breaks a test in NFTReward_Unit.t.sol due to the use of a constructor to build arguments to call this initializer

Description

While this codebase already makes some use of calldata for function arguments, it can be used more pervasively than it is.

Savings

Output of forge snapshot --diff after changing just 3 instances of memory to calldata in JBTiered721DelegateProjectDeployer.sol

testMintAndTransferTieredVotingUnits(uint8,bool) (gas: -5658 (-0.062%)) 
testMintAndDelegateVotingUnits(uint8,bool) (gas: -5712 (-0.063%)) 
testRedeemAll() (gas: -7572 (-0.083%)) 
testRedeemAll() (gas: -7572 (-0.083%)) 
testRedeemAll() (gas: -7572 (-0.083%)) 
testMintAndDelegateTieredVotingUnits(uint8,bool) (gas: -7572 (-0.083%)) 
testMintAndTransferGlobalVotingUnits(uint8,bool) (gas: -7572 (-0.084%)) 
testMintOnPayIfMultipleTiersArePassed() (gas: -7572 (-0.085%)) 
testMintOnPayIfMultipleTiersArePassed() (gas: -7572 (-0.085%)) 
testMintOnPayIfMultipleTiersArePassed() (gas: -7572 (-0.085%)) 
testMintBeforeAndAfterTierChange(uint72) (gas: -7572 (-0.086%)) 
testMintBeforeAndAfterTierChange(uint72) (gas: -7572 (-0.086%)) 
testMintBeforeAndAfterTierChange(uint72) (gas: -7572 (-0.086%)) 
testMintReservedToken() (gas: -7572 (-0.087%)) 
testMintReservedToken() (gas: -7572 (-0.087%)) 
testMintReservedToken() (gas: -7572 (-0.087%)) 
testRedeemToken(uint16) (gas: -7572 (-0.087%)) 
testRedeemToken(uint16) (gas: -7572 (-0.087%)) 
testRedeemToken(uint16) (gas: -7572 (-0.087%)) 
testMintOnPayIfOneTierIsPassed(uint16) (gas: -7572 (-0.088%)) 
testMintOnPayIfOneTierIsPassed(uint16) (gas: -7572 (-0.088%)) 
testMintOnPayIfOneTierIsPassed(uint16) (gas: -7572 (-0.088%)) 
testMintOnPayUsingFallbackTiers(uint16) (gas: -7572 (-0.090%)) 
testMintOnPayUsingFallbackTiers(uint16) (gas: -7572 (-0.090%)) 
testMintOnPayUsingFallbackTiers(uint16) (gas: -7572 (-0.090%)) 
testDeployAndLaunchProject() (gas: -7572 (-0.093%)) 
testDeployAndLaunchProject() (gas: -7572 (-0.093%)) 
testDeployAndLaunchProject() (gas: -7572 (-0.093%)) 
testLaunchProjectFor_shouldLaunchProject(uint128) (gas: -7572 (-0.153%)) 
testLaunchProjectFor_shouldLaunchProject_nonFuzzed() (gas: -7572 (-0.153%)) 
Overall gas change: -223386 (-2.696%)

The range for all changes is approximately 5000-8000gas, with some large outliers, probably due to loops.

Remove revert check on JB721TieredGovernance.getPastTierVotes()

Location

Description

The JB721TieredGovernance contract contains functions for retrieving data from storage using the Checkpoints import.

The function Checkpoints.getAtBlock() already contains a require test to make sure the block being requested isn't the current block or later. While the if revert syntax used in JB721TieredGovernance is cheaper than the require, in the success case the same condition is tested twice, wasting gas.

Although this is a view function, it's plausible that it would be consumed by external integrations to make decisions based on voting power.

Mitigation

Remove the extraneous check.

Extra

If the check is required/intended for some other purpose, then getPastTierVotes() in the same contract should be updated to perform the same check, as it uses the same underlying call.

Rewrite function to not duplicate computation.

Locations:
1
2

Description

JB721Delegate.didRedeem() and JB721Delegate.redeemParams() contain some byte slicing to access a value that is treated as a bytes4. However, that same value could be extracted via a call to abi.decode already present later in the function.

Even with the uint256[] allocation from abi.decode before all of the revert checks have completed, it is still cheaper to move the line, name the bytes4 value and use it in the boolean check.

(bytes4 _interfaceId, uint256[] memory _decodedTokenIds) = abi.decode(_data.metadata, (bytes4, uint256[]));

// Check the 4 bytes interfaceId and handle the case where the metadata was not intended for this contract
if ( _data.metadata.length < 4  
    || _interfaceId != type(IJB721Delegate).interfaceId) { 
    revert INVALID_REDEMPTION_METADATA();
}

// Get a reference to the number of token IDs being checked.
uint256 _numberOfTokenIds = _decodedTokenIds.length;

Savings

Implementing the change in both applicable locations saves ~13200 gas throughout the test suite, with outliers at ~170 gas. A sample of forge snapshot --diff is provided:

testJBTieredNFTRewardDelegate_didPay_mintFirstBestTierIfMultipleAvailableAtSameFloor() (gas: -13235 (-0.169%)) 
testJBTieredNFTRewardDelegate_getvotingUnits_returnsTheTotalVotingUnits() (gas: -13236 (-0.170%)) 
testJBTieredNFTRewardDelegate_balanceOf_returnsCompleteBalance_coverage() (gas: -13236 (-0.170%)) 
testJBTieredNFTRewardDelegate_tiers_returnsAllTiersExcludingRemovedOnes_coverage() (gas: -13237 (-0.170%)) 
testJBTieredNFTRewardDelegate_adjustTiers_removeTiers(uint16,uint256,uint8) (gas: -12935 (-0.170%)) 
testJBTieredNFTRewardDelegate_tiers_returnsAllTiers_coverage() (gas: -13236 (-0.170%)) 
testJBTieredNFTRewardDelegate_cleanTiers_removeTheInactiveTiers(uint16,uint256,uint8) (gas: -13114 (-0.172%)) 
testJBTieredNFTRewardDelegate_totalRedemptionWeight_returnsCorrectTotalWeightAsFloorsCumSum_coverage() (gas: -13236 (-0.172%)) 
testJBTieredNFTRewardDelegate_totalSupply_returnsTotalSupply_coverage() (gas: -13236 (-0.172%)) 
testJBTieredNFTRewardDelegate_tier_returnsTheGivenTier_coverage() (gas: -39686 (-0.172%)) 
testJBTieredNFTRewardDelegate_adjustTiers_revertIfAddingWithReservedRate_coverage() (gas: -13236 (-0.172%)) 
testJBTieredNFTRewardDelegate_adjustTiers_revertIfAddingWithVotingPower_coverage() (gas: -13236 (-0.172%)) 
testJBTieredNFTRewardDelegate_adjustTiers_revertIfEmptyQuantity_coverage() (gas: -13236 (-0.172%)) 
testJBTieredNFTRewardDelegate_redemptionWeightOf_returnsCorrectWeightAsFloorsCumSum_coverage() (gas: -13236 (-0.172%)) 
testJBTieredNFTRewardDelegate_mintReservesFor_revertIfReservedMintingIsPausedInFundingCycle() (gas: -13235 (-0.173%)) 
testJBTieredNFTRewardDelegate_tiers_returnsAllTiers(uint16) (gas: -13235 (-0.175%)) 
testJBTieredNFTRewardDelegate_tier_returnsTheGivenTier(uint16,uint16) (gas: -13235 (-0.175%)) 
testJBTieredNFTRewardDelegate_mintFor_revertIfManualMintNotAllowed() (gas: -13235 (-0.175%)) 
testJBTieredNFTRewardDelegate_getvotingUnits_returnsTheTotalVotingUnits(uint16,address) (gas: -13235 (-0.175%)) 
testJBTieredNFTRewardDelegate_balanceOf_returnsCompleteBalance(uint16,address) (gas: -13236 (-0.175%)) 
testJBTieredNFTRewardDelegate_totalRedemptionWeight_returnsCorrectTotalWeightAsFloorsCumSum(uint16) (gas: -13236 (-0.175%)) 
testJBTieredNFTRewardDelegate_totalSupply_returnsTotalSupply(uint16) (gas: -13236 (-0.175%)) 
testJBTieredNFTRewardDelegate_redemptionWeightOf_returnsCorrectWeightAsFloorsCumSum(uint16,uint16,uint16) (gas: -13235 (-0.176%)) 
testLaunchProjectFor_shouldLaunchProject(uint128) (gas: -13233 (-0.269%)) 
testLaunchProjectFor_shouldLaunchProject_nonFuzzed() (gas: -13233 (-0.269%)) 
testJBTieredNFTRewardDelegate_adjustTiers_revertIfAddingWithVotingPower(uint8,uint8) (gas: -71272 (-0.889%)) 
testJBTieredNFTRewardDelegate_adjustTiers_revertIfAddingWithReservedRate(uint8,uint8) (gas: -71347 (-0.890%)) 
testJBTieredNFTRewardDelegate_adjustTiers_revertIfEmptyQuantity(uint8,uint8) (gas: -76315 (-0.953%)) 
Overall gas change: -1412710 (-15.835%)

Modify if statement with different cast for gas saving

Locations:
1
2

Description

Some functions contain a check to only perform an action if an argument isn't the zero address.

The test can be modified slightly to be symantically identical and save gas by avoiding a more-expensive cast.

// Set the token URI resolver if provided.
//if (_tokenUriResolver != IJBTokenUriResolver(address(0))) { 
if (address(_tokenUriResolver) != address(0)) {
    _store.recordSetTokenUriResolver(_tokenUriResolver);
}

Savings

This method saves 3200-3800 gas.

Running 1 test for contracts/forge-test/governance/JB721GlobalGovernance.t.sol:TestJBGlobalGovernance
[PASS] testMintAndTransferGlobalVotingUnits(uint8,bool) (runs: 256, μ: 8964598, ~: 8933856)
Test result: ok. 1 passed; 0 failed; finished in 1.80s
testMintAndTransferGlobalVotingUnits(uint8,bool) (gas: -3827 (-0.043%)) 
Overall gas change: -3827 (-0.043%)

Multiples initializations of `JBTiered721Delegate`

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L218

Vulnerability details

Impact

The initialize method of the JBTiered721Delegate contract has as a flag that the _store argument is different from address(0), however, it can be initialized by anyone with this value to allow the project to continue with its usual initialization, the attacker could have interfered and modified the corresponding values to carry out an attack.

Proof of Concept

Looking at the method below, we highlight in green the parts that need to be initialized to prevent a call to store=address(0) from failing.

  function initialize(
    uint256 _projectId,
    IJBDirectory _directory,
    string memory _name,
    string memory _symbol,
    IJBFundingCycleStore _fundingCycleStore,
    string memory _baseUri,
    IJBTokenUriResolver _tokenUriResolver,
    string memory _contractUri,
    JB721PricingParams memory _pricing,
    IJBTiered721DelegateStore _store,
    JBTiered721Flags memory _flags
  ) public override {
    // Make the original un-initializable.
    require(address(this) != codeOrigin);
    // Stop re-initialization.
    require(address(store) == address(0));

    // Initialize the sub class.
    JB721Delegate._initialize(_projectId, _directory, _name, _symbol);

    fundingCycleStore = _fundingCycleStore;
    store = _store;
    pricingCurrency = _pricing.currency;
    pricingDecimals = _pricing.decimals;
    prices = _pricing.prices;

    // Store the base URI if provided.
+   if (bytes(_baseUri).length != 0) _store.recordSetBaseUri(_baseUri);

    // Set the contract URI if provided.
+   if (bytes(_contractUri).length != 0) _store.recordSetContractUri(_contractUri);

    // Set the token URI resolver if provided.
+   if (_tokenUriResolver != IJBTokenUriResolver(address(0)))
      _store.recordSetTokenUriResolver(_tokenUriResolver);

    // Record adding the provided tiers.
+   if (_pricing.tiers.length > 0) _store.recordAddTiers(_pricing.tiers);

    // Set the flags if needed.
    if (
+     _flags.lockReservedTokenChanges ||
+     _flags.lockVotingUnitChanges ||
+     _flags.lockManualMintingChanges ||
+     _flags.pausable
    ) _store.recordFlags(_flags);

    // Transfer ownership to the initializer.
    _transferOwnership(msg.sender);
  }

So if the attacker initializes the contract as follows:

  • _baseUri = ""
  • _contractUri = ""
  • _tokenUriResolver = address(0)
  • _pricing.tiers = []
  • _flags = all false

The contract will be initialized and transfered the ownership to msg.sender.

After that, the owner can call didPay with the the fake data provided in JBTiered721Delegate.sol:221 and increase creditsOf of anyone JBTiered721Delegate.sol:587 without touching any store call.

  • The attacker can transfer the ownership to the contract, and the project will be able to initialize the contract again without notice.

Recommended Mitigation Steps

  • Ensure that the store address is not empty.

Anyone can create a project with a forged JB721Delegate

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateDeployer.sol#L69
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateProjectDeployer.sol#L70
https://github.com/jbx-protocol/juice-contracts-v3/blob/main/contracts/JBController.sol#L411

Vulnerability details

Impact

An attacker can create a project with a forged JB721Delegate fully controlled by the attacker. The contributors of such a malicious project will lost their funds.

Proof of Concept

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateDeployer.sol#L69

  function deployDelegateFor(
    uint256 _projectId,
    JBDeployTiered721DelegateData memory _deployTiered721DelegateData
  ) external override returns (IJBTiered721Delegate) {
    // Deploy the governance variant that was requested
    address codeToCopy;
    if (_deployTiered721DelegateData.governanceType == JB721GovernanceType.NONE)
      codeToCopy = address(noGovernance);
    else if (_deployTiered721DelegateData.governanceType == JB721GovernanceType.TIERED)
      codeToCopy = address(tieredGovernance);
    else if (_deployTiered721DelegateData.governanceType == JB721GovernanceType.GLOBAL)
      codeToCopy = address(globalGovernance);
    else revert INVALID_GOVERNANCE_TYPE();

The intention of the protocol is to accept only three types of delegates. The deployed delegate will be used as the dataSource of the project (pass through metadata)

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateProjectDeployer.sol#L70

  function launchProjectFor(
    address _owner,
    JBDeployTiered721DelegateData memory _deployTiered721DelegateData,
    JBLaunchProjectData memory _launchProjectData
  ) external override returns (uint256 projectId) {
    // Get the project ID, optimistically knowing it will be one greater than the current count.
    projectId = controller.projects().count() + 1;

    // Deploy the delegate contract.
    IJBTiered721Delegate _delegate = delegateDeployer.deployDelegateFor(
      projectId,
      _deployTiered721DelegateData
    );

    // Set the delegate address as the data source of the provided metadata.
    _launchProjectData.metadata.dataSource = address(_delegate);

    // Set the project to use the data source for its pay function.
    _launchProjectData.metadata.useDataSourceForPay = true;

    // Launch the project.
    _launchProjectFor(_owner, _launchProjectData);
  }

However, launchProjectFor in JBController is a external function without any access control. Anyone can create a project with arbitrary _metadata, or said with arbitrary delegate that is not noGovernance, tieredGovernance or globalGovernance

https://github.com/jbx-protocol/juice-contracts-v3/blob/main/contracts/JBController.sol#L411

  function launchProjectFor(
    address _owner,
    JBProjectMetadata calldata _projectMetadata,
    JBFundingCycleData calldata _data,
    JBFundingCycleMetadata calldata _metadata,
    uint256 _mustStartAtOrAfter,
    JBGroupedSplits[] calldata _groupedSplits,
    JBFundAccessConstraints[] calldata _fundAccessConstraints,
    IJBPaymentTerminal[] memory _terminals,
    string memory _memo
  ) external virtual override returns (uint256 projectId) {

This breaks the assumption of the protocol and such a malformed project can result in fund loss of the contributors afterward.

Recommended Mitigation Steps

Have a check on the delegate contract when creating the project. Or add a new version of launchProjectFor() which has access control to make sure that if it accepts the metadata argument, only specific contracts like JBTiered721DelegateProjectDeployer can call it.

The beneficiary could be the zero address

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L370-L375

Vulnerability details

Impact

Detailed description of the impact of this finding.
There is no zero address check for the beneficiary ,therefore it is possible the beneficiary is zero and we lose all the reserves

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L370-L375

Tools Used

eyes

Recommended Mitigation Steps

Add a line to check the beneficiary is not zero address

QA Report

JUICE-NFT-REWARDS

Low Risk And Non-Critical Issues

Adding a return statement when the function defines a named return variable, is redundant

There are 1 instances of this issue:

File: contracts/JBTiered721Delegate.sol

613:  function _mintBestAvailableTier(
631:  return leftoverAmount;

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721Delegate.sol

Events not emmited on important state changes

Emmiting events is recommended each time when a state variable's value is being changed or just some critical event for the contract has occurred. It also helps off-chain monitoring of the contract's state.

There are 2 instances of this issue:

File: contracts/JBTiered721Delegate.sol

202:  function initialize(

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721Delegate.sol

File: contracts/abstract/JB721Delegate.sol

203:  function _initialize(

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/abstract/JB721Delegate.sol

public functions not called by the contract should be declared external instead

There are 8 instances of this issue:

File: contracts/JB721TieredGovernance.sol

147:  function setTierDelegates(JBTiered721SetTierDelegatesData[] memory _setTierDelegatesData)

177:  function setTierDelegate(address _delegatee, uint256 _tierId) public virtual override {

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JB721TieredGovernance.sol

File: contracts/JBTiered721Delegate.sol

138:  function tokenURI(uint256 _tokenId) public view override returns (string memory) {

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721Delegate.sol

File: contracts/JBTiered721DelegateStore.sol

499:  function balanceOf(address _nft, address _owner) public view override returns (uint256 balance) {

523:  function redemptionWeightOf(address _nft, uint256[] calldata _tokenIds)

550:  function totalRedemptionWeight(address _nft) public view override returns (uint256 weight) {

585:  function tierIdOfToken(uint256 _tokenId) public pure override returns (uint256) {

599:  function reservedTokenBeneficiaryOf(address _nft, uint256 _tierId)

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721DelegateStore.sol

Use 1e18 instead of 10**18 or 1000000000000000000

There are 2 instances of this issue:

File: contracts/JBTiered721Delegate.sol

531:  10**pricingDecimals,

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721Delegate.sol

File: contracts/JBTiered721DelegateDeployer.sol

124:  let _mask := mul(_codeSize, 0x100000000000000000000000000000000000000000000000000000000)

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721DelegateDeployer.sol

Non-library/interface files should use fixed compiler versions, not floating ones

There are 7 instances of this issue:

File: contracts/JB721GlobalGovernance.sol

2:    pragma solidity ^0.8.16;

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JB721GlobalGovernance.sol

File: contracts/JB721TieredGovernance.sol

2:    pragma solidity ^0.8.16;

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JB721TieredGovernance.sol

File: contracts/JBTiered721Delegate.sol

2:    pragma solidity ^0.8.16;

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721Delegate.sol

File: contracts/JBTiered721DelegateDeployer.sol

2:    pragma solidity ^0.8.16;

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721DelegateDeployer.sol

File: contracts/JBTiered721DelegateProjectDeployer.sol

2:    pragma solidity ^0.8.16;

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721DelegateProjectDeployer.sol

File: contracts/JBTiered721DelegateStore.sol

2:    pragma solidity ^0.8.16;

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721DelegateStore.sol

File: contracts/abstract/JB721Delegate.sol

2:    pragma solidity ^0.8.16;

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/abstract/JB721Delegate.sol

Natspec is missing

There are 2 instances of this issue:

File: contracts/libraries/JBTiered721FundingCycleMetadataResolver.sol

1:    // SPDX-License-Identifier: MIT

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/libraries/JBTiered721FundingCycleMetadataResolver.sol

Gas Optimizations

1. [G-1] For loops: ++i cost less gas compare to i++

Instances include:

File contracts/libraries/JBIpfsDecoder.sol, line 68:       for (uint256 i = 0; i < _length; i++) {
File contracts/libraries/JBIpfsDecoder.sol, line 76:       for (uint256 i = 0; i < _input.length; i++) {
File contracts/libraries/JBIpfsDecoder.sol, line 84:       for (uint256 i = 0; i < _indices.length; i++) {

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

2. [G-2] For loops: Cache the length of arrays in the loops to save gas

File contracts/libraries/JBIpfsDecoder.sol, line 49:       for (uint256 i = 0; i < _source.length; ++i) {
File contracts/libraries/JBIpfsDecoder.sol, line 76:       for (uint256 i = 0; i < _input.length; i++) {
File contracts/libraries/JBIpfsDecoder.sol, line 84:       for (uint256 i = 0; i < _indices.length; i++) {

I suggest storing the array’s length in a variable before the for-loop, and use it instead.

3. [G-3] For loops: increments in for loop can be uncheck to save gas

Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn’t possible (as an example, when a comparison is made before the arithmetic operation), some gas can be saved by using an unchecked block

File contracts/libraries/JBIpfsDecoder.sol, line 49:       for (uint256 i = 0; i < _source.length; ++i) {
File contracts/libraries/JBIpfsDecoder.sol, line 51:       for (uint256 j = 0; j < digitlength; ++j) {
File contracts/libraries/JBIpfsDecoder.sol, line 68:       for (uint256 i = 0; i < _length; i++) {
File contracts/libraries/JBIpfsDecoder.sol, line 76:       for (uint256 i = 0; i < _input.length; i++) {
File contracts/libraries/JBIpfsDecoder.sol, line 84:       for (uint256 i = 0; i < _indices.length; i++) {

The code would go from:

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

to:

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

4. [G-4] Variables: 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.

Instances include:

File contracts/libraries/JBIpfsDecoder.sol, line 49:       for (uint256 i = 0; i < _source.length; ++i) {
File contracts/libraries/JBIpfsDecoder.sol, line 51:       for (uint256 j = 0; j < digitlength; ++j) {
File contracts/libraries/JBIpfsDecoder.sol, line 68:       for (uint256 i = 0; i < _length; i++) {
File contracts/libraries/JBIpfsDecoder.sol, line 76:       for (uint256 i = 0; i < _input.length; i++) {
File contracts/libraries/JBIpfsDecoder.sol, line 84:       for (uint256 i = 0; i < _indices.length; i++) {

I suggest removing explicit initializations for default values.

5. [G-5] Variable: Incrementing and Decrementing by 1, ++number cost less gas compare number++ or number += 1

I suggest using ++number instead of number++ or number += 1 here:

File contracts/JBTiered721DelegateStore.sol, line 1106:      numberOfBurnedFor[msg.sender][_tierId]++;
File contracts/JBTiered721DelegateStore.sol, line 1108:      _storedTierOf[msg.sender][_tierId].remainingQuantity++;

File contracts/libraries/JBIpfsDecoder.sol, line 59:       digitlength++;

6. [G-6] Parameter: If we are not modifying the passed parameter we should pass it as calldata because calldata is more gas efficient than memory.

I suggest using calldata instead of memory here:

File contracts/JB721GlobalGovernance.sol, function `_afterTokenTransferAccounting` - line 51.

File contracts/JB721TieredGovernance.sol, functions: `setTierDelegates` - line 147, `_afterTokenTransferAccounting` - line 313.

File contracts/JBTiered721Delegate.sol, functions: `mintReservesFor` - line 264,  `mintFor` - line 290, `mintFor` - line 480, 

File contracts/JBTiered721DelegateDeployer.sol, functions: `deployDelegateFor` - line 69.  

File contracts/JBTiered721DelegateStore.sol, functions: `recordAddTiers` - line 628,  `recordBurn` - line 1091, `_numberOfReservedTokensOutstandingFor` - line 1224.

File contracts/libraries/JBIpfsDecoder.sol, functions: `_toBase58` - line 44, `_truncate` - line 66, `_reverse` - line 74, `_toAlphabet` - line 82.

7. [G-7] Arithmetics: uncheck blocks for arithmetics operations that can't underflow/overflow

Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn't possible, some gas can be saved by using an unchecked block.

I suggest wrapping with an unchecked block here:

File contracts/JB721TieredGovernance.sol, line 323:       return a + b;
File contracts/JB721TieredGovernance.sol, line 327:       return a - b;

File contracts/JBTiered721Delegate.sol, line 529:       
        _value = PRBMath.mulDiv(
            _data.amount.value,
            10**pricingDecimals,
            prices.priceFor(_data.amount.currency, pricingCurrency, _data.amount.decimals)
        );
File contracts/JBTiered721Delegate.sol, line 540:     uint256 _leftoverAmount = _value + _credits;  

File contracts/JBTiered721DelegateStore.sol, line 354:       supply += _storedTier.initialQuantity - _storedTier.remainingQuantity;
File contracts/JBTiered721DelegateStore.sol, line 409:       units += _balance * _storedTierOf[_nft][_i].votingUnits;
File contracts/JBTiered721DelegateStore.sol, line 438:       return _balance * _storedTierOf[_nft][_tierId].votingUnits;
File contracts/JBTiered721DelegateStore.sol, line 506:       balance += tierBalanceOf[_nft][_owner][_i];
File contracts/JBTiered721DelegateStore.sol, line 563:       
        weight +=
            (_storedTier.contributionFloor *
            (_storedTier.initialQuantity - _storedTier.remainingQuantity)) +
            _numberOfReservedTokensOutstandingFor(_nft, _i, _storedTier);
File contracts/JBTiered721DelegateStore.sol, line 685:       uint256 _tierId = _currentMaxTierIdOf + _i + 1;
File contracts/JBTiered721DelegateStore.sol, line 827:       numberOfReservesMintedFor[msg.sender][_tierId] += _count;
File contracts/JBTiered721DelegateStore.sol, line 874:       --tierBalanceOf[msg.sender][_from][_tierId];
File contracts/JBTiered721DelegateStore.sol, line 837-840:       
        tokenIds[_i] = _generateTokenId(
            _tierId,
            _storedTier.initialQuantity - --_storedTier.remainingQuantity + _numberOfBurnedFromTier
        );
File contracts/JBTiered721DelegateStore.sol, line 997:       leftoverAmount = _amount - _bestContributionFloor;
File contracts/JBTiered721DelegateStore.sol, line 1077:       leftoverAmount = leftoverAmount - _storedTier.contributionFloor;
File contracts/JBTiered721DelegateStore.sol, line 1106:       numberOfBurnedFor[msg.sender][_tierId]++;
File contracts/JBTiered721DelegateStore.sol, line 1248:       uint256 _numerator = uint256(_numberOfNonReservesMinted * _storedTier.reservedRate);
File contracts/JBTiered721DelegateStore.sol, line 1251:       uint256 _numberReservedTokensMintable = _numerator / JBConstants.MAX_RESERVED_RATE;
File contracts/JBTiered721DelegateStore.sol, line 1255:       ++_numberReservedTokensMintable;
File contracts/JBTiered721DelegateStore.sol, line 1258:       return _numberReservedTokensMintable - _reserveTokensMinted;

8. [G-8] Variables: Cache read variables in memory will save gas

File contracts/JBTiered721DelegateStore.sol, line 344-359:

        JBStored721Tier storage _storedTier;

        // Keep a reference to the greatest tier ID.
        uint256 _maxTierId = maxTierIdOf[_nft];

        for (uint256 _i = _maxTierId; _i != 0; ) {
                // Set the tier being iterated on.
                _storedTier = _storedTierOf[_nft][_i];

                // Increment the total supply with the amount used already.
                supply += _storedTier.initialQuantity - _storedTier.remainingQuantity;

                unchecked {
                    --_i;
                }
        }

I suggest code replace:

        JBStored721Tier memory _storedTier;
        mapping(address => mapping(uint256 => JBStored721Tier)) memory storedTierOf =  _storedTierOf;

        // Keep a reference to the greatest tier ID.
        uint256 _maxTierId = maxTierIdOf[_nft];

        for (uint256 _i = _maxTierId; _i != 0; ) {
                // Set the tier being iterated on.
                _storedTier = storedTierOf [_nft][_i];

                // Increment the total supply with the amount used already.
                supply += _storedTier.initialQuantity - _storedTier.remainingQuantity;

                unchecked {
                    --_i;
                }
        }

File contracts/JBTiered721DelegateStore.sol, line 403-414:

        for (uint256 _i = _maxTierId; _i != 0; ) {
                // Get a reference to the account's balance in this tier.
                _balance = tierBalanceOf[_nft][_account][_i];
                if (_balance != 0)
                    // Add the tier's voting units.
                    units += _balance * _storedTierOf[_nft][_i].votingUnits; 

                unchecked {
                    --_i;
                }
        }

I suggest code replace:

        mapping(address => mapping(address => mapping(uint256 => uint256))) memory _tierBalanceOf = tierBalanceOf;
        mapping(address => mapping(uint256 => JBStored721Tier)) memory storedTierOf =  _storedTierOf;

        for (uint256 _i = _maxTierId; _i != 0; ) {
                    // Get a reference to the account's balance in this tier.
                    _balance = _tierBalanceOf [_nft][_account][_i];
                    if (_balance != 0)
                        // Add the tier's voting units.
                        units += _balance * storedTierOf[_nft][_i].votingUnits; 

                    unchecked {
                        --_i;
                    }
        }

File contracts/JBTiered721DelegateStore.sol, line 504-511:

        for (uint256 _i = _maxTierId; _i != 0; ) {
                // Get a reference to the account's balance in this tier.
                balance += tierBalanceOf[_nft][_owner][_i];

                unchecked {
                    --_i;
                }
        }

I suggest code replace:

        mapping(address => mapping(address => mapping(uint256 => uint256))) memory _tierBalanceOf = tierBalanceOf;
        for (uint256 _i = _maxTierId; _i != 0; ) {
                // Get a reference to the account's balance in this tier.
                balance += _tierBalanceOf[_nft][_owner][_i];

                unchecked {
                    --_i;
                }
        }

File contracts/JBTiered721DelegateStore.sol, line 533-539:

        for (uint256 _i; _i < _numberOfTokenIds; ) {
                weight += _storedTierOf[_nft][tierIdOfToken(_tokenIds[_i])].contributionFloor;

                unchecked {
                    ++_i;
                }
        }

I suggest code replace:

        mapping(address => mapping(uint256 => JBStored721Tier)) memory storedTierOf =  _storedTierOf;
        for (uint256 _i; _i < _numberOfTokenIds; ) {
                weight += storedTierOf[_nft][tierIdOfToken(_tokenIds[_i])].contributionFloor;

                unchecked {
                    ++_i;
                }
        }

File contracts/JBTiered721DelegateStore.sol, line 558-571:

        for (uint256 _i; _i < _maxTierId; ) {
                _storedTier = _storedTierOf[_nft][_i + 1];

                ...
        }

I suggest code replace:

        mapping(address => mapping(uint256 => JBStored721Tier)) memory storedTierOf =  _storedTierOf;
        for (uint256 _i; _i < _numberOfTokenIds; ) {
                _storedTier = storedTierOf[_nft][_i + 1];

                ...
        }

File contracts/JBTiered721DelegateStore.sol, line 898-911:

        for (uint256 _i; _i < _numTiers; ) {
                ...
                if (_storedTierOf[msg.sender][_tierId].lockedUntil >= block.timestamp) revert TIER_LOCKED();
                ...
        }

I suggest code replace:

        mapping(address => mapping(uint256 => JBStored721Tier)) memory storedTierOf =  _storedTierOf;
        for (uint256 _i; _i < _numTiers; ) {
                ...
                if (storedTierOf[msg.sender][_tierId].lockedUntil >= block.timestamp) revert TIER_LOCKED();
                ...
        }

Gas Optimizations

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol#L133
as >= is more expensive than <
Change the code to the following:

if (_blockNumber < block.number) return _totalTierCheckpoints[_tier].getAtBlock(_blockNumber);
else revert BLOCK_NOT_YET_MINED();

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol#L147
change the argument from memory data to calldata to save gas

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol#L160
No need to cache it into _data, user the _setTierDelegatesData calldata variable directly.

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol#L313
change _tier to a calldata argument to save gas

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol
change codeOrigin, store, fundingCycleStore, prices, pricingCurrency, pricingDecimals, creditsOf to immmutable or possiblly private too since they will set once by the constructor to save gas

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L233
check !=0 is cheaper than checking
change 240 to : if (_pricing.tiers.length != 0) _store.recordAddTiers(_pricing.tiers);

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L202
First of all, change memory _mintReservesForTiersData to calldata _mintReservesForTiersData to save gas since the argument will never be updated. Second, no need to cache it to _data in 273, read each field from _mintReservesForTiersData directly to save gas.

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L290
First of all, change memory _mintForTiersData to calldata _mintForTiersData to save gas since the argument will never be updated. Second, no need to cache it to _data in 300, read each field from _mintForTiersData directly to save gas.

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBBitmap.sol#L52
since it is a storage variable, it can save gas by changing
self[_depth] |= uint256(1 << (_index % 256));
to
self[_depth] = self[_depth] | uint256(1 << (_index % 256));
if the function is called within another write transaction

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L44
if function _toBase58 will be called in another write transaction, then there would be several gas-saving opportunities:

  • _source can be changed to a calldata variable as it has never been updated

  • _source.length should be cached in a local variable to avoid repeated call in a for loop

  • for (uint256 i = 0; i < _source.length; ++i) should be changed to
        for (uint256; i < sourcelength;  unchecked{++i}) to avoid unnecessary initialization, repleated calling of _source.length (use the cached variable), and the unnecessary check
    
  • change
    for (uint256 j = 0; j < digitlength; ++j)
    to
    for (uint256 j; j < digitlength; unchecked{++j}) for similar reason

  • Line 59, change digitallength++ to ++digitallength to save gas

similar for loop optimization can be done for lines 68, 76, and 84.

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L66
_array can be changed to a calldata variable as it is never updated in the function

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L74
_input can be changed to a calldata variable as it is never updated in the function

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L83
_indices can be changed to a calldata variable as it is never updated in the function

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L349
change the forloop to iterate from zero to save the first assignment gas

At the end of B721TieredGovernenance, the _add and _subtract functions are not necessary, simply using SafeMath or compiler version > 8.0 will take care of overflow/underflow issues, and if that is not a concern, simply used unchecked{}.

For function payParams in contract JB721Delegate, the returned value delegateAllocations has no dependency on the input, the function can be simplified to save gas

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/abstract/JB721Delegate.sol#L203
change _name and _symbol to calldata to save gas for the _initialize function

Initialize: No access control for initializatino and possible multiple initializations

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L202

Vulnerability details

Impact

Detailed description of the impact of this finding.
First of all, there is no modifier for access control, any one can call initialize and the two require statements won't provent it.
Second, it is possible that the initialize function is called multiple times as long as the the argument for store is a zero address. This is not desirable

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L202

Tools Used

Read and analyze

Recommended Mitigation Steps

Use a immutable variable uint _isInitialized with a default value of 1, and set it to 2 once the Initialize has been called successfully, in this way, we can make sure the function initialize will be called only once.

Also introduce a list of custom error revert to check all arguments and make sure the initialize function will be "all-or-nothing" and "once-for-all".

Dependence on predictable environment variable

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L891

Vulnerability details

Impact

A control flow decision is made based on The block.timestamp environment variable.
The block.timestamp environment variable is used to determine a control flow decision. Note that the values of variables like coinbase, gaslimit, block number and timestamp are predictable and can be manipulated by a malicious miner. Also keep in mind that attackers know hashes of earlier blocks. Don't use any of those environment variables as sources of randomness and be aware that use of these variables introduces a certain level of trust into miners.

Proof of Concept

  1. https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L891

  2. Contract: JBTiered721DelegateStore
    Function name: recordRemoveTierIds(uint256[])
    PC address: 3814

  3. In file: JBTiered721DelegateStore.sol:891

if (_storedTierOf[msg.sender][_tierId].lockedUntil >= block.timestamp) revert TIER_LOCKED()

Initial State:

Account: [CREATOR], balance: 0x80001218193183, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}

Transaction Sequence:

Caller: [CREATOR], calldata: , value: 0x0
Caller: [SOMEGUY], function: recordRemoveTierIds(uint256[]), txdata: 0x20512ba1000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000004000020, value: 0x0

Tools Used

Manually done

Recommended Mitigation Steps

Developers should write smart contracts with the notion that block values are not precise, and the use of them can lead to unexpected effects. Alternatively, they may make use oracles.

Delegates votes might get lost during transfer

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol#L309-L329

Vulnerability details

Impact

Detailed description of the impact of this finding.
Although zero address check has been performed in function _moveTierDelegateVotes for the _from and _to addresses,
votes get lost when _to is a zero address since the transaction does not revert in this case.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol#L309-L329

Tools Used

By reading

Recommended Mitigation Steps

Make sure to revert the transaction when _from or _to are zero addresses.

Uninitialized Storage Variables

Lines of code

github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L344
github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a6649568016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1024

Vulnerability details

Uninitialized Storage Variables can point to unexpected storage locations.
SWC- 109

Recommendation:
Initialize variable "_storedTier" or set the storage attribute "memory.

`JBTiered721DelegateDeployer.deployDelegateFor` cast every governance type to `JB721GlobalGovernance`

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateDeployer.sol#L83

Vulnerability details

Impact

JBTiered721DelegateDeployer.deployDelegateFor cast every governance type to JB721GlobalGovernance.
If the chosen governance contract type is JB721TieredGovernance or JB721GlobalGovernance, the cloned contract is casted to the wrong contract type (JB721GlobalGovernance).
Therefore, regardless of the choice of the caller, the contract type of the delegate is JB721GlobalGovernance instead of the chosen type of governance.

Proof of Concept

There is 3 types of delegate depending to the governance type attached to it:

  • JB721GlobalGovernance (on-chain governance across all tiers)
  • JB721TieredGovernance (on-chain governance per-tier)
  • JBTiered721Delegate (no on-chain governance)

In the JBTiered721DelegateDeployer.deployDelegateFor function the newly deployed delegate of the chosen type of the caller is casted to the JB721GlobalGovernance contract type and assigned to the newDelegate variable of type JB721GlobalGovernance regardless of the governance type.

Link: https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateDeployer.sol#L83

    JB721GlobalGovernance newDelegate = JB721GlobalGovernance(_clone(codeToCopy));

Tools Used

Manual review.

Recommended Mitigation Steps

I recommend to clone, deploy and cast to the corresponding contract type depending on the governance type of the delegate in the conditionnal statements like this:

    if (_deployTiered721DelegateData.governanceType == JB721GovernanceType.NONE)
      codeToCopy = address(noGovernance);
      JBTiered721Delegate newDelegate = JBTiered721Delegate(_clone(codeToCopy));
    else if (_deployTiered721DelegateData.governanceType == JB721GovernanceType.TIERED)
      codeToCopy = address(tieredGovernance);
      JB721TieredGovernance newDelegate = JB721TieredGovernance(_clone(codeToCopy));
    else if (_deployTiered721DelegateData.governanceType == JB721GovernanceType.GLOBAL)
      codeToCopy = address(globalGovernance);
      JB721GlobalGovernance newDelegate = JB721GlobalGovernance(_clone(codeToCopy));
    else revert INVALID_GOVERNANCE_TYPE();

From my understanding the team want the return variable of the JBTiered721DelegateDeployer.deployDelegateFor function to be a governance agnostic delegate, which make sense considering how the variable is used in JBTiered721DelegateProjectDeployer (only to set the delegate address as the data source). Therefore using IJBTiered721Delegate as return variable type is fine.

Gas Optimizations

Gas

1. Don't use the length of an array for loops condition

It's cheaper to store the length of the array inside a local variable and iterate over it.

Affected source code:

2. Avoid compound assignment operator in state variables

Using compound assignment operators for state variables (like State += X or State -= X ...) it's more expensive than using operator assignment (like State = State + X or State = State - X ...).

Proof of concept (without optimizations):

pragma solidity 0.8.15;

contract TesterA {
uint private _a;
function testShort() public {
_a += 1;
}
}

contract TesterB {
uint private _a;
function testLong() public {
_a = _a + 1;
}
}

Gas saving executing: 13 per entry

TesterA.testShort: 43507
TesterB.testLong:  43494

Affected source code:

Total gas saved: 13 * 1 = 13

3. Use string.concat instead of abi.encodePacked

abi.encodePacked has a cost difference with respect to string.concat depending on the types it uses. In the case of the reference shown below you can see that the type address is affected, so I have decided to do a test on it.

Also there is a discussion about removing abi.encodePacked from future versions of Solidity (ethereum/solidity#11593), so using abi.encode now will ensure compatibility in the future.

Affected source code:

4. Use calldata instead of memory

Some methods are declared as external but the arguments are defined as memory instead of as calldata.

By marking the function as external it is possible to use calldata in the arguments shown below and save significant gas.

Recommended change:

- function mintReservesFor(JBTiered721MintReservesForTiersData[] memory _mintReservesForTiersData)
+ function mintReservesFor(JBTiered721MintReservesForTiersData[] calldata _mintReservesForTiersData)
    external
    override
  {
  ...
  }

Affected source code:

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

++i costs less gas compared to i++ or i += 1 for unsigned integers, 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
I suggest using ++i instead of i++ to increment the value of an uint variable. Same thing for --i and i--

Keep in mind that this change can only be made when we are not interested in the value returned by the operation, since the result is different, you only have to apply it when you only want to increase a counter.

Affected source code:

6. 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 Custom Errors in Solidity:

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).

Proof of concept (without optimizations):

pragma solidity 0.8.15;

contract TesterA {
function testRevert(bool path) public view {
 require(path, "test error");
}
}

contract TesterB {
error MyError(string msg);
function testError(bool path) public view {
 if(path) revert MyError("test error");
}
}

Gas saving executing: 9 per entry

TesterA.testRevert: 21611
TesterB.testError:  21602     

Affected source code:

Total gas saved: 9 * 2 = 18

7. delete optimization

Use delete instead of set to default value (false or 0).

5 gas could be saved per entry in the following affected lines:

Affected source code:

Total gas saved: 5 * 4 = 20

8. There's no need to set default values for variables

If a variable is not set/initialized, the default value is assumed (0, false, 0x0 ... depending on the data type). You are simply wasting gas if you directly initialize it with its default value.

Proof of concept (without optimizations):

pragma solidity 0.8.15;

contract TesterA {
function testInit() public view returns (uint) { uint a = 0; return a; }
}

contract TesterB {
function testNoInit() public view returns (uint) { uint a; return a; }
}

Gas saving executing: 8 per entry

TesterA.testInit:   21392
TesterB.testNoInit: 21384

Affected source code:

Total gas saved: 8 * 1 = 9

9. Remove natspec complaints

Remove natspec complaints in order to save gas. This is not the best way to avoid some warnings.

Affected source code:

10. Use the unchecked keyword

When an underflow or overflow cannot occur, one might conserve gas by using the unchecked keyword to prevent unnecessary arithmetic underflow/overflow tests.

Affected source code:

Lack of access control modifier for initialize function

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L202-L252

Vulnerability details

Impact

  • Lack of validation by using access control modifier will most likely result in loss of funds. It allow an attacker to call critical functions without validating (checking) permission, which could result in loss of funds or change of ownership, etc.

Proof of Concept

Tools Used

  • Foundry

Recommended Mitigation Steps

JBTiered721DelegateStore: Incorrect calculation of totalRedemptionWeight

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L563-L566

Vulnerability details

Impact

In the totalRedemptionWeight() function of the JBTiered721DelegateStore contract, the calculation of the weight in the following code is incorrect, and the result of _numberOfReservedTokensOutstandingFor() should also be multiplied by _storedTier.contributionFloor, which will make the result of totalRedemptionWeight too small.

      weight +=
        (_storedTier.contributionFloor *
          (_storedTier.initialQuantity - _storedTier.remainingQuantity)) +
        _numberOfReservedTokensOutstandingFor(_nft, _i, _storedTier);

And JBSingleTokenPaymentTerminalStore contract's recordRedemptionFor function calls the JB721Delegate contract's redeemParams function, which returns a larger reclaimAmount variable

    uint256 _redemptionWeight = _redemptionWeightOf(_decodedTokenIds);

    // Get a reference to the total redemption weight.
    uint256 _total = _totalRedemptionWeight();

    // Get a reference to the linear proportion.
    uint256 _base = PRBMath.mulDiv(_data.overflow, _redemptionWeight, _total);

    // These conditions are all part of the same curve. Edge conditions are separated because fewer operation are necessary.
    if (_data.redemptionRate == JBConstants.MAX_REDEMPTION_RATE)
      return (_base, _data.memo, delegateAllocations);

    // Return the weighted overflow, and this contract as the delegate so that tokens can be deleted.
    return (
      PRBMath.mulDiv(
        _base,
        _data.redemptionRate +
          PRBMath.mulDiv(
            _redemptionWeight,
            JBConstants.MAX_REDEMPTION_RATE - _data.redemptionRate,
            _total
          ),
        JBConstants.MAX_REDEMPTION_RATE
      ),
      _data.memo,
      delegateAllocations
    );
...
        (reclaimAmount, memo, delegate) = IJBFundingCycleDataSource(fundingCycle.dataSource())
          .redeemParams(_data);
      } else {
        memo = _memo;
      }
    }

    // The amount being reclaimed must be within the project's balance.
    if (reclaimAmount > balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId])
      revert INADEQUATE_PAYMENT_TERMINAL_STORE_BALANCE();

    // Remove the reclaimed funds from the project's balance.
    if (reclaimAmount > 0)
      balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId] =
        balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId] -
        reclaimAmount;
  }

Finally, in the _redeemTokensOf function of the JBPayoutRedemptionPaymentTerminal contract, the user will receive more tokens

      (_fundingCycle, reclaimAmount, _delegateAllocations, _memo) = store.recordRedemptionFor(
        _holder,
        _projectId,
        _tokenCount,
        _memo,
        _metadata
      );
...
    if (reclaimAmount > 0) _transferFrom(address(this), _beneficiary, reclaimAmount);

Proof of Concept

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L563-L566
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/abstract/JB721Delegate.sol#L136-L159
https://github.com/jbx-protocol/juice-jbcontroller-v2.1/blob/4711be3644e02fd47792b6e573402bafe3589541/contracts/JBSingleTokenPaymentTerminalStore.sol#L506-L521
https://github.com/jbx-protocol/juice-contracts-v3/blob/main/contracts/abstract/JBPayoutRedemptionPaymentTerminal.sol#L832

Tools Used

None

Recommended Mitigation Steps

Change to

      weight +=
        (_storedTier.contributionFloor *
          (_storedTier.initialQuantity - _storedTier.remainingQuantity + _numberOfReservedTokensOutstandingFor(_nft, _i, _storedTier)));

Gas Optimizations

JUICE-NFT-REWARDS

Gas Optimizations Report

The usage of ++i will cost less gas than i++. The same change can be applied to i-- as well.

This change would save up to 6 gas per instance/loop.

There are 5 instances of this issue:

File: contracts/JBTiered721DelegateStore.sol

1108: _storedTierOf[msg.sender][_tierId].remainingQuantity++;

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721DelegateStore.sol

File: contracts/libraries/JBIpfsDecoder.sol

59:    digitlength++;

68:   for (uint256 i = 0; i < _length; i++) {

76:   for (uint256 i = 0; i < _input.length; i++) {

84:   for (uint256 i = 0; i < _indices.length; i++) {

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/libraries/JBIpfsDecoder.sol

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

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

There are 1 instances of this issue:

File: contracts/JBTiered721Delegate.sol

      /// @audit Cache `store`. Used 3 times in `tokenURI`
143:  IJBTokenUriResolver _resolver = store.tokenUriResolverOf(address(this));
151:  store.baseUriOf(address(this)),
152:  store.encodedTierIPFSUriOf(address(this), _tokenId)

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721Delegate.sol

internal and private functions that are called only once should be inlined.

The execution of a non-inlined function would cost up to 40 more gas because of two extra jumps as well as some other instructions.

There are 4 instances of this issue:

File: contracts/libraries/JBIpfsDecoder.sol

44:   function _toBase58(bytes memory _source) private pure returns (string memory) {

66:   function _truncate(uint8[] memory _array, uint8 _length) private pure returns (uint8[] memory) {

74:   function _reverse(uint8[] memory _input) private pure returns (uint8[] memory) {

82:   function _toAlphabet(uint8[] memory _indices) private pure returns (bytes memory) {

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/libraries/JBIpfsDecoder.sol

Using != 0 on uints costs less gas than > 0.

This change saves 3 gas per instance/loop

There are 2 instances of this issue:

File: contracts/JBTiered721DelegateStore.sol

1254: if (_numerator - JBConstants.MAX_RESERVED_RATE * _numberReservedTokensMintable > 0)

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721DelegateStore.sol

File: contracts/libraries/JBIpfsDecoder.sol

57:   while (carry > 0) {

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/libraries/JBIpfsDecoder.sol

It costs more gas to initialize non-constant/non-immutable variables to zero than to let the default of zero be applied

Not overwriting the default for stack variables saves 8 gas. Storage and memory variables have larger savings

There are 5 instances of this issue:

File: contracts/libraries/JBIpfsDecoder.sol

49:   for (uint256 i = 0; i < _source.length; ++i) {

51:   for (uint256 j = 0; j < digitlength; ++j) {

68:   for (uint256 i = 0; i < _length; i++) {

76:   for (uint256 i = 0; i < _input.length; i++) {

84:   for (uint256 i = 0; i < _indices.length; i++) {

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/libraries/JBIpfsDecoder.sol

Functions that are access-restricted from most users may be marked as payable

Marking a function as payable reduces gas cost since the compiler does not have to check whether a payment was provided or not. This change will save around 21 gas per function call.

There are 4 instances of this issue:

File: contracts/JBTiered721Delegate.sol

370:  function setDefaultReservedTokenBeneficiary(address _beneficiary) external override onlyOwner {

386:  function setBaseUri(string memory _baseUri) external override onlyOwner {

402:  function setContractUri(string calldata _contractUri) external override onlyOwner {

418:  function setTokenUriResolver(IJBTokenUriResolver _tokenUriResolver) external override onlyOwner {

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721Delegate.sol

++i/i++ should be unchecked{++I}/unchecked{I++} in for-loops

When an increment or any arithmetic operation is not possible to overflow it should be placed in unchecked{} block. \This is because of the default compiler overflow and underflow safety checks since Solidity version 0.8.0. \In for-loops it saves around 30-40 gas per loop

There are 7 instances of this issue:

File: contracts/JBTiered721Delegate.sol

341:    ++_i;

355:    ++_i;

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721Delegate.sol

File: contracts/libraries/JBIpfsDecoder.sol

49:   for (uint256 i = 0; i < _source.length; ++i) {

51:   for (uint256 j = 0; j < digitlength; ++j) {

68:   for (uint256 i = 0; i < _length; i++) {

76:   for (uint256 i = 0; i < _input.length; i++) {

84:   for (uint256 i = 0; i < _indices.length; i++) {

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/libraries/JBIpfsDecoder.sol

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

There are 1 instances of this issue:

File: contracts/JBTiered721DelegateStore.sol

827:  numberOfReservesMintedFor[msg.sender][_tierId] += _count;

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721DelegateStore.sol

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.15/internals/layout_in_storage.html \ Use a larger size then downcast where needed

There are 2 instances of this issue:

File: contracts/libraries/JBIpfsDecoder.sol

48:   uint8 digitlength = 1;

66:   function _truncate(uint8[] memory _array, uint8 _length) private pure returns (uint8[] memory) {

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/libraries/JBIpfsDecoder.sol

Use calldata instead of memory for function parameters

If a reference type function parameter is read-only, it is cheaper in gas to use calldata instead of memory. Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory. Try to use calldata as a data location because it will avoid copies and also makes sure that the data cannot be modified.

There are 41 instances of this issue:

File: contracts/JB721GlobalGovernance.sol

55:   JB721Tier memory _tier

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JB721GlobalGovernance.sol

File: contracts/JB721TieredGovernance.sol

147:  function setTierDelegates(JBTiered721SetTierDelegatesData[] memory _setTierDelegatesData)

313:  JB721Tier memory _tier

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JB721TieredGovernance.sol

File: contracts/JBTiered721Delegate.sol

205:  string memory _name,

206:  string memory _symbol,

208:  string memory _baseUri,

210:  string memory _contractUri,

211:  JB721PricingParams memory _pricing,

213:  JBTiered721Flags memory _flags

264:  function mintReservesFor(JBTiered721MintReservesForTiersData[] memory _mintReservesForTiersData)

290:  function mintFor(JBTiered721MintForTiersData[] memory _mintForTiersData)

386:  function setBaseUri(string memory _baseUri) external override onlyOwner {

480:  function mintFor(uint16[] memory _tierIds, address _beneficiary)

598:  function _didBurn(uint256[] memory _tokenIds) internal override {

652:  uint16[] memory _mintTierIds,

695:  function _redemptionWeightOf(uint256[] memory _tokenIds)

789:  JB721Tier memory _tier

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721Delegate.sol

File: contracts/JBTiered721DelegateDeployer.sol

71:   JBDeployTiered721DelegateData memory _deployTiered721DelegateData

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721DelegateDeployer.sol

File: contracts/JBTiered721DelegateProjectDeployer.sol

72:   JBDeployTiered721DelegateData memory _deployTiered721DelegateData,

73:   JBLaunchProjectData memory _launchProjectData

109:  JBDeployTiered721DelegateData memory _deployTiered721DelegateData,

110:  JBLaunchFundingCyclesData memory _launchFundingCyclesData

152:  JBDeployTiered721DelegateData memory _deployTiered721DelegateData,

153:  JBReconfigureFundingCyclesData memory _reconfigureFundingCyclesData

191:  function _launchProjectFor(address _owner, JBLaunchProjectData memory _launchProjectData)

218:  JBLaunchFundingCyclesData memory _launchFundingCyclesData

244:  JBReconfigureFundingCyclesData memory _reconfigureFundingCyclesData

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721DelegateProjectDeployer.sol

File: contracts/JBTiered721DelegateStore.sol

628:  function recordAddTiers(JB721TierParams[] memory _tiersToAdd)

1091: function recordBurn(uint256[] memory _tokenIds) external override {

1227: JBStored721Tier memory _storedTier

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721DelegateStore.sol

File: contracts/abstract/JB721Delegate.sol

206:  string memory _name,

207:  string memory _symbol

311:  function _didBurn(uint256[] memory _tokenIds) internal virtual {

323:  function _redemptionWeightOf(uint256[] memory _tokenIds) internal view virtual returns (uint256) {

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/abstract/JB721Delegate.sol

File: contracts/libraries/JBBitmap.sol

29:   function isTierIdRemoved(JBBitmapWord memory self, uint256 _index) internal pure returns (bool) {

59:   function refreshBitmapNeeded(JBBitmapWord memory self, uint256 _index)

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/libraries/JBBitmap.sol

File: contracts/libraries/JBIpfsDecoder.sol

22:   function decode(string memory _baseUri, bytes32 _hexString)

      /// @audit Store `_source` in calldata.
44:   function _toBase58(bytes memory _source) private pure returns (string memory) {

      /// @audit Store `_array` in calldata.
66:   function _truncate(uint8[] memory _array, uint8 _length) private pure returns (uint8[] memory) {

      /// @audit Store `_input` in calldata.
74:   function _reverse(uint8[] memory _input) private pure returns (uint8[] memory) {

      /// @audit Store `_indices` in calldata.
82:   function _toAlphabet(uint8[] memory _indices) private pure returns (bytes memory) {

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/libraries/JBIpfsDecoder.sol

Replace x <= y with x < y + 1, and x >= y with x > y - 1

In the EVM, there is no opcode for >= or <=. When using greater than or equal, two operations are performed: > and =. Using strict comparison operators hence saves gas

There are 2 instances of this issue:

File: contracts/JB721TieredGovernance.sol

133:  if (_blockNumber >= block.number) revert BLOCK_NOT_YET_MINED();

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JB721TieredGovernance.sol

File: contracts/JBTiered721DelegateStore.sol

903:  if (_storedTierOf[msg.sender][_tierId].lockedUntil >= block.timestamp) revert TIER_LOCKED();

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721DelegateStore.sol

Use immutable & constant for state variables that do not change their value

There are 1 instances of this issue:

File: contracts/JBTiered721Delegate.sol

48:   address public override codeOrigin;

https://github.com/jbx-protocol/juice-nft-rewards/tree/main/contracts/JBTiered721Delegate.sol

Gas Optimizations

1.Unused Custom error defined which is cause directly a waste of gas:

error PRICING_RESOLVER_CHANGES_LOCKED();
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L37

error PRICING_RESOLVER_CHANGES_PAUSED();
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L36

2.!= 0 is a cheaper operation compared to > 0, when dealing with uint.

if (_numerator - JBConstants.MAX_RESERVED_RATE * _numberReservedTokensMintable > 0)
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1254

if (_pricing.tiers.length > 0) _store.recordAddTiers(_pricing.tiers);

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L240

Incorrect calculation of totalRedemptionWeight in JBTiered721DelegateStore.sol, we use weight + number of reserved token instead of weight + weight

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L550
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L563
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L566

Vulnerability details

Impact

After NFT minted, the NFT has redemption weight based on the contribution floor price,

the logic that calculate the total redemption weight is

_storedTier = _storedTierOf[_nft][_i + 1];

// Add the tier's contribution floor multiplied by the quantity minted.
weight +=
(_storedTier.contributionFloor *
  (_storedTier.initialQuantity - _storedTier.remainingQuantity)) +
_numberOfReservedTokensOutstandingFor(_nft, _i, _storedTier);

However, I think the calculation is incorrect. we are trying to calcuate the total weight, but _numberOfReservedTokensOutstandingFor(_nft, _i, _storedTier) just return us the number of reserved token.

We are using contribute floor price * minted quanity + number of reserved token, we want to get weight, but we are using the weight to add with a number.

We should use contribute floor price * (minted quanity + number of reserved token) to get total weight.

Proof of Concept

The number of reserved token is 6, the we set the contributionFloor to 1 ether, and initial supply to 10,

the reserved token weight is negilible comparing to contribute floor price * minted quanity.

let us say,

all the inital supply is minted, the total contribution power

(_storedTier.contributionFloor * (_storedTier.initialQuantity - _storedTier.remainingQuantity))

=

1 ether * 10 = 10 ether = 10 ** 19

the _numberOfReservedTokensOutstandingFor(_nft, _i, _storedTier) is just 6

6 is negilible comparing to 10 ** 19 for sure.

Tools Used

Manual Review

Recommended Mitigation Steps

I think use weight * (minted quanity + number of reserved token) to get total weight should be valid.

uint256 nftNumbers = _storedTier.initialQuantity - _storedTier.remainingQuantity + _numberOfReservedTokensOutstandingFor(_nft, _i, _storedTier);

weight += nftNumbers * _storedTier.contributionFloor

QA Report

Please lock all solidity compiler versions for best optimization

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol#L312
delete _tokenId as it has never needed

One suggestion is to use custom error Revert to process exception in most write transactions

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol#L177

Use a locked version of solidity compiler for all contracts

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol#L177
Suggest to change function setTierDelegate to external if it will not be called by the contract itself

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JB721TieredGovernance.sol#L44-L53
Consider combing the two mappings into one mapping from address->uint256->struct

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L216-L218
use custom error revert instead of require without an error msg

I suggest to lock all solidity compiler versions for all files and use the most recent version

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBTiered721FundingCycleMetadataResolver.sol
all the three functions can be simplified to save gas if these functions are called in other write transactions.

change
return (_data & 1) == 1;
to
return _data & 1;

change
return ((_data >> 1) & 1) == 1;
to
return (_data & 2) == 2;

change
return ((_data >> 2) & 1) == 1;
to
return (_data & 4) == 4;

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1123
Consider to combine several of mappings Address - X (.....maxTierIdOf, tierBalanceOf, numberOfReservesMintedFor, numberOfBurnedFor, defaultReservedTokenBeneficiaryOf, firstOwnerOf, baseUriOf, tokenUriResolverOf, contractUriOf, encodedIPFSUriOf) into one mapping Address-> Struct to improve code readability and possibly gas saving due to packing
In particular define
struct NFTContractInfo {
mapping(uint256 => uint256) maxTierIdOf;
mapping(uint256 => address) _reservedTokenBeneficiaryOf;
mapping(uint256 => JBStored721Tier) _storedTierOf;
JBTiered721Flags _flagsOf;
mapping(uint256 => uint256) _isTierRemoved;
uint256 trackedLastSortTierIdOf;
uint256 maxTierIdOf;
mapping(address => mapping(uint256 => uint256)) tierBalanceOf;
....
}
Then define a mapping(address => NFTContractInfo)
This is just an example, the authors can define several structs as well, to group related variables in one struct, for example.

It might be helpful to refactor all custom erros into one file and import in each contract.

Maybe it is a good idea to unify the concepts of JB721Tier and JBStored721Tier to avoid the conversion between them back and forth. Much code looks like this:

JB721Tier({
        id: _id,
        contributionFloor: _storedTier.contributionFloor,
        lockedUntil: _storedTier.lockedUntil,
        remainingQuantity: _storedTier.remainingQuantity,
        initialQuantity: _storedTier.initialQuantity,
        votingUnits: _storedTier.votingUnits,
        reservedRate: _storedTier.reservedRate,
        reservedTokenBeneficiary: reservedTokenBeneficiaryOf(_nft, _id),
        encodedIPFSUri: encodedIPFSUriOf[_nft][_id],
        allowManualMint: _storedTier.allowManualMint
      });
which wastes much gas.

User can mint more NFT than initial supply because of improper check of _storedTier.remainingQuantity in /JBTiered721DelegateStore#recordMint

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L480
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L487
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L504
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1012
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1070

Vulnerability details

Impact

User can mint more NFT initial supply because of inproper check of _storedTier.remainingQuantity

For example, the owner can set the initial amount to 3, but 6 NFT can be minted.

when the mintFor is called, the function calls JBTiered721DelegateStore.sol to update the remaining amount and mint.

function mintFor(uint16[] memory _tierIds, address _beneficiary)
public
override
onlyOwner
returns (uint256[] memory tokenIds)

then we call

(tokenIds, ) = store.recordMint(
  type(uint256).max, // force the mint.
  _tierIds,
  true // manual mint
);

then we just compute the tokenId and mint

 if (
	_storedTier.remainingQuantity -
	  _numberOfReservedTokensOutstandingFor(msg.sender, _tierId, _storedTier) ==
	0
  ) revert OUT();

  // Mint the tokens.
  unchecked {
	// Keep a reference to the token ID.
	uint256 generatedId = _generateTokenId(
	  _tierId,
	  _storedTier.initialQuantity -
		--_storedTier.remainingQuantity +
		numberOfBurnedFor[msg.sender][_tierId]
	);
	tokenIds[_i] = generatedId;
  }

and we mint

for (uint256 _i; _i < _numberOfTokens; ) {
// Set the token ID.
_tokenId = tokenIds[_i];

// Mint the token.
_mint(_beneficiary, _tokenId);

however, the function failed to check if there are enough remaining amount of NFT to be minted, result in over-mint.

the code below should underflow and revert

--_storedTier.remainingQuantity

but it will not underflow and revert because it is wrapped in unchecked block scope

uint256 generatedId = _generateTokenId(
  _tierId,
  _storedTier.initialQuantity -
	--_storedTier.remainingQuantity +
	numberOfBurnedFor[msg.sender][_tierId]
);

the impact is that owner (usre) can mint more NFT than initial supply. NFT has value because it has limited supply. allowing nft to over-mint is not fair for normal user and it decrease the NFT value.

as shown in POC.

Proof of Concept

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/forge-test/NFTReward_Unit.t.sol#L1689

we change the test name from

testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTier

to

testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTier_HIGH

the initial supply is set to 100

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/forge-test/NFTReward_Unit.t.sol#L1706

for (uint256 i; i < nbTiers; i++) {
  _tiers[i] = JB721TierParams({
	contributionFloor: uint80((i + 1) * 10),
	lockedUntil: uint48(0),
	initialQuantity: uint40(100),
	votingUnits: uint16(0),
	reservedRate: uint16(0),
	reservedTokenBeneficiary: reserveBeneficiary,
	encodedIPFSUri: tokenUris[i],
	allowManualMint: true, // Allow this type of mint
	shouldUseBeneficiaryAsDefault: false
});

we can change the initial supply to 2

for (uint256 i; i < nbTiers; i++) {
  _tiers[i] = JB721TierParams({
	contributionFloor: uint80((i + 1) * 10),
	lockedUntil: uint48(0),
	initialQuantity: uint40(2),
	votingUnits: uint16(0),
	reservedRate: uint16(0),
	reservedTokenBeneficiary: reserveBeneficiary,
	encodedIPFSUri: tokenUris[i],
	allowManualMint: true, // Allow this type of mint
	shouldUseBeneficiaryAsDefault: false
});

we add a console.log for debugging at

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/forge-test/NFTReward_Unit.t.sol#L1744

console.log(_delegate.balanceOf(beneficiary))
assertEq(_delegate.balanceOf(beneficiary), 6);

we run the test

forge test -vv --match testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTiers_HIGH

the test pass, the test should not pass

the test is expect to fail!!

because the initial supply is set to 2 but the balanceOf nft return 6.

Running 1 test for contracts/forge-test/NFTReward_Unit.t.sol:TestJBTieredNFTRewardDelegate
[PASS] testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTiers_HIGH() (gas: 7929232)
Logs:
  6

Test result: ok. 1 passed; 0 failed; finished in 11.20ms

Tools Used

Foundry, Manual Review

Recommended Mitigation Steps

We recommend the project check if the we are available remaining balance before doing the nft.

we can add the check

if (_storedTier.remainingQuantity == 0) {
	revert Insufficient_supply
}

before the nft id generation.

also we can move the _storedTier.remainingQuantity out of the unchecked scope so the --_storedTier.remainingQuantity can properly underflow and revert instead of fail sliently and result in potential over-mint.

Missing modifier allow infinite mint for tier

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/main/contracts/JBTiered721Delegate.sol#L436
https://github.com/jbx-protocol/juice-nft-rewards/blob/main/contracts/JBTiered721Delegate.sol#L264

Vulnerability details

Impact

It seems mintReservesFor function is missing the onlyOwner modifier which means Attacker can call this function to mint any amount of reserved tokens

Proof of Concept

  1. Observe the mintReservesFor function
function mintReservesFor(JBTiered721MintReservesForTiersData[] memory _mintReservesForTiersData)
    external
    override
  {
    // Keep a reference to the number of tiers there are to mint reserved for.
    uint256 _numberOfTiers = _mintReservesForTiersData.length;

    for (uint256 _i; _i < _numberOfTiers; ) {
      // Get a reference to the data being iterated on.
      JBTiered721MintReservesForTiersData memory _data = _mintReservesForTiersData[_i];

      // Mint for the tier.
      mintReservesFor(_data.tierId, _data.count);

      unchecked {
        ++_i;
      }
    }
  }
  1. There is no modifier check and any user can call this function with arbitrary _mintReservesForTiersData to mint any amount of reserved tokens

Recommended Mitigation Steps

Add a modifier onlyOwner to mintReservesFor function

QA Report

QA ISSUES FOR JUICE-NFT-REWARDS

[N-01] Adding a return statement when the function defines a named return variable, is redundant

./contracts/JBTiered721Delegate.sol

L613:  function _mintBestAvailableTier(
L631:  return leftoverAmount;

[L-02] Not emiting events in some important functions

Description: Emmiting events is recommended each time when a state variable's value is being changed or just some critical event for the contract has occurred. It also helps off-chain monitoring of the contract's state.

./contracts/JBTiered721Delegate.sol

L202:  function initialize(

./contracts/abstract/JB721Delegate.sol

L203:  function _initialize(

[N-03] Non-library/interface files should use fixed compiler versions, not floating ones

./contracts/JB721TieredGovernance.sol

L2:    pragma solidity ^0.8.16;

./contracts/JBTiered721DelegateProjectDeployer.sol

L2:    pragma solidity ^0.8.16;

./contracts/JB721GlobalGovernance.sol

L2:    pragma solidity ^0.8.16;

./contracts/JBTiered721DelegateDeployer.sol

L2:    pragma solidity ^0.8.16;

./contracts/JBTiered721DelegateStore.sol

L2:    pragma solidity ^0.8.16;

./contracts/abstract/JB721Delegate.sol

L2:    pragma solidity ^0.8.16;

./contracts/JBTiered721Delegate.sol

L2:    pragma solidity ^0.8.16;

[N-04] Not using the NatSpec format

./contracts/libraries/JBTiered721FundingCycleMetadataResolver.sol

L1:    // SPDX-License-Identifier: MIT

QA Report

  1. It is good practice to use error strings in require statements

Using error strings in require statements enables the caller to be notified as to why the call failed if it fails. Consider adding understandable strings in require statements as error strings.

Instances:
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L216
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L218

Gas Optimizations

Owner can mint and reserve vast majority of nft, which is not fair to other user.

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L290
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L480

Vulnerability details

Impact

The nft ownership is meanted to be decentralized for on-chain governance, however, owner can mint and reserve vast majority of nft, which is not fair to other user, undermining the decentralized assumption.

the ower can call

function mintFor(uint16[] memory _tierIds, address _beneficiary)
public
override
onlyOwner
returns (uint256[] memory tokenIds)
{

to mint vast majority of nft.

Proof of Concept

we can change the test name for POC purpose

Proof of Concept

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/forge-test/NFTReward_Unit.t.sol#L1689

We can add this POC

 function testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTiers_Centralize_POC() public {
    uint256 nbTiers = 1;

    vm.mockCall(
      mockJBProjects,
      abi.encodeWithSelector(IERC721.ownerOf.selector, projectId),
      abi.encode(owner)
    );

    JB721TierParams[] memory _tiers = new JB721TierParams[](nbTiers);
    uint16[] memory _tiersToMint = new uint16[](nbTiers);

    // Temp tiers, will get overwritten later (pass the constructor check)
    for (uint256 i; i < nbTiers; i++) {
      _tiers[i] = JB721TierParams({
        contributionFloor: uint80((i + 1) * 10),
        lockedUntil: uint48(0),
        initialQuantity: uint40(5),
        votingUnits: uint16(0),
        reservedRate: uint16(0),
        reservedTokenBeneficiary: reserveBeneficiary,
        encodedIPFSUri: tokenUris[i],
        allowManualMint: true, // Allow this type of mint
        shouldUseBeneficiaryAsDefault: false
      });

      _tiersToMint[i] = uint16(i)+1;
      _tiersToMint[_tiersToMint.length - 1 - i] = uint16(i)+1;
    }

    ForTest_JBTiered721DelegateStore _ForTest_store = new ForTest_JBTiered721DelegateStore();
    ForTest_JBTiered721Delegate _delegate = new ForTest_JBTiered721Delegate(
      projectId,
      IJBDirectory(mockJBDirectory),
      name,
      symbol,
      IJBFundingCycleStore(mockJBFundingCycleStore),
      baseUri,
      IJBTokenUriResolver(mockTokenUriResolver),
      contractUri,
      _tiers,
      IJBTiered721DelegateStore(address(_ForTest_store)),
      JBTiered721Flags({
        lockReservedTokenChanges: false,
        lockVotingUnitChanges: false,
        lockManualMintingChanges: true,
        pausable: true
      })
    );

    _delegate.transferOwnership(owner);

    vm.startPrank(owner);

    for(uint256 i = 0; i < 4; i++) {
        _delegate.mintFor(_tiersToMint, beneficiary);
    }

    vm.stopPrank();

    console.log("_delegate.balanceOf(beneficiary)");
    console.log(_delegate.balanceOf(beneficiary));
 
}

note the initial amount (total supply) is set to 5

initialQuantity: uint40(5)

we run the test

forge test -vv --match testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTiers_Centralize_POC

the result is

[PASS] testJBTieredNFTRewardDelegate_mintFor_mintArrayOfTiers_Centralize_POC() (gas: 7797133)
Logs:
  _delegate.balanceOf(beneficiary)
  4

Test result: ok. 1 passed; 0 failed; finished in 11.37ms

now 4 out of 5 is minted to owner, the nft is greatly centralized.

then only one of the nft is available for user to purchase via didPay -> _processPayment

Tools Used

manual review, foundry

Recommended Mitigation Steps

We recommend the project limit the percetage of the reserve nft amount / nft total supply to make sure the owner cannot mint vast majority of nft supply.

Contracts without GAP can lead a upgrade disaster

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L29

Vulnerability details

Impact

Some contracts do not implement good upgradeable logic.

Proof of Concept

Upgrading a contract will need a __gap storage in order to avoid storage collisions.

Proxied contracts MUST include an empty reserved space in storage, usually named __gap, in all the inherited contracts.

For example, the contract Ownable or JB721Delegate are inherited by upgradeable contracts (JBTiered721Delegate), but these contracts don't have a __gap storage, so if a new variable is created it could lead to storage collisions that will produce a contract disaster, including loss of funds.

Reference:

Affected source code:

Recommended Mitigation Steps

  • Add uint256[50] private __gap; to all contracts.

QA Report

Low

1. Outdated packages

Some used packages are out of date, it is good practice to use the latest version of these packages:

"@openzeppelin/contracts": "^4.6.0"

Reference:

2. Use string.concat instead of abi.encodePacked

Tthere is a discussion about removing abi.encodePacked from future versions of Solidity (ethereum/solidity#11593), so using abi.encode now will ensure compatibility in the future.

Affected source code:

3. Integer overflow by unsafe casting

Keep in mind that the version of solidity used, despite being greater than 0.8, does not prevent integer overflows during casting, it only does so in mathematical operations.

It is necessary to safely convert between the different numeric types.

Recommendation:

Use a safeCast from Open Zeppelin.

  function tierIdOfToken(uint256 _tokenId) public pure override returns (uint256) {
    // The tier ID is in the first 16 bits.
    return uint256(uint16(_tokenId));
  }

Affected source code:

4. Lack of checks supportsInterface

The EIP-165 standard helps detect that a smart contract implements the expected logic, prevents human error when configuring smart contract bindings, so it is recommended to check that the received argument is a contract and supports the expected interface.

Reference:

Affected source code:

5. Lack of initialize protections

The following contracts are updateable, and follow the update pattern, these contracts contain an initialize method to set the contract once, but anyone can call this method, this will spend project fuel if someone tries to initialize it before the project.

It is recommended to check the sender when initializing the contract.

Affected source code:

QA Report

Out-of-scope*******
////Low-Risk/////
In contract
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/structs/JB721Tier.sol#L24

address reservedTokenBeneficiary;
but here in your comments it defined on the basis of 'Rate': @member reservedRateBeneficiary The beneificary of the reserved tokens for this tier.

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/structs/JB721Tier.sol#L12


Issue:Low-Risk
1.Zero-address checks is missing in JBTiered721DelegateStore.tier and JBTiered721DelegateStore.balanceOf as it is considered a best-practise for input validation of critical address parameters.

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L279
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L499
https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L523

Make a address check on parameter _nft,_owner:

2.Make a require check condition on JBTiered721DelegateStore.tierOfTokenId so that revert would not get execute on undesired input that can be made willingly/unwillingly will leads to waste of gas:

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L307

Make a check on _tokenId != 0: As here in this function it 'Return the tier for the specified token ID'.

Making a payment to the protocol with `_dontMint` parameter will result in lost fund for user.

Lines of code

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L524-L590

Vulnerability details

Impact

User will have their funds lost if they tries to pay the protocol with _dontMint = False. A payment made with this parameter set should increase the creditsOf[] balance of user.

In _processPayment(), creditsOf[_data.beneficiary] is updated at the end if there are leftover funds. However, If metadata is provided and _dontMint == true, it immediately returns.
JBTiered721Delegate.sol#L524-L590

  function _processPayment(JBDidPayData calldata _data) internal override {
    // Keep a reference to the amount of credits the beneficiary already has.
    uint256 _credits = creditsOf[_data.beneficiary];
    ...
    if (
      _data.metadata.length > 36 &&
      bytes4(_data.metadata[32:36]) == type(IJB721Delegate).interfaceId
    ) {
      ...
      // Don't mint if not desired.
      if (_dontMint) return;
      ...
    }
    ...
    // If there are funds leftover, mint the best available with it.
    if (_leftoverAmount != 0) {
      _leftoverAmount = _mintBestAvailableTier(
        _leftoverAmount,
        _data.beneficiary,
        _expectMintFromExtraFunds
      );

      if (_leftoverAmount != 0) {
        // Make sure there are no leftover funds after minting if not expected.
        if (_dontOverspend) revert OVERSPENDING();

        // Increment the leftover amount.
        creditsOf[_data.beneficiary] = _leftoverAmount;
      } else if (_credits != 0) creditsOf[_data.beneficiary] = 0;
    } else if (_credits != 0) creditsOf[_data.beneficiary] = 0;
  }

Proof of Concept

I've wrote a coded POC to illustrate this. It uses the same Foundry environment used by the project. Simply copy this function to E2E.t.sol to verify.

  function testPaymentNotAddedToCreditsOf() public{
    address _user = address(bytes20(keccak256('user')));
    (
      JBDeployTiered721DelegateData memory NFTRewardDeployerData,
      JBLaunchProjectData memory launchProjectData
    ) = createData();

    uint256 projectId = deployer.launchProjectFor(
      _projectOwner,
      NFTRewardDeployerData,
      launchProjectData
    );

    // Get the dataSource
    IJBTiered721Delegate _delegate = IJBTiered721Delegate(
      _jbFundingCycleStore.currentOf(projectId).dataSource()
    );

    address NFTRewardDataSource = _jbFundingCycleStore.currentOf(projectId).dataSource();

    uint256 _creditBefore = IJBTiered721Delegate(NFTRewardDataSource).creditsOf(_user);

    // Project is initiated with 10 different tiers with contributionFee of 10,20,30,40, .... , 100

    // Make payment to mint 1 NFT
    uint256 _payAmount = 10;
    _jbETHPaymentTerminal.pay{value: _payAmount}(
      projectId,
      100,
      address(0),
      _user,
      0,
      false,
      'Take my money!',
      new bytes(0)
    );

    // Minted 1 NFT
    assertEq(IERC721(NFTRewardDataSource).balanceOf(_user), 1);

    // Now, we make the payment but supply _dontMint metadata
    bool _dontMint = true;
    uint16[] memory empty;
    _jbETHPaymentTerminal.pay{value: _payAmount}(
      projectId,
      100,
      address(0),
      _user,
      0,
      false,
      'Take my money!',
      //new bytes(0)
      abi.encode(
        bytes32(0),
        type(IJB721Delegate).interfaceId,
        _dontMint,
        false,
        false,
        empty
        )
    );

    // NFT not minted
    assertEq(IERC721(NFTRewardDataSource).balanceOf(_user), 1);

    // Check that credits of user is still the same as before even though we have made the payment
    assertEq(IJBTiered721Delegate(NFTRewardDataSource).creditsOf(_user),_creditBefore);

  }

Tools Used

Foundry

Recommended Mitigation Steps

Update the creditsOf[] in the if(_dontMint) check.

- if(_dontMint) return;
+ if(_dontMint){ creditsOf[_data.beneficiary] += _value; }

Gas Optimizations

Gas Optimization

++i costs less gas than i++ and i+=1 (--i/i--/i-=1 too).

File: contracts/libraries/JBIpfsDecoder.sol

    digitlength++;

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L59

File: contracts/libraries/JBIpfsDecoder.sol

for (uint256 i = 0; i < _length; i++) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L68

File: contracts/libraries/JBIpfsDecoder.sol

for (uint256 i = 0; i < _input.length; i++) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L76

File: contracts/libraries/JBIpfsDecoder.sol

for (uint256 i = 0; i < _indices.length; i++) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L84

Initializing a variable to its default value costs unnecessary gas.

File: contracts/libraries/JBIpfsDecoder.sol

for (uint256 i = 0; i < _source.length; ++i) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L49

File: contracts/libraries/JBIpfsDecoder.sol

  for (uint256 j = 0; j < digitlength; ++j) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L51

File: contracts/libraries/JBIpfsDecoder.sol

for (uint256 i = 0; i < _length; i++) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L68

File: contracts/libraries/JBIpfsDecoder.sol

for (uint256 i = 0; i < _input.length; i++) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L76

File: contracts/libraries/JBIpfsDecoder.sol

for (uint256 i = 0; i < _indices.length; i++) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L84

Array length should not be looked up in every loop. Instead, use a variable to store the length before the loop starts.

File: contracts/libraries/JBIpfsDecoder.sol

for (uint256 i = 0; i < _source.length; ++i) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L49

File: contracts/libraries/JBIpfsDecoder.sol

for (uint256 i = 0; i < _input.length; i++) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L76

File: contracts/libraries/JBIpfsDecoder.sol

for (uint256 i = 0; i < _indices.length; i++) {

https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/libraries/JBIpfsDecoder.sol#L84

QA Report

L01:_SAFEMINT() SHOULD BE USED RATHER THAN _MINT() WHEREVER POSSIBLE

problem

_mint() is discouraged in favor of _safeMint() which ensures that the recipient is either an EOA or implements IERC721Receiver. Both OpenZeppelin and solmate have versions of this function

prof

JBTiered721Delegate.sol, 461, b' _mint(_reservedTokenBeneficiary, _tokenId);'
JBTiered721Delegate.sol, 504, b' _mint(_beneficiary, _tokenId);'
JBTiered721Delegate.sol, 635, b' _mint(_beneficiary, _tokenId);'
JBTiered721Delegate.sol, 677, b' _mint(_beneficiary, _tokenId);'

L02: INPUT ARRAY LENGTHS MAY DIFFER

problem

If the caller makes a copy-paste error, the lengths may be mismatchd and an operation believed to have been completed may not in fact have been completed
function withdrawMultipleERC721(address[] memory _tokens, uint256[] memory _tokenId, address _to) external override {

prof

JBTiered721Delegate.sol, 359, b' function adjustTiers(JB721TierParams[] calldata _tiersToAdd, uint256[] calldata _tierIdsToRemove)\n external\n override\n onlyOwner\n '

L03: address variable should check if it is zero MISSING CHECKS FOR ADDRESS(0X0) WHEN ASSIGNING VALUES TO ADDRESS STATE VARIABLES

prof

JBTiered721Delegate.sol, 223, b' fundingCycleStore = _fundingCycleStore;'
JBTiered721Delegate.sol, 224, b' store = _store;'
JBTiered721Delegate.sol, 227, b' prices = _pricing.prices;'
JBTiered721DelegateDeployer.sol, 51, b' globalGovernance = _globalGovernance;'
JBTiered721DelegateDeployer.sol, 52, b' tieredGovernance = _tieredGovernance;'
JBTiered721DelegateDeployer.sol, 53, b' noGovernance = _noGovernance;'
abstract/JB721Delegate.sol, 212, b' directory = _directory;'
JBTiered721DelegateProjectDeployer.sol, 52, b' controller = _controller;'
JBTiered721DelegateProjectDeployer.sol, 53, b' delegateDeployer = _delegateDeployer;'

Agreements & Disclosures

Agreements

If you are a C4 Certified Contributor by commenting or interacting with this repo prior to public release of the contest report, you agree that you have read the Certified Warden docs and agree to be bound by:

To signal your agreement to these terms, add a 👍 emoji to this issue.

Code4rena staff reserves the right to disqualify anyone from this role and similar future opportunities who is unable to participate within the above guidelines.

Disclosures

Sponsors may elect to add team members and contractors to assist in sponsor review and triage. All sponsor representatives added to the repo should comment on this issue to identify themselves.

To ensure contest integrity, the following potential conflicts of interest should also be disclosed with a comment in this issue:

  1. any sponsor staff or sponsor contractors who are also participating as wardens
  2. any wardens hired to assist with sponsor review (and thus presenting sponsor viewpoint on findings)
  3. any wardens who have a relationship with a judge that would typically fall in the category of potential conflict of interest (family, employer, business partner, etc)
  4. any other case where someone might reasonably infer a possible conflict of interest.

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.