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: 29/168
Findings: 6
Award: $768.01
π Selected for report: 1
π Solo Findings: 0
π 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
baseTokenId
update is wrong, the id generating mechanism might not work as expected. Some NFTs are reserved for founders, but now bashTokenId
could be above 100, the reserved id
will not be minted for them, equivalent to loss those NFTs.
According to the following, % 100
would not take effect
src/token/Token.sol
function _addFounders() internal { 118: (baseTokenId += schedule) % 100;
The check for the baseTokenId
above 100 will never be reached, since in _isForFounder()
the % 100
will limit the id
below 100.
function _isForFounder(uint256 _tokenId) private returns (bool) { // Get the base token id uint256 baseTokenId = _tokenId % 100; // If there is no scheduled recipient: if (tokenRecipient[baseTokenId].wallet == address(0)) { return false; // Else if the founder is still vesting: } else if (block.timestamp < tokenRecipient[baseTokenId].vestExpiry) { // Mint the token to the founder _mint(tokenRecipient[baseTokenId].wallet, _tokenId); return true; // Else the founder has finished vesting: } else { // Remove them from future lookups delete tokenRecipient[baseTokenId]; return false; } }
Effectively those founders will lose those reserved NFTs.
Manual analysis.
change to:
baseTokenId = (baseTokenId + schedule) % 100;
#0 - GalloDaSballo
2022-09-20T13:00:18Z
π Selected for report: azephiar
Also found by: 0x52, 0xSmartContract, Ch_301, Tointer, __141345__, bin2chen, indijanc
161.6008 USDC - $161.60
totalSupply
can not reflect the true number of tokens minted.
The governance function quorum()
will refer to totalSupply
, inaccurate value will influence the governance function.
If no bid for an auction, the corresponding tokenId
will be burned.
src/auction/Auction.sol
194-198: // Else no bid was placed: } else { // Burn the token token.burn(_auction.tokenId); }
But totalSupply
is not decreased here.
Manual analysis.
totalSupply
when NFT is burned.#0 - GalloDaSballo
2022-09-20T13:34:02Z
This report is borderline empty, I recommend adding more context as without seeing the other hundreds this would have meant very little to me.
Bulking with the no change of total supply on burn reports
#1 - GalloDaSballo
2022-09-20T14:44:24Z
Dup of #405
π Selected for report: __141345__
Also found by: pauliax, rbserver, rvierdiiev, sorrynotsorry
354.6794 USDC - $354.68
The auction parameters can be changed anytime, even during ongoing auctions, and take effect immediately. Users may need time to react to the changes. The impacts maybe followings:
setReservePrice()
and setMinimumBidIncrement()
setDuration()
and setTimeBuffer()
, with different time parameters, bidders will use different strategysrc/auction/Auction.sol
function setDuration(uint256 _duration) external onlyOwner { settings.duration = SafeCast.toUint40(_duration); emit DurationUpdated(_duration); } function setReservePrice(uint256 _reservePrice) external onlyOwner { settings.reservePrice = _reservePrice; emit ReservePriceUpdated(_reservePrice); } function setTimeBuffer(uint256 _timeBuffer) external onlyOwner { settings.timeBuffer = SafeCast.toUint40(_timeBuffer); emit TimeBufferUpdated(_timeBuffer); } function setMinimumBidIncrement(uint256 _percentage) external onlyOwner { settings.minBidIncrement = SafeCast.toUint8(_percentage); emit MinBidIncrementPercentageUpdated(_percentage); }``` ## Tools Used Manual analysis. ## Recommended Mitigation Steps - do not apply changed parameters on ongoing auctions - add a timelock for the changes
#0 - GalloDaSballo
2022-09-26T12:15:20Z
The Warden has shown how settings can be changed mid-auction, this can create undefined behaviour (insta-settlement, change of price).
While max loss is one token or the inability to settle the auction, because this could create a "social accident", I believe that Medium Severity is appropriate.
Changes should either be cached in the Auction Parameters, or should be applied to the next auction to avoid any unexpected surprise
π Selected for report: azephiar
Also found by: R2, Solimander, __141345__, bin2chen, cccz, davidbrai, pauliax, rbserver
When NFT totalSupply
is low, the quorum()
might be more sensitive to rounding errors. Sometimes the error could be large, or even return 0 result. The vote results based on these quorum()
will deviate from the intention of governance.
Any new bidder can call createBid()
to place a bid:
// src/governance/governor/Governor.sol function quorum() public view returns (uint256) { unchecked { return (settings.token.totalSupply() * settings.quorumThresholdBps) / 10_000; } }
In test file, quorumThresholdBps
is set to 2_000, then:
totalSupply
of NFT is less than 5, quorum()
will return 0.totalSupply
of NFT is 9, quorum()
will return 1.Seems the error is too big for small totalSupply
and quorumThresholdBps
.
Manual analysis.
totalSupply
, do additional checks for quorum()
results, maybe round up.quorumThresholdBps
in initialize()
and updateProposalThresholdBps()
.#0 - GalloDaSballo
2022-09-20T13:35:08Z
π 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
61.6083 USDC - $61.61
Each event should use three indexed fields if there are three or more fields.
src/auction/IAuction.sol 22: event AuctionBid(uint256 tokenId, address bidder, uint256 amount, bool extended, uint256 endTime); 28: event AuctionSettled(uint256 tokenId, address winner, uint256 amount); 34: event AuctionCreated(uint256 tokenId, uint256 startTime, uint256 endTime); src/governance/governor/IGovernor.sol 18-26: event ProposalCreated( bytes32 proposalId, address[] targets, uint256[] values, bytes[] calldatas, string description, bytes32 descriptionHash, Proposal proposal ); 42: event VoteCast(address voter, bytes32 proposalId, uint256 support, uint256 weight, string reason); src/governance/treasury/ITreasury.sol 22: event TransactionExecuted(bytes32 proposalId, address[] targets, uint256[] values, bytes[] payloads); src/lib/interfaces/IERC721Votes.sol 19: event DelegateVotesChanged(address indexed delegate, uint256 prevTotalVotes, uint256 newTotalVotes); src/manager/IManager.sol 21: event DAODeployed(address token, address metadata, address auction, address treasury, address governor); src/token/IToken.sol 21: event MintScheduled(uint256 baseTokenId, uint256 founderId, Founder founder);
The pragma declared across the contract range from ^0.8.4- ^0.8.15. Locking the pragma (for e.g. by not using ^ in pragma solidity 0.8.15) ensures that contracts do not accidentally get deployed using an older compiler version with unfixed bugs. (see here)
string.concat()
instead of abi.encodePacked()
src/token/metadata/MetadataRenderer.sol 208-213: queryString = abi.encodePacked( "?contractAddress=", Strings.toHexString(uint256(uint160(address(this))), 20), "&tokenId=", Strings.toString(_tokenId) ); 243-244: aryAttributes = abi.encodePacked(aryAttributes, '"', property.name, '": "', item.name, '"', isLast ? "" : ","); queryString = abi.encodePacked(queryString, "&images=", _getItemImage(item, property.name)); 258-260: string( abi.encodePacked(ipfsData[_item.referenceSlot].baseUri, _propertyName, "/", _item.name, ipfsData[_item.referenceSlot].extension) ) 272-280: abi.encodePacked( '{"name": "', settings.name, '", "description": "', settings.description, '", "image": "', settings.contractImage, '"}' ) 290-303: abi.encodePacked( '{"name": "', settings.name, " #", LibUintToString.toString(_tokenId), '", "description": "', settings.description, '", "image": "', settings.rendererBase, queryString, '", "properties": {', aryAttributes, "}}" ) 309: return string(abi.encodePacked("data:application/json;base64,", Base64.encode(_jsonBlob)));
Refer to Solidity Documents.
#0 - GalloDaSballo
2022-09-26T20:52:10Z
2 NC
π Selected for report: pfapostol
Also found by: 0x1f8b, 0x4non, 0x5rings, 0xA5DF, 0xSmartContract, 0xc0ffEE, 0xkatana, Aymen0909, Bnke0x0, CertoraInc, Chandr, CodingNameKiki, Cr4ckM3, Deivitto, DimSon, Franfran, JAGADESH, JC, Jeiwan, Lambda, LeoS, Matin, Metatron, Migue, MiloTruck, PPrieditis, PaludoX0, R2, RaymondFam, Respx, ReyAdmirado, Rolezn, Saintcode_, Samatak, SnowMan, StevenL, Tointer, TomJ, Tomo, WatchDogs, Waze, _Adam, __141345__, ajtra, asutorufos, ballx, brgltd, bulej93, c3phas, ch0bu, dharma09, djxploit, durianSausage, easy_peasy, fatherOfBlocks, gianganhnguyen, gogo, imare, leosathya, lucacez, martin, oyc_109, pauliax, peiw, prasantgupta52, ret2basic, rfa, robee, sikorico, simon135, tofunmi, volky, wagmi, zishansami
55.228 USDC - $55.23
If a variable is not set/initialized, it is assumed to have the default value (0 for uint, false for bool, address(0) for addressβ¦). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
src/governance/treasury/Treasury.sol 162: for (uint256 i = 0; i < numTargets; ++i) { src/token/metadata/MetadataRenderer.sol 119: for (uint256 i = 0; i < numNewProperties; ++i) { 133: for (uint256 i = 0; i < numNewItems; ++i) { 189: for (uint256 i = 0; i < numProperties; ++i) { 229: for (uint256 i = 0; i < numProperties; ++i) {
The demo of the loop gas comparison can be seen here.
X = X + Y / X = X - Y
IS CHEAPER THAN X += Y / X -= Y
The demo of the gas comparison can be seen here.
Consider use X = X + Y / X = X - Y
to save gas.
src/governance/governor/Governor.sol 280: proposal.againstVotes += uint32(weight); 285: proposal.forVotes += uint32(weight); 290: proposal.abstainVotes += uint32(weight); src/token/Token.sol 88: if ((totalOwnership += uint8(founderPct)) > 100) revert INVALID_FOUNDER_OWNERSHIP(); 118: (baseTokenId += schedule) % 100; src/token/metadata/MetadataRenderer.sol 140: _propertyId += numStoredProperties; src/token/metadata/MetadataRenderer.sol 197: seed >>= 16;
keccak256()
,` SHOULD USE IMMUTABLE RATHER THAN CONSTANTConstant expressions are left as expressions, not constants.
When a constant declared as:
src/lib/token/ERC721Votes.sol 21: bytes32 internal constant DELEGATION_TYPEHASH = keccak256("Delegation(address from,address to,uint256 nonce,uint256 deadline)"); src/lib/utils/EIP712.sol 19: bytes32 internal constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
It is expected that the value should be converted into a constant value at compile time. But the expression is re-calculated each time the constant is referenced.
consequences:
Suggestion: Change these expressions from constant to immutable and implement the calculation in the constructor or hardcode these values in the constants and add a comment to say how the value was calculated.
reference: https://github.com/ethereum/solidity/issues/9232
Use bytes32 instead of string to save gas whenever possible. String is a dynamic data structure and therefore is more gas consuming then bytes32.
Bytes32 can be used instead of string in the following:
src/token/metadata/types/MetadataRendererTypesV1.sol struct ItemParam { uint256 propertyId; string name; bool isNewProperty; } struct IPFSGroup { string baseUri; string extension; } struct Item { uint16 referenceSlot; string name; } struct Property { string name; Item[] items; } struct Settings { address token; address treasury; string name; string description; string contractImage; string rendererBase; }
When arguments are read-only on external functions, the data location can be calldata.
The demo of the gas comparison can be seen here.
src/governance/governor/Governor.sol 116-121: function propose( address[] memory _targets, uint256[] memory _values, bytes[] memory _calldatas, string memory _description ) external returns (bytes32) { 192-196: function castVoteWithReason( bytes32 _proposalId, uint256 _support, string memory _reason ) external returns (uint256) {
External call cost is less expensive than of public functions.
The difference is because in public functions, Solidity immediately copies array arguments to memory, while external functions can read directly from calldata. Memory allocation is expensive, whereas reading from calldata is cheap.
src/token/metadata/MetadataRenderer.sol 206: function getAttributes(uint256 _tokenId) public view returns (bytes memory aryAttributes, bytes memory queryString) {
Saves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot.
src/governance/governor/storage/GovernorStorageV1.sol 14-19: /// @dev Proposal Id => Proposal mapping(bytes32 => Proposal) internal proposals; /// @dev Proposal Id => User => Has Voted mapping(bytes32 => mapping(address => bool)) internal hasVoted; src/lib/token/ERC721.sol 24-34: /// @dev ERC-721 token id => Owner mapping(uint256 => address) internal owners; /// @dev ERC-721 token id => Manager mapping(uint256 => address) internal tokenApprovals; 28-38: /// @dev Owner => Balance mapping(address => uint256) internal balances; /// @dev Owner => Operator => Approved mapping(address => mapping(address => bool)) internal operatorApprovals; src/lib/token/ERC721Votes.sol 28-37: /// @notice Account => Delegate mapping(address => address) internal delegation; /// @dev Account => Num Checkpoints mapping(address => uint256) internal numCheckpoints; /// @dev Account => Checkpoint Id => Checkpoint mapping(address => mapping(uint256 => Checkpoint)) internal checkpoints;
`` seem to be frequently called functions.
src/lib/token/ERC721Votes.sol
middle = high - (high - low) / 2;
Suggestion: Change the above to
middle = high - (high - low) >> 1;
The demo of the gas comparison can be seen here.
#0 - GalloDaSballo
2022-09-26T14:51:59Z
About 500 gas, mostly from the calldata / memory
#1 - GalloDaSballo
2022-09-26T14:52:09Z
500