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: 60/168
Findings: 2
Award: $207.55
π Selected for report: 1
π Solo Findings: 0
161.6008 USDC - $161.60
https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L76 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L596-L602
The veto power is import functionality for current Nouns DAO logic in order to protect their treasury from malicious proposals. However there is lack of zero address check and lack of 2 step address changing process for vetoer address. This might lead to DAO owner losing their veto power unintentionally and open to 51% attack which can drain their entire treasury.
https://dialectic.ch/editorial/nouns-governance-attack https://dialectic.ch/editorial/nouns-governance-attack-2
Lack of 0-address check for vetoer address at initialize() of Governor.sol Also I recommend to make changing address process of vetoer at updateVetoer() into 2-step process to avoid accidently setting vetoer to arbitrary address and end up lossing veto power unintentionally.
Governor.sol: 57: function initialize( ... 76: settings.vetoer = _vetoer;
596: function updateVetoer(address _newVetoer) external onlyOwner { 597: if (_newVetoer == address(0)) revert ADDRESS_ZERO(); 599: emit VetoerUpdated(settings.vetoer, _newVetoer); 601: settings.vetoer = _newVetoer; 602: }
Manual Analysis
Add zero address check for vetoer address at initialize(). Change updateVetoer() vetoer address changing process to 2-step process like explained below.
First make the updateVetoer() function approve a new vetoer address as a pending vetoer. Next that pending vetoer has to claim the ownership in a separate transaction to be a new vetoer.
#0 - GalloDaSballo
2022-09-25T22:56:35Z
Given the informations that we have, the settings that are available and the historical context, considering that the contract can allow burning the Vetoer, the Warden has demonstrated a risk that applies to all DAOs built via the factory, as well as other Governance Processes which share those traits.
Because this exploit is contingent on external factors, I think Medium Severity to be appropriate
#1 - GalloDaSballo
2022-09-25T22:57:41Z
I personally believe that a Vetoer is a challenge toward a decentralized governance process, however I must agree with the evidence that a 51% attack is possible and has been exploited in the past via code similar to that which is in scope
#2 - kulkarohan
2022-09-29T04:28:35Z
The zero-address check is done in Manager.deploy()
-- see the following lines:
I agree however that the vetoer should be set in 2-steps instead of directly.
#3 - kulkarohan
2022-10-03T20:21:25Z
Believe this is QA IMO
#4 - GalloDaSballo
2022-10-09T15:38:30Z
Agree with the sponsor that 2 step checks are QA.
However, am choosing to keep the report as Med for the 51% attack part.
Historically this has been exploited and can create a dramatic impact.
My specific reasoning is that a 51% attack can happen, exclusively if Vetoer is removed, apathetic or malicious.
This guiding principle has opened up, for this specific contest, a set of decision that inherently create some attrition between me and the sponsor, specifically: This Report #479 and #622
My reasoning is that all of these findings are logically equivalent.
I can only empathize with the Sponsor in that the system does what it's supposed to do, which is being a factory of Nouns DAO, at the same time, because of our rules, and the historical context of previous judging, Admin Privilege is a valid Medium Severity finding (as highlighted by other reports in this same contest and others).
Unfortunately Admin privilege in the case of governance falls onto the Vetoer, the one entity that is necessary to avoid a riskier (potentially) situation of a 51% attack. 51% voting power can be reached by bribing any time it's economically feasible.
It is indeed circular logic, which to me reflects the current state of onChain governance. Which means I don't have a clear mitigation.
For those reasons I can agree to disagree with the Sponsor and also recommend a nofix as at this time, with the given architecture, we either have a risk of Malicious Governance, Bribeable Voters, or risk of Malicious Vetoer.
When we talk about "Smart Contract" risk today, we can talk about Qualitative Risks and Quantitative Risks, the idea that a Vetoer could be malicious seems to fall into a Qualitative Risk, due to the bleeding-edge state of the tech and industry, I believe every person using the system is aware of those risks, however the acknowledgement of those risks doesn't make it disappear.
This report and the two linked below also show a limitation of our current rules as well as the need to clarify what is "acceptable" Admin Privilege vs what is not.
Because of the context detailed above, I believe that I must judge those 3 findings equivalently, and while an argument for downgrading them to QA is legitimate, I believe that the correct severity, consistent over the organization's lifetime (over 1 year and a half) is Medium Severity.
I understand other Auditors and projects offer a different rating (see Consensys Medium for Flagging staff up, vs our Medium which means Loss of Funds Conditional on External Conditions)
And I believe these findings will have to be discussed within the org to decide if Medium severity is appropriate. I'll be flagging these findings up to discuss them with the broader community and discuss whether it is correct or appropriate for C4 to judge findings that ultimately "protect the end user" while they create attrition with the Sponsor.
While we have those discussions, at this time, C4 has prided itself in calling out unspoken risks that are inherent with a specific system (Smart Contract Risk), and in the case of the governor, as detailed by this report, #479 and #622 there's an inherent risk which we made our best effort to quantify, and hopefully in talking about instead of taking it for granted, as a industry, we can find a way to build software that addresses these concerns.
π 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.9534 USDC - $45.95
β
If the function is not called internally, it is cheaper to set your function visibility to external instead of public.
Total of 6 instances found.
owner() of Ownable.sol https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/utils/Ownable.sol#L52
pendingOwner() of Ownable.sol https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/utils/Ownable.sol#L57
transferOwnership() of Ownable.sol https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/utils/Ownable.sol#L63
safeTransferOwnership() of Ownable.sol https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/utils/Ownable.sol#L71
acceptOwnership() of Ownable.sol https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/utils/Ownable.sol#L78
cancelOwnershipTransfer() of Ownable.sol https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/utils/Ownable.sol#L87
Change the function visibility to external.
β
Certain function is defined internal even though it is called only once. Inline it instead to where it is called to avoid usage of extra gas.
Total of 2 instances found.
_addFounders() of Token.sol This function is called only once at line 56 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#L56
_getNextTokenId() of Token.sol This function is called only once at line 110 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L130 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L110
I recommend to not define above functions and instead inline it at place it is called.
β
The MUL and DIV opcodes cost 5 gas but SHL and SHR only costs 3 gas. Since MUL/DIV and SHL/SHR result the same, use cheaper bit shifting.
Total of 1 instances found.
ERC721Votes.sol:95: middle = high - (high - low) / 2;
Use bit shifting instead of multiplication/division. Change to:
middle = high - (high - low) >> 2;
β
It is cheaper gas to use calldata than memory if the function parameter is read only. Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory. More details on following link. link: https://docs.soliditylang.org/en/v0.8.15/types.html#data-location
Total of 9 instances found.
MetadataRenderer.sol:347: function updateContractImage(string memory _newContractImage) external onlyOwner { MetadataRenderer.sol:355: function updateRendererBase(string memory _newRendererBase) external onlyOwner { MetadataRenderer.sol:363: function updateDescription(string memory _newDescription) external onlyOwner { Governor.sol:117: address[] memory _targets, Governor.sol:118: uint256[] memory _values, Governor.sol:119: bytes[] memory _calldatas, Governor.sol:120: string memory _description Governor.sol:195: string memory _reason UUPS.sol:55: function upgradeToAndCall(address _newImpl, bytes memory _data) external payable onlyProxy {
Change memory to calldata
β
When using constant it is expected that the value should be converted into a constant value at compile time. However when using a call to keccak256(), the expression is re-calculated each time the constant is referenced. Resulting in costing about 100 gas more on each access to this constant. link for more details: https://github.com/ethereum/solidity/issues/9232
Total of 3 instances found.
Governor.sol:27: bytes32 public constant VOTE_TYPEHASH = keccak256("Vote(address voter,uint256 proposalId,uint256 support,uint256 nonce,uint256 deadline)"); ERC721Votes.sol:21: bytes32 internal constant DELEGATION_TYPEHASH = keccak256("Delegation(address from,address to,uint256 nonce,uint256 deadline)"); EIP712.sol:19: bytes32 internal constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
Change the variable from constant to immutable.
β
Since EVM operates on 32 bytes at a time, if the element is smaller than that, the EVM must use more operations in order to reduce the elements from 32 bytes to specified size.
Reference: https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html
Total of 4 instances found.
Initializable.sol:17: uint8 internal _initialized; Initializable.sol:55: modifier reinitializer(uint8 _version) { Governor.sol:213: uint8 _v, ERC721Votes.sol:148: uint8 _v,
I suggest using uint256 instead of anything smaller or downcast where needed.
β
When variable is not initialized, it will have its default values. For example, 0 for uint, false for bool and address(0) for address. Reference: https://docs.soliditylang.org/en/v0.8.15/control-structures.html#scoping-and-declarations
Total of 5 instances found.
MetadataRenderer.sol:119: for (uint256 i = 0; i < numNewProperties; ++i) { MetadataRenderer.sol:133: for (uint256 i = 0; i < numNewItems; ++i) { MetadataRenderer.sol:189: for (uint256 i = 0; i < numProperties; ++i) { MetadataRenderer.sol:229: for (uint256 i = 0; i < numProperties; ++i) { Treasury.sol:162: for (uint256 i = 0; i < numTargets; ++i) {
I suggest removing default value initialization. For example,
β
Prefix increments/decrements (++i or --i) costs cheaper gas than postfix increment/decrements (i++ or i--).
Total of 6 instances found.
Token.sol:91: uint256 founderId = settings.numFounders++; Token.sol:154: tokenId = settings.totalSupply++; Governor.sol:230: keccak256(abi.encode(VOTE_TYPEHASH, _voter, _proposalId, _support, nonces[_voter]++, _deadline)) ERC721Votes.sol:162: abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), keccak256(abi.encode(DELEGATION_TYPEHASH, _from, _to, nonces[_from]++, _deadline))) ERC721Votes.sol:207: uint256 nCheckpoints = numCheckpoints[_from]++; ERC721Votes.sol:222: uint256 nCheckpoints = numCheckpoints[_to]++;
Change it to postfix increments/decrements. It saves about 6 gas per instance.
β
#0 - GalloDaSballo
2022-09-26T20:40:24Z
45 + 500 (memory -> calldata)
545