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: 95/160
Findings: 1
Award: $35.45
🌟 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
35.4484 USDC - $35.45
castVoteBySig
The castVoteBySig
function is vulnerable to replay attacks. Users could submit the signature to the castVoteBySig
function multiple times, and the signature will be processed successfully.
However, due to the validation check implemented within the castVoteInternal
function, the function will revert if the user attempts to vote for a proposal more than once. Thus, this issue has been marked as "Low" risk. However, it is still recommended to add in nonce to ensure that the signature cannot be replayed as a defense in-depth measure.
function castVoteBySig( uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s ) external { bytes32 domainSeparator = keccak256( abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainIdInternal(), address(this)) ); bytes32 structHash = keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support)); bytes32 digest = keccak256(abi.encodePacked('\x19\x01', domainSeparator, structHash)); address signatory = ecrecover(digest, v, r, s); require(signatory != address(0), 'NounsDAO::castVoteBySig: invalid signature'); emit VoteCast(signatory, proposalId, support, castVoteInternal(signatory, proposalId, support), ''); }
function castVoteBySig( uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s ) external { bytes32 domainSeparator = keccak256( abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainIdInternal(), address(this)) ); bytes32 structHash = keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support)); bytes32 digest = keccak256(abi.encodePacked('\x19\x01', domainSeparator, structHash)); address signatory = ecrecover(digest, v, r, s); require(signatory != address(0), 'NounsDAO::castVoteBySig: invalid signature'); emit VoteCast(signatory, proposalId, support, castVoteInternal(signatory, proposalId, support), ''); }
Within the Nouns Governance, Nounders have the veto right to reject any proposal submitted. The Nounders are believed to act in good faith and only reject a proposal that is harmful to the Nouns DAO. However, users should be aware that nothing is permanent and this might change in the future unless the veto right has been burned. It is possible that a proposal that puts the Nounders in a disadvantageous position might be rejected by them. Therefore, users should take note of this risk and perform their own due diligence.
/** * @notice Vetoes a proposal only if sender is the vetoer and the proposal has not been executed. * @param proposalId The id of the proposal to veto */ function veto(uint256 proposalId) external { require(vetoer != address(0), 'NounsDAO::veto: veto power burned'); require(msg.sender == vetoer, 'NounsDAO::veto: only vetoer'); require(state(proposalId) != ProposalState.Executed, 'NounsDAO::veto: cannot veto executed proposal'); Proposal storage proposal = _proposals[proposalId]; proposal.vetoed = 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 ); } emit ProposalVetoed(proposalId); }
The contract makes use of the floating-point pragma ^0.8.6. Contracts should be deployed using the same compiler version. Locking the pragma helps ensure that contracts are not unintentionally deployed using another pragma, such as an obsolete version, that may introduce issues in the contract system. Consider locking the pragma version. It is advised that floating pragma should not be used in production
pragma solidity ^0.8.6;
Affected Contracts:
https://github.com/code-423n4/2022-08-nounsdao/blob/main/contracts/governance/NounsDAOLogicV1.sol
https://github.com/code-423n4/2022-08-nounsdao/blob/main/contracts/governance/NounsDAOLogicV2.sol
https://github.com/code-423n4/2022-08-nounsdao/blob/main/contracts/governance/NounsDAOProxy.sol
https://github.com/code-423n4/2022-08-nounsdao/blob/main/contracts/base/ERC721Checkpointable.sol
https://github.com/code-423n4/2022-08-nounsdao/blob/main/contracts/base/ERC721Enumerable.sol
Vetoer calls _setVetoer
function to transfer the veto right to the new address directly. As such, there is a risk that the veto right is transferred to an invalid address, thus accidentally burning the veto right. Consider implementing a two-step process where the vetoer nominates an account, and the nominated account needs to call an acceptVetoRight()
function for the transfer of veto right to fully succeed. This ensures the nominated EOA account is a valid and active account.
function _setVetoer(address newVetoer) public { require(msg.sender == vetoer, 'NounsDAO::_setVetoer: vetoer only'); emit NewVetoer(vetoer, newVetoer); vetoer = newVetoer; }