Foundation contest - cmichel'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: 3/28

Findings: 6

Award: $10,603.80

🌟 Selected for report: 4

🚀 Solo Findings: 4

Findings Information

🌟 Selected for report: thankthedark

Also found by: Afanasyevich, cmichel

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/4d8c8931baffae31c7506872bf1100e1598f2754/contracts/mixins/NFTMarketPrivateSale.sol#L166

Vulnerability details

Impact

The private sale signature used in buyFromPrivateSaleFor does not prevent replay attacks. If the NFT ends up in the original seller's wallet again (within the deadline) it can be purchased from them again.

POC
  • S sells to B using a private sale. S creates a signature for it and B submits it and receives the NFT.
  • The NFT market changes and the NFT is now worth more. (This can happen quickly, for example, if the NFT team announces new utility for the collection.) B lists it on some market for a higher price
  • S wants the NFT again, buys it at the higher price
  • B can resubmit the buyFromPrivateSaleFor(..., v, r, s) to buy it from S a second time at the lower initial price. S loses their NFT and B could list it for a higher price again.

Use a nonce, indexed by (nftContract, tokenId), as part of the message to be signed. Increase the nonce on each buyFromPrivateSaleFor. Indexing the nonce by (nftContract, tokenId) ensures that each signature is only valid for a private sale of that NFT once.

- keccak256(abi.encode(BUY_FROM_PRIVATE_SALE_TYPEHASH, nftContract, tokenId, msg.sender, amount, deadline))
+ keccak256(abi.encode(BUY_FROM_PRIVATE_SALE_TYPEHASH, nftContract, tokenId, msg.sender, amount, deadline, nonces[nftContract][tokenId]++))

#0 - HardlyDifficult

2022-03-02T16:24:17Z

#1 - alcueca

2022-03-14T11:36:48Z

Agree with a Medium severity risk, due to the very particular set of conditions for this attack to happen.

Findings Information

🌟 Selected for report: cmichel

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/4d8c8931baffae31c7506872bf1100e1598f2754/contracts/FETH.sol#L433

Vulnerability details

Impact

The FETH.withdrawFrom function does not validate its to parameter. Funds can be lost if to is the zero address.

Similar issues have been judged as medium recently, see Sandclock M-15 / Github issue

Check that to != 0.

#0 - HardlyDifficult

2022-03-02T20:55:34Z

Yes, this is a good recommendation which may prevent user's losing their funds due to user error. We have made the following change as recommended:

 } else if (to == address(0)) {
      revert FETH_Cannot_Withdraw_To_Address_Zero();
    } else if (to == address(this)) {
      revert FETH_Cannot_Withdraw_To_FETH();
    }

Findings Information

🌟 Selected for report: cmichel

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/blob/4d8c8931baffae31c7506872bf1100e1598f2754/contracts/libraries/LockedBalance.sol#L56

Vulnerability details

Impact

The LockedBalance contract takes 256-bit amount values but performs bit math on them as if they were 96 bit values. Bits could spill over to a different locked balance in the else part (lockedBalance stores two 128-bit locked balances in one 256-bit storage field):

function set(
  Lockups storage lockups,
  uint256 index,
  uint256 expiration,
  uint256 totalAmount
) internal {
  unchecked {
    // @audit-issue should drop totalAmount to 96, expiration to 128-96=32
    uint256 lockedBalanceBits = totalAmount | (expiration << 96);
    if (index % 2 == 0) {
      // set first 128 bits.
      index /= 2;
      // @audit-info clears upper bits, then sets them
      lockups.lockups[index] = (lockups.lockups[index] & last128BitsMask) | (lockedBalanceBits << 128);
    } else {
      // set last 128 bits.
      index /= 2;
      // @audit-info clears lower bits, then sets them
      // @audit-issue sets entire 256-bit lockedBalanceBits instead of just 128-bit
      lockups.lockups[index] = (lockups.lockups[index] & first128BitsMask) | lockedBalanceBits;
    }
  }
}

It could then increase the other, unrelated locked balance's amount leading to stealing funds from the protocol. All callers of this function currently seem to ensure that totalAmount is indeed less than 96 bits but the LockedBalance library should be self-contained and not depend on the calling side to perform all checks. If the code is ever extended and more calls to these functions are performed, it'll likely cause issues.

The same issue happens in setTotalAmount

Make sure that there are only 96/32 bits set in totalAmount and expiration by dropping them to their respective types.

function set(
  Lockups storage lockups,
  uint256 index,
  uint256 expiration,
  uint256 totalAmount
) internal {
  unchecked {
-    uint256 lockedBalanceBits = totalAmount | (expiration << 96);
+    // cast it to uint256 again for the << 96 to work on 256-bits
+    uint256 lockedBalanceBits = uint256(uint96(totalAmount)) | (uint256(uint32(expiration)) << 96);
    
    ...
  }
}

#0 - HardlyDifficult

2022-03-07T12:23:54Z

This is a good concern to flag. With this release we use a lot of slot packing strategies to try and save gas. We considered this suggestion, as well as potentially using the safe cast helpers from the OpenZeppelin library. For the moment at least, we decided not to make any changes here. We believe the assumptions behind these values are sound, and although it's mathematically possible for the assumptions to be violated, we don't believe it'll occur in production.

For totalAmount for example, that value is always bound by the amount of ETH a user has sent to the FETH contract. Here are some notes we have written up about the size assumptions in our codebase:

  • ETH: uint96
    • Circulating supply is currently 119,440,269 ETH (119440269000000000000000000 wei / 1.2 * 10^26).
    • Max uint96 is 7.9 * 10^28.
    • Therefore any value capped by msg.value should never overflow uint96 assuming ETH total supply remains under 70,000,000,000 ETH.
      • There is currently ~2% inflation in ETH total supply (which fluctuates) and this value is expected to do down. We expect Ether will become deflationary before the supply reaches more than 500x current values.
    • 96 bits packs perfectly into a single slot with an address.
  • Dates: uint32
    • Current date in seconds is 1643973923 (1.6 * 10^9).
    • Max uint32 is 4 * 10^9.
    • Dates will not overflow uint32 until 2104.
    • To ensure we don't behave unexpectedly in the future, we should require dates are <= max uint32.
    • uint40 would allow enough space for any date, but it's an awkward size to pack with.
  • Sequence ID indexes: uint32
    • Our max sequence ID today is 149,819 auctions.
    • Max uint32 is 4 * 10^9.
    • Indexes will not overflow uint32 until we see >28,000x growth. This is the equiv to ~300 per block for every block in Ethereum to date.

Findings Information

🌟 Selected for report: cmichel

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/blob/4d8c8931baffae31c7506872bf1100e1598f2754/contracts/mixins/NFTMarketFees.sol#L78

Vulnerability details

Impact

The creator payouts are capped at MAX_ROYALTY_RECIPIENTS_INDEX. It's currently set to 4 and only 5 creators are paid out. Other creators are ignored.

I don't think cases with more than 5 creators / royalty receivers are unlikely. It can and should probably be increased, especially as the transfers are already gas restricted.

#0 - HardlyDifficult

2022-03-02T21:04:21Z

Yes this is a fair point. The limit we put in place is arbitrary. We want some limit in order to ensure that the gas costs (which are often pushed to users other than the original creator) never get to be very expensive.

What we can and should do is document this limitation so that it's more clear what exactly will happen when too many recipients are defined. Additionally we should comment on a possible workaround: If you have a contract that splits with too many participants you could use the Royalty Override in order to define a single contract recipient instead to handle the splits - e.g. https://www.0xsplits.xyz/

Findings Information

🌟 Selected for report: cmichel

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/blob/4d8c8931baffae31c7506872bf1100e1598f2754/contracts/mixins/NFTMarketPrivateSale.sol#L156

Vulnerability details

Impact

Similar to spoofing in finance, users can create private sales with correct signatures but then frontrun the buy with a transfer to a different wallet they control.

No funds are lost as this the NFT <> FETH exchange is atomic but it can be bad if third parties create a naive off-chain centralized NFT market based on this signature feature. It's also frustrating for the users if they try to accept the private sale but their transaction fails.

This is made possible because private sales do not keep the NFT in escrow. Consider escrowing the NFT also for private sales.

#0 - HardlyDifficult

2022-03-02T21:07:54Z

We are actually considering moving private sales to an escrow system. But we'll leave it as-is for now for backwards compatibility and to simplify our upcoming launch.

We understand this could happen and will update the comments to make that more clear.

Findings Information

Awards

207.419 USDC - $207.42

Labels

bug
QA (Quality Assurance)

External Links

QA Report

  • FETH._deductAllowanceFrom does not emit an Approval event. It's not strictly required by EIP20 for transferFrom calls but without it off-chain scripts cannot correctly track the actual approvals. (It's also done in the OZ implementation)
  • FETH._freeFromEscrow: comment should say: "if bucket NOT yet expired, that's the new start index"
  • NFTMarketReserveAuction._transferFromEscrow: comment should say: "If the auction has ended, the highest bidder will be the new owner" as this reverts if auction has not ended
  • NFTMarketFees: misleading naming. It's an amount, not a share as in creatorShares or totalShares. How about fee or royalty?
  • NFTMarketOffer.makeOffer: The error should be "cannot be created" instead of "cannot be accepted" because this is an error in makeOffer, not in acceptOffer.

#0 - HardlyDifficult

2022-03-07T12:39:44Z

#1 - alcueca

2022-03-17T09:38:14Z

Unadjusted score: 20

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