Nouns Builder contest - TomJ's results

A permissionless, governed protocol to deploy nouns-style DAOs complete with treasury, generative collections, and governance mechanisms.

General Information

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

Nouns Builder

Findings Distribution

Researcher Performance

Rank: 60/168

Findings: 2

Award: $207.55

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: TomJ

Also found by: 0xSky, Chom, PwnPatrol, ayeslick, pedr02b2, yixxas, zkhorse

Labels

bug
2 (Med Risk)
disagree with severity

Awards

161.6008 USDC - $161.60

External Links

Lines of code

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

Vulnerability details

Impact

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

Proof of Concept

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: }

Tools Used

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

#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.

  • There's a risk of brute forcing the system
  • To avoid the brute force we have a trusted third party
  • The trusted third party creates a new set of complications

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.

Table of Contents

  • Change Function Visibility Public to External
  • Internal Function Called Only Once can be Inlined
  • Use Bit Shifting Instead of Multiplication/Division of 2
  • Use Calldata instead of Memory for Read Only Function Parameters
  • Constant Value of a Call to keccak256() should Use Immutable
  • Using Elements Smaller than 32 bytes (256 bits) Might Use More Gas
  • Unnecessary Default Value Initialization
  • ++i Costs Less Gas than i++

 

Change Function Visibility Public to External

Issue

If the function is not called internally, it is cheaper to set your function visibility to external instead of public.

PoC

Total of 6 instances found.

  1. owner() of Ownable.sol https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/utils/Ownable.sol#L52

  2. pendingOwner() of Ownable.sol https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/utils/Ownable.sol#L57

  3. transferOwnership() of Ownable.sol https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/utils/Ownable.sol#L63

  4. safeTransferOwnership() of Ownable.sol https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/utils/Ownable.sol#L71

  5. acceptOwnership() of Ownable.sol https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/utils/Ownable.sol#L78

  6. cancelOwnershipTransfer() of Ownable.sol https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/utils/Ownable.sol#L87

Mitigation

Change the function visibility to external.

 

Internal Function Called Only Once Can be Inlined

Issue

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.

PoC

Total of 2 instances found.

  1. _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

  2. _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

Mitigation

I recommend to not define above functions and instead inline it at place it is called.

 

Use Bit Shifting Instead of Multiplication/Division of 2

Issue

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.

PoC

Total of 1 instances found.

ERC721Votes.sol:95:                middle = high - (high - low) / 2;
Mitigation

Use bit shifting instead of multiplication/division. Change to:

middle = high - (high - low) >> 2;

 

Use Calldata instead of Memory for Read Only Function Parameters

Issue

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

PoC

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 {
Mitigation

Change memory to calldata

 

Constant Value of a Call to keccak256() should Use Immutable

Issue

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

PoC

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)");
Mitigation

Change the variable from constant to immutable.

 

Using Elements Smaller than 32 bytes (256 bits) Might Use More Gas

Issue

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

PoC

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,
Mitigation

I suggest using uint256 instead of anything smaller or downcast where needed.

 

Unnecessary Default Value Initialization

Issue

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

PoC

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) {
Mitigation

I suggest removing default value initialization. For example,

  • for (uint256 i; i < numNewProperties; ++i) {

 

++i Costs Less Gas than i++

Issue

Prefix increments/decrements (++i or --i) costs cheaper gas than postfix increment/decrements (i++ or i--).

PoC

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]++;
Mitigation

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter