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: 24/168
Findings: 7
Award: $876.33
π 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
getPastVotes() uses the binary search algorithm, which returns the result if it encounters the same timestamp, and since timestamps are not unique in the array The number of votes can be controlled by adding checkpoints to locate any one of the same timestamps.
example:
.. . .. . t1 0 t1 1 t1 0
user2:
.. . .. . t1 1 t1 0 t1 1
user3: .. user4: ..
2.when voting , before vote , increase the user's checkpoints to control "high", so that you can control "middle", you can freely select the votes = 1 record in checkpoints (user actually has no votes)
function getPastVotes(address _account, uint256 _timestamp) public view returns (uint256) { ... while (high > low) { middle = high - (high - low) / 2; //****control hight,then control middle****// cp = accountCheckpoints[middle]; if (cp.timestamp == _timestamp) { // Return the voting weight return cp.votes;
function _castVote( bytes32 _proposalId, address _voter, uint256 _support, string memory _reason ) internal returns (uint256) { .. .. weight = getVotes(_voter, proposal.timeCreated); //**control weight**// ...
Ensure that the timestamp of checkpoints is unique, _writeCheckpoint() overwrites the last one if it is the same timestamp
ReferenceοΌ
#0 - GalloDaSballo
2022-09-20T21:43:38Z
when call delegateBySig() , delegatee set to address(0), the delegatee is still itself, but user Checkpoints.votes will be reduced. it will cause the NFT of the user being delegate to be locked, can no set other delegatee or NFT transfer
Example:
function _moveDelegateVotes( address _from, address _to, uint256 _amount ) internal { unchecked { // If voting weight is being transferred: if (_from != _to && _amount > 0) { // If this isn't a token mint: if (_from != address(0)) { // Get the sender's number of checkpoints uint256 nCheckpoints = numCheckpoints[_from]++; // Used to store the sender's previous voting weight uint256 prevTotalVotes; // If this isn't the sender's first checkpoint: Get their previous voting weight if (nCheckpoints != 0) prevTotalVotes = checkpoints[_from][nCheckpoints - 1].votes; // Update their voting weight //**** (prevTotalVotes - _amount) will underflows ****// _writeCheckpoint(_from, nCheckpoints, prevTotalVotes, prevTotalVotes - _amount); }
delegateBySig() add check to !=address(0)
#0 - GalloDaSballo
2022-09-26T14:44:33Z
Dup of #203
#1 - GalloDaSballo
2022-09-26T14:44:50Z
POC is wrong but delegation to address(0) is proven to be dangerous
MetadataRenderer.sol addProperties() can add any number of properties, in token mint() will generate tokenAttributes based on properties, but tokenAttributes = uint16[16] If there are more than 15 properties it will cause an overflow and token will never to mint()
function addProperties( string[] calldata _names, ItemParam[] calldata _items, IPFSGroup calldata _ipfsGroup ) external onlyOwner { ... unchecked { // For each new property: for (uint256 i = 0; i < numNewProperties; ++i) { // Append storage space properties.push(); //***** any number ****///
function onMinted(uint256 _tokenId) external returns (bool) { uint256 numProperties = properties.length; uint16[16] storage tokenAttributes = attributes[_tokenId]; //*** max 16 ***// unchecked { // For each property: for (uint256 i = 0; i < numProperties; ++i) { // Get the number of items to choose from uint256 numItems = properties[i].items.length; // Use the token's seed to select an item tokenAttributes[i + 1] = uint16(seed % numItems); //**** when properties.lenght > 15 will raise "Index out of bounds" ****// // Adjust the randomness seed >>= 16; } } }
function _mint(address _to, uint256 _tokenId) internal override { ... //*** mint will always fail ***// if (!settings.metadataRenderer.onMinted(_tokenId)) revert NO_METADATA_GENERATED(); }
addProperties() check length <= 15
function addProperties( string[] calldata _names, ItemParam[] calldata _items, IPFSGroup calldata _ipfsGroup ) external onlyOwner { ... +++ require(properties.length<=15, "too much properties") }
#0 - GalloDaSballo
2022-09-20T18:43:34Z
π Selected for report: azephiar
Also found by: 0x52, 0xSmartContract, Ch_301, Tointer, __141345__, bin2chen, indijanc
token.sol totalSupply does not reduce the number when burn, resulting in inaccurate totalSupply and incorrect vote approval rate.
when mint() totalSupply++, but when burn not totalSupply-- , resulting in incorrect percentage of votes
function mint() external nonReentrant returns (uint256 tokenId) { ... ... unchecked { do { // Get the next token to mint tokenId = settings.totalSupply++; //****** totalSupply++ ******// // Lookup whether the token is for a founder, and mint accordingly if so } while (_isForFounder(tokenId)); } // Mint the next available token to the auction house for bidding _mint(minter, tokenId); }
function burn(uint256 _tokenId) external { // Ensure the caller is the auction house if (msg.sender != settings.auction) revert ONLY_AUCTION(); //***** without settings.totalSupply-- *****// // Burn the token _burn(_tokenId); }
causes proposalThreshold() and quorum() to be inaccurate
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; } }
function burn(uint256 _tokenId) external { // Ensure the caller is the auction house if (msg.sender != settings.auction) revert ONLY_AUCTION(); // Burn the token +++ settings.totalSupply--; _burn(_tokenId); }
function mint() external nonReentrant returns (uint256 tokenId) { // Cache the auction address address minter = settings.auction; // Ensure the caller is the auction if (msg.sender != minter) revert ONLY_AUCTION(); // Cannot realistically overflow unchecked { do { // Get the next token to mint --- tokenId = settings.totalSupply++; +++ tokenId = settings.currentTokenId++; +++ settings.totalSupply++; // Lookup whether the token is for a founder, and mint accordingly if so } while (_isForFounder(tokenId)); } // Mint the next available token to the auction house for bidding _mint(minter, tokenId); }
#0 - GalloDaSballo
2022-09-25T22:11:25Z
The system is using totalSupply
as a way to determine quorum.
Because the system is adapted to not look at totalSupply
at a certain block, I assume the Sponsor removed the code to reduce totalSupply.
However this can cause issues, such as the inability to reach quorum, or quorum being too high compared to circulatingSupply
.
I believe that mitigation for this issue is not straightforward, as to allow dynamic totalSupply
changes, checkpoints for total supply should be brought back as well.
From checking NounsDAO code, they handle this by using a list for the totalSupply
and popping an entry out of it:
https://etherscan.io/address/0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03#code#F11#L183
Meaning that totalSupply
does change in what is the reference implementation for this codebase.
Given the above, I believe the finding to be valid and of Medium Severity
#1 - GalloDaSballo
2022-09-25T22:14:01Z
Dup of #423
π Selected for report: azephiar
Also found by: R2, Solimander, __141345__, bin2chen, cccz, davidbrai, pauliax, rbserver
129.2807 USDC - $129.28
When the initial deployment, totalSupply = 0, if the proposal is submitted at this time maliciously, it can be submitted directly, while proposal.quorumVotes = 0 Since quorumVotes = 0 may be able to enter the execution queue without voting
when totalSupply = 0
function propose( address[] memory _targets, uint256[] memory _values, bytes[] memory _calldatas, string memory _description ) external returns (bytes32) { unchecked { //**** totalSupply = 0 pass without votes ****/// if (getVotes(msg.sender, block.timestamp - 1) < proposalThreshold()) revert BELOW_PROPOSAL_THRESHOLD(); } proposal.quorumVotes = uint32(quorum()); //**** quorumVotes = 0 ****//
propose() add
if (totalSupply < MIN_SUPPLY) revert()
#0 - GalloDaSballo
2022-09-16T19:00:26Z
The warden is showing how, because totalSupply == 0 initially, any proposal can be submitted and be made to pass
An example of a malicious proposal would be:
While some measures can be taken, for example a re-deployment, this vector seems to be always available on first setup
The finding looks valid to me
#1 - GalloDaSballo
2022-09-20T13:18:36Z
Dup of #604
π Selected for report: CertoraInc
Also found by: 0xSky, PwnPatrol, Tomo, V_B, antonttc, bin2chen, pcarranzav, peritoflores, rbserver, scaraven, wagmi, zzzitron
49.075 USDC - $49.08
Judge has assessed an item in Issue #409 as Medium risk. The relevant finding follows:
#0 - GalloDaSballo
2022-09-27T14:46:43Z
Token.sol _addFounders() Need to check ownershipPct <100,don't check uint8(founderPct) after conversion to compare
function _addFounders(IManager.FounderParams[] calldata _founders) internal { // Cache the number of founders uint256 numFounders = _founders.length; // Used to store the total percent ownership among the founders uint256 totalOwnership; unchecked { // For each founder: for (uint256 i; i < numFounders; ++i) { // Cache the percent ownership uint256 founderPct = _founders[i].ownershipPct; // Continue if no ownership is specified if (founderPct == 0) continue; // Update the total ownership and ensure it's valid --- if ((totalOwnership += uint8(founderPct)) > 100) revert INVALID_FOUNDER_OWNERSHIP(); +++ if ((totalOwnership += founderPct) > 100) revert INVALID_FOUNDER_OWNERSHIP();
#1 - GalloDaSballo
2022-09-27T14:47:04Z
Dup of #303
π 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.7742 USDC - $60.77
1.Auction.sol unpause() Judgment whether the first auction by tokenId = 0, but there tokenId can be equal to 0, it is recommended to add judgment startTime == 0
function unpause() external onlyOwner { _unpause(); // If this is the first auction: --- if (auction.tokenId == 0) { +++ if (auction.tokenId == 0 && auction.startTime == 0) { // Transfer ownership of the contract to the DAO transferOwnership(settings.treasury); // Start the first auction _createAuction(); }
function _addFounders(IManager.FounderParams[] calldata _founders) internal { // Cache the number of founders uint256 numFounders = _founders.length; // Used to store the total percent ownership among the founders uint256 totalOwnership; unchecked { // For each founder: for (uint256 i; i < numFounders; ++i) { // Cache the percent ownership uint256 founderPct = _founders[i].ownershipPct; // Continue if no ownership is specified if (founderPct == 0) continue; // Update the total ownership and ensure it's valid --- if ((totalOwnership += uint8(founderPct)) > 100) revert INVALID_FOUNDER_OWNERSHIP(); +++ if ((totalOwnership += founderPct) > 100) revert INVALID_FOUNDER_OWNERSHIP();
#0 - GalloDaSballo
2022-09-26T21:05:16Z
TODO -> Med
TODO -> MED
#1 - GalloDaSballo
2022-09-27T14:20:45Z
This could have been a med.
But without any extra info I 'll leave it as Low
L
1L