Platform: Code4rena
Start Date: 14/06/2022
Pot Size: $50,000 USDC
Total HM: 19
Participants: 99
Period: 5 days
Judge: HardlyDifficult
Total Solo HM: 4
Id: 136
League: ETH
Rank: 6/99
Findings: 6
Award: $2,342.09
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: WatchPug
Also found by: 0xsanson, PwnedNoMore, unforgiven
844.1541 USDC - $844.15
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L178
An order can specify a numItems
in MakerOrder.constraints[0]
. This number is the min/max number of items the order wants to buy/sell.
For example a buy order can provide a list of nfts and say that wants to buy only 3 of them from that list.
The function matchOneToManyOrders
has an issue where it doesn't check the numItems
of the "many" orders.
This leads to orders selling more items than specified by the owner.
As an example, let's consider two orders:
We can call matchOneToManyOrders(makerOrder, manyMakerOrders)
with makerOrder = Order1
and manyMakerOrders = [Order2]
.
The result is that all 3 items get transferred, contrary to the will of Order2's signer.
I've proved this example in a hardhat test, link to gist.
Check somewhere that manyMakerOrders[i].constraints[0]
is valid during the execution of matchOneToManyOrders
.
#0 - 0xlgtm
2022-06-20T01:24:38Z
numItems is checked inside doTokenIdsIntersect
on line 293.
#1 - nneverlander
2022-06-22T09:41:49Z
#2 - nneverlander
2022-07-05T11:26:06Z
#3 - HardlyDifficult
2022-07-09T14:15:16Z
The hardhat test included here makes it easy to understand and confirm this issue. Thank you! Because of that detail I'm inclined to make this report the primary.
This is a High risk issue. Effectively the system supports 2 relevant criteria on orders here - which tokens and how many, but the latter is not checked in this flow. This allows a buyer to purchase more NFTs from the seller than they intended to sell.
#254 is very similar but flips the impact to explore how a buyer's offer could be attacked and how empty token lists are relevant. Duping into that issue.
455.8432 USDC - $455.84
Let's consider an example.
Alice makes an order for an ERC1155, where she wants to buy 10 items with id=1
and 10 with id=2
.
This order can be matched using matchOneToManyOrders
with two orders that sell both 10 items with id=1
.
Basically Alice gets 20 id1
instead of the expected 10 id1 + 10 id2
. If id2
is a more expensive item this is definitely a problem.
I've made an hardhat test to prove the concept. Link to gist
The functions in InfinityOrderBookComplication
that check if items intersect should be reworked a little to consider possible overlapping (when dealing with multiple-to-one matching).
#0 - nneverlander
2022-06-22T11:20:14Z
#1 - nneverlander
2022-07-05T11:30:21Z
276.9248 USDC - $276.92
_transferNFTs
checks if an item is ERC721 or ERC1155 by using IERC165(item.collection).supportsInterface(...)
.
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 issue is that if an item is neither of that, the function doesn't revert. Basically the execution continues as if an item was transferred. If an user mistakenly used a wrong address, or if the NFT is something weird, someone will end up paying for buying nothing.
Add a else
with a revert statement.
#0 - nneverlander
2022-06-22T16:34:03Z
Duplicate
#1 - HardlyDifficult
2022-07-12T00:17:03Z
136.753 USDC - $136.75
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L202 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L149 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L273
During a match, there's some accounting on how much gas we're spending, so that the executor can be reimbursed. The gas cost is split between multiple orders, computing the difference between the gas at the start and at the end. The gas at the start is calculated as this (inside a loop):
uint256 startGasPerOrder = gasleft() + ((startGas - gasleft()) / numMakerOrders);
The second part should mean that a "heading" gas is to be split between all numMakerOrders
. However this is true only in the first iteration of the loop, indeed increases in next iterations.
The impact is that when dealing with multiple orders, the last ones pay more than the due. It can be proven by switching orders in the array.
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L273
The heading part should be calculated outside the loop, or in the first iteration, and saved in a variable commonGas
. Then startGasPerOrder
becomes
uint256 startGasPerOrder = gasleft() + commonGas;
#0 - nneverlander
2022-06-22T16:34:15Z
Duplicate
#1 - nneverlander
2022-07-07T03:54:01Z
375.1796 USDC - $375.18
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L739
Gas cost when matching orders is payed by the buyer. Since buyers don't have control on order execution, they may spend more gas than what they are willing to. Examples: periods of high gasPrice, or if NFTs for some reason consume a extra amount of gas.
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L739 and other similar functions...
Consider adding a parameter to a MakerOrder where the user may specify the max amount of gas they're willing to spend.
#0 - nneverlander
2022-06-22T16:11:06Z
Duplicate
#1 - HardlyDifficult
2022-07-12T00:57:28Z
In takeOrders
, msg.sender can pay the items in ether (or other native coins).
// check to ensure that for ETH orders, enough ETH is sent // for non ETH orders, IERC20 safeTransferFrom will throw error if insufficient amount is sent if (isMakerSeller && currency == address(0)) { require(msg.value >= totalPrice, 'invalid total price'); }
However the extra payment isn't returned to the caller. This can be a risk considering that they may send extra for items with "moving" price. Also they may mistakenly send ether to the contract when it's not needed. Indeed the function doesn't revert in that case.
Add a else
with a require(msg.value==0)
. Also consider returning any extra value to msg.sender.
#0 - KenzoAgada
2022-06-21T12:15:33Z
Duplicate of #244 (extra ether not returned) and #346 (ether might be sent along with erc20)
#1 - HardlyDifficult
2022-07-19T16:49:42Z
Duping to https://github.com/code-423n4/2022-06-infinity-findings/issues/346
It does also touch on https://github.com/code-423n4/2022-06-infinity-findings/issues/244 but the impact/poc/rec wasn't fully explored for that issue.