Platform: Code4rena
Start Date: 06/09/2022
Pot Size: $90,000 USDC
Total HM: 33
Participants: 168
Period: 9 days
Judge: GalloDaSballo
Total Solo HM: 10
Id: 157
League: ETH
Rank: 72/168
Findings: 2
Award: $121.62
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: zzzitron
Also found by: 0xSmartContract, ChristianKuri, ElKu, Lambda, MiloTruck, davidbrai, elad, hansfriese, immeas, ladboy233, scaraven, volky
https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/auction/Auction.sol#L244-L260 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/auction/Auction.sol#L204-L237 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L143-L162 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L177-L199
When the founder creates the DAO, he can assign himself any percentage of the generated tokens from 0 - 100%. If the founder selects to have the 100% of the tokens it will break the application.
A user creates a DAO and assigns himself the 100% of the tokens. To start receiving the tokens he unpauses the auction, which transfers the ownership of the auction to the treasury and creates the first auction.
When the first auction is created the Auction contract mints a token using token.mint()
, in that function at line 152 it stats a do while
, that do while
checks if that token id is for founder (this will be always true). If thats true the auction mints the token, and repeats the process until it finds a token that is not for the founder. Since all the tokens are for the founder, it never stops minting. This will create an infinite loop and the transaction will fail.
At the end the DAO wont work for him because the auction will never be able to create a new token. This will make him lose trust in the DAO builder and he will probably not use it again.
auction/Auction.sol#L244-L260 & #L204-L237
File: auction/Auction.sol#L244-L260 function unpause() external onlyOwner { _unpause(); // If this is the first auction: if (auction.tokenId == 0) { // Transfer ownership of the contract to the DAO transferOwnership(settings.treasury); // Start the first auction _createAuction(); } // Else if the contract was paused and the previous auction was settled: else if (auction.settled) { // Start the next auction _createAuction(); } } File: auction/Auction.sol#L204-L237 function _createAuction() private { // Get the next token available for bidding try token.mint() returns (uint256 tokenId) { // Store the token id auction.tokenId = tokenId; // Cache the current timestamp uint256 startTime = block.timestamp; // Used to store the auction end time uint256 endTime; // Cannot realistically overflow unchecked { // Compute the auction end time endTime = startTime + settings.duration; } // Store the auction start and end time auction.startTime = uint40(startTime); auction.endTime = uint40(endTime); // Reset data from the previous auction auction.highestBid = 0; auction.highestBidder = address(0); auction.settled = false; emit AuctionCreated(tokenId, startTime, endTime); // Pause the contract if token minting failed } catch Error(string memory) { _pause(); } }
token/Token.sol#L143-L162 & #L177-L199
File: token/Token.sol#L143-L162 function mint() external nonReentrant returns (uint256 tokenId) { // Cache the auction address address minter = settings.auction; // Ensure the caller is the auction if (msg.sender != minter) revert ONLY_AUCTION(); // Cannot realistically overflow unchecked { do { // Get the next token to mint tokenId = settings.totalSupply++; // Lookup whether the token is for a founder, and mint accordingly if so } while (_isForFounder(tokenId)); } // Mint the next available token to the auction house for bidding _mint(minter, tokenId); } File: token/Token.sol#L177-L199 function _isForFounder(uint256 _tokenId) private returns (bool) { // Get the base token id uint256 baseTokenId = _tokenId % 100; // If there is no scheduled recipient: if (tokenRecipient[baseTokenId].wallet == address(0)) { return false; // Else if the founder is still vesting: } else if (block.timestamp < tokenRecipient[baseTokenId].vestExpiry) { // Mint the token to the founder _mint(tokenRecipient[baseTokenId].wallet, _tokenId); return true; // Else the founder has finished vesting: } else { // Remove them from future lookups delete tokenRecipient[baseTokenId]; return false; } }
If the founder wants to have the 100% of the tokens, it should ask him how many tokens he wants, and then it should mint him that amount of tokens. If later he wants to mint more tokens he can start the auction but now he wont maintain the 100% of the tokens. In this case if the auction is started, the auction wont mint him any more tokens. Basically adding the functionality to mint a specific amount of tokens instead of a percent.
VSCode and Manual Analysis
#0 - horsefacts
2022-09-15T21:07:22Z
#1 - GalloDaSballo
2022-09-20T19:47:38Z
🌟 Selected for report: Lambda
Also found by: 0x1337, 0x1f8b, 0x4non, 0x85102, 0xA5DF, 0xNazgul, 0xSmartContract, 0xbepresent, 0xc0ffEE, 8olidity, Aymen0909, B2, Bnke0x0, CRYP70, Captainkay, CertoraInc, Ch_301, Chom, ChristianKuri, CodingNameKiki, Deivitto, Diana, DimitarDimitrov, ElKu, EthLedger, Franfran, Funen, GimelSec, JansenC, Jeiwan, Jujic, Lead_Belly, MEP, MasterCookie, MiloTruck, Noah3o6, PPrieditis, PaludoX0, Picodes, PwnPatrol, R2, Randyyy, RaymondFam, Respx, ReyAdmirado, Rolezn, Samatak, Tointer, Tomo, V_B, Waze, _Adam, __141345__, a12jmx, ak1, asutorufos, azephiar, ballx, bharg4v, bin2chen, bobirichman, brgltd, bulej93, c3phas, cccz, ch0bu, cloudjunky, cryptonue, cryptostellar5, cryptphi, csanuragjain, d3e4, datapunk, davidbrai, delfin454000, dharma09, dic0de, dipp, djxploit, eierina, erictee, fatherOfBlocks, gogo, hansfriese, hyh, imare, indijanc, izhuer, jonatascm, ladboy233, leosathya, lucacez, lukris02, m9800, martin, minhtrng, ne0n, neumo, oyc_109, p_crypt0, pashov, pauliax, pcarranzav, pedr02b2, peritoflores, pfapostol, rbserver, ret2basic, robee, rvierdiiev, sach1r0, sahar, scaraven, sikorico, simon135, slowmoses, sorrynotsorry, tnevler, tonisives, volky, yixxas, zkhorse, zzzitron
72.5396 USDC - $72.54
If the initializer is not executed in the same transaction as the constructor, a malicious user can front-run the initialize()
call. In the best case, the owner is forced to waste gas and re-deploy. In the worst case, the owner does not notice that his/her call reverts, and everyone starts using a contact that the attacker would have control of (the manager) since he will be the owner.
If the manager contract is initialized before upgrading to v2, the manager proxy would also have incorrect values, and all the contract for the logic of the token, auction, governance, treasury and metadata would have an incorrect manager address. Making the deployer deploy everything again.
File: manager/Manager.sol #1 80 function initialize(address _owner) external initializer { 81 // Ensure an owner is specified 82 if (_owner == address(0)) revert ADDRESS_ZERO(); 83 84 // Set the contract owner 85 __Ownable_init(_owner); 86 }
The Treasury initialize()
function have a check for address(0)
in the governor param.
treasury/Treasury.sol#L43-L60
File: treasury/Treasury.sol#L47-L48 47 // Ensure a governor address was provided 48 if (_governor == address(0)) revert ADDRESS_ZERO();
The Governor initialize()
function have a check for address(0)
in the treasury and token params.
governor/Governor.sol#L57-L87
File: governor/Governor.sol#L69-L71 69 // Ensure non-zero addresses are provided 70 if (_treasury == address(0)) revert ADDRESS_ZERO(); 71 if (_token == address(0)) revert ADDRESS_ZERO();
To keep consistency, all the initializers should have address(0)
check as well.
token/Token.sol#L43 metadata/MetadataRenderer.sol#L45 auction/Auction.sol#L54
The auction can be paused and then started again. If the auction is paused it can be bid on and settled. I consider that if an auction is paused it should not allow anyone to bid on it or settle it. When the auction is unpaused it should increment the auction duration by the duration of the pause, this way the auction will be correctly paused.
auction/Auction.sol#L90-L127 auction/Auction.sol#L268-L270
The auction duration can be set to a really low number, this can cause the auction to end before anyone can interact with it. The auction should have a minimum duration. When the auction finishes and settles it burns the token if no one bid, but keeps increasing the totalSupply of the token.
When calling a uint++
the value that is returned is the previous one.
File: token/Token.sol #1 /// Some developers could thing that founderId is the new value from settings.numFounders (1) /// But instead in the first iteration founderId will be 0 uint256 founderId = settings.numFounders++;
My recommendation is do it this way.
uint256 founderId = settings.numFounders; settings.numFounders++;
The % after the assignment is not necessary, it does not do anything. Also the Schedule cannot be greater than 100.
(baseTokenId += schedule) % 100;
#0 - GalloDaSballo
2022-09-26T21:20:44Z
3L 1NC
Disputing: Modulus after the set is not needed, this was a higher severity bug you missed) Disputing : A Paused Auction that already started can still be bid on and settled as the system works fine