Platform: Code4rena
Start Date: 12/09/2022
Pot Size: $75,000 USDC
Total HM: 19
Participants: 110
Period: 7 days
Judge: HardlyDifficult
Total Solo HM: 9
Id: 160
League: ETH
Rank: 40/110
Findings: 2
Award: $117.98
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Lambda
Also found by: 0x1f8b, 0x4non, 0x52, 0x5rings, 0xDanielC, 0xNazgul, 0xSmartContract, 0xbepresent, Anth3m, Aymen0909, B2, CRYP70, CertoraInc, Ch_301, Chom, ChristianKuri, CodingNameKiki, Deivitto, Funen, JC, JansenC, Jeiwan, KIntern_NA, MasterCookie, MiloTruck, Olivierdem, PaludoX0, R2, RaymondFam, ReyAdmirado, StevenL, The_GUILD, Tomo, Trust, V_B, __141345__, asutorufos, ayeslick, bin2chen, brgltd, bulej93, c3phas, cccz, ch0bu, cryptphi, csanuragjain, d3e4, delfin454000, djxploit, erictee, fatherOfBlocks, gogo, hansfriese, indijanc, ladboy233, leosathya, lukris02, malinariy, martin, pedr02b2, pfapostol, rvierdiiev, slowmoses, smiling_heretic, tnevler, wagmi
82.3548 USDC - $82.35
Each event should use three indexed fields if there are three or more fields.
crowdfund/BuyCrowdfundBase.sol 52: event Won(Party party, IERC721 token, uint256 tokenId, uint256 settledPrice); crowdfund/Crowdfund.sol 82: event Burned(address contributor, uint256 ethUsed, uint256 ethOwed, uint256 votingPower); 83: event Contributed(address contributor, uint256 amount, address delegate, uint256 previousTotalContributions); distribution/ITokenDistributor.sol 37-43: event DistributionFeeClaimed( ITokenDistributorParty indexed party, address indexed feeRecipient, TokenType tokenType, address token, uint256 amount ); party/PartyGovernance.sol 151-162: event Proposed( uint256 proposalId, address proposer, Proposal proposal ); event ProposalAccepted( uint256 proposalId, address voter, uint256 weight ); event PartyInitialized(GovernanceOpts opts, IERC721[] preciousTokens, uint256[] preciousTokenIds); 167: event DistributionCreated(ITokenDistributor.TokenType tokenType, address token, uint256 tokenId); proposals/ArbitraryCallsProposal.sol 35: event ArbitraryCallExecuted(uint256 proposalId, uint256 idx, uint256 count); proposals/FractionalizeProposal.sol 22-28: event FractionalV1VaultCreated( IERC721 indexed token, uint256 indexed tokenId, uint256 vaultId, IERC20 vault, uint256 listPrice ); proposals/ListOnOpenseaProposal.sol 63-97: event OpenseaOrderListed( IOpenseaExchange.OrderParameters orderParams, bytes32 orderHash, IERC721 token, uint256 tokenId, uint256 listPrice, uint256 expiry ); event OpenseaOrderSold( bytes32 orderHash, IERC721 token, uint256 tokenId, uint256 listPrice ); event OpenseaOrderExpired( bytes32 orderHash, IERC721 token, uint256 tokenId, uint256 expiry ); // Coordinated event w/OS team to track on-chain orders. event OrderValidated( bytes32 orderHash, address indexed offerer, address indexed zone, IOpenseaExchange.OfferItem[] offer, IOpenseaExchange.ConsiderationItem[] consideration, IOpenseaExchange.OrderType orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 counter ); proposals/ListOnZoraProposal.sol 45-52: event ZoraAuctionCreated( uint256 auctionId, IERC721 token, uint256 tokenId, uint256 startingPrice, uint40 duration, uint40 timeoutTime );
The pragma declared across the solution is ^0.8. Locking the pragma (for e.g. by not using ^ in pragma solidity 0.8) ensures that contracts do not accidentally get deployed using an older compiler version with unfixed bugs. (see here)
Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require()
strings.
Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value.
🌟 Selected for report: CertoraInc
Also found by: 0x1f8b, 0x4non, 0x5rings, 0x85102, 0xNazgul, 0xSmartContract, 0xkatana, Amithuddar, Aymen0909, B2, Bnke0x0, CRYP70, Chom, ChristianKuri, CodingNameKiki, Deivitto, Diraco, Fitraldys, Funen, IgnacioB, JAGADESH, JC, Lambda, LeoS, Matin, Metatron, MiloTruck, Noah3o6, Ocean_Sky, Olivierdem, PaludoX0, RaymondFam, ReyAdmirado, Rohan16, Rolezn, Saintcode_, Sm4rty, SnowMan, StevenL, Tomio, Tomo, V_B, Waze, __141345__, ajtra, asutorufos, aysha, brgltd, bulej93, c3phas, ch0bu, d3e4, delfin454000, dharma09, djxploit, erictee, fatherOfBlocks, francoHacker, gianganhnguyen, gogo, got_targ, ignacio, jag, karanctf, ladboy233, leosathya, lukris02, m_Rassska, malinariy, martin, natzuu, pashov, peanuts, peiw, pfapostol, prasantgupta52, robee, simon135, slowmoses, sryysryy, tnevler
35.6296 USDC - $35.63
The for loop length can be cached to memory before the loop, even for memory variables. The demo of the loop gas comparison can be seen here.
crowdfund/CollectionBuyCrowdfund.sol 62: for (uint256 i; i < hosts.length; i++) { crowdfund/Crowdfund.sol 180: for (uint256 i = 0; i < contributors.length; ++i) { 300: for (uint256 i = 0; i < preciousTokens.length; ++i) { distribution/TokenDistributor.sol 230: for (uint256 i = 0; i < infos.length; ++i) { 239: for (uint256 i = 0; i < infos.length; ++i) { party/PartyGovernance.sol 306: for (uint256 i=0; i < opts.hosts.length; ++i) { proposals/ArbitraryCallsProposal.sol 52: for (uint256 i = 0; i < hadPreciouses.length; ++i) { 61: for (uint256 i = 0; i < calls.length; ++i) { 78: for (uint256 i = 0; i < hadPreciouses.length; ++i) { proposals/LibProposal.sol 14: for (uint256 i = 0; i < preciousTokens.length; ++i) { 32: for (uint256 i = 0; i < preciousTokens.length; ++i) { proposals/ListOnOpenseaProposal.sol 291: for (uint256 i = 0; i < fees.length; ++i) {
Suggestion: Cache the length before the loop.
uint length = names.length;
unchecked{++i}
COSTS LESS GAS COMPARED TO i++
OR i += 1
Use ++i
 instead of i++
 to increment the value of an uint variable, and for guaranteed non-overflow/underflow, it can be unchecked.
And the optimizer need to be turned on.
The demo of the loop gas comparison can be seen here.
crowdfund/CollectionBuyCrowdfund.sol 62: for (uint256 i; i < hosts.length; i++) { crowdfund/Crowdfund.sol 180: for (uint256 i = 0; i < contributors.length; ++i) { 242: for (uint256 i = 0; i < numContributions; ++i) { 300: for (uint256 i = 0; i < preciousTokens.length; ++i) { 348: for (uint256 i = 0; i < numContributions; ++i) { distribution/TokenDistributor.sol 230: for (uint256 i = 0; i < infos.length; ++i) { 239: for (uint256 i = 0; i < infos.length; ++i) { party/PartyGovernance.sol 306: for (uint256 i=0; i < opts.hosts.length; ++i) { proposals/ArbitraryCallsProposal.sol 52: for (uint256 i = 0; i < hadPreciouses.length; ++i) { 61: for (uint256 i = 0; i < calls.length; ++i) { 78: for (uint256 i = 0; i < hadPreciouses.length; ++i) { proposals/LibProposal.sol 14: for (uint256 i = 0; i < preciousTokens.length; ++i) { 32: for (uint256 i = 0; i < preciousTokens.length; ++i) { proposals/ListOnOpenseaProposal.sol 291: for (uint256 i = 0; i < fees.length; ++i) {
Suggestion: For readability, uncheck the whole for loop is the same.
unchecked{ for (uint256 i; i < length; ++i) { } }
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.
crowdfund/Crowdfund.sol 180: for (uint256 i = 0; i < contributors.length; ++i) { 242: for (uint256 i = 0; i < numContributions; ++i) { 300: for (uint256 i = 0; i < preciousTokens.length; ++i) { 348: for (uint256 i = 0; i < numContributions; ++i) { distribution/TokenDistributor.sol 230: for (uint256 i = 0; i < infos.length; ++i) { 239: for (uint256 i = 0; i < infos.length; ++i) { party/PartyGovernance.sol 306: for (uint256 i=0; i < opts.hosts.length; ++i) { proposals/ArbitraryCallsProposal.sol 52: for (uint256 i = 0; i < hadPreciouses.length; ++i) { 61: for (uint256 i = 0; i < calls.length; ++i) { 78: for (uint256 i = 0; i < hadPreciouses.length; ++i) { proposals/LibProposal.sol 14: for (uint256 i = 0; i < preciousTokens.length; ++i) { 32: for (uint256 i = 0; i < preciousTokens.length; ++i) { proposals/ListOnOpenseaProposal.sol 291: for (uint256 i = 0; i < fees.length; ++i) {
Suggestion: For readability, uncheck the whole for loop is the same.
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.
crowdfund/Crowdfund.sol 243: ethContributed += contributions[i].amount; 352: ethOwed += c.amount; 355: ethUsed += c.amount; 359: ethUsed += partialEthUsed; 374: votingPower += (splitBps_ * totalEthUsed + (1e4 - 1)) / 1e4; // round up 411: totalContributions += amount; 427: lastContribution.amount += amount; party/PartyGovernance.sol 595: values.votes += votingPower; 959: newDelegateDelegatedVotingPower -= oldSnap.intrinsicVotingPower; distribution/TokenDistributor.sol 381: _storedBalances[balanceId] -= amount; proposals/ArbitraryCallsProposal.sol 72: ethAvailable -= calls[i].value;
!= 0
uses less gas than > 0
!= 0
costs less gas compared to > 0
for unsigned integers with the optimizer enabled (6 gas).
Here is the code as a demo.
Opcode discussion.
crowdfund/Crowdfund.sol 471: if (votingPower > 0) {
Suggestion:
> 0
with != 0
.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.
distribution/TokenDistributor.sol 63-71: /// @notice Last distribution ID for a party. mapping(ITokenDistributorParty => uint256) public lastDistributionIdPerParty; // tokenDistributorParty => distributionId => DistributionState mapping(ITokenDistributorParty => mapping(uint256 => DistributionState)) private _distributionStates; globals/Globals.sol 9-12: // key -> word value mapping(uint256 => bytes32) private _wordValues; // key -> word value -> isIncluded mapping(uint256 => mapping(bytes32 => bool)) private _includedWordValues; party/PartyGovernance.sol 207-215: mapping(address => bool) public isHost; /// @notice The last person a voter delegated its voting power to. mapping(address => address) public delegationsByVoter; // Constant governance parameters, fixed from the inception of this party. GovernanceValues private _governanceValues; // Snapshots of voting power per user, each sorted by increasing time. mapping(address => VotingPowerSnapshot[]) private _votingPowerSnapshotsByVoter;
keccak256()
,` SHOULD USE IMMUTABLE RATHER THAN CONSTANTConstant expressions are left as expressions, not constants.
When a constant declared as:
proposals/ProposalExecutionEngine.sol 80: uint256 private constant _STORAGE_SLOT = uint256(keccak256('ProposalExecutionEngine.Storage')); proposals/ProposalStorage.sol 19: uint256 private constant SHARED_STORAGE_SLOT = uint256(keccak256("ProposalStorage.SharedProposalStorage"));
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
Reference: Layout of State Variables in Storage.
An address occupies 20 bytes, another 12 bytes can be packed together into the same slot. In struct AuctionCrowdfundOptions
, uint40 duration
and bytes12 gateKeeperId
can be placed next to an address variable. Originally, the struct IGateKeeper
and FixedGovernanceOpts
each occupy 1 slot, the other uint
and address
occupy total 5 slots. After re-arrangement, uint
and address
will occupy total 3 slots, 2 slots storage in total can be saved.
// How long this crowdfund has to bid on the NFT, in seconds. uint40 duration; // Maximum bid allowed. uint96 maximumBid; // An address that receives a portion of the final voting power // when the party transitions into governance. address payable splitRecipient; // What percentage (in bps) of the final total voting power `splitRecipient` // receives. uint16 splitBps; // If ETH is attached during deployment, it will be interpreted // as a contribution. This is who gets credit for that contribution. address initialContributor; // If there is an initial contribution, this is who they will delegate their // voting power to when the crowdfund transitions to governance. address initialDelegate; // The gatekeeper contract to use (if non-null) to restrict who can // contribute to this crowdfund. IGateKeeper gateKeeper; // The gate ID within the gateKeeper contract to use. bytes12 gateKeeperId;
can be rearranged as (see the tag with // ~rearrange):
crowdfund/AuctionCrowdfund.sol // slot 1 uint96 + address (12 + 20 = 32) bytes // Maximum bid allowed. uint96 maximumBid; // An address that receives a portion of the final voting power // when the party transitions into governance. address payable splitRecipient; // slot 2 uint40 + uint16 + address (5 + 2 + 20 = 27) bytes // ~rearrange // How long this crowdfund has to bid on the NFT, in seconds. uint40 duration; // What percentage (in bps) of the final total voting power `splitRecipient` // receives. uint16 splitBps; // If ETH is attached during deployment, it will be interpreted // as a contribution. This is who gets credit for that contribution. address initialContributor; // slot 3 bytes12 + address (12 + 20 = 32) bytes // ~rearrange // The gate ID within the gateKeeper contract to use. bytes12 gateKeeperId; // If there is an initial contribution, this is who they will delegate their // voting power to when the crowdfund transitions to governance. address initialDelegate; // The gatekeeper contract to use (if non-null) to restrict who can // contribute to this crowdfund. IGateKeeper gateKeeper;
uint96 public totalContributions
can be moved before uint16 public splitBps
, 1 slot storage can be saved.
/// @notice The total (recorded) ETH contributed to this crowdfund. uint96 public totalContributions; /// @notice The gatekeeper contract to use (if non-null) to restrict who can /// contribute to the party. IGateKeeper public gateKeeper; /// @notice The ID of the gatekeeper strategy to use. bytes12 public gateKeeperId; /// @notice Who will receive a reserved portion of governance power when /// the governance party is created. address payable public splitRecipient; /// @notice How much governance power to reserve for `splitRecipient`, /// in bps, where 10,000 = 100%. uint16 public splitBps; // Whether the share for split recipient has been claimed through `burn()`. bool private _splitRecipientHasBurned;
can be rearranged as
/// @notice The gatekeeper contract to use (if non-null) to restrict who can /// contribute to the party. IGateKeeper public gateKeeper; /// @notice The ID of the gatekeeper strategy to use. bytes12 public gateKeeperId; /// @notice Who will receive a reserved portion of governance power when /// the governance party is created. address payable public splitRecipient; // ~rearrange /// @notice The total (recorded) ETH contributed to this crowdfund. uint96 public totalContributions; /// @notice How much governance power to reserve for `splitRecipient`, /// in bps, where 10,000 = 100%. uint16 public splitBps; // Whether the share for split recipient has been claimed through `burn()`. bool private _splitRecipientHasBurned;
bool approved
can be moved before uint8 curatorFeePercentage
, 1 slot storage can be saved.
// Whether or not the auction curator has approved the auction to start bool approved; // The current highest bid amount uint256 amount; // The length of time to run the auction for, after the first bid was made uint256 duration; // The time of the first bid uint256 firstBidTime; // The minimum price of the first bid uint256 reservePrice; // The sale percentage to send to the curator uint8 curatorFeePercentage;
can be rearranged as
// The current highest bid amount uint256 amount; // The length of time to run the auction for, after the first bid was made uint256 duration; // The time of the first bid uint256 firstBidTime; // The minimum price of the first bid uint256 reservePrice; // ~rearrange // Whether or not the auction curator has approved the auction to start bool approved; // The sale percentage to send to the curator uint8 curatorFeePercentage;