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: 4/168
Findings: 9
Award: $3,703.17
🌟 Selected for report: 1
🚀 Solo Findings: 1
https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L262-L268 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L189
In ERC721Votes.sol
, when an ERC721 token is minted and transferred to a user who has delegated their tokens, the token voting weight is incorrectly added to the voting weight of the user in _afterTokenTransfer()
. This means that tokens are inconsistently delegated, as part of the user's tokens are delegated towards themselves and the rest to another user. When a user then tries to change their delegation through delegate()
, the contract calls balanceOf()
and assumes all user tokens are delegated to the delegatee which can cause an underflow and allow a user to gain a very large number of votes.
This can be exploited to vote for malicious proposals or DOS the governance and vote against all proposals.
High severity is suitable as this can be done by any user at any time and allows a user to gain complete control over the governance protocol
Auction.sol
calls token.transferFrom(address(this), alice.address, tokenId);
_afterTokenTransfer()
is called which then subsequently calls _moveDelegateVotes(_from, _to, 1);
where _to
is Alice's addressdelegate(_to)
=> _moveDelegateVotes(prevDelegate, _to, balanceOf(_from);
unchecked
is used) and becomes type(uint256).max
VS Code
Modify _afterTokenTransfer()
to:
function _afterTokenTransfer( address _from, address _to, uint256 _tokenId ) internal override { //if sender or recipient have not delegated, then automatically self-delegate if(delegation[_from] == address(0) && _from != address(0)) { delegation[_from] = _from; } if(delegation[_to] == address(0) && _to != address(0)) { delegation[_to] = _to; } // Transfer 1 vote from the sender's delegatee to the recipient's delegatee _moveDelegateVotes(delegation[_from], delegation[_to], 1); super._afterTokenTransfer(_from, _to, _tokenId); }
Additionally, ensure that a user is not able to delegate to address(0)
as that will break the above logic.
#0 - GalloDaSballo
2022-09-20T21:05:38Z
🌟 Selected for report: zzzitron
Also found by: 0xSmartContract, ChristianKuri, ElKu, Lambda, MiloTruck, davidbrai, elad, hansfriese, immeas, ladboy233, scaraven, volky
When the total founder ownership is 100%, all NFT tokens which are minted will be automatically minted for the founders. However, as token.mint()
will constantly increase tokenID
, unbounded gas consumption occurs which will cause the external call to revert due to exceeding block gas limits, thereby pausing the auction contract without the founders receiving any NFTs. Therefore, the auction will only properly work once the founder's claim has expired, with founders not receiving a single NFT.
I believe medium severity is suitable as this issue breaks a pretty significant selling point of the protocol however requires that the founders setup the governance with 100% initial ownership
founderPct = 100
unpause()
in Auction.sol
_createAuction()
calls token.mint()
which starts an infinite loop and revertsVS Code
Add a check to make sure that founder ownership does not reach 100% e.g.
In _addFounders()
from Token.sol
add a greater or equal than sign:
if ((totalOwnership += uint8(founderPct)) >= 100) revert INVALID_FOUNDER_OWNERSHIP();
#0 - GalloDaSballo
2022-09-20T19:47:23Z
🌟 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#L118 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L179
Due to improperly updating baseTokenId
in _addFounders()
, baseTokenId
may grow larger than 100 which means subsequently allocated NFTs are not properly rewarded to the founders. This can cause some founders to receive drastically fewer NFTs and voting rights than they should have.
baseTokenId
s : 0, 1, ... , 49, 50 because schedule
rounds down to 100 / 51 = 1
100 / 49 = 2
and receives baseTokenId
: 51, 53, ... , 99, ... , 147isForFounder()
, baseTokenId
is assigned using:uint256 baseTokenId = _tokenId % 100;
Thereby ensuring that 0 <= baseTokenId <= 99
. Therefore, all baseTokenId
s assigned to Bob which are larger than 100
is useless, meaning that Bob receives only 25% of all tokens instead of his 49%
VS Code
Properly update baseTokenId
i.e.
Change
(baseTokenId += schedule) % 100;
to
baseTokenId = (baseTokenId + schedule) % 100;
#0 - GalloDaSballo
2022-09-20T12:58:35Z
A founder can rug pull governance by creating a malicious "sleeper" proposal with a very large delay to ensure that it does not expire. When the proposal is ready for execution, the founder can execute it and steal all funds stored in the treasury. Through this method, a founder can create the malicious proposal and then make legitimate and seemingly trustworthy actions such as reducing their voting weight, updating the vetoer to a trusted third party (multi-sig wallet or governance) etc...
I believe medium severity is suitable as this can cause substantial loss of user funds, however requires people to buy NFTs from the malicious governance. Considering that unprivileged attackers have been able to sneak past malicious proposals to reck governance protocols in the past (even with vetoers), this situation is not far-fetched.
settings.delay
through updateDelay()
to be a very large period of time (1 year, 2 years etc...)updateDelay()
to a normal period e.g. 1 week and then voluntarily gives up the majority of their power to gain trustVS Code
I recommend adding functionality which requires a certain number of votes to approve execution of a proposal so that proposals which might not be otherwise checked are given proper attention
This means that users who have a stake in governance now have control over proposals which may have been proposed a very long time ago which they had no control over
#0 - GalloDaSballo
2022-09-27T01:36:04Z
When token.mint()
is called at the creation of an auction, Token.sol
makes an external call with MetadataRenderer.onMinted()
which generates the attributes for the specific token using a seed. The attributes of the token is stored in a static array of length 16 called tokenAttributes
and the function then goes on to loop through each property and assign one item from each property to the static array. If the number of properties is larger than 15 (as index 0 is used to store numProperties
) then the function will revert due to out of bounds access. This would mean that minting tokens will always fail and Auction.sol
will stop working.
I believe medium severity is suitable, as this can permanently or temporarily break the protocol if the number of properties is above 15 (external circumstances).
auction.unpause()
and a token is minted through token.mint()
_mint()
, the contract makes an external call to metadataRenderer.onMinted()
which fails due to out of bounds accessVS Code
There are two possible solutions:
metadataRenderer.addProperties()
e.g.if(numStoredProperties + numNewProperties > 15) revert TOO_MANY_PROPERTIES();
tokenAttributes
to a dynamic arrayThe second option is more preferable as it allows more flexibility for founders which want to create a protocol with many properties.
#0 - GalloDaSballo
2022-09-25T21:44:53Z
Dup of #523
🌟 Selected for report: berndartmueller
Also found by: CertoraInc, m9800, scaraven
https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L116 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L98-L105 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L353
In hashProposal()
from Governer.sol
, the generated proposalID
is independent of msg.sender
and can be generated by anyone. Therefore, if a user tries to create a new proposal through propose()
an attacker can frontrun the transaction where they call propose()
with the same call data and then call cancel()
. As the contract does not allow duplicate propsalID
, the user's proposal will not be registered.
I believe medium severity is suitable as it can cause in temporary or permanent failure of the protocol however requires that the attacker can frontrun a user transaction
propose()
is added to the public mempoolauction.proposer
to the attacker's addresscancel()
VS Code
In hashProposal()
, add msg.sender so that any proposalID
is unique to that user
e.g.
function hashProposal( address[] memory _targets, uint256[] memory _values, bytes[] memory _calldatas, bytes32 _descriptionHash ) public pure returns (bytes32) { return keccak256(abi.encode(msg.sender, _targets, _values, _calldatas, _descriptionHash)); }
#0 - GalloDaSballo
2022-09-20T21:23:45Z
🌟 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
Judge has assessed an item in Issue #593 as Medium risk. The relevant finding follows:
#0 - GalloDaSballo
2022-09-27T14:50:11Z
Issue 2 founderPct may be larger than 100 Due to inconsistently casting founderPct to uint(8), founderPct may be truncated and pass all verification checks however when the baseTokenIDs is assigned, the function through loops through the full value which can cause the founder to receive full ownership of all NFTs even if OwnershipPct is set to 0.
Consider adding a check to make sure that founderPct < type(uint8).max
Dup of #303
🌟 Selected for report: scaraven
2702.9374 USDC - $2,702.94
https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L21 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/utils/Ownable.sol#L63 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/auction/Auction.sol#L22
governor.transferOwnership()
and Auction.transferOwnership()
is exposed to Treasury.sol
and can be called in a governance proposal. If this is used then it causes an inconsistency between the owner of these contracts and settings.treasury
which would prevent governance from ever modifying its own parameters and Ethereum from auctions will be sent to the wrong address.
I believe medium severity is suitable because we lose partial control of governance and/or ETH from auctions, assuming that a proposal (malicious or not) is passed which changes contract ownership.
governor.transferOwnership()
is executed and transfers ownership to a new treasury addressGovernor.sol
will make an external call to settings.treasury
which is not modified when ownership is passedsettings.treasury
is not able to modify any governance parameters such as proposalThreshold
The same issue applies to Auction.sol
VS Code
Modify transferOwnership()
in the relevant contracts so that if ownership is transferred, then settings.treasury
is changed accordingly.
#0 - GalloDaSballo
2022-09-26T22:35:34Z
The Warden has shown how, due to the possibility of decoupling Treasury and Governor via Upgradeable.transferOwnership Auction.treasury may be set to the wrong address.
Personally I think that the decoupling between Auction, Governor and Treasury should be removed as ultimately this system is meant to be "immutable" and a migration could be achieved via a full redeploy and then a proposal to transfer funds.
However, the situation described by the Warden can happen and in those cases funds would be lost or sent to the wrong contract.
Because a timelock needs to vote this, I'm inclined to reduce severity.
However, because the contract functionality allows Ownership Transfer by default, but there's no custom code to update the settings, I think Medium Severity is appropriate as in certain cases the contract will not behave as intended
🌟 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
62.7813 USDC - $62.78
When looping through each founder parameters in Token._addFounders()
, there is no check that a founder address is address(0)
, this can cause inconsistencies between OwnershipPct
and the actual percentage of NFTs which have been minted
Consider adding a check to make sure that a founder address is not 0
founderPct
may be larger than 100Due to inconsistently casting founderPct
to uint(8), founderPct
may be truncated and pass all verification checks however when the baseTokenID
s is assigned, the function through loops through the full value which can cause the founder to receive full ownership of all NFTs even if OwnershipPct
is set to 0.
Consider adding a check to make sure that founderPct < type(uint8).max
#0 - GalloDaSballo
2022-09-27T00:53:33Z
1L
TODO