Nouns DAO - K42's results

A DAO-driven NFT project on Ethereum.

General Information

Platform: Code4rena

Start Date: 03/07/2023

Pot Size: $100,000 USDC

Total HM: 4

Participants: 36

Period: 10 days

Judge: gzeon

Total Solo HM: 3

Id: 257

League: ETH

Nouns DAO

Findings Distribution

Researcher Performance

Rank: 15/36

Findings: 2

Award: $274.10

Gas:
grade-b
Analysis:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Labels

bug
G (Gas Optimization)
grade-b
G-14

Awards

55.3038 USDC - $55.30

External Links

Gas Optimization Report for NounsDAO by K42

Possible Optimization in NounsDAOLogicV2.sol

General Optimization =

  • Functions declared as external can be more gas efficient than public functions because they read directly from calldata, whereas public functions copy data to memory, which costs more gas. For example, the castVoteBySig function could be made external if it's not called from within the contract. This could save around 600 gas per call.

Possible Optimization 1 =

  • In the state function, multiple calls to proposal storage variable are made. Each storage load operation consumes 800 gas. We can reduce the gas consumption by loading the proposal into memory once and reusing it.

Here is the optimized code snippet:

function state(uint256 proposalId) public view returns (ProposalState) { require(proposalCount >= proposalId, 'NounsDAO::state: invalid proposal id'); Proposal storage proposal = _proposals[proposalId]; Proposal memory _proposal = proposal; // load proposal into memory if (_proposal.vetoed) { return ProposalState.Vetoed; } else if (_proposal.canceled) { return ProposalState.Canceled; } else if (block.number <= _proposal.startBlock) { return ProposalState.Pending; } else if (block.number <= _proposal.endBlock) { return ProposalState.Active; } else if (_proposal.forVotes <= _proposal.againstVotes || _proposal.forVotes < quorumVotes(_proposal.id)) { return ProposalState.Defeated; } else if (_proposal.eta == 0) { return ProposalState.Succeeded; } else if (_proposal.executed) { return ProposalState.Executed; } else if (block.timestamp >= _proposal.eta + timelock.GRACE_PERIOD()) { return ProposalState.Expired; } else { return ProposalState.Queued; } }

  • Estimated gas saved = Around 6400 gas per call to the state function. The exact amount of gas saved will depend on the frequency of calls to this function. If this function is called frequently, the gas savings could be significant.

Possible Optimization 2 =

  • In the _setPendingAdmin function, the condition msg.sender == admin is checked. This is redundant because the AdminOnly modifier already checks this condition. Removing this could save around 1000 gas.

  • Estimated gas saved = 1000 gas.

Possible Optimization in NounsDAOProxy.sol

Possible Optimization 1 = -- In the constructor, the admin state variable is set twice. The first assignment to msg.sender is unnecessary and can be removed to save gas. Here is the optimized code

constructor( address timelock_, address nouns_, address vetoer_, address admin_, address implementation_, uint256 votingPeriod_, uint256 votingDelay_, uint256 proposalThresholdBPS_, uint256 quorumVotesBPS_ ) { delegateTo( implementation_, abi.encodeWithSignature( 'initialize(address,address,address,uint256,uint256,uint256,uint256)', timelock_, nouns_, vetoer_, votingPeriod_, votingDelay_, proposalThresholdBPS_, quorumVotesBPS_ ) ); _setImplementation(implementation_); admin = admin_; }

  • Estimated gas saved = Around 20000 gas for the constructor call.

Possible Optimization 2 =

  • In the _setImplementation function, the condition implementation_ != address(0) is checked. This check is redundant because the delegateTo function called within the constructor will fail if the implementation address is zero. So removing this could save around 800 gas per call.

  • Estimated gas saved = 800 gas per call to the _setImplementation function.

Possible Optimizations in NounsDAOLogicV3.sol

Possible Optimization 1 =

For example:

function MIN_PROPOSAL_THRESHOLD_BPS() public pure returns (uint256) { return NounsDAOV3Admin.MIN_PROPOSAL_THRESHOLD_BPS; }

Change to:

uint256 public constant MIN_PROPOSAL_THRESHOLD_BPS = NounsDAOV3Admin.MIN_PROPOSAL_THRESHOLD_BPS;

You can the same for the other functions mentioned above.

  • Estimated gas saved = 2000 gas per call due to the elimination of the function call overhead.

Possible Optimization 2 =

  • Redundant storage reads: The contract reads from storage multiple times for the same variables. This could be optimized better by reading once and storing the result in a local variable.

Before:

function minQuorumVotes() public view returns (uint256) { return ds.minQuorumVotes(ds.adjustedTotalSupply()); }

function maxQuorumVotes() public view returns (uint256) { return ds.maxQuorumVotes(ds.adjustedTotalSupply()); }

After:

function minQuorumVotes() public view returns (uint256) { uint256 adjustedTotalSupply = ds.adjustedTotalSupply(); return ds.minQuorumVotes(adjustedTotalSupply); } and function maxQuorumVotes() public view returns (uint256) { uint256 adjustedTotalSupply = ds.adjustedTotalSupply(); return ds.maxQuorumVotes(adjustedTotalSupply); }

  • Estimated gas saved = 800 gas per storage read.

Possible Optimizations in NounsDAOV3Votes.sol

Possible Optimization =

Here is how you modify the code:

function castRefundableVoteInternal( NounsDAOStorageV3.StorageV3 storage ds, uint256 proposalId, uint8 support, string memory reason ) internal { uint256 startGas = gasleft(); (uint96 votes, address voter) = castVoteInternal(ds, msg.sender, proposalId, support); if (votes > 0) { _refundGas(startGas); } } and function castVoteInternal( NounsDAOStorageV3.StorageV3 storage ds, address voter, uint256 proposalId, uint8 support ) internal returns (uint96, address) { NounsDAOStorageV3.ProposalState proposalState = ds.stateInternal(proposalId); uint96 votes; if (proposalState == NounsDAOStorageV3.ProposalState.Active) { votes = castVoteDuringVotingPeriodInternal(ds, proposalId, voter, support); } else if (proposalState == NounsDAOStorageV3.ProposalState.ObjectionPeriod) { if (support != 0) revert CanOnlyVoteAgainstDuringObjectionPeriod(); votes = castObjectionInternal(ds, proposalId, voter); } else { revert('NounsDAO::castVoteInternal: voting is closed'); } emit VoteCast(voter, proposalId, support, votes, ''); return (votes, voter); }

  • Estimated gas saved = 375 gas per castRefundableVote call due to the elimination of one event emission.

Possible Optimization 2 =

  • In the _refundGas function, you can replace the min function calls with ternary operators to save some gas.

Before:

function _refundGas(uint256 startGas) internal { unchecked { uint256 balance = address(this).balance; if (balance == 0) { return; } uint256 basefee = min(block.basefee, MAX_REFUND_BASE_FEE); uint256 gasPrice = min(tx.gasprice, basefee + MAX_REFUND_PRIORITY_FEE); uint256 gasUsed = min(startGas - gasleft() + REFUND_BASE_GAS, MAX_REFUND_GAS_USED); uint256 refundAmount = min(gasPrice * gasUsed, balance); (bool refundSent, ) = msg.sender.call{ value: refundAmount }(''); emit RefundableVote(msg.sender, refundAmount, refundSent); } }

After Optimization:

function _refundGas(uint256 startGas) internal { unchecked { uint256 balance = address(this).balance; if (balance == 0) { return; } uint256 basefee = block.basefee < MAX_REFUND_BASE_FEE ? block.basefee : MAX_REFUND_BASE_FEE; uint256 gasPrice = tx.gasprice < (basefee + MAX_REFUND_PRIORITY_FEE) ? tx.gasprice : (basefee + MAX_REFUND_PRIORITY_FEE); uint256 gasUsed = (startGas - gasleft() + REFUND_BASE_GAS) < MAX_REFUND_GAS_USED ? (startGas - gasleft() + REFUND_BASE_GAS) : MAX_REFUND_GAS_USED; uint256 refundAmount = gasPrice * gasUsed < balance ? gasPrice * gasUsed : balance; (bool refundSent, ) = msg.sender.call{ value: refundAmount }(''); emit RefundableVote(msg.sender, refundAmount, refundSent); } }

  • Estimated gas saved = In optimized function, we are eliminating four function calls, so you could save at least 2800 gas.

Possible Optimization in NounsDAOV3Admin.sol

Possible Optimization =

  • In the checkForDuplicates function, the current implementation uses a nested loop to check for duplicate addresses in the erc20tokens array. This results in a time complexity of O(n^2), which can be expensive in terms of gas cost when the array size is large. We can optimize this by using a mapping to check for duplicates, which reduces the time complexity to O(n) In the original function, for each pair of addresses, we perform an SLOAD operation to load the addresses from storage. In the optimized function, we perform an SLOAD operation once for each address, and an SSTORE operation to store the address in the seen mapping.

Before:

function checkForDuplicates(address[] calldata erc20tokens) internal pure { if (erc20tokens.length == 0) return; for (uint256 i = 0; i < erc20tokens.length - 1; i++) { for (uint256 j = i + 1; j < erc20tokens.length; j++) { if (erc20tokens[i] == erc20tokens[j]) revert DuplicateTokenAddress(); } }

After Optimization:

function checkForDuplicates(address[] calldata erc20tokens) internal pure { if (erc20tokens.length == 0) return; mapping(address => bool) memory seen; for (uint256 i = 0; i < erc20tokens.length; i++) { if (seen[erc20tokens[i]]) revert DuplicateTokenAddress(); seen[erc20tokens[i]] = true; } }

  • Estimated gas saved = For an array of size n, the original function performs n^2 SLOAD operations, while the optimized function performs n SLOAD operations and n SSTORE operations. Since SSTORE is more expensive than SLOAD, the gas saved is approximately (n^2 - 2n) * gas cost of SLOAD.

Possible Optimization in ForkDAODeployer.sol

  • For this contract: I suggest using CREATE2: I put this in depth in my advanced-anaylsis report as it is an implementation change.

Possible Optimizations in NounsDAOExecutorV2.sol

Possible Optimization =

  • In the executeTransaction function, the current implementation uses the keccak256 function to generate a transaction hash twice, once at the beginning of the function and once in the emit ExecuteTransaction statement. This is redundant and increases gas costs. We can optimize this by storing the transaction hash in a variable and reusing it.

Before Optimization:

function executeTransaction( address target, uint256 value, string memory signature, bytes memory data, uint256 eta ) public returns (bytes memory) { require(msg.sender == admin, 'NounsDAOExecutor::executeTransaction: Call must come from admin.'); bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); require(queuedTransactions[txHash], "NounsDAOExecutor::executeTransaction: Transaction hasn't been queued."); require( getBlockTimestamp() >= eta, "NounsDAOExecutor::executeTransaction: Transaction hasn't surpassed time lock." ); require( getBlockTimestamp() <= eta + GRACE_PERIOD, 'NounsDAOExecutor::executeTransaction: Transaction is stale.' ); queuedTransactions[txHash] = false; bytes memory callData; if (bytes(signature).length == 0) { callData = data; } else { callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); } // solium-disable-next-line security/no-call-value (bool success, bytes memory returnData) = target.call{ value: value }(callData); require(success, 'NounsDAOExecutor::executeTransaction: Transaction execution reverted.'); emit ExecuteTransaction(keccak256(abi.encode(target, value, signature, data, eta)), target, value, signature, data, eta); return returnData; }

After Optimization:

function executeTransaction( address target, uint256 value, string memory signature, bytes memory data, uint256 eta ) public returns (bytes memory) { require(msg.sender == admin, 'NounsDAOExecutor::executeTransaction: Call must come from admin.'); bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); require(queuedTransactions[txHash], "NounsDAOExecutor::executeTransaction: Transaction hasn't been queued."); require( getBlockTimestamp() >= eta, "NounsDAOExecutor::executeTransaction: Transaction hasn't surpassed time lock." ); require( getBlockTimestamp() <= eta + GRACE_PERIOD, 'NounsDAOExecutor::executeTransaction: Transaction is stale.' ); queuedTransactions[txHash] = false; bytes memory callData; if (bytes(signature).length == 0) { callData = data; } else { callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); } // solium-disable-next-line security/no-call-value (bool success, bytes memory returnData) = target.call{ value: value }(callData); require(success, 'NounsDAOExecutor::executeTransaction: Transaction execution reverted.'); emit ExecuteTransaction(txHash, target, value, signature, data, eta); return returnData; }

  • Estimated gas saved = The keccak256 function consumes approximately 30 gas for each byte of data. Assuming an average transaction data size of 100 bytes, the gas saved is approximately 30 * 100 = 3000 gas.

Possible Optimizations in NounsDAOV3Proposals.sol

Possible Optimization = Reducing redundant storage reads:

  • In the createNewProposal function, the block.number is read twice. Reading from storage is expensive in terms of gas, so it's better to read once and store the value in memory.

Here is the optimized function:

function createNewProposal( NounsDAOStorageV3.StorageV3 storage ds, uint256 proposalId, uint256 proposalThreshold_, uint256 adjustedTotalSupply, ProposalTxs memory txs ) internal returns (NounsDAOStorageV3.Proposal storage newProposal) { uint64 currentBlock = SafeCast.toUint64(block.number); uint64 updatePeriodEndBlock = currentBlock + ds.proposalUpdatablePeriodInBlocks; uint256 startBlock = updatePeriodEndBlock + ds.votingDelay; uint256 endBlock = startBlock + ds.votingPeriod; newProposal = ds._proposals[proposalId]; newProposal.id = proposalId; newProposal.proposer = msg.sender; newProposal.proposalThreshold = proposalThreshold_; newProposal.targets = txs.targets; newProposal.values = txs.values; newProposal.signatures = txs.signatures; newProposal.calldatas = txs.calldatas; newProposal.startBlock = startBlock; newProposal.endBlock = endBlock; newProposal.totalSupply = adjustedTotalSupply; newProposal.creationBlock = currentBlock; newProposal.updatePeriodEndBlock = updatePeriodEndBlock; }

  • Estimated gas saved = By eliminating one redundant storage read (SLOAD) we save 800 gas.

Possible Optimization in NounsDAOV3DynamicQuorum.sol

Possible Optimization = Reducing redundant storage reads:

  • In the quorumVotes function, proposal.totalSupply is read twice from storage. By storing the value in a memory variable proposalTotalSupply after the first read, we can eliminate the second storage read, saving gas.

Here is the optimized function:

function quorumVotes(NounsDAOStorageV3.StorageV3 storage ds, uint256 proposalId) internal view returns (uint256) { NounsDAOStorageV3.Proposal storage proposal = ds._proposals[proposalId]; uint256 proposalTotalSupply = proposal.totalSupply; if (proposalTotalSupply == 0) { return proposal.quorumVotes; } return dynamicQuorumVotes( proposal.againstVotes, proposalTotalSupply, getDynamicQuorumParamsAt(ds, proposal.creationBlock) ); }

  • Estimated gas saved = By eliminating one redundant storage read (SLOAD) we save 800 gas.

Possible Optimizations in NounsDAOForkEscrow.sol

Possible Optimization = Remove redundant currentOwnerOf checks:

  • In the returnTokensToOwner and withdrawTokens functions, you are checking if the currentOwnerOf a token is the expected owner before transferring it. However, the transferFrom function in the ERC721 standard will automatically revert if the msg.sender is not the owner or an approved operator of the token, making these checks redundant.

You can remove these checks to save some gas:

function returnTokensToOwner(address owner, uint256[] calldata tokenIds) external onlyDAO { for (uint256 i = 0; i < tokenIds.length; i++) { nounsToken.transferFrom(address(this), owner, tokenIds[i]); escrowedTokensByForkId[forkId][tokenIds[i]] = address(0); } numTokensInEscrow -= tokenIds.length; }

and

function withdrawTokens(uint256[] calldata tokenIds, address to) external onlyDAO { for (uint256 i = 0; i < tokenIds.length; i++) { nounsToken.transferFrom(address(this), to, tokenIds[i]); } }

  • Estimated gas saved = Removing each redundant currentOwnerOf check will save the gas cost of a storage read, which is 800 gas. So if you have n tokens, you will save 800 * n gas.

Possible Optimizations in NounsDAOExecutor.sol

Possible Optimization = Use calldata instead of memory for function parameters:

  • In the queueTransaction and executeTransaction functions, the signature and data parameters are declared as memory. However, these parameters are not modified within the function, so they can be declared as calldata to save some gas.

For queueTransaction: string calldata signature, bytes calldata data, and the same for executeTransaction

  • Estimated gas saved = = memory to calldata saves 200 gas per parameter. So for each call to these functions, you will save 200 * 2 = 400 gas.

Possible Optimizations in NounsAuctionHouseFork.sol

Possible Optimization =

  • In the _settleAuction function, you are updating the auction struct in storage multiple times. Each time you update a storage slot, it costs 5000 gas. You can reduce the gas cost by creating a new Auction struct in memory, updating the fields in memory, and then storing the entire struct in storage at once.

Here is the optimized function:

function _settleAuction() internal { INounsAuctionHouse.Auction memory _auction = auction; require(_auction.startTime != 0, "Auction hasn't begun"); require(!_auction.settled, 'Auction has already been settled'); require(block.timestamp >= _auction.endTime, "Auction hasn't completed"); _auction.settled = true; if (_auction.bidder == address(0)) { nouns.burn(_auction.nounId); } else { nouns.transferFrom(address(this), _auction.bidder, _auction.nounId); } if (_auction.amount > 0) { _safeTransferETHWithFallback(owner(), _auction.amount); } auction = _auction; emit AuctionSettled(_auction.nounId, _auction.bidder, _auction.amount); }

  • Estimated gas saved = By using a single SSTORE operation instead of multiple, we save (5000 * number of fields updated - 5000) gas. In this case, we are updating 1 field, so we save (5000 * 1 - 5000) = 0 gas. However, if more fields are updated in the future, this optimization will save more gas.

Possible Optimization in NounsDAOLogicV1Fork.sol

Possible Optimization = Reduce the number of SSTORE operations:

  • Currently, the propose function stores each field of the Proposal struct individually. Instead, we could create a memory instance of the Proposal struct, set its fields, and then store the entire struct in one operation.

Before Optimization:

Proposal storage newProposal = _proposals[proposalCount]; newProposal.id = proposalCount; newProposal.proposer = msg.sender; newProposal.proposalThreshold = temp.proposalThreshold; newProposal.quorumVotes = bps2Uint(quorumVotesBPS, temp.totalSupply); newProposal.eta = 0; newProposal.targets = targets; newProposal.values = values; newProposal.signatures = signatures; newProposal.calldatas = calldatas; newProposal.startBlock = temp.startBlock; newProposal.endBlock = temp.endBlock; newProposal.forVotes = 0; newProposal.againstVotes = 0; newProposal.abstainVotes = 0; newProposal.canceled = false; newProposal.executed = false; newProposal.creationBlock = block.number;

After Optimization:

// Create a memory instance of the Proposal struct and set its fields Proposal memory newProposal = Proposal({ id: proposalCount, proposer: msg.sender, proposalThreshold: temp.proposalThreshold, quorumVotes: bps2Uint(quorumVotesBPS, temp.totalSupply), eta: 0, targets: targets, values: values, signatures: signatures, calldatas: calldatas, startBlock: temp.startBlock, endBlock: temp.endBlock, forVotes: 0, againstVotes: 0, abstainVotes: 0, canceled: false, executed: false, creationBlock: block.number }); // Store the entire struct in one operation _proposals[proposalCount] = newProposal;

  • Estimated gas saved = This optimization reduces the number of SSTORE operations from 15 to 1. Therefore, the savings would be (15 - 1) * 5000 = 70,000 gas. If all the storage slots are being changed from a non-zero value to another non-zero value, the savings would be (15 - 1) * 20,000 = 280,000 gas.

Possible Optimizations in NounsTokenFork.sol

Possible Optimization =

  • In the _mintWithOriginalSeed function, store the seed in a local variable before setting it in the mapping and emitting the event. This would reduce the number of SSTORE operations from two to one.

Before Optimization:

function _mintWithOriginalSeed(address to, uint256 nounId) internal { (uint48 background, uint48 body, uint48 accessory, uint48 head, uint48 glasses) = NounsTokenFork( address(escrow.nounsToken()) ).seeds(nounId); INounsSeeder.Seed memory seed = INounsSeeder.Seed(background, body, accessory, head, glasses); seeds[nounId] = seed; _mint(to, nounId); emit NounCreated(nounId, seed); }

After Optimization:

function _mintWithOriginalSeed(address to, uint256 nounId) internal { (uint48 background, uint48 body, uint48 accessory, uint48 head, uint48 glasses) = NounsTokenFork( address(escrow.nounsToken()) ).seeds(nounId); INounsSeeder.Seed memory seed = INounsSeeder.Seed(background, body, accessory, head, glasses); // Store the seed in a local variable INounsSeeder.Seed memory localSeed = seed; // Use the local variable instead of the mapping seeds[nounId] = localSeed; _mint(to, nounId); emit NounCreated(nounId, seed); }

  • Estimated gas saved = This optimization could save between 5000 and 20000 gas, depending on the current state of the storage slots.

#0 - c4-judge

2023-07-25T10:15:25Z

gzeon-c4 marked the issue as grade-b

Findings Information

🌟 Selected for report: 0xnev

Also found by: 0xSmartContract, K42, Matin, ihtishamsudo, shark

Labels

analysis-advanced
grade-b
A-04

Awards

218.7996 USDC - $218.80

External Links

Advanced Analysis Report for NounsDAO

Overview:

  • NounsDAO is a decentralized autonomous organization (DAO) that operates on the Ethereum blockchain. It is a unique project that combines the elements of digital art and blockchain technology. The main product of NounsDAO is the Nouns, which are unique, programmatically generated pieces of digital art that are minted and auctioned off each day. The proceeds from these auctions are directed to the NounsDAO treasury, which is governed by Nouns token holders.

Understanding the Ecosystem:

  • The NounsDAO ecosystem is built around the Nouns, the unique pieces of digital art. These Nouns are minted and auctioned off each day, with the proceeds going to the NounsDAO treasury. The treasury is governed by the holders of the Nouns tokens, who can vote on proposals regarding the use of the treasury funds. This creates a self-sustaining ecosystem where the creation and sale of Nouns fund the DAO, and the DAO is governed by the token holders who have a vested interest in the success of the project.

Architecture Recommendations:

  • I have suggested optimization, which I left out of my gas report due to it being a structural change, therefore I recommended it here instead, see below:

  • Possible Optimization in ForkDAODeployer.sol

  • In the deployForkDAO function, the current implementation deploys four new contracts (token, auction, governor, treasury) using the ERC1967Proxy contract. Each deployment of a contract involves a CREATE opcode which is quite expensive in terms of gas. We can optimize this by using a CREATE2 opcode which allows us to deploy a contract at a deterministic address. This would reduce the gas cost as CREATE2 is cheaper than CREATE and also provides additional benefits like being able to compute the address of the contract before it's deployed.

Before:

token = address(new ERC1967Proxy(tokenImpl, '')); address auction = address(new ERC1967Proxy(auctionImpl, '')); address governor = address(new ERC1967Proxy(governorImpl, '')); treasury = address(new ERC1967Proxy(treasuryImpl, ''));

After Optimization:

bytes32 salt = keccak256(abi.encodePacked(tokenImpl, auctionImpl, governorImpl, treasuryImpl)); token = deployContract(tokenImpl, salt); auction = deployContract(auctionImpl, salt); governor = deployContract(governorImpl, salt); treasury = deployContract(treasuryImpl, salt);

function deployContract(address implementation, bytes32 salt) internal returns (address) { bytes20 targetBytes = bytes20(implementation); address result; // solhint-disable-next-line no-inline-assembly assembly { let clone := mload(0x40) mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) mstore(add(clone, 0x14), targetBytes) mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) result := create2(0, clone, 0x37, salt) } return result; }

Estimated gas saved = The exact gas savings would depend on the size of the contracts being deployed. However, as a rough estimate, the CREATE opcode costs 32000 gas, while the CREATE2 opcode costs 20000 gas. Therefore, for each contract deployment, we save approximately 12000 gas. Since we are deploying four contracts, the total gas saved is approximately 48000 gas.

  • Other than this, the current architecture of NounsDAO is robust and well-designed, as the project grows and evolves, it may benefit from a more modular architecture. This would involve breaking down the code into smaller, more manageable components that can be developed, tested, and deployed independently, which v3 seems to offer well. This approach can improve the maintainability of the code and make it easier to add new features in the future.

Centralization Risks:

  • As a DAO, NounsDAO is designed to be decentralized, with decision-making power distributed among the holders of the governance token. However, like all DAOs, it faces the risk of centralization if a small number of entities acquire a large portion of the governance tokens. This could allow them to exert disproportionate influence over the DAO's decisions. To mitigate this risk, it's important to ensure a fair and wide distribution of the governance tokens.

Mechanism Review:

  • The mechanism of auctioning off Nouns and directing the proceeds to the DAO's treasury is a novel and effective way of generating value for the DAO and its token holders. However, it's important to ensure that the auction process is fair and transparent, and that the value of the Nouns is not artificially inflated.

Systemic Risks:

  • The systemic risks for NounsDAO include the overall volatility and unpredictability of the cryptocurrency market, potential regulatory changes, and the reliance on the Ethereum blockchain, which itself faces issues such as scalability and high transaction fees.

Areas of Concern

  • One area of concern could be the sustainability of the daily auction model. While it is a novel approach, it's important to consider whether this model can sustain interest and participation in the long term. given the context of current activity, NounsDAO is leading the way in this already. Additionally, the reliance on the Ethereum network could pose challenges due to high gas fees and scalability issues, therefore I did my best on a gas report to further optimize this project to increase its longevity and sustainability.

Codebase Analysis

  • The codebase of NounsDAO is well-structured and follows best practices for smart contract development. It makes use of well-tested libraries and frameworks such as OpenZeppelin for implementing standard functionalities. The code is modular, which makes it easier to understand and maintain. It also has extensive comments, which aids in understanding the functionality of different parts of the code.

  • However, the complexity of the system also poses potential risks. With so many interacting components, there is a higher chance of unexpected behaviour or vulnerabilities. Therefore, thorough future testing and auditing are crucial to ensure the security of the system.

Recommendations

  • Consider a even more modular architecture to improve maintainability and ease of adding new features.

  • Ensure a fair and wider distribution of governance tokens to mitigate centralization risks.

  • Regularly review and update the auction mechanism to ensure fairness and transparency.

  • Explore solutions for potential scalability issues and high transaction fees on the Ethereum network.

Conclusion

  • NounsDAO is a unique and innovative project that combines the concepts of NFTs and DAOs and I had fun auditing the project. Its daily auction model is a novel approach to generating value for the DAO and its token holders. However, like all projects in the crypto space, it faces potential risks and challenges that need to be carefully managed. With a well-structured and maintained codebase, and by following best practices for DAO governance, NounsDAO has the potential of longevity, and to be a notable and influential project in the NFT and DAO space, even in 5 years from now.

Time spent:

40 hours

#0 - c4-judge

2023-07-25T10:24:20Z

gzeon-c4 marked the issue as grade-b

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