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: 17/99
Findings: 5
Award: $671.42
🌟 Selected for report: 0
🚀 Solo Findings: 0
276.9248 USDC - $276.92
User funds might be lost due to malicious orders.
in _transferNFTs
, the function doesn’t take care of the case if item.collection
is not an NFT. It will still be treated like a “valid order”, but it won’t actually be exchanged when the trade is triggered.
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); } // if item.collection is not an NFT, nothing happen and the trade proceeeds }
If a malicious user creates a non-standard ERC721 that returns false on supportsInterface
, the exchange will skip the ERC721 transfer, but the buyer still pay the currency.
This opens up wide range of phishing attack opportunities, a malicious attacker can put in an collectionId that either returns false, or have a fallback function that won’t revert on supportsInterface
and trick people into signing an order.
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 an NFT!!!"); } }
#0 - nneverlander
2022-06-22T11:10:40Z
#1 - nneverlander
2022-07-05T11:28:44Z
#2 - HardlyDifficult
2022-07-09T19:44:18Z
🌟 Selected for report: horsefacts
Also found by: 0x29A, GimelSec, GreyArt, Lambda, Ruhum, antonttc, berndartmueller, byterocket, cccz, codexploder, dipp, oyc_109, unforgiven
84.0967 USDC - $84.10
Buyer cannot stop the matcher from executing their order with high gas price (or during the period when the gas price is high) and forcing a high fee.
in all the matching functions, the buyer are forced to pay the gas cost of execution, and there is no way for a buyer to prevent himself from being charged too much except constantly maintaining its weth allowance to the contract, which is not a good UX.
When the gas price is higher, buyer can easily be charge hundreds of dollars because of an outstanding order.
Consider adding gasPrice protection as one field in the constraints, and revert when tx.gasPrice is higher than this value.
#0 - nneverlander
2022-06-23T11:47:36Z
Duplicate
#1 - HardlyDifficult
2022-07-12T01:02:46Z
136.753 USDC - $136.75
https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L149 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L202
Gas calculation is wrong in matchOneToOneOrders
and matchOneToManyOrders
, and can leads to buyers paying enormous weth as fees
The current code calculates startGasPerOrder
at the “start of each iteration” with the following formula:
uint256 startGasPerOrder = gasleft() + ((startGas - gasleft()) / numMakerOrders);
the pseudo code for the current calculation.
// matchOneToOneOrders uint256 startGas = gasleft(); ... for (uint256 i = 0; i < numMakerOrders; ) { uint256 startGasPerOrder = gasleft() + ((startGas - gasleft()) / numMakerOrders); // will charge startGas - gasLeft() from buyer _matchOneToOneOrders(... ,startGasPerOrder,...); }
The idea for using ((startGas - gasleft()) / numMakerOrders)
is to share the cost among all buyers for the gas used in the shared execution logic.
The problem with this being used in the loop, is that “shared cost” for all orders changes with each iteration. If the first order is very expensive to execute, it will cost the difference between startGas
and gasLeft()
to be huge, therefore forcing the second buyer to pay a lot in fees.
Or even if all orders are normal, the later it is for the order to be in the loop, the more the buyer pays, because startGas - gasLeft()
will only get bigger with iterations going on.
The shared cost should be calculated separated before the loop, and added to each startGasPerOrder
as follow:
// matchOneToOneOrders uint256 startGas = gasleft(); ... uint256 sharedCost = (startGas - gasleft()) / numMakerOrders; for (uint256 i = 0; i < numMakerOrders; ) { uint256 startGasPerOrder = gasleft() + sharedCost; _matchOneToOneOrders(... ,startGas,...); }
#0 - nneverlander
2022-06-22T09:32:59Z
Duplicate. Fixed in https://github.com/infinitydotxyz/exchange-contracts-v2/commit/5a3f81b82a9bee2de7517b3a5f18953cb5ec3684
Agree with risk assessment.
#1 - nneverlander
2022-07-05T11:27:20Z
🌟 Selected for report: joestakey
Also found by: 0x1f8b, 0x29A, 0x52, 0xDjango, 0xNazgul, 0xNineDec, 0xf15ers, 0xkowloon, 0xmint, 8olidity, BowTiedWardens, Chom, Cityscape, Czar102, ElKu, FSchmoede, Funen, GimelSec, GreyArt, IllIllI, KIntern, Kaiziron, Kenshin, Lambda, MadWookie, MiloTruck, PPrieditis, Picodes, Ruhum, Sm4rty, StErMi, TerrierLover, TomJ, Treasure-Seeker, VAD37, WatchPug, Wayne, _Adam, a12jmx, abhinavmir, antonttc, apostle0x01, asutorufos, berndartmueller, cccz, cloudjunky, codexploder, cryptphi, csanuragjain, defsec, delfin454000, fatherOfBlocks, georgypetrov, hake, hansfriese, horsefacts, hyh, k, kenta, nxrblsrpr, oyc_109, peritoflores, rajatbeladiya, reassor, rfa, robee, sach1r0, saian, samruna, shenwilly, simon135, sorrynotsorry, sseefried, throttle, unforgiven, wagmi, zzzitron
48.9862 USDC - $48.99
protocol fee
and wethTrasnferGas
are not cappedthese governable variables should have upper bounds. For free it should be something like 10%, and trasnferGas should probably be 60000 just to make sure even owner can’t potentially use these variables to steal from users. This would reduce the trust assumption users need to make to interact with the protocol.
weth
is paid for the matchingthe contract currently pulls the weth into its own contract, along with protocol fee.
There is no event emitted when protocol fee is paid and buyer fee is paid for auto matching, making it hard to separate how much should be consider “gas compensation” vs “protocol revenue”. This might make it hard to do analysis, or do protocol fee distribution via governance in the future.
I’d suggest just transfer the “match fee” to matcher (msg.sender) and consider all weth in the contract to be protocol revenue. Using an event for one of the fee charge event can also help for offchain calculation, but won’t make it easier for future on-chain governance operations.
🌟 Selected for report: IllIllI
Also found by: 0v3rf10w, 0x1f8b, 0x29A, 0xAsm0d3us, 0xDjango, 0xKitsune, 0xNazgul, 0xf15ers, 0xkatana, 0xkowloon, BowTiedWardens, Chom, ElKu, FSchmoede, Funen, GimelSec, Kaiziron, Kenshin, Lambda, MadWookie, MiloTruck, PPrieditis, Picodes, PwnedNoMore, StErMi, Tadashi, TerrierLover, TomJ, Tomio, Wayne, Waze, _Adam, antonttc, apostle0x01, asutorufos, c3phas, codexploder, defsec, delfin454000, fatherOfBlocks, hake, hansfriese, hyh, joestakey, k, kenta, oyc_109, peritoflores, reassor, rfa, robee, sach1r0, simon135, slywaters, zer0dot
124.6631 USDC - $124.66
_getCurrentPric
can save few hundred gas on averageA few optimisation can be applied to this function:
Example Implementation:
// assuming PRECISION is declared as constant function _getCurrentPrice(OrderTypes.MakerOrder calldata order) internal view returns (uint256) { (uint256 startPrice, uint256 endPrice) = (order.constraints[1], order.constraints[2]); // return early to optimize fixed price trades if (startPrice == endPrice) return startPrice; uint256 duration = order.constraints[4] - order.constraints[3]; if (duration == 0) return startPrice; uint256 elapsedTime = block.timestamp - order.constraints[3]; unchecked { // elapsedTime * PRECISION will not overflow uint256 portionBps = elapsedTime > duration ? PRECISION : ((elapsedTime * PRECISION) / duration); if (startPrice > endPrice) { // startPrice - endPrice will not underflow uint256 priceDiff = (startPrice - endPrice) * portionBps / PRECISION; return startPrice - priceDiff; } else { // endPrice - startPrice will not underflow uint256 priceDiff = (endPrice - startPrice) * portionBps / PRECISION; return startPrice + priceDiff; } } }
This saves average of 500 gas for takeOrders
, and 1500 gas for matchOneToManyOrders
more gas can be solved if we update the same function in OrderBookComplication
too.
OrderBookComplication
All orders are already checked if constraints[3] < now && constraint[4] > now
in InfinityExchange
, so relevent checks can be removed from the Complication layer to save gas.
Gas saving: 2000 ~ 50000. (benchmark on passing tests)
matchOneToManyOrders
: 352545 → 294291matchOneToOneOrders
: 816451 → 810531matchOrders:
424195 → 422034takeOrders
: 210136 → 205584safeTransfe
on WETHsafeERC20 library are designed to be used on non-standard ERC20 tokens. Removing the use of the library from well tested weth
token shouldn’t be consider risky, and save you some gas.
packing the following 2 variables into 1 storage slot can save gas when trying to read and write these 2 variables:currentEpoch
previousEpochTimestamp
.
Gas saving
advanceEpoch
: 118230 ⇒ 109376currentEpochTimestamp
should be used as immutablethe variable currentEpochTimestamp
is not updated outside of the constructor, defining it as immutable can save gas on SLOAD.
Gas saving
advanceEpoch
: 109376 ⇒ 107273#0 - nneverlander
2022-06-22T16:07:45Z
Thank you.