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: 20/168
Findings: 6
Award: $1,187.48
🌟 Selected for report: 0
🚀 Solo Findings: 0
492.6103 USDC - $492.61
https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/auction/Auction.sol#L247-L254 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/auction/Auction.sol#L206-L208 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L154
Token ids start from 0, however, this value is also checked against when unpausing the contract:
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(); } ... }
If the owner unpauses the auction twice, the second time the value of auction.tokenId
is again 0 meaning the check will pass and re-create the auction again even though the current auction might have active bids.
It is highly likely because I think the highest chances of unpausing are initially if something does not go according to the plan or an owner wants to adjust some parameters before continuing the sale. This could happen accidentally and the current highest bid and NFT #0 will be lost or this could be a griefing attack by the malicious owner which I suppose is very unlikely because the owner has too many privileges to exploit the system anyway.
Yet I think this is a significant flaw in the codebase that should be prevented and deserves a severity of Medium.
PoC of a simplified version: https://gist.github.com/pauliax/07ba76afb89a431f78eadef488e296e0
Steps to reproduce are in the gist's comments section.
Token ids should either not start from 0 or you should enhance this check, e.g. if (auction.tokenId == 0 && auction.startTime == 0)
.
#0 - GalloDaSballo
2022-09-25T21:30:58Z
Dup of #376
104.7173 USDC - $104.72
This situation happens when the proposer's voting power and proposalThreshold
are equal because the contract does not respect that:
function cancel(bytes32 _proposalId) external { // Ensure the caller is the proposer or the proposer's voting weight has dropped below the proposal threshold if (msg.sender != proposal.proposer && getVotes(proposal.proposer, block.timestamp - 1) > proposal.proposalThreshold) revert INVALID_CANCEL();
The original Nouns contract had these checks in an opposite way:
require( msg.sender == proposal.proposer || nouns.getPriorVotes(proposal.proposer, block.number - 1) < proposal.proposalThreshold, 'NounsDAO::cancel: proposer above threshold' );
so when inverting, <
should become >=
. Now when proposer votes == proposal.proposalThreshold
it allows canceling such a proposal by anyone. This is a huge flow compared to ERC20s-based votes because here this 1 vote means 1 NFT which is supposed to be significant and hard to obtain. E.g. when the proposalThreshold
is 2 NFTs the proposer must have 3 NFTs to avoid being canceled.
getVotes(proposal.proposer, block.timestamp - 1) >= proposal.proposalThreshold
#0 - Chomtana
2022-09-19T07:49:39Z
Dup #589
#1 - GalloDaSballo
2022-09-21T14:24:28Z
Dup of #194
🌟 Selected for report: __141345__
Also found by: pauliax, rbserver, rvierdiiev, sorrynotsorry
354.6794 USDC - $354.68
Judge has assessed an item in Issue #704 as Medium risk. The relevant finding follows:
#0 - GalloDaSballo
2022-09-27T14:43:55Z
I think it would be better if changes to essential auction parameters (setDuration, setReservePrice, setTimeBuffer, setMinimumBidIncrement, etc) did not impact the current auction. They should take effect only when the next auction round starts. The current ongoing auction should finish with the old settings so that users won't be front-runned with unexpected new settings that they were not bidding on. Or at least these settings should have reasonable upper/lower boundaries to prevent the possibility of griefing. This is submitted as a QA issue because it was explicitly mentioned: "Wardens should assume that governance variables are set sensibly".
Dup of #450
🌟 Selected for report: azephiar
Also found by: R2, Solimander, __141345__, bin2chen, cccz, davidbrai, pauliax, rbserver
129.2807 USDC - $129.28
In the initial phase, when not many tokens are minted, a malicious actor can start submitting proposals and later execute them. E.g. when the first token is minted, this first owner can instantly submit proposals to retrieve all the eth back from the treasury. proposalThreshold
and quorum
will likely be low (0 or 1 depending on proposalThresholdBps
and quorumThresholdBps
).
Others would not have a chance to block this proposal later because their past votes were 0. The admins can jump in and veto such proposals but this does not prevent an attacker from spamming multiple proposals simultaneously making it infeasible to fight if the attacker is willing to spend enough ETH for gas.
A similar issue is when the total supply is 0. Then anyone can submit proposals because the threshold and quorum are basically 0. Admins will not have enough supplies to combat the aggregated power of all the spammers.
Thus, this initial spam of proposals can become insurmountable even with the veto function.
I believe veto is not enough to combat this issue and there should be more on-chain precautions. Consider something like introducing a minimum token supply when the proposals can start or limiting the number of active proposals per token.
#0 - GalloDaSballo
2022-09-20T13:18:42Z
Dup of #604
🌟 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
60.7742 USDC - $60.77
I think it would be better if changes to essential auction parameters (setDuration
, setReservePrice
, setTimeBuffer
, setMinimumBidIncrement
, etc) did not impact the current auction. They should take effect only when the next auction round starts. The current ongoing auction should finish with the old settings so that users won't be front-runned with unexpected new settings that they were not bidding on. Or at least these settings should have reasonable upper/lower boundaries to prevent the possibility of griefing.
This is submitted as a QA issue because it was explicitly mentioned: "Wardens should assume that governance variables are set sensibly".
There are many constructors that are payable for no reason, especially when there is no way to retrieve the native asset later unless the contract is upgraded.
Token
in AuctionStorageV1
should probably be of an interface type, that is IToken
, not implementation:
Token public token;
minBid
can have a minimum value of 1%, consider increasing bps to allow broader spreads:minBid = highestBid + ((highestBid * settings.minBidIncrement) / 100);
msg.value ==
sum of all _values
, the caller will lose excess eth, it will remain in the treasury:function execute( ... ) external payable returns (bytes32) { ... settings.treasury.execute{ value: msg.value }(_targets, _values, _calldatas, _descriptionHash);
createBid
does not have whenNotPaused
modifier, meaning the bids for the current auction can still come even when the contract is paused:function createBid(uint256 _tokenId) external payable nonReentrant
On one hand, it is bad in case you discover a flaw and want to stop bidding, on the other hand, it is good that you let finish the current auction and the owners cannot interrupt it by pausing and unpausing when it is beneficial to them. So because I was not sure what was the original intention, I am submitting this as a QA issue.
#0 - GalloDaSballo
2022-09-27T00:37:14Z
TODO
R
R
Disagree as it's meant to use the ETH from the Treasury
Disputed
2R
🌟 Selected for report: pfapostol
Also found by: 0x1f8b, 0x4non, 0x5rings, 0xA5DF, 0xSmartContract, 0xc0ffEE, 0xkatana, Aymen0909, Bnke0x0, CertoraInc, Chandr, CodingNameKiki, Cr4ckM3, Deivitto, DimSon, Franfran, JAGADESH, JC, Jeiwan, Lambda, LeoS, Matin, Metatron, Migue, MiloTruck, PPrieditis, PaludoX0, R2, RaymondFam, Respx, ReyAdmirado, Rolezn, Saintcode_, Samatak, SnowMan, StevenL, Tointer, TomJ, Tomo, WatchDogs, Waze, _Adam, __141345__, ajtra, asutorufos, ballx, brgltd, bulej93, c3phas, ch0bu, dharma09, djxploit, durianSausage, easy_peasy, fatherOfBlocks, gianganhnguyen, gogo, imare, leosathya, lucacez, martin, oyc_109, pauliax, peiw, prasantgupta52, ret2basic, rfa, robee, sikorico, simon135, tofunmi, volky, wagmi, zishansami
45.4217 USDC - $45.42
Repeated call to proposalThreshold()
:
// Get the current proposal threshold uint256 currentProposalThreshold = proposalThreshold(); // Cannot realistically underflow and `getVotes` would revert unchecked { // Ensure the caller's voting weight is greater than or equal to the threshold if (getVotes(msg.sender, block.timestamp - 1) < proposalThreshold()) revert BELOW_PROPOSAL_THRESHOLD(); }
#0 - GalloDaSballo
2022-09-26T20:16:11Z
200 gas