Platform: Code4rena
Start Date: 06/09/2022
Pot Size: $90,000 USDC
Total HM: 33
Participants: 168
Period: 9 days
Judge: GalloDaSballo
Total Solo HM: 10
Id: 157
League: ETH
Rank: 26/168
Findings: 6
Award: $802.02
π Selected for report: 0
π Solo Findings: 0
π Selected for report: berndartmueller
Also found by: 0x52, 0xSky, Chom, PwnPatrol, bin2chen, cccz, davidbrai, elprofesor, izhuer, m9800, rvierdiiev
In the _writeCheckpoint function of the ERC721Votes contract, votes for the same block time are not recorded in the same Checkpoint status variable, which results in multiple Checkpoint status variables for the same block time.
function _writeCheckpoint( address _account, uint256 _id, uint256 _prevTotalVotes, uint256 _newTotalVotes ) private { // Get the pointer to store the checkpoint Checkpoint storage checkpoint = checkpoints[_account][_id]; // Record the updated voting weight and current time checkpoint.votes = uint192(_newTotalVotes); checkpoint.timestamp = uint64(block.timestamp); emit DelegateVotesChanged(_account, _prevTotalVotes, _newTotalVotes); }
The getPastVotes function returns the votes corresponding to the block time, which means that the result of getPastVotes can be manipulated.
Consider the following scenario: User A currently has the following checkpoints
timestamp 100 100 100 100 100 150 votes 10 20 30 20 50 getPastVotes(A,100) == 30
User B votes on User A
timestamp 100 100 100 100 100 150 150 160 votes 10 20 30 20 50 51 getPastVotes(A,100) == 20
User B manipulates the result of getPastVotes(A,100) by voting for User A
None
function _writeCheckpoint( address _account, uint256 _id, uint256 _prevTotalVotes, uint256 _newTotalVotes ) private { // Get the pointer to store the checkpoint Checkpoint storage checkpoint = checkpoints[_account][_id]; if (_id > 0 && checkpoints[_account][_id-1].timestamp == block.timestamp) { checkpoints[_account][_id-1].votes = uint192(_newTotalVotes); numCheckpoints[_account]--; } else { checkpoint.votes = uint192(_newTotalVotes); checkpoint.timestamp = uint64(block.timestamp); } emit DelegateVotesChanged(_account, _prevTotalVotes, _newTotalVotes); }
#0 - GalloDaSballo
2022-09-20T21:43:40Z
In the proposalThreshold() and quorum() of the Governor contract, if the result of the multiplication is less than 10000, the return value will be 0 due to precision loss.
function proposalThreshold() public view returns (uint256) { unchecked { return (settings.token.totalSupply() * settings.proposalThresholdBps) / 10_000; } } /// @notice The current number of votes required to be in favor of a proposal in order to reach quorum function quorum() public view returns (uint256) { unchecked { return (settings.token.totalSupply() * settings.quorumThresholdBps) / 10_000; } }
In propose(), proposalThreshold() returns 0 which allows anyone to create a proposal, and quorum() returns 0 which allows the proposal to be executed without being votes.
unchecked { // Ensure the caller's voting weight is greater than or equal to the threshold if (getVotes(msg.sender, block.timestamp - 1) < proposalThreshold()) revert BELOW_PROPOSAL_THRESHOLD(); } ... proposal.quorumVotes = uint32(quorum()); ... } else if (proposal.forVotes < proposal.againstVotes || proposal.forVotes < proposal.quorumVotes) { return ProposalState.Defeated;
https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L466-L477 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L122-L129 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L441-L442
None
One suggestion is to use <= in propose() and state() instead of <.
propose():
unchecked { // Ensure the caller's voting weight is greater than or equal to the threshold - if (getVotes(msg.sender, block.timestamp - 1) < proposalThreshold()) revert BELOW_PROPOSAL_THRESHOLD(); + if (getVotes(msg.sender, block.timestamp - 1) <= proposalThreshold()) revert BELOW_PROPOSAL_THRESHOLD(); }
state():
- } else if (proposal.forVotes < proposal.againstVotes || proposal.forVotes < proposal.quorumVotes) { + } else if (proposal.forVotes < proposal.againstVotes || proposal.forVotes <= proposal.quorumVotes) { // or proposal.forVotes <= proposal.againstVotes || proposal.forVotes < proposal.quorumVotes return ProposalState.Defeated;
#0 - GalloDaSballo
2022-09-20T21:29:31Z
π Selected for report: MEP
Also found by: 0xSky, CertoraInc, MiloTruck, PwnPatrol, R2, Tointer, V_B, __141345__, antonttc, azephiar, cccz, d3e4, datapunk, davidbrai, easy_peasy, hansfriese, minhtrng, neumo, pcarranzav, peritoflores, scaraven, teawaterwire, tonisives, unforgiven, wagmi, zkhorse, zzzitron
5.6134 USDC - $5.61
In the _addFounders function of a Token contract, the baseTokenId is limited to 99 by %100, but in the following calculation, the baseTokenId will not be the value after %100, causing the baseTokenId to exceed 99.
(baseTokenId += schedule) % 100;
If _addFounders() adds two Founders, founderPct is 50, 33, then baseTokenId will exceed 99 and will be 100,103,106 ....
Later, in the _isForFounder function, tokenid greater than 99 will not be vested in the founder.
function _isForFounder(uint256 _tokenId) private returns (bool) { // Get the base token id uint256 baseTokenId = _tokenId % 100;
https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L118-L119 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L177-L179
None
for (uint256 j; j < founderPct; ++j) { // Get the available token id baseTokenId = _getNextTokenId(baseTokenId); // Store the founder as the recipient tokenRecipient[baseTokenId] = newFounder; emit MintScheduled(baseTokenId, founderId, newFounder); // Update the base token id - (baseTokenId += schedule) % 100; + baseTokenId = (baseTokenId + schedule) % 100; }
#0 - GalloDaSballo
2022-09-20T20:49:33Z
104.7173 USDC - $104.72
In the propose() of the Governor contract, the user can create a proposal when his votes are equal to proposalThreshold().
unchecked { // Ensure the caller's voting weight is greater than or equal to the threshold if (getVotes(msg.sender, block.timestamp - 1) < proposalThreshold()) revert BELOW_PROPOSAL_THRESHOLD(); }
But in cancel(), the proposal can be cancelled because the user's votes are equal to proposalThreshold.
unchecked { // Ensure the caller is the proposer or the proposer's voting weight has dropped below the proposal threshold if (msg.sender != proposal.proposer && getVotes(proposal.proposer, block.timestamp - 1) > proposal.proposalThreshold) revert INVALID_CANCEL(); }
https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L126-L129 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L361-L365
None
Use <= in propose() instead of <.
propose():
unchecked { // Ensure the caller's voting weight is greater than or equal to the threshold - if (getVotes(msg.sender, block.timestamp - 1) < proposalThreshold()) revert BELOW_PROPOSAL_THRESHOLD(); + if (getVotes(msg.sender, block.timestamp - 1) <= proposalThreshold()) revert BELOW_PROPOSAL_THRESHOLD(); }
#0 - GalloDaSballo
2022-09-16T19:14:51Z
Great catch by the warden
I'm not convinced there is a risk worth Med, but I think the finding is valid
#1 - GalloDaSballo
2022-09-26T23:47:57Z
π Selected for report: azephiar
Also found by: R2, Solimander, __141345__, bin2chen, cccz, davidbrai, pauliax, rbserver
In proposalThreshold() and quorum() of the Governor contract, if the totalSupply of token is 0, the return value will be 0.
function proposalThreshold() public view returns (uint256) { unchecked { return (settings.token.totalSupply() * settings.proposalThresholdBps) / 10_000; } } /// @notice The current number of votes required to be in favor of a proposal in order to reach quorum function quorum() public view returns (uint256) { unchecked { return (settings.token.totalSupply() * settings.quorumThresholdBps) / 10_000; } }
In propose(), proposalThreshold() returns 0 which allows anyone to create a proposal, and quorum() returns 0 which allows the proposal to be executed without being votes.
unchecked { // Ensure the caller's voting weight is greater than or equal to the threshold if (getVotes(msg.sender, block.timestamp - 1) < proposalThreshold()) revert BELOW_PROPOSAL_THRESHOLD(); } ... proposal.quorumVotes = uint32(quorum()); ... } else if (proposal.forVotes < proposal.againstVotes || proposal.forVotes < proposal.quorumVotes) { return ProposalState.Defeated;
None
Consider forbidding the creation of proposals when the totalSupply of token is 0
#0 - GalloDaSballo
2022-09-20T13:18:25Z
Dup of #604
π Selected for report: Lambda
Also found by: 0x1337, 0x1f8b, 0x4non, 0x85102, 0xA5DF, 0xNazgul, 0xSmartContract, 0xbepresent, 0xc0ffEE, 8olidity, Aymen0909, B2, Bnke0x0, CRYP70, Captainkay, CertoraInc, Ch_301, Chom, ChristianKuri, CodingNameKiki, Deivitto, Diana, DimitarDimitrov, ElKu, EthLedger, Franfran, Funen, GimelSec, JansenC, Jeiwan, Jujic, Lead_Belly, MEP, MasterCookie, MiloTruck, Noah3o6, PPrieditis, PaludoX0, Picodes, PwnPatrol, R2, Randyyy, RaymondFam, Respx, ReyAdmirado, Rolezn, Samatak, Tointer, Tomo, V_B, Waze, _Adam, __141345__, a12jmx, ak1, asutorufos, azephiar, ballx, bharg4v, bin2chen, bobirichman, brgltd, bulej93, c3phas, cccz, ch0bu, cloudjunky, cryptonue, cryptostellar5, cryptphi, csanuragjain, d3e4, datapunk, davidbrai, delfin454000, dharma09, dic0de, dipp, djxploit, eierina, erictee, fatherOfBlocks, gogo, hansfriese, hyh, imare, indijanc, izhuer, jonatascm, ladboy233, leosathya, lucacez, lukris02, m9800, martin, minhtrng, ne0n, neumo, oyc_109, p_crypt0, pashov, pauliax, pcarranzav, pedr02b2, peritoflores, pfapostol, rbserver, ret2basic, robee, rvierdiiev, sach1r0, sahar, scaraven, sikorico, simon135, slowmoses, sorrynotsorry, tnevler, tonisives, volky, yixxas, zkhorse, zzzitron
60.7948 USDC - $60.79
In the *StorageV1 contract, the Settings structure variable is at the top of the storage, which causes a storage collision if the Settings structure needs to be extended later.
contract TokenStorageV1 is TokenTypesV1 { /// @notice The token settings Settings internal settings; /// @notice The vesting details of a founder /// @dev Founder id => Founder mapping(uint256 => Founder) internal founder; /// @notice The recipient of a token /// @dev ERC-721 token id => Founder mapping(uint256 => Founder) internal tokenRecipient; }
Also, a storage gap should be added to the *StorageV1 contract to allow new variables to be added when the contract is upgraded.
https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/auction/storage/AuctionStorageV1.sol#L10-L20 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/storage/GovernorStorageV1.sol#L9-L20 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/treasury/storage/TreasuryStorageV1.sol#L9-L16 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/storage/TokenStorageV1.sol#L9-L20
None
Consider moving the Settings structure to the end of the *StorageV1 contract and adding a storage gap at the end of the *StorageV1 contract.
uint256[50] private __gap;
#0 - GalloDaSballo
2022-09-16T19:22:31Z
I think the report has some validity but ultimately slot math has to be performed on each upgrade and no perfect solution exists
Even adding a gap can still cause issues if not handled properly
#1 - GalloDaSballo
2022-09-20T23:12:26Z
Dup of #358