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: 32/160
Findings: 2
Award: $55.52
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0bi, 0x040, 0x1337, 0x1f8b, 0xDjango, 0xNazgul, 0xNineDec, 0xRajeev, 0xSky, 0xSmartContract, 0xbepresent, 0xkatana, 0xmatt, 8olidity, Aymen0909, Bjorn_bug, Bnke0x0, CertoraInc, Ch_301, Chom, CodingNameKiki, Deivitto, DevABDee, DimitarDimitrov, Dravee, ElKu, Funen, GalloDaSballo, GimelSec, Guardian, Haruxe, JC, JansenC, Jeiwan, JohnSmith, KIntern_NA, Lambda, LeoS, Noah3o6, Olivierdem, R2, RaymondFam, Respx, ReyAdmirado, Rohan16, Rolezn, Ruhum, Saintcode_, Sm4rty, SooYa, Soosh, TomJ, Tomo, Trabajo_de_mates, Waze, _Adam, __141345__, ajtra, android69, asutorufos, auditor0517, berndartmueller, bobirichman, brgltd, c3phas, cRat1st0s, carlitox477, catchup, cccz, csanuragjain, d3e4, delfin454000, dipp, djxploit, durianSausage, erictee, exd0tpy, fatherOfBlocks, gogo, hyh, ladboy233, lukris02, mics, mrpathfindr, natzuu, oyc_109, p_crypt0, pashov, pauliax, pfapostol, prasantgupta52, rajatbeladiya, rbserver, ret2basic, rfa, robee, rokinot, rvierdiiev, sach1r0, saian, seyni, shenwilly, sikorico, simon135, sryysryy, sseefried, throttle, tnevler, tonisives, wagmi, xiaoming90, yixxas, z3s, zkhorse, zzzitron
38.8441 USDC - $38.84
Below are findings, sorted by severity, along with notes on how to remediate them.
While the purpose of the Governor is to ensure governance votes on proposals, certain operations can be executed by the admin
without delay nor voting, one of them is changing admin.
In spirit of the contract I recommend adding a delay, or forcing admin
changes to be performed by the Timelock
A sweep, similar to the withdraw
function, ideally transferring the tokens to the timelock, for a proposal to then release them, will allow recouping back tokens sent back by mistake.
A lack of sweep on the Governor means that tokens sent to it by mistake will not be retrievable, a catch-all sweep, that sends tokens to the Timelock will allow to use governance to claw them back.
payable.call
is a low-level call, it will not cause a revert on failure.
(bool sent, ) = msg.sender.call{ value: amount }('');
This is not particularly risky as it can always be called again until it works
function _setVetoer(address newVetoer) public { require(msg.sender == vetoer, 'NounsDAO::_setVetoer: vetoer only');
Is a low level call, if implementation
= random address it will still return true.
That's because the low level call delegatecall
will not check for the contract existence
Adding a check for contract existence on the setter will avoid this low likelihood risk: https://github.com/code-423n4/2022-08-nounsdao/blob/452695d4764ba9d5e1d3eef0d5ecca3d004f215a/contracts/governance/NounsDAOProxyV2.sol#L86
Due to the structure of ECDSA, each signature has a corresponding also valid signature, for that reason Open Zeppelin's implementation of tryRecover
ensures the value of the signature is in the lower bits to avoid receiving a "reflected key"
You can see how OZ solved this problem here: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/93bc3b657b69e4886dcfef7a2502e165f2cd90d2/contracts/utils/cryptography/ECDSA.sol#L147
Because of the fact that you use receipts
for voting, malleability won't be exploitable effectively, however the way you verify signatures is open to that attack
delegateTo
is only used in constructor, would recommend reconciling code or using it in both the constructor and the fallback.
_setVotingDelay
is prefixed by an _
which is used to signify internal functions
This nomenclature is inconsistent, I'd recommend swapping it
function _setVotingDelay(uint256 newVotingDelay) external {
Change to:
function setVotingDelay(uint256 newVotingDelay) external {
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; }
uint256 public constant proposalMaxOperations = 10; // 10 actions
Recommend using ALL_CAPS to maintain consistency
In cancel
we can cancel if the proposer doesn't meet proposalThreshold
nouns.getPriorVotes(proposal.proposer, block.number - 1) < proposal.proposalThreshold,
However that is by definition impossible due to the check
nouns.getPriorVotes(msg.sender, block.number - 1) > temp.proposalThreshold,
I recommend deleting the check
function bps2Uint(uint256 bps, uint256 number) internal pure returns (uint256) { return (number * bps) / 10000; }
Because the only parameter passed as bps is maxQuorumVotesBPS
, the code is fine, I'd recommend adding a comment for clarity (also consider changing uint256
to uint16
which is in line with the size of maxQuorumVotesBPS
)
Especially to avoid the herd of C4ooooors
pragma solidity ^0.8.6;
The caret will reduce risk of breaking changes however, due to plenty of bugs for versions between 0.8.1 to 0.8.14, it would be best to settle on a specific version and "pick your poison".
Considering that Hardhat is set to use 0.8.15, you may as well commit to using that one
/** * @notice Burns veto priviledges * @dev Vetoer function destroying veto power forever */ function _burnVetoPower() public { // Check caller is pendingAdmin and pendingAdmin ≠address(0) require(msg.sender == vetoer, 'NounsDAO::_burnVetoPower: vetoer only'); _setVetoer(address(0)); }
There is no pendingAdmin
in this function
Also minor typo priviledges
-> privileges
🌟 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
16.6802 USDC - $16.68
The codebase can benefit by a more aggressive use of the unchecked
keyword, additionally a packing opportunity will save plenty of gas.
Below findings are sorted by effectiveness as well as uniqueness, the bottom of the report will contain findings most other wardens will have submitted.
Each finding lists estimated gas savings. These are estimates based on EVM Opcodes.
Address uses 20 bytes, each bool 1, by moving the bool
declaration for Proposal
to be below address proposer
you can pack them all in one Storage Slot
/// @notice Creator of the proposal /// @audit Move bools below proposer for packing address proposer; /// @notice Flag marking whether the proposal has been canceled bool canceled; /// @notice Flag marking whether the proposal has been vetoed bool vetoed; /// @notice Flag marking whether the proposal has been executed bool executed;
Will save one storage slot, meaning each proposal creation will cost 15k less gas, and reading that data will cost 2000 less gas each time
Also applies to:
The code below is emitting an event using the Storage variable admin
emit NewAdmin(admin);
This will cost 100 gas (SLOAD), it would be cheaper to use msg.sender
(the accepting admin) to save 98 gas (100 - 2 gas for CALLER).
This pattern is used pretty widely, so this change will bring hundreds of gas savings throughout the codebase
Also see:
The below additions can be made unchecked, saving around 40 gas per operation (between 20 and 80)
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; }
targets.length == values.length &&
Instead of recomputing targets.length
each time (which costs an extra 3 gas), you can store the value in a local variable
temp.startBlock = block.number + votingDelay; temp.endBlock = temp.startBlock + votingPeriod; proposalCount++;
Also save the return value and read from memory
https://github.com/code-423n4/2022-08-nounsdao/blob/452695d4764ba9d5e1d3eef0d5ecca3d004f215a/contracts/governance/NounsDAOLogicV2.sol#L228 ```solidity // Delete // proposalCount++; // Use unchecked { newProposal.id = ++proposalCount; }
Saves an SLOAD - 100 gas, and the unchecked math
uint256 lower = 0; uint256 upper = len - 1; while (upper > lower) { uint256 center = upper - (upper - lower) / 2; DynamicQuorumParamsCheckpoint memory cp = quorumParamsCheckpoints[center]; if (cp.fromBlock == blockNumber) { return cp.params; } else if (cp.fromBlock < blockNumber) { lower = center; } else { upper = center - 1; } }
You don't need to assign the default value to i
- Saves 3 gas
uint256 i = 0; => uint256 i;
Cache the Array Length - 100 gas per iteration (SLOAD)
proposal.targets.length => uint256 length = proposal.targets.length;
Change the increment to unchecked pre-increment
i++ => unchecked { ++i; }
for (uint256 i = 0; i < proposal.targets.length; i++) {
< 3
to save 3 gas as <=
costs twice as much - 3 gasrequire(support <= 2, 'NounsDAO::castVoteInternal: invalid vote type');
Can be changed to
require(support < 3, 'NounsDAO::castVoteInternal: invalid vote type');
Saving 3 gas