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: 11/168
Findings: 4
Award: $2,818.40
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: MEP
Also found by: 0xSky, CertoraInc, MiloTruck, PwnPatrol, R2, Tointer, V_B, __141345__, antonttc, azephiar, cccz, d3e4, datapunk, davidbrai, easy_peasy, hansfriese, minhtrng, neumo, pcarranzav, peritoflores, scaraven, teawaterwire, tonisives, unforgiven, wagmi, zkhorse, zzzitron
5.6134 USDC - $5.61
https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L71 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L118
There is _addFounders
function in the Token
contract. There is the following loop:
// Used to store the base token id the founder will recieve uint256 baseTokenId; // For each token to vest: for (uint256 j; j < founderPct; ++j) { // Get the available token id baseTokenId = _getNextTokenId(baseTokenId); // Store the founder as the recipient tokenRecipient[baseTokenId] = newFounder; emit MintScheduled(baseTokenId, founderId, newFounder); // Update the base token id (baseTokenId += schedule) % 100; }
As we can see line (baseTokenId += schedule) % 100;
have a bug, there is no reason to do % 100
operation over the result value of the incrementing baseTokenId
by schedule
.
We recommend removal of such modulo operation. If case it is necessary to perform a modulo operation over baseTokenId
inside the loop, it will be correct to do the following:
baseTokenId += schedule; baseTokenId %= 100;
#0 - GalloDaSballo
2022-09-20T12:59:20Z
🌟 Selected for report: V_B
2702.9374 USDC - $2,702.94
https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/auction/Auction.sol#L204 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/auction/Auction.sol#L206 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/auction/Auction.sol#L235
There is a function _createAuction
in Auction
contract.
It consist the following logic:
/// @dev Creates an auction for the next token function _createAuction() private { // Get the next token available for bidding try token.mint() returns (uint256 tokenId) { **creating of the auction for token with id equal to tokenId** // Pause the contract if token minting failed } catch Error(string memory) { _pause(); } }
According to the EIP-150 call
opcode can consume as most 63/64
of parrent calls' gas. That means token.mint()
can fail since there will be no gas.
All in all, if token.mint()
fail on gas and the rest gas is enough for pausing the contract by calling _pause
in catch
statement the contract will be paused.
Please note, that a bug can be exploitable if the token.mint() consume more than 1.500.000 of gas, because 1.500.000 / 64 > 20.000 that need to pause the contract. Also, the logic of token.mint()
includes traversing the array up to 100 times, that's heavy enough to reach 1.500.000 gas limit.
Contract can be paused by any user by passing special amount of gas for the call of settleCurrentAndCreateNewAuction
(which consists of two internal calls of _settleAuction
and _createAuction
functions).
Add a special check for upper bound of gasLeft
at start of _createAuction
function.
#0 - GalloDaSballo
2022-09-19T22:59:00Z
Honestly I'm really impressed by the submission, however I think the quote:
Please note, that a bug can be exploitable if the token.mint() consume more than 1.500.000 of gas, because 1.500.000 / 64 > 20.000 that need to pause the contract. Also, the logic of token.mint() includes traversing the array up to 100 times, that's heavy enough to reach 1.500.000 gas limit.
shows that the likelihood of this happening is extremely small, the base mint will cost between 20k and 40k gas, and each instance of the loop should roughly cost 5k, with the minting instance costing around 5k + up to 50k
From basic napkin math this is actually surprisingly possible <img width="721" alt="Screenshot 2022-09-20 at 00 56 54" src="https://user-images.githubusercontent.com/13383782/191133542-1a17c58b-2cf1-4eb4-a0c8-36b5546aa62c.png">
However I believe the odds of this happening are extremely low as you'd need to have at least 20 tokens being minted to founders <img width="795" alt="Screenshot 2022-09-20 at 00 58 15" src="https://user-images.githubusercontent.com/13383782/191133675-9b226a11-b391-4d7a-a9a6-beaed8cd88b3.png">
I think Med is more appropriate for now, I really like the catch of the OOG exploit, however I may have to downgrade further as the worst case scenario being pausing is not impactful
#1 - GalloDaSballo
2022-09-26T00:42:34Z
I believe the finding to be valid and Medium Severity as the conditions are non-trivial, but the impact is Denial of Service which can be triggered predictably given the circumnstances
#2 - davidbrai
2022-11-04T16:31:24Z
@GalloDaSballo @iainnash catch Error(string memory)
doesn't catch out-of-gas errors
to catch out-of-gas errors one would need to use catch (bytes memory lowLevelData)
Therefore, IIUC, this issue is invalid
This was intentionally not catching out-of-gas errors in the Nouns contract this code is based on. (credit to @solimander)
#3 - GalloDaSballo
2022-11-07T22:35:44Z
@davidbrai Thank you for the clarification, I have ran my own sim and must agree with you.
Errors of the type:
Will not be caught by the Catch Statement.
I have to agree with you that I should have disputed this report for lacking a specific POC and the POC I wrote indicates that it is invalid.
I'd like to flag that the POC I wrote seems to suggest that the function will not catch custom errors as well, meaning that as far as I can tell, the custom errors will not be caught.
POC follows (can be quickly setup using the excellent: https://github.com/0xKitsune/gas-lab)
<img width="876" alt="Screenshot 2022-11-07 at 23 34 11" src="https://user-images.githubusercontent.com/13383782/200429755-eb018301-e223-480d-9e65-a2dccbb58952.png">// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.13; import "../../lib/test.sol"; import "../../lib/Console.sol"; enum Values { First, Second } contract Exploit is DSTest { Reverter testC; function testExploit() public { testC = new Reverter(); try testC.doTheRevertingThing{gas: 123}() returns (uint256) { } catch Error(string memory) { return; } require(false, "Did not catch"); } } contract Reverter { error INVALID_APPROVAL(); function doTheRevertingThing() external returns (uint256){ revert INVALID_APPROVAL(); return 123; } }
I apologize for the mistake and hope it didn't cause needless refactorings. The site is looking great, wish you the best with the product!
🌟 Selected for report: CertoraInc
Also found by: 0xSky, PwnPatrol, Tomo, V_B, antonttc, bin2chen, pcarranzav, peritoflores, rbserver, scaraven, wagmi, zzzitron
49.075 USDC - $49.08
https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L71 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L102 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L108
There is a function _addFounders
in Token
contract. It accepts array of FounderParams
as an input. For each of founders it uses founderPct
as an variable to store percent ownership for such founder.
It is unsafe to cast it to from uint256
to uint8
in some of places, but not all:
... if ((totalOwnership += uint8(founderPct)) > 100) revert INVALID_FOUNDER_OWNERSHIP(); ... newFounder.ownershipPct = uint8(founderPct); ... uint256 schedule = 100 / founderPct; ... // For each token to vest: for (uint256 j; j < founderPct; ++j) { ... } ...
Lack of casting founderPct
to uint8
type can lead to incorrect calculation of schedule
variable and incorrect number of iterations in the loop that goes for each token to vest for such founder.
Use uint8
type for ownershipPct
in FounderParams
struct or cast founderPct
to uint8
type in all possible places in _addFounders
function.
#0 - GalloDaSballo
2022-09-26T19:21:26Z
Dup of #303
🌟 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
There is castVoteBySig
function in Governor
contract. It accepts _deadline
as a parameter and checks it is valid comparing to block.timestamp
. Specifically, the function performs such checks.
// Ensure the deadline has not passed if (block.timestamp > _deadline) revert EXPIRED_SIGNATURE();
There is another logic with non-strict comparison in different parts of the code:
Treasury.sol
/// @notice If a proposal is ready to execute (does not consider expiration) /// @param _proposalId The proposal id function isReady(bytes32 _proposalId) public view returns (bool) { return timestamps[_proposalId] != 0 && block.timestamp >= timestamps[_proposalId]; }
Auction.sol
// Ensure the auction is still active if (block.timestamp >= _auction.endTime) revert AUCTION_OVER();
It is reasonable to have consistent logic in all places -- we propose to use non-strict comparison in castVoteBySig
function.
#0 - GalloDaSballo
2022-09-27T01:12:05Z
R