Platform: Code4rena
Start Date: 22/08/2022
Pot Size: $50,000 USDC
Total HM: 4
Participants: 160
Period: 5 days
Judge: gzeon
Total Solo HM: 2
Id: 155
League: ETH
Rank: 27/160
Findings: 1
Award: $66.52
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0x040, 0x1f8b, 0xDjango, 0xNazgul, 0xNineDec, 0xSmartContract, 0xbepresent, 0xc0ffEE, 0xkatana, 2997ms, ACai, Amithuddar, Aymen0909, Ben, BipinSah, Bjorn_bug, Bnke0x0, CertoraInc, Ch_301, Chom, CodingNameKiki, Deivitto, DevABDee, DimitarDimitrov, Diraco, Dravee, ElKu, EthLedger, Fitraldys, Funen, GalloDaSballo, GimelSec, Guardian, IgnacioB, JC, JohnSmith, Junnon, KIntern_NA, Lambda, LeoS, Noah3o6, Olivierdem, Polandia94, R2, Randyyy, RaymondFam, Respx, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, Ruhum, SaharAP, Saintcode_, SerMyVillage, Shishigami, Sm4rty, SooYa, TomJ, Tomio, Tomo, Waze, Yiko, _Adam, __141345__, a12jmx, ajtra, ak1, bobirichman, brgltd, bulej93, c3phas, cRat1st0s, carlitox477, catchup, ch0bu, d3e4, delfin454000, djxploit, durianSausage, erictee, exolorkistis, fatherOfBlocks, francoHacker, gogo, hyh, ignacio, jag, joestakey, karanctf, ladboy233, lucacez, lukris02, m_Rassska, martin, medikko, mics, mrpathfindr, natzuu, newfork01, oyc_109, pauliax, peritoflores, pfapostol, prasantgupta52, rbserver, ret2basic, rfa, robee, rokinot, rotcivegaf, rvierdiiev, sach1r0, saian, samruna, seyni, shark, shr1ftyy, sikorico, simon135, sryysryy, tay054, tnevler, wagmi, zishansami
66.5215 USDC - $66.52
unchecked{++i}
instead of i++
in loopsa / 2^x, x > 0
MLOAD
<< SLOAD
require()
before some computations, if it makes senseInternal
functions can be inlinedprivate/internal
for constants/immutable/state
variables instead of public
type(uint256).max
uint
instead of uint8
, uint64
, if it's possibleexternal
instead of public
, if there are no internal callscalldataload
instead of mload
i++
, the compiler has to to create a temp variable to return i
(if there's a need) and then i
gets incremented.++i
, the compiler just simply returns already incremented value.Contracts:
file: contracts/governance/NounsDAOLogicV1.sol ............................... // Lines: [281-281] for (uint256 i = 0; i < proposal.targets.length; i++) {}
3 more occurances in the current file and 4 in NounsDaoLogicV2.sol
unchecked{++i};
instead of i++;
/++i;
in loops<a name="G-02"></a>Since Solidity 0.8.0, all arithmetic operations revert on over- and underflow by default Therefore, unchecked
box can be used to prevent all unnecessary checks, if it's no a way to get a reversion.
There are ~1e80 atoms in the universe, so 2^256 is closed to that number, therefore it's no a way to be overflowed, because of the gas limit as well.
Contracts:
file: contracts/governance/NounsDAOLogicV1.sol ............................... // Lines: [281-281] for (uint256 i = 0; i < proposal.targets.length; i++) {}
NounsDaoLogicV2.sol
onlyOwner
modifier will be reverted, if the user invoke following methods.Contracts:
file: contracts/governance/NounsDAOLogicV2.sol ............................... // Lines: [748-752] function _setDynamicQuorumParams( uint16 newMinQuorumVotesBPS, uint16 newMaxQuorumVotesBPS, uint32 newQuorumCoefficient ) public {}
a / 2^x, x > 0
or a * 2^x, x > 0
<a name="G-05"></a>Contracts:
file: contracts/base/ERC721Checkpointable.sol ............................... // Lines: [184-184] uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow
MLOAD
<< SLOAD
<a name="G-06"></a>MLOAD
costs only 3 units of gas, SLOAD
(warm access) is about 100 units. Therefore, cache, when it's possible.Contracts:
file: contracts/governance/NounsDAOLogicV2.sol ............................... // Lines: [184-279] // Comment: allocate newProposal in memory as well in order to avoid warm access in providing newProposal.id, newProposal.startBlock, newProposal.endBlock in events and for return value as well. emit ProposalCreated( newProposal.id, msg.sender, targets, values, signatures, calldatas, newProposal.startBlock, newProposal.endBlock, description ); emit ProposalCreatedWithRequirements( newProposal.id, msg.sender, targets, values, signatures, calldatas, newProposal.startBlock, newProposal.endBlock, newProposal.proposalThreshold, minQuorumVotes(), description ); return newProposal.id; // Lines: [292-300] // Comment: also, use memory in order to read. for (uint256 i = 0; i < proposal.targets.length; i++) { queueOrRevertInternal( proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], eta ); } // Lines: [330-338] for (uint256 i = 0; i < proposal.targets.length; i++) { timelock.executeTransaction( proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta ); } // Lines: [350-365] require( msg.sender == proposal.proposer || nouns.getPriorVotes(proposal.proposer, block.number - 1) < proposal.proposalThreshold, 'NounsDAO::cancel: proposer above threshold' ); proposal.canceled = true; for (uint256 i = 0; i < proposal.targets.length; i++) { timelock.cancelTransaction( proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta ); } // Lines: [382-390] for (uint256 i = 0; i < proposal.targets.length; i++) { timelock.cancelTransaction( proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta ); } // Lines: [434-453] // Comment: there is no need to allocate storage var here, since the function logic performs only reading operations. Proposal storage proposal = _proposals[proposalId]; 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; } // Lines: [462-482] // Comment: the same as above function proposals(uint256 proposalId) external view returns (ProposalCondensed memory) { Proposal storage proposal = _proposals[proposalId]; return ProposalCondensed({ id: proposal.id, proposer: proposal.proposer, proposalThreshold: proposal.proposalThreshold, quorumVotes: quorumVotes(proposal.id), eta: proposal.eta, startBlock: proposal.startBlock, endBlock: proposal.endBlock, forVotes: proposal.forVotes, againstVotes: proposal.againstVotes, abstainVotes: proposal.abstainVotes, canceled: proposal.canceled, vetoed: proposal.vetoed, executed: proposal.executed, totalSupply: proposal.totalSupply, creationBlock: proposal.creationBlock }); } // Lines: [593-612] // Comment: `receipt`, `proposal` could be cached require(state(proposalId) == ProposalState.Active, 'NounsDAO::castVoteInternal: voting is closed'); require(support <= 2, 'NounsDAO::castVoteInternal: invalid vote type'); Proposal storage proposal = _proposals[proposalId]; Receipt storage receipt = proposal.receipts[voter]; require(receipt.hasVoted == false, 'NounsDAO::castVoteInternal: voter already voted'); /// @notice: Unlike GovernerBravo, votes are considered from the block the proposal was created in order to normalize quorumVotes and proposalThreshold metrics uint96 votes = nouns.getPriorVotes(voter, proposalCreationBlock(proposal)); if (support == 0) { proposal.againstVotes = proposal.againstVotes + votes; } else if (support == 1) { proposal.forVotes = proposal.forVotes + votes; } else if (support == 2) { proposal.abstainVotes = proposal.abstainVotes + votes; } receipt.hasVoted = true; receipt.support = support; receipt.votes = votes; // Lines: [866-871] // Args via calldata instead of storage ref function proposalCreationBlock(Proposal storage proposal) internal view returns (uint256) { if (proposal.creationBlock == 0) { return proposal.startBlock - votingDelay; } return proposal.creationBlock; } // Lines: [877-889] function quorumVotes(uint256 proposalId) public view returns (uint256) { Proposal storage proposal = _proposals[proposalId]; if (proposal.totalSupply == 0) { return proposal.quorumVotes; } return dynamicQuorumVotes( proposal.againstVotes, proposal.totalSupply, getDynamicQuorumParamsAt(proposal.creationBlock) ); } ```
require() before some computations, if it makes sense
<a name="G-07"></a>require()
takes some gas for execution, therefore if the statement reverts gas will not be retrieved.Contracts:
file: contracts/governance/NounsDAOLogicV2.sol ............................... // Lines: [197-208] // Comment: the following checks could be performed before interacting with `ProposalTemp` require( nouns.getPriorVotes(msg.sender, block.number - 1) > temp.proposalThreshold, 'NounsDAO::propose: proposer votes below proposal threshold' ); require( targets.length == values.length && targets.length == signatures.length && targets.length == calldatas.length, 'NounsDAO::propose: proposal function information arity mismatch' ); require(targets.length != 0, 'NounsDAO::propose: must provide actions'); require(targets.length <= proposalMaxOperations, 'NounsDAO::propose: too many actions');
Contracts:
file: contracts/governance/NounsDAOLogicV2.sol ............................... // Lines: [323-340] // Comment: after a proposal got executed, it is possible to delete the proposal if it's no longer used. function execute(uint256 proposalId) external { require( state(proposalId) == ProposalState.Queued, 'NounsDAO::execute: proposal can only be executed if it is queued' ); Proposal storage proposal = _proposals[proposalId]; proposal.executed = true; for (uint256 i = 0; i < proposal.targets.length; i++) { timelock.executeTransaction( proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta ); } emit ProposalExecuted(proposalId); }
Internal
functions can be inlined<a name="G-10"></a>JUMP
s which costs around 40-50 gas uints. In loops it will save significant amount of gas.Contracts:
file: contracts/governance/NounsDAOLogicV1.sol ............................... // Lines: [294-306] function queueOrRevertInternal( address target, uint256 value, string memory signature, bytes memory data, uint256 eta ) internal { require( !timelock.queuedTransactions(keccak256(abi.encode(target, value, signature, data, eta))), 'NounsDAO::queueOrRevertInternal: identical proposal action already queued at eta' ); timelock.queueTransaction(target, value, signature, data, eta); }
DUP<X>
for every iteration and also removes warm access per each iteration.Contracts:
file: contracts/governance/NounsDAOLogicV2.sol ............................... // Lines: [292-292] // Please, check all loops, it's a significant amount of gas usage for each call as well. for (uint256 i = 0; i < proposal.targets.length; i++) {}
private/internal
for constants/immutable/state
variables instead of public
<a name="G-15"></a>public
instance.Contracts:
file: contracts/governance/NounsDAOLogicV2.sol ............................... // Lines: [59-105] string public constant name = 'Nouns DAO'; /// @notice The minimum setable proposal threshold uint256 public constant MIN_PROPOSAL_THRESHOLD_BPS = 1; // 1 basis point or 0.01% /// @notice The maximum setable proposal threshold uint256 public constant MAX_PROPOSAL_THRESHOLD_BPS = 1_000; // 1,000 basis points or 10% /// @notice The minimum setable voting period uint256 public constant MIN_VOTING_PERIOD = 5_760; // About 24 hours /// @notice The max setable voting period uint256 public constant MAX_VOTING_PERIOD = 80_640; // About 2 weeks /// @notice The min setable voting delay uint256 public constant MIN_VOTING_DELAY = 1; /// @notice The max setable voting delay uint256 public constant MAX_VOTING_DELAY = 40_320; // About 1 week /// @notice The lower bound of minimum quorum votes basis points uint256 public constant MIN_QUORUM_VOTES_BPS_LOWER_BOUND = 200; // 200 basis points or 2% /// @notice The upper bound of minimum quorum votes basis points uint256 public constant MIN_QUORUM_VOTES_BPS_UPPER_BOUND = 2_000; // 2,000 basis points or 20% /// @notice The upper bound of maximum quorum votes basis points uint256 public constant MAX_QUORUM_VOTES_BPS_UPPER_BOUND = 6_000; // 4,000 basis points or 60% /// @notice The maximum setable quorum votes basis points uint256 public constant MAX_QUORUM_VOTES_BPS = 2_000; // 2,000 basis points or 20% /// @notice The maximum number of actions that can be included in a proposal uint256 public constant proposalMaxOperations = 10; // 10 actions /// @notice The maximum priority fee used to cap gas refunds in `castRefundableVote` uint256 public constant MAX_REFUND_PRIORITY_FEE = 2 gwei; /// @notice The vote refund gas overhead, including 7K for ETH transfer and 29K for general transaction overhead uint256 public constant REFUND_BASE_GAS = 36000; /// @notice The EIP-712 typehash for the contract's domain bytes32 public constant DOMAIN_TYPEHASH = keccak256('EIP712Domain(string name,uint256 chainId,address verifyingContract)'); /// @notice The EIP-712 typehash for the ballot struct used by the contract bytes32 public constant BALLOT_TYPEHASH = keccak256('Ballot(uint256 proposalId,uint8 support)');
Contracts:
file: contracts/governance/NounsDAOLogicV1.sol ............................... // Lines: [123-123] require(msg.sender == admin, 'NounsDAO::initialize: admin only'); // Lines: [581-581] require(msg.sender == admin, 'NounsDAO::_setPendingAdmin: admin only'); // Lines: [563-563] require(msg.sender == admin, 'NounsDAO::_setProposalThresholdBPS: admin only'); // Lines: [599-599] require(msg.sender == admin, 'NounsDAO::_setPendingAdmin: admin only');
type(uint256).max
<a name="G-17"></a>Contracts:
file: contracts/governance/NounsDAOLogicV2.sol ............................... // Lines: [1019-1019] require(n <= type(uint32).max, errorMessage); // Lines: [1024-1024] if (n > type(uint16).max) {}
uint
instead of uint8
, uint64
, if it's possible<a name="G-19"></a>error VaultAlreadyUnderAuction(bytes12 vaultId, address witch);
.error VaultAlreadyUnderAuction(bytes32 vaultId, address witch);
.bytes32
doesn't give a huge optimization.Contracts:
file: contracts/base/ERC721Checkpointable.sol ............................... // Lines: [152-152] uint32 nCheckpoints = numCheckpoints[account]; // Lines: [166-166] uint32 nCheckpoints = numCheckpoints[account]; // Lines: [181-182] uint32 lower = 0; uint32 upper = nCheckpoints - 1; // Lines: [184-184] uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow // Lines: [217-217] uint32 srcRepNum = numCheckpoints[srcRep]; uint96 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0; uint96 srcRepNew = sub96(srcRepOld, amount, 'ERC721Checkpointable::_moveDelegates: amount underflows'); // etc... ```
external
instead of public
, if there are no internal calls<a name="G-20"></a>external
use call data to read arguments, where public
will first allocate in local memory and then load them.Contracts:
file: contracts/governance/NounsDAOLogicV1.sol ............................... // Lines: [113-121] function initialize( address timelock_, address nouns_, address vetoer_, uint256 votingPeriod_, uint256 votingDelay_, uint256 proposalThresholdBPS_, uint256 quorumVotesBPS_ ) public virtual {} // etc.. ```
calldataload
instead of mload
<a name="G-21"></a>calldataload
, or replace memory
with calldata
. If the args are pretty huge, allocating args in memory will cost a lot.Contracts:
file: contracts/governance/NounsDAOLogicV1.sol ............................... // Lines: [174-180] function propose( address[] memory targets, uint256[] memory values, string[] memory signatures, bytes[] memory calldatas, string memory description ) public returns (uint256) {} // Lines: [294-300] function queueOrRevertInternal( address target, uint256 value, string memory signature, bytes memory data, uint256 eta ) internal { // Lines: [392-400] // Comments: you can also return what you need directly from calldata here. function getActions(uint256 proposalId) external view returns ( address[] memory targets, uint256[] memory values, string[] memory signatures, bytes[] memory calldatas ) ```