Foundation contest - leastwood's results

Building the new creative economy

General Information

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

Foundation

Findings Distribution

Researcher Performance

Rank: 1/28

Findings: 9

Award: $18,292.76

🌟 Selected for report: 6

🚀 Solo Findings: 4

Findings Information

🌟 Selected for report: 0xliumin

Also found by: leastwood

Labels

bug
duplicate
3 (High Risk)

Awards

3652.1324 USDC - $3,652.13

External Links

Lines of code

https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L325-L349 https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L299-L316

Vulnerability details

Impact

The createReserveAuction() function allows users to create duplicate auctions with the same NFT but different auctionIds. As a result, a user could back out of an active auction by creating and then cancelling a duplicate auction. This leads to locked users funds as the auction cannot be finalized on a non-existent NFT.

Proof of Concept

The following scenario outlines the exploit:

  • Alice creates an auction.
  • Bob subsequently bids on this auction and is the highest bidder for the duration of the auction.
  • Before the auction has ended, Alice calls createReserveAuction() on the same NFT.
  • Alice is then able to call cancelReserveAuction() as the auction is inactive.
  • As a result, it will not be possible to finalize the currently active auction and Bob will lose his funds while Alice gets to retrieve her NFT from the contract.

Tools Used

Manual code review.

Consider preventing users from calling createReserveAuction() if there is a pre-existing auction.

#0 - HardlyDifficult

2022-03-02T20:53:41Z

Duplicate of https://github.com/code-423n4/2022-02-foundation-findings/issues/23

This is a great find and we have put a fix in place.

Findings Information

🌟 Selected for report: hyh

Also found by: WatchPug, leastwood, shenwilly

Labels

bug
duplicate
3 (High Risk)

Awards

1479.1136 USDC - $1,479.11

External Links

Lines of code

https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketOffer.sol#L246-L274 https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L552-L560 https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L490-L518

Vulnerability details

Impact

NFTs can be sold to users in a variety of ways. One main method is for a seller to auction their NFT by creating a reserve auction. Once the auction is active, it is guaranteed that the sale will go to the highest bidder. Once an auction has ended, anyone can finalize the auction by calling finalizeReserveAuction(). This function will remove any auction data and transfer the NFT to auction.bidder.

Alternatively, auction.bidder can avoid redundant transfers by selling their new NFT to a user with an unexpired market offer. However, this process does not correctly transfer the NFT as seen in NFTMarketReserveAuction._transferFromEscrow(). The NFT is transferred from escrow to the auction.bidder account and not offer.buyer. As a result, auction.bidder receives the funds from the market offer while simultaneously retaining ownership of the auctioned NFT.

Proof of Concept

Consider the following scenario:

  • Alice creates an auction selling her NFT.
  • She bids on her own auction such that once the auction ends, she is the highest bidder.
  • The auction has not been finalized as only the NFT seller is incentivized to do so.
  • Alice waits for Bob to make an offer on her NFT and then accepts his offer.
  • Due to the aforementioned logic bug, Alice receives Bob's funds and retains ownership of the underlying NFT.

There are a couple of important conditions for this attack to be successful. The NFT being sold must be left in an unfinalized state. A user must make an offer after the auction has ended but before the auction is finalized.

Tools Used

Manual code review.

Consider updating NFTMarketReserveAuction._transferFromEscrow() such that the NFT is held in escrow.

By updating the statement to _finalizeReserveAuction(auctionId, true); and removing the return keyword, the highlighted should be mitigated.

#0 - HardlyDifficult

2022-03-02T17:01:55Z

Duplicate of https://github.com/code-423n4/2022-02-foundation-findings/issues/49

This is an excellent find and the report is very detailed & clear! We are implementing the recommended change.

Findings Information

🌟 Selected for report: leastwood

Also found by: cccz

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1095.6397 USDC - $1,095.64

External Links

Lines of code

https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L263-L292 https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketBuyPrice.sol#L125-L141

Vulnerability details

Impact

The adminAccountMigration() function is called by the operator role to update all sellers' auctions. The auction.seller account is updated to the new address, however, the protocol fails to update buyPrice.seller. As a result, the protocol is put in a deadlock situation where the new address cannot cancel the auction and withdraw their NFT without the compromised account first cancelling the buy price and vice-versa. This is only recoverable if the new account is migrated back to the compromised account and then cancelBuyPrice() is called before migrating back.

Proof of Concept

Tools Used

Manual code review.

Consider invalidating the buy offer before account migration.

#0 - HardlyDifficult

2022-03-07T12:18:34Z

Correct - if we were to use adminAccountMigration while the NFT had both an auction reserve price and had a buy price set this would have created a deadlock type situation where the NFT is in a bad state and we'd likely need to migrate the NFT back to the original owner in order to correct it.

There were two possible solutions to this:

  • Update adminAccountMigration to update both auction and buy price at the same time.
  • Cut adminAccountMigration completely.

We went with the latter solution. This is not a feature we have used in some time and as we continue to grow, it's not scalable since it required Foundation to get involved directly and included manual verification steps.

Removing this feature has saved over 2KB in contract space as well, which we really needed in order to make room for new features and changes.

Findings Information

🌟 Selected for report: leastwood

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

2434.7549 USDC - $2,434.75

External Links

Lines of code

https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketPrivateSale.sol#L143-L150

Vulnerability details

Impact

The buyFromPrivateSaleFor() function allows sellers to make private sales to users. If insufficient ETH is provided to the function call, the protocol will attempt to withdraw the amount difference from the user's unlocked balance. However, if the same user has an open offer on the same NFT, then these funds will remain locked until expiration. As a result, the user cannot make use of these locked funds even though they may be needed for a successful sale.

Proof of Concept

Tools Used

Manual code review.

Consider adding a _cancelBuyersOffer() call to the buyFromPrivateSaleFor() function. This should be added only to the case where insufficient ETH was provided to the trade. By cancelling the buyer's offer on the same NFT, we can guarantee that the user has access to the correct amount of funds.

#0 - HardlyDifficult

2022-03-03T13:00:09Z

Yes - completely agree. This was an oversight on our end - and as a result it created an inconsistent experience for users. Since we leveraged an outstanding offer balance for a buy purchase, the same behavior should occur when using Private Sales so that user's are not in a state where they cannot make the purchase due to incorrectly having their funds locked up.

As you point out without this change, it's possible that the buyer using private sales continues to have their funds locked up for an Offer that can now only be accepted by themselves. It's an awkward state that should be avoided.

We have made the recommended change.

Findings Information

🌟 Selected for report: leastwood

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

2434.7549 USDC - $2,434.75

External Links

Lines of code

https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketCreators.sol#L49-L251

Vulnerability details

Impact

The _getCreatorPaymentInfo() function is utilised by _distributeFunds() whenever an NFT sale is made. The function uses try and catch statements to handle bad API endpoints. As such, a revert in this function would lead to NFTs that are locked in the contract. Some API endpoints receive an array of recipient addresses which are iterated over. If for whatever reason the function reverts inside of a try statement, the revert is actually not handled and it will not fall through to the empty catch statement.

Proof of Concept

The end result is that valid and honest NFT contracts may revert if the call runs out of gas due to an unbounded _recipients array. try statements are only able to handle external calls.

Tools Used

Manual code review.

Consider bounding the number of iterations to MAX_ROYALTY_RECIPIENTS_INDEX as this is already enforced by _distributeFunds(). It may be useful to identify other areas where the try statement will not handle reverts on internal calls.

#0 - HardlyDifficult

2022-03-09T11:31:05Z

Yes - this code is trying to be very defensive so that NFTs cannot get stuck in escrow. This is a great suggestion on how we could continue to improve.

Instead of capping the loop lengths in _getCreatorPaymentInfo we have opted to remove them entirely. There was another simplification that happened recently to this function that made removal a viable option. Now the only other loop for this data is in _distributeFunds and we already cap the max length there.

Findings Information

🌟 Selected for report: pedroais

Also found by: WatchPug, leastwood

Labels

bug
duplicate
2 (Med Risk)

Awards

657.3838 USDC - $657.38

External Links

Lines of code

https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketFees.sol#L188-L189

Vulnerability details

Impact

The primary foundation fee is charged on the initial sale of newly minted NFTs. However, the isCreator && !_nftContractToTokenIdToFirstSaleCompleted[nftContract][tokenId] check can easily be bypassed to avoid paying the primary foundation fee by selling the NFTs through a non-creator account. As a result, the creator receives a greater proportion of the sale, and the protocol generates less revenue.

Proof of Concept

A creator intends to mint and sell a new NFT. By first trading this NFT to another account, the creator can avoid ever paying the primary foundation fee and actually receive more funds for their sale.

Tools Used

Manual code review.

Ensure this is understood and consider updating the _getFees() function to handle this edge case properly.

#0 - HardlyDifficult

2022-03-03T12:58:05Z

Findings Information

🌟 Selected for report: leastwood

Labels

bug
2 (Med Risk)
sponsor acknowledged

Awards

2434.7549 USDC - $2,434.75

External Links

Lines of code

https://github.com/code-423n4/2022-02-foundation/tree/main/contracts

Vulnerability details

Impact

Cryptopunks are at the core of the NFT ecosystem. As one of the first NFTs, it embodies the culture of NFT marketplaces. By not supporting the trading of cryptopunks, Foundation is at a severe disadvantage when compared to other marketplaces. Cryptopunks have their own internal marketplace which allows users to trade their NFTs to other users. As such, cryptopunks does not adhere to the ERC721 standard, it will always fail when the protocol attempts to trade them.

Proof of Concept

Here is an example implementation of what it might look like to integrate cryptopunks into the Foundation protocol.

Tools Used

Manual code review.

Consider designing a wrapper contract for cryptopunks to facilitate standard ERC721 transfers. The logic should be abstracted away from the user such that their user experience is not impacted.

#0 - HardlyDifficult

2022-03-03T12:45:41Z

Yes, crypto punks is an important part of the ecosystem and this is on our radar. We are not planning on adding support at this time but we will revisiting this in the future.

I like the idea of using a wrapper contract of sorts. We will be looking for a way to keep the complexity out of the market contract itself like you suggest if at all possible.

Findings Information

🌟 Selected for report: leastwood

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

2434.7549 USDC - $2,434.75

External Links

Lines of code

https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketOffer.sol#L255-L271 https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L557 https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L510-L515 https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketFees.sol#L188-L189

Vulnerability details

Impact

Once an auction has ended, the highest bidder now has sole rights to the underlying NFT. By finalizing the auction, fees are charged on the sale and the NFT is transferred to auction.bidder. However, if auction.bidder accepts an offer before finalization, fees will be charged on the auction.bidder sale before the original sale. As a result, it is possible to avoid paying the primary foundation fee as a creator if the NFT is sold by auction.bidder before finalization.

Proof of Concept

Consider the following scenario:

  • Alice creates an auction and is the NFT creator.
  • Bob bids on the auction and is the highest bidder.
  • The auction ends but Alice leaves it in an unfinalized state.
  • Carol makes an offer on the NFT which Bob accepts.
  • _acceptOffer() will distribute funds on the sale between Bob and Carol before distributing funds on the sale between Alice and Bob.
  • The first call to _distributeFunds() will set the _nftContractToTokenIdToFirstSaleCompleted to true, meaning that future sales will only be charged the secondary foundation fee.

Tools Used

Manual code review.

Ensure the _nftContractToTokenIdToFirstSaleCompleted is correctly tracked. It might be useful to ensure the distribution of funds are in the order of when the trades occurred. For example, an unfinalized auction should always have its fees paid before other sales.

#0 - HardlyDifficult

2022-03-03T12:43:05Z

Yes! This is a good find.

The core of the problem is we were calling _distributeFunds before calling _transferFromEscrow where the auction finalization would be processed. We have flipped those calls so now the auction will be fully settled before we calculate fees for the offer sale.

Findings Information

Awards

1669.5007 USDC - $1,669.50

Labels

bug
QA (Quality Assurance)

External Links

Signature Re-Use

The adminAccountMigration() function intends to allow a seller to update auction.seller in the event their account is compromised. However, the signature in requireAuthorizedAccountMigration() can be re-used because there is no use of a nonce to prevent this.

Consider adding a nonce to account for this behaviour.

Approval Race Condition

There is no use of the increaseAllowance() and decreaseAllowance() functions. As a result, users could frontrun calls to approve() by using the allocated amount before the new amount is set. Consequently, it might be possible for the approved spender to spend more tokens then they were allocated.

Ensure this is understand and consider adding the aforementioned functions.

Unclear Function Naming

The _cancelBuyersOffer() function isn't exactly clear as it will only cancel the offer if the buyer is msg.sender. Therefore, it would be more clear to rename this to _cancelSellersOffer() or similar to avoid confusion.

Improper BASIS_POINTS Check in _distributeFunds()

The _distributeFunds() function checks if creatorShares[i] > BASIS_POINTS when it should actually be checking if totalShares > BASIS_POINTS. This ensures that the sum of creator shares do not exceed an expected amount.

Improper State Handling

If _autoAcceptBuyPrice() is executed by a new buyer that is not offer.buyer, then there will be an excess of ETH. The original offer.buyer will then have to wait until the offer expires as it isn't invalidated.

Ensure this is understood.

Unhandled marketLockupFor() Edge Case

marketLockupFor() will revert if too much ETH is provided. This can be modified to refund any surplus ETH to the user's FETH balance.

No Support For ERC1155 Tokens

Many new NFT contracts adhere to the ERC1155 standard. Currently, it is not possible to trade these tokens on the Foundation marketplace.

Consider making the necessary integrations.

Malicious Contract Upgrades Will Lock Funds And NFTs

If the protocol owner becomes malicious or their account is compromised for whatever reason, they can lock users' funds by upgrading the relevant contracts to an malicious contract which always reverts. As a result, NFTs and ETH will be forever locked by the protocol.

Unclear _recipients Array Restriction

The _recipients array is restricted to just 5 receivers. Ensure this is understand by NFT creators as they may intend to use additional receivers but these receivers may be disproportionally rewarded if the majority of creator shares are not included.

Inconsistent Use of sendValue()

_buy() should make use of _sendValueWithFallbackWithdraw() instead of sendValue(). This will ensure invalid transfers are correctly handled.

#0 - HardlyDifficult

2022-03-09T11:24:29Z

Thanks for the feedback!

  • Signature re-use: In this use case, signature reuse is actually desirable. However after a different issue was reported we opted to remove the adminAccountMigration function completely instead. It's not something we have used in some time.
  • Approval race condition: Yes, we will address this. ATM it's not a concern because approvals won't be used on Foundation thanks to the special market relationship, but we'll want to change this in the future. It does expose a potential vulnerability related to another contract, not included in this review, so we are looking to patch that before adding increaseAllowance.
  • Unclear naming: Yes, we agree. The function has been renamed.
  • Improper BASIS_POINTS Check: This is a fair point. It's somewhat an arbitrary decision though. We are expecting values to be in BP and this logic is simply trying to handle invalid results gracefully. Either way we bring the total back down to 10% and guard against overflowing. ATM we are going to leave it as is.
  • Improper State Handling: This is a valid concern but currently working as designed. When auto buy now applies we treat this the same as if they called buy directly. When someone purchases using buy we have opted to allow the highest offer to remain valid. It may not be common, but it's not 100% clear that the offer should have been canceled. e.g. the new owner may accept that offer in order to have a fast exit at a small loss due to buyers remorse.
  • Unhandled marketLockupFor() Edge Case: ATM there is no use case we are aware of where it would make sense for too much ETH to be provided. Because of this we are going to revert instead of refunding - it'll save a bit of gas for other users and may prevent an invalid transaction (possibly user error or a frontend bug).
  • No Support For ERC1155 Tokens: Agree, we will add support for ERC-1155 at some point. It's a non trivial change though and we don't want to rush into it just yet.
  • Malicious Contract Upgrades: Agree. Obviously we have no desire to do something like this. Longer term we are looking to progressively decentralized - possibly by giving upgrade permissions to a DOA or even removing that capability completely. For now though, this is a fast and easy way to continue adding features to our market.
  • Unclear _recipients Array Restriction: Agree. We need some cap in order to limit amount of gas that non-creators would be required to pay. 5 is arbitrary and we should at least provide clear documentation. For now, we have added some comments about this inline. Longer term we can continue to communicate this more clearly, and encourage those with more than 5 recipients to use solutions such as https://www.0xsplits.xyz/.
  • Inconsistent Use of sendValue(): Yes, this is inconsistent. However the use case here is slightly different. Generally the use of sendValueWithFallbackWithdraw is when we are sending ETH to an address other than the msg.sender - we want to ensure that a malicious user cannot cause other user's transactions to revert. Here the refund is going to the msg.sender - if they are non-receivable they should be able to retry using the correct value instead. So we are leaving it as-is for now.

#1 - alcueca

2022-03-17T09:53:27Z

Unadusted score: 100 (80 + 20 from clean formatting)

#2 - alcueca

2022-05-13T09:24:32Z

Signature Re-Use - Non-Critical (Lack of documentation on desiderability) Approval Race Condition - Invalid in my opinion, since this has never been exploited and is uneconomical to exploit. Unclear Function Naming - Non-Critical Improper BASIS_POINTS Check in _distributeFunds() - Non-Critical (Lack of documentation on trade-offs) Improper State Handling - Non-Critical (Lack of documentation on feature set) Unhandled marketLockupFor() Edge Case - Low No Support For ERC1155 Tokens - Non-Critical (Accepted suggestion) Malicious Contract Upgrades Will Lock Funds And NFTs - Invalid, this kind of issue belongs in a governance audit. Unclear _recipients Array Restriction - Non-Critical (Lack of documentation on trade-offs) Inconsistent Use of sendValue() - Non-Critical (Lack of documentation on flow and trade-offs)

#3 - liveactionllama

2022-05-17T06:44:32Z

Just a note that C4 is excluding the invalid entries from the official report.

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter