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
Rank: 26/36
Findings: 1
Award: $58.98
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: shark
Also found by: 0xMilenov, Bauchibred, Kaysoft, MohammedRizwan, codegpt, descharre, fatherOfBlocks, ihtishamsudo, klau5, koxuan, nadin
58.9765 USDC - $58.98
One or more logic contracts do not protect their initializers. An attacker can call the initializer and assume ownership of the logic contract, whereby she can perform privileged operations that trick unsuspecting users into believing that she is the owner of the upgradeable contract.
function initialize( address _owner, address _minter, INounsDAOForkEscrow _escrow, uint32 _forkId, uint256 startNounId, uint256 tokensToClaim, uint256 _forkingPeriodEndTimestamp ) external initializer { __ERC721_init('Nouns', 'NOUN'); _transferOwnership(_owner); minter = _minter; escrow = _escrow; forkId = _forkId; _currentNounId = startNounId; remainingTokensToClaim = tokensToClaim; forkingPeriodEndTimestamp = _forkingPeriodEndTimestamp; NounsTokenFork originalToken = NounsTokenFork(address(escrow.nounsToken())); descriptor = originalToken.descriptor(); seeder = originalToken.seeder(); }
function initialize( address _owner, INounsToken _nouns, address _weth, uint256 _timeBuffer, uint256 _reservePrice, uint8 _minBidIncrementPercentage, uint256 _duration ) external initializer { __Pausable_init(); __ReentrancyGuard_init(); _transferOwnership(_owner); _pause(); nouns = _nouns; weth = _weth; timeBuffer = _timeBuffer; reservePrice = _reservePrice; minBidIncrementPercentage = _minBidIncrementPercentage; duration = _duration; }
function initialize(address admin_, uint256 delay_) public virtual initializer { require(delay_ >= MINIMUM_DELAY, 'NounsDAOExecutor::constructor: Delay must exceed minimum delay.'); require(delay_ <= MAXIMUM_DELAY, 'NounsDAOExecutor::setDelay: Delay must not exceed maximum delay.'); admin = admin_; delay = delay_; }
We advise calling _disableInitializers
in the constructor or giving the constructor the initializer
modifier to prevent the intializer from being called on the logic contract.
https://github.com/nounsDAO/nouns-monorepo/blob/718211e063d511eeda1084710f6a682955e80dcb/packages/nouns-contracts/contracts/governance/NounsDAOLogicV2.sol#L138
https://github.com/nounsDAO/nouns-monorepo/blob/718211e063d511eeda1084710f6a682955e80dcb/packages/nouns-contracts/contracts/governance/NounsDAOLogicV2.sol#L836
https://github.com/nounsDAO/nouns-monorepo/blob/718211e063d511eeda1084710f6a682955e80dcb/packages/nouns-contracts/contracts/governance/NounsDAOLogicV2.sol#L876
https://github.com/nounsDAO/nouns-monorepo/blob/718211e063d511eeda1084710f6a682955e80dcb/packages/nouns-contracts/contracts/governance/NounsDAOV3Admin.sol#L568
https://github.com/nounsDAO/nouns-monorepo/blob/718211e063d511eeda1084710f6a682955e80dcb/packages/nouns-contracts/contracts/governance/fork/newdao/token/NounsTokenFork.sol#L120
https://github.com/nounsDAO/nouns-monorepo/blob/718211e063d511eeda1084710f6a682955e80dcb/packages/nouns-contracts/contracts/governance/fork/newdao/token/NounsTokenFork.sol#L121
Addresses should be checked before assignment or external call to make sure they are not zero addresses.
We advise adding a zero-check for the passed-in address value to prevent unexpected errors.
In the function castVoteInternal()
of NounsDAOLogicV2
contract, it is allowed to cast a zero vote for proposals.
function castVoteInternal( address voter, uint256 proposalId, uint8 support ) internal returns (uint96) { 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; return votes; }
If a voter has a zero vote power in the block of proposal creation, the function will allow them to cast a vote, but it will not have any effect on the overall vote count since zero votes are added to the proposal's vote tally.
The same issue also exists in the functions castVoteDuringVotingPeriodInternal()
and castObjectionInternal()
of library NounsDAOV3Votes
.
function test_castZeroVote() public { address someone = makeAddr("someone"); console2.log("Before voting"); printProposal(proposalId); NounsDAOStorageV3.ProposalCondensed memory proposal = dao.proposalsV3(proposalId); vm.roll(proposal.startBlock); console2.log("someone's vote power at creationBlock is:"); printPriorVotes(someone, proposal.creationBlock); console2.log("Starting voting"); vm.roll(block.number + 1); vm.prank(someone); dao.castVote(proposalId, 1); console2.log("After voting"); printProposal(proposalId); }
[PASS] test_castZeroVote() (gas: 168783) Logs: Before voting ----------------Proposal #1---------------- proposer=0x6bEf539e8319dACba4C2DaD055006E79682C0f32, proposalThreshold=0, quorumVotes=0 eta=0, startBlock=15, endBlock=7215 forVotes=0, againstVotes=0, abstainVotes=0 canceled=false, vetoed=false, executed=false totalSupply=4, creationBlock=4 ----------------Proposal #1 End---------------- someone's vote power at creationBlock is: ---0x69979820B003b34127eAdBA93BD51CAaC2f768dB in block#15---- Prior vote is 0 block=0, votes=0 Starting voting After voting ----------------Proposal #1---------------- proposer=0x6bEf539e8319dACba4C2DaD055006E79682C0f32, proposalThreshold=0, quorumVotes=0 eta=0, startBlock=15, endBlock=7215 forVotes=0, againstVotes=0, abstainVotes=0 canceled=false, vetoed=false, executed=false totalSupply=4, creationBlock=4 ----------------Proposal #1 End---------------- Traces: [168783] NounsDAOLogicV3VotesTest::test_castZeroVote() ├─ [0] VM::addr(<pk>) │ └─ ← someone: [0x69979820B003b34127eAdBA93BD51CAaC2f768dB] ├─ [0] VM::label(someone: [0x69979820B003b34127eAdBA93BD51CAaC2f768dB], someone) │ └─ ← () ├─ [0] console::log(Before voting) [staticcall] │ └─ ← () ├─ [52573] NounsDAOProxyV3::proposalsV3(1) [staticcall] │ ├─ [47137] NounsDAOLogicV3::proposalsV3(1) [delegatecall] │ │ ├─ [40853] NounsDAOV3Proposals::100e0d6d(00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001) [delegatecall] │ │ │ └─ ← 0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006bef539e8319dacba4c2dad055006e79682c0f32000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000001c2f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 │ │ └─ ← (1, 0x6bEf539e8319dACba4C2DaD055006E79682C0f32, 0, 0, 0, 15, 7215, 0, 0, 0, false, false, false, 4, 4, [], 14, 0, false) │ └─ ← (1, 0x6bEf539e8319dACba4C2DaD055006E79682C0f32, 0, 0, 0, 15, 7215, 0, 0, 0, false, false, false, 4, 4, [], 14, 0, false) ├─ [0] console::log(----------------Proposal #%d----------------, 1) [staticcall] │ └─ ← () ├─ [0] console::log(proposer=%s, proposalThreshold=%d, quorumVotes=%d, proposer: [0x6bEf539e8319dACba4C2DaD055006E79682C0f32], 0, 0) [staticcall] │ └─ ← () ├─ [0] console::log(eta=%d, startBlock=%d, endBlock=%d, 0, 15, 7215) [staticcall] │ └─ ← () ├─ [0] console::log(forVotes=%d, againstVotes=%d, abstainVotes=%d, 0, 0, 0) [staticcall] │ └─ ← () ├─ [0] console::log(canceled=%s, vetoed=%s, executed=%s, false, false, false) [staticcall] │ └─ ← () ├─ [0] console::log(totalSupply=%d, creationBlock=%d, 4, 4) [staticcall] │ └─ ← () ├─ [0] console::log(----------------Proposal #%d End----------------, 1) [staticcall] │ └─ ← () ├─ [11573] NounsDAOProxyV3::proposalsV3(1) [staticcall] │ ├─ [10637] NounsDAOLogicV3::proposalsV3(1) [delegatecall] │ │ ├─ [6853] NounsDAOV3Proposals::100e0d6d(00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001) [delegatecall] │ │ │ └─ ← 0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006bef539e8319dacba4c2dad055006e79682c0f32000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000001c2f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 │ │ └─ ← (1, 0x6bEf539e8319dACba4C2DaD055006E79682C0f32, 0, 0, 0, 15, 7215, 0, 0, 0, false, false, false, 4, 4, [], 14, 0, false) │ └─ ← (1, 0x6bEf539e8319dACba4C2DaD055006E79682C0f32, 0, 0, 0, 15, 7215, 0, 0, 0, false, false, false, 4, 4, [], 14, 0, false) ├─ [0] VM::roll(15) │ └─ ← () ├─ [0] console::log(someone's vote power at creationBlock is:) [staticcall] │ └─ ← () ├─ [2807] NounsToken::getPriorVotes(someone: [0x69979820B003b34127eAdBA93BD51CAaC2f768dB], 4) [staticcall] │ └─ ← 0 ├─ [0] console::log(---%s in block#%d----, someone: [0x69979820B003b34127eAdBA93BD51CAaC2f768dB], 15) [staticcall] │ └─ ← () ├─ [0] console::log(Prior vote is %d, 0) [staticcall] │ └─ ← () ├─ [574] NounsToken::numCheckpoints(someone: [0x69979820B003b34127eAdBA93BD51CAaC2f768dB]) [staticcall] │ └─ ← 0 ├─ [2828] NounsToken::checkpoints(someone: [0x69979820B003b34127eAdBA93BD51CAaC2f768dB], 0) [staticcall] │ └─ ← 0, 0 ├─ [0] console::log(block=%d, votes=%d, 0, 0) [staticcall] │ └─ ← () ├─ [0] console::log(Starting voting) [staticcall] │ └─ ← () ├─ [0] VM::roll(16) │ └─ ← () ├─ [0] VM::prank(someone: [0x69979820B003b34127eAdBA93BD51CAaC2f768dB]) │ └─ ← () ├─ [41225] NounsDAOProxyV3::castVote(1, 1) │ ├─ [40600] NounsDAOLogicV3::castVote(1, 1) [delegatecall] │ │ ├─ [37285] NounsDAOV3Votes::46742c7e(000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001) [delegatecall] │ │ │ ├─ [807] NounsToken::getPriorVotes(someone: [0x69979820B003b34127eAdBA93BD51CAaC2f768dB], 4) [staticcall] │ │ │ │ └─ ← 0 │ │ │ ├─ emit VoteCast(voter: someone: [0x69979820B003b34127eAdBA93BD51CAaC2f768dB], proposalId: 1, support: 1, votes: 0, reason: ) │ │ │ └─ ← () │ │ └─ ← () │ └─ ← () ├─ [0] console::log(After voting) [staticcall] │ └─ ← () ├─ [11573] NounsDAOProxyV3::proposalsV3(1) [staticcall] │ ├─ [10637] NounsDAOLogicV3::proposalsV3(1) [delegatecall] │ │ ├─ [6853] NounsDAOV3Proposals::100e0d6d(00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001) [delegatecall] │ │ │ └─ ← 0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006bef539e8319dacba4c2dad055006e79682c0f32000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000001c2f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 │ │ └─ ← (1, 0x6bEf539e8319dACba4C2DaD055006E79682C0f32, 0, 0, 0, 15, 7215, 0, 0, 0, false, false, false, 4, 4, [], 14, 0, false) │ └─ ← (1, 0x6bEf539e8319dACba4C2DaD055006E79682C0f32, 0, 0, 0, 15, 7215, 0, 0, 0, false, false, false, 4, 4, [], 14, 0, false) ├─ [0] console::log(----------------Proposal #%d----------------, 1) [staticcall] │ └─ ← () ├─ [0] console::log(proposer=%s, proposalThreshold=%d, quorumVotes=%d, proposer: [0x6bEf539e8319dACba4C2DaD055006E79682C0f32], 0, 0) [staticcall] │ └─ ← () ├─ [0] console::log(eta=%d, startBlock=%d, endBlock=%d, 0, 15, 7215) [staticcall] │ └─ ← () ├─ [0] console::log(forVotes=%d, againstVotes=%d, abstainVotes=%d, 0, 0, 0) [staticcall] │ └─ ← () ├─ [0] console::log(canceled=%s, vetoed=%s, executed=%s, false, false, false) [staticcall] │ └─ ← () ├─ [0] console::log(totalSupply=%d, creationBlock=%d, 4, 4) [staticcall] │ └─ ← () ├─ [0] console::log(----------------Proposal #%d End----------------, 1) [staticcall] │ └─ ← () └─ ← () Test result: ok. 1 passed; 0 failed; finished in 27.89ms
It is recommended to modify the function to explicitly check whether the voter has any tokens before allowing them to cast a vote. This can be done by adding a check for a non-zero vote count before executing the rest of the code in the function.
There is no upper limit for _minBidIncrementPercentage
. It is possible to set the percentage to an arbitrary value.
It is recommended to add a reasonable boundary for the _minBidIncrementPercentage
.
The calcProposalEncodeData()
function calculates the encoded data for a proposal using the abi.encode()
function. However, it does not include a nonce
as part of the signature, which can cause signature replay attacks.
A signature replay attack occurs when a valid signature is reused to execute the same transaction multiple times. If the encoded data for a proposal does not include a nonce, the proposer can create the same proposal after the previous proposal was finished. This maybe unexpected.
To prevent signature replay attacks, it is recommended to include a unique nonce
as part of the signature. The nonce
should be different for each proposal and should be included in the encoded data. It can be a simple counter that is incremented for each proposal, or it can be a more complex value that incorporates additional variables or randomness to make it more difficult to predict.
File: contracts/governance/fork/NounsDAOForkEscrow.sol
In the NounsDAOForkEscrow
contract, function returnTokensToOwner()
violates the Check-Effects-Interactions pattern. This pattern is a best practice for writing secure smart contracts that involves performing all state changes before making any external function calls.
nounsToken.transferFrom(address(this), owner, tokenIds[i]);
escrowedTokensByForkId[forkId][tokenIds[i]] = address(0);
An external function call to the nounsToken.transferFrom()
function is made before the state changes are made.
To prevent such potential issue, it is important to ensure that all external function calls are made after the state changes have been made.
It is recommended to follow the Checks-Effects-Interactions Pattern to avoid the risk of calling unknown contracts or applying OpenZeppelin ReentrancyGuard library - nonReentrant
modifier for the aforementioned functions to prevent reentrancy attacks.
NounsDAOExecutorV2
to Wrap Original TimelockFile: contracts/governance/fork/ForkDAODeployer.sol
The getOriginalTimelock()
function is used to return the original time lock contract instance. However, it uses the NounsDAOExecutorV2
contract to wrap the ABI code of the originalToken.owner()
. The NounsDAOExecutorV2
contract is the current time lock, and it provides an interface for interacting with the original time lock contract.
If the sender call a function which exists in current time lock but not exists in original, it will revert.
It's recommended to use NounsDAOExecutor
to wrap the ABI code of original time lock.
Performing division by zero would raise an error and revert the transaction. The totalSupply
should potentially be zero, in this case, the function should directly return zero.
uint256 againstVotesBPS = (10000 * againstVotes) / totalSupply;
It is recommended to either reformulate the divisor expression, or to use conditionals or require statements to rule out the possibility of a divide-by-zero.
File: contracts/governance/fork/NounsDAOV3Fork.sol
In the current design of the Nouns Fork
, if the minority initiates a forked Nouns DAO
, the majority of the original Nouns DAO
can choose to join the forked Nouns DAO
and potentially force the minority to fork again if they disagree with the direction of the new DAO. This is because the majority still holds a significant amount of voting power and can use it to influence the decisions of the new DAO. However, this scenario is not unique to the Nouns Fork
and can occur in any DAO where the majority holds a significant amount of power.
It highlights the importance of having clear governance mechanisms in place to ensure that the decisions made are fair and representative of the interests of all members, regardless of their voting power.
#0 - c4-judge
2023-07-25T09:54:48Z
gzeon-c4 marked the issue as grade-c
#1 - c4-judge
2023-07-25T09:55:24Z
gzeon-c4 marked the issue as grade-b