Platform: Code4rena
Start Date: 24/02/2022
Pot Size: $75,000 USDC
Total HM: 21
Participants: 28
Period: 7 days
Judge: alcueca
Total Solo HM: 15
Id: 94
League: ETH
Rank: 14/28
Findings: 2
Award: $837.51
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: leastwood
Also found by: 0x1f8b, 0xliumin, 0xwags, CertoraInc, Dravee, IllIllI, Ruhum, TerrierLover, WatchPug, cmichel, csanuragjain, defsec, gzeon, hubble, jayjonah8, kenta, kirk-baird, rfa, robee
193.2041 USDC - $193.20
cancelReserveAuction
function in the NFTMarketReserveAuction
contractNothing very critical here, just wanted you to know that you had a mistake in the comment (the has
should be removed):
@dev The NFT is transferred back to the owner unless there is still has a buy price set.
FETH contract that has an ether balance that is not related to any user is not a wanted behavior. We can see that it is not a wanted behavior for example in the transferFrom
, where the sender isn't allowed to transfer FETH to the FETH contract.
Although, we can use the depositFor
, that allows a user to deposit ether for every account, and even for the FETH contract. But also if that will be fixed, There is another way to do it.
As you probably know, contracts can be deleted from the blockchain by calling selfdestruct
. selfdestruct
sends all remaining Ether stored in the contract to a designated address. A malicious contract can use selfdestruct
to force sending Ether to any contract. This can mess up the totalSupply
function, because nobody will be able to use the sent ether, but they will still be considered as supplied FETH, because of the totalSupply function returns the ether balance of the FETH contract.
#0 - HardlyDifficult
2022-03-02T20:49:12Z
selfdestruct
: Yes you are correct about this. We cannot stop self destruct, it is possible to track totalSupply a different way but this should be very rare so not clear it's worth the gas cost to do so. Nothing currently depends on totalSupply
and we have added a comment to be clear about this potential.Thanks!
#1 - alcueca
2022-03-17T09:21:34Z
Unadjusted score: 15
🌟 Selected for report: Dravee
Also found by: 0x1f8b, CertoraInc, TerrierLover, csanuragjain, gzeon, iain, kenta, rfa, robee, thankthedark
Loops can be optimized in several ways. Let's take for example the loop in the adminAccountMigration
function of the NFTMarketReserveAuction
contract.
for (uint256 i = 0; i < listedAuctionIds.length; ++i) { uint256 auctionId = listedAuctionIds[i]; ReserveAuction storage auction = auctionIdToAuction[auctionId]; if (auction.seller != address(0)) { // Only if the auction was found and not finalized before this transaction. if (auction.seller != originalAddress) { // Confirm that the signature approval was the correct owner of this auction. revert NFTMarketReserveAuction_Cannot_Migrate_Non_Matching_Seller(auction.seller); } // Update the auction's seller address. auction.seller = newAddress; emit ReserveAuctionSellerMigrated(auctionId, originalAddress, newAddress); } }
To optimize this loop and make it consume less gas, we can do the foloowing things:
listedAuctionIds
array length in a local variable instead of accessing it in every iteration.auction.seller
in a local variable instead of accessing it 3 times in every iteration. This will save accssing the struct's field 2/3 times in every iteration.uint length = listedAuctionIds.length; address seller_i; for (uint256 i; i < length; ++i) { uint256 auctionId = listedAuctionIds[i]; ReserveAuction storage auction = auctionIdToAuction[auctionId]; seller_i = auction.seller; if (seller_i != address(0)) { // Only if the auction was found and not finalized before this transaction. if (seller_i != originalAddress) { // Confirm that the signature approval was the correct owner of this auction. revert NFTMarketReserveAuction_Cannot_Migrate_Non_Matching_Seller(seller_i); } // Update the auction's seller address. auction.seller = newAddress; emit ReserveAuctionSellerMigrated(auctionId, originalAddress, newAddress); } }
Another example is the loop in the adminCancelOffers
function of the NFTMarketOffer
contract:
for (uint256 i = 0; i < nftContracts.length; ++i) { Offer memory offer = nftContractToIdToOffer[nftContracts[i]][tokenIds[i]]; delete nftContractToIdToOffer[nftContracts[i]][tokenIds[i]]; if (offer.expiration >= block.timestamp) { // Unlock from escrow and emit an event only if the offer is still active feth.marketUnlockFor(offer.buyer, offer.expiration, offer.amount); emit OfferCanceledByAdmin(nftContracts[i], tokenIds[i], reason); } // Else continue on so the rest of the batch transaction can process successfully }
Here we can also save the nftContracts[i]
and the tokenIds[i]
variables instead of accessing them 3 times. The code will look something like this:
uint length = nftContracts.length; address nft_contract, token_id; for (uint256 i; i < length; ++i) { nft_contract = nftContracts[i]; token_id = tokenIds[i]; Offer memory offer = nftContractToIdToOffer[nft_contract][token_id]; delete nftContractToIdToOffer[nft_contract][token_id]; if (offer.expiration >= block.timestamp) { // Unlock from escrow and emit an event only if the offer is still active feth.marketUnlockFor(offer.buyer, offer.expiration, offer.amount); emit OfferCanceledByAdmin(nft_contract, token_id, reason); } // Else continue on so the rest of the batch transaction can process successfully }
_transferFromEscrowIfAvailable
functionIn the cancelReserveAuction
function, we delete nftContractToTokenIdToAuctionId[auction.nftContract][auction.tokenId]
and then calling the _transferFromEscrowIfAvailable
function which checks if nftContractToTokenIdToAuctionId[auction.nftContract][auction.tokenId] == 0
, and calls the super function if so. We can call the super function directly instead of calling the _transferFromEscrowIfAvailable
function.
placeBid
without placeBidOf
(also in NFTMarketPrivateSale
)The placeBid
function calls the placeBidOf
with amount = msg.value
. Implementing the function without calling the placeBidOf
will save gas spending on 2 if statements:
if (amount < msg.value) { // The amount is specified by the bidder, so if too much ETH is sent then something went wrong. revert NFTMarketReserveAuction_Too_Much_Value_Provided(); } else if (amount > msg.value) { // Withdraw additional ETH required from their available FETH balance. unchecked { // The if above ensures delta will not underflow. uint256 delta = amount - msg.value; // Withdraw ETH from the buyer's account in the FETH token contract. feth.marketWithdrawFrom(msg.sender, delta); } } ...
_getMinIncrement
in the placeBidOf
Instead of calling the `` function twice, its return value can be saved in order to save gas in case the statement inside the if it true.
code before:
... else if (amount < _getMinIncrement(auction.amount)) { // If this bid outbids another, it must be at least 10% greater than the last bid. revert NFTMarketReserveAuction_Bid_Must_Be_At_Least_Min_Amount(_getMinIncrement(auction.amount));
code after:
uint min_inc = _getMinIncrement(auction.amount); ... else if (amount < min_inc) { // If this bid outbids another, it must be at least 10% greater than the last bid. revert NFTMarketReserveAuction_Bid_Must_Be_At_Least_Min_Amount(min_inc);
transfer
without using transferFrom
in FETH
contractThe transfer
function calls the transferFrom
function with from = msg.sender
. Implementing it without the call to transferFrom
will save an if statement:
if (from != msg.sender) { _deductAllowanceFrom(fromAccountInfo, amount); }
because we know for sure that from == msg.sender
.
_deductAllowanceFrom
In the _deductAllowanceFrom
function in the FETH
contract, a variable can be saved in order to prevent calling it multiple times. The accountInfo.allowance[msg.sender]
is accessed multiple times, and can be saved as a local variable to prevent all these accesses.
code before:
function _deductAllowanceFrom(AccountInfo storage accountInfo, uint256 amount) private { if (accountInfo.allowance[msg.sender] != type(uint256).max) { if (accountInfo.allowance[msg.sender] < amount) { revert FETH_Insufficient_Allowance(accountInfo.allowance[msg.sender]); } // The check above ensures allowance cannot underflow. unchecked { accountInfo.allowance[msg.sender] -= amount; } } }
code after:
function _deductAllowanceFrom(AccountInfo storage accountInfo, uint256 amount) private { uint allowence = accountInfo.allowance[msg.sender]; if (allowence != type(uint256).max) { if (allowance < amount) { revert FETH_Insufficient_Allowance(allowence); } // The check above ensures allowance cannot underflow. unchecked { accountInfo.allowance[msg.sender] = allowance - amount; } } }
_getMinIncrement
in NFTMarketCore
Instead of calculating 10% of the amount (minIncrement
) and then adding it to the current amount, we can calculate the sum by calculating 100 + increment % (in our example 110%), and adding one if it is equal to the amount before.
code before:
function _getMinIncrement(uint256 currentAmount) internal pure returns (uint256) { uint256 minIncrement = currentAmount * MIN_PERCENT_INCREMENT_IN_BASIS_POINTS; unchecked { minIncrement /= BASIS_POINTS; if (minIncrement == 0) { // Since minIncrement reduces from the currentAmount, this cannot overflow. // The next amount must be at least 1 wei greater than the current. return currentAmount + 1; } } return minIncrement + currentAmount; }
code after:
function _getMinIncrement(uint256 currentAmount) internal pure returns (uint256) { uint256 amountAfter = currentAmount * MIN_PERCENT_INCREMENT_IN_BASIS_POINTS; // this will be 110% instead of 10% unchecked { amountAfter /= BASIS_POINTS; if (amountAfter == currentAmount) { // Since minIncrement reduces from the currentAmount, this cannot overflow. // The next amount must be at least 1 wei greater than the current. return currentAmount + 1; } } return amountAfter; }
In the placeBidOf
function in NFTMarketReserveAuction
, a calculation can be saved - that's becuase of these 2 statements are equivilent:
auction.endTime - block.timestamp < auction.extensionDuration
auction.endTime < block.timestamp + auction.extensionDuration
So the code can be changed from this:
... unchecked { // When a bid outbids another, check to see if a time extension should apply. // We confirmed that the auction has not ended, so endTime is always >= the current timestamp. if (auction.endTime - block.timestamp < auction.extensionDuration) { // Current time plus extension duration (always 15 mins) cannot overflow. auction.endTime = block.timestamp + auction.extensionDuration; } }
To this:
... unchecked { // When a bid outbids another, check to see if a time extension should apply. // We confirmed that the auction has not ended, so endTime is always >= the current timestamp. uint timeWithExtension = block.timestamp + auction.extensionDuration; if (auction.endTime < timeWithExtension) { // Current time plus extension duration (always 15 mins) cannot overflow. auction.endTime = timeWithExtension; } }
#0 - HardlyDifficult
2022-03-02T20:45:00Z
Thanks for the suggestions!
placeBid
is here just for backwards compatibility. We hope to remove it once all usage has migrated to placeBidOf
. I did add a comment to the function to indicate that it's being deprecated. Same for the private sale function.transfer
without transferFrom
: Valid, we are currently planning on not making a change here in order to keep the code simple. This is very similar to the pattern that OpenZeppelin uses in their contracts.#1 - alcueca
2022-03-17T15:31:42Z
Regardless of the sponsor adopting the savings or not, since they were in scope, they are valid. Significant, as well.
Score: 2000