For projects with a 0% founder percentage ownership, it is possible for the highest bidder of the first auction to not receive the minted token, and funds from the first auction to be stuck in the contract.
Proof of Concept
In Auction.sol, the contract determines if the first auction has been created using the statement auction.tokenId == 0, as shown below:
244: function unpause() external onlyOwner {
245: _unpause();
246:
247: // If this is the first auction:
248: if (auction.tokenId == 0) {
249: // Transfer ownership of the contract to the DAO
250: transferOwnership(settings.treasury);
251:
252: // Start the first auction
253: _createAuction();
254: }
255: // Else if the contract was paused and the previous auction was settled:
256: else if (auction.settled) {
257: // Start the next auction
258: _createAuction();
259: }
260: }
If the check at line 248 passes, the unpause() function will directly call _createAuction() without checking that the current auction has been settled.
Thus, in a situation where the tokenId of the first auction is 0, the owner could potentially call unpause() again without settling the first auction, causing the current auction to be overwritten due to the call to _createAuction().
This would cause the following:
Funds deposited by the highest bidder from the first auction would become stuck in the contract
The highest bidder for the first auction will never receive the minted token
For example:
A project is deployed with a total percentage ownership of 0% from founders
The founder calls unpause() to start the first auction
At line 250, ownership is transferred to the treasury
The first auction is started via _createAuction() at line 253
As founders have no ownership percentage of the minted tokens, the first token minted (tokenId = 0), will be used as the token for the first auction
The DAO pauses the auction for some reason via pause()
If the DAO calls unpause() without first calling settleAuction()
A new auction with tokenId = 1 would be created, overwriting the first auction
In this scenario, funds from the first auction would be unretrievable as the Auction contract does not have a function to send any amount of ETH to an address. Furthermore, as settleAuction() was not called, the highest bidder would not receive the token of the first auction.
Lines of code
https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/auction/Auction.sol#L244-L260
Vulnerability details
Impact
For projects with a 0% founder percentage ownership, it is possible for the highest bidder of the first auction to not receive the minted token, and funds from the first auction to be stuck in the contract.
Proof of Concept
In
Auction.sol
, the contract determines if the first auction has been created using the statementauction.tokenId == 0
, as shown below:If the check at line 248 passes, the
unpause()
function will directly call_createAuction()
without checking that the current auction has been settled.Thus, in a situation where the
tokenId
of the first auction is0
, the owner could potentially callunpause()
again without settling the first auction, causing the current auction to be overwritten due to the call to_createAuction()
.This would cause the following:
For example:
unpause()
to start the first auction_createAuction()
at line 253tokenId = 0
), will be used as the token for the first auctionpause()
unpause()
without first callingsettleAuction()
tokenId = 1
would be created, overwriting the first auctionIn this scenario, funds from the first auction would be unretrievable as the
Auction
contract does not have a function to send any amount of ETH to an address. Furthermore, assettleAuction()
was not called, the highest bidder would not receive the token of the first auction.The test in this gist demonstrates the above.
Recommended Mitigation Steps
At line 248, instead of checking
auction.tokenId == 0
to determine if the first auction has been created, check ifauction.startTime == 0
instead.