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: 81/110
Findings: 1
Award: $36.30
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
36.3024 USDC - $36.30
calldata
instead of memory
for read-only arguments in external
functions saves gasWhen a function with a memory
array is called externally, the abi.decode()
step has to use a for-loop to copy each index of the calldata
to the memory
index. Each iteration of this for-loop costs at least 60 gas (i.e. 60 * <mem_array>.length
). Using calldata
directly, obliviates the need for such a loop in the contract code and runtime execution.
If the array is passed to an internal
function which passes the array to another internal function where the array is modified and therefore memory
is used in the external
call, it’s still more gass-efficient to use calldata
when the external
function uses modifiers, since the modifiers may prevent the internal functions from being called. Structs have the same overhead as an array of length one
// Links to github files PartyFactory.sol
contracts/party/PartyFactory.sol:29: IERC721[] memory preciousTokens, contracts/party/PartyFactory.sol:30: uint256[] memory preciousTokenIds
contracts/party/PartyGovernance.sol:656: IERC721[] memory preciousTokens, contracts/party/PartyGovernance.sol:657: uint256[] memory preciousTokenIds,
contracts/party/IPartyFactory.sol:28: IERC721[] memory preciousTokens, contracts/party/IPartyFactory.sol:29: uint256[] memory preciousTokenIds
use calldata
instead of memory
Use a more recent version of solidity
The unchecked keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are. This saves 30-40 gas PER LOOP
// Links to github files CollectionBuyCrowdfund.sol:L62
contracts/crowdfund/CollectionBuyCrowdfund.sol:62: for (uint256 i; i < hosts.length; i++) {
As notDelegated() is only used once in this contract (in function proxiableUUID()), it should get inlined to save gas:
// Links to github files PartyGovernance.sol:L258
contracts/party/PartyGovernance.sol:258: modifier onlyWhenEmergencyExecuteAllowed() { contracts/party/PartyGovernance.sol:791: onlyWhenEmergencyExecuteAllowed
CollectionBuyCrowdfund.sol:L60
contracts/crowdfund/CollectionBuyCrowdfund.sol:60: modifier onlyHost(address[] memory hosts) { contracts/crowdfund/CollectionBuyCrowdfund.sol:121: onlyHost(governanceOpts.hosts)
<ARRAY>.LENGTH
SHOULD NOT BE LOOKED UP IN EVERY LOOP OF A FOR-LOOPThe overheads outlined below are PER LOOP
, excluding the first loop
storage arrays incur a Gwarmaccess (100 gas) memory arrays use MLOAD
(3 gas)
calldata arrays use CALLDATALOAD
(3 gas) Caching the length changes each of these to a DUP<N>
(3 gas), and gets rid of the extra DUP<N>
needed to store the stack offset.
contracts/proposals/ArbitraryCallsProposal.sol:52: for (uint256 i = 0; i < hadPreciouses.length; ++i) { contracts/proposals/ArbitraryCallsProposal.sol:61: for (uint256 i = 0; i < calls.length; ++i) { contracts/proposals/ArbitraryCallsProposal.sol:78: for (uint256 i = 0; i < hadPreciouses.length; ++i) { contracts/proposals/LibProposal.sol:14: for (uint256 i = 0; i < preciousTokens.length; ++i) { contracts/proposals/LibProposal.sol:32: for (uint256 i = 0; i < preciousTokens.length; ++i) { contracts/proposals/ListOnOpenseaProposal.sol:291: for (uint256 i = 0; i < fees.length; ++i) { contracts/party/PartyGovernance.sol:306: for (uint256 i=0; i < opts.hosts.length; ++i) { contracts/crowdfund/Crowdfund.sol:180: for (uint256 i = 0; i < contributors.length; ++i) { contracts/crowdfund/Crowdfund.sol:300: for (uint256 i = 0; i < preciousTokens.length; ++i) { contracts/crowdfund/CollectionBuyCrowdfund.sol:62: for (uint256 i; i < hosts.length; i++) { contracts/distribution/TokenDistributor.sol:230: for (uint256 i = 0; i < infos.length; ++i) { contracts/distribution/TokenDistributor.sol:239: for (uint256 i = 0; i < infos.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.
As an example: for (uint256 i = 0; i < numIterations; ++i)
{ should be replaced with for (uint256 i; i < numIterations; ++i) {
ArbitraryCallsProposal.sol:L52
contracts/proposals/ArbitraryCallsProposal.sol:52: for (uint256 i = 0; i < hadPreciouses.length; ++i) { contracts/proposals/ArbitraryCallsProposal.sol:61: for (uint256 i = 0; i < calls.length; ++i) { contracts/proposals/ArbitraryCallsProposal.sol:78: for (uint256 i = 0; i < hadPreciouses.length; ++i) {
contracts/proposals/LibProposal.sol:14: for (uint256 i = 0; i < preciousTokens.length; ++i) { contracts/proposals/LibProposal.sol:32: for (uint256 i = 0; i < preciousTokens.length; ++i) {
ListOnOpenseaProposal.sol:L291
contracts/proposals/ListOnOpenseaProposal.sol:291: for (uint256 i = 0; i < fees.length; ++i) {
contracts/party/PartyGovernance.sol:306: for (uint256 i=0; i < opts.hosts.length; ++i) {
contracts/crowdfund/Crowdfund.sol:180: for (uint256 i = 0; i < contributors.length; ++i) { contracts/crowdfund/Crowdfund.sol:242: for (uint256 i = 0; i < numContributions; ++i) { contracts/crowdfund/Crowdfund.sol:300: for (uint256 i = 0; i < preciousTokens.length; ++i) { contracts/crowdfund/Crowdfund.sol:348: for (uint256 i = 0; i < numContributions; ++i) {
contracts/distribution/TokenDistributor.sol:230: for (uint256 i = 0; i < infos.length; ++i) { contracts/distribution/TokenDistributor.sol:239: for (uint256 i = 0; i < infos.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.
We can use uint number;
instead of uint number = 0;
// Links to github files PartyGovernance.sol:L432
contracts/party/PartyGovernance.sol:432: uint256 low = 0;
I suggest removing explicit initializations for default values.
abi.encodePacked()
not abi.encode()
Changing abi.encode
to abi.encodePacked
can save gas. abi.encode
pads extra null bytes at the end of the call data which is normally unnecessary. In general, abi.encodePacked
is more gas-efficient.
// Links to github files
contracts/proposals/ListOnZoraProposal.sol:115: return abi.encode(ZoraStep.ListedOnZora, ZoraProgressData({
contracts/proposals/ListOnOpenseaProposal.sol:164: return abi.encode(ListOnOpenseaStep.ListedOnZora, ZoraProgressData({ contracts/proposals/ListOnOpenseaProposal.sol:219: return abi.encode(ListOnOpenseaStep.ListedOnOpenSea, orderHash, expiry);
contracts/utils/ReadOnlyDelegateCall.sol:23: abi.encode(s, r).rawRevert();
You should change abi.encode to abi.encodePacked
mapping
of an address
to a struct
, where appropriateSaves 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. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.
There are 1 instances of this issue:
// Links to github files PartyGovernance.sol:L207-L209
contracts/party/PartyGovernance.sol:207: mapping(address => bool) public isHost; contracts/party/PartyGovernance.sol:208: /// @notice The last person a voter delegated its voting power to. contracts/party/PartyGovernance.sol:209: mapping(address => address) public delegationsByVoter;
If needed, the values can be read from the verified contract source code, or if there are multiple values there can be a single getter function that returns a tuple of the values of all currently-public constants. Saves approximate of 200-400 gas in deployment gas due to the compiler not having to create non-payable getter functions for deployment calldata, not having to store the bytes of the value outside of where it’s used, and not adding another entry to the method ID table
// Links to github files ListOnZoraProposal.sol:L64
contracts/proposals/ListOnZoraProposal.sol:64: IZoraAuctionHouse public immutable ZORA;
contracts/proposals/ListOnOpenseaProposal.sol:100: IOpenseaExchange public immutable SEAPORT; contracts/proposals/ListOnOpenseaProposal.sol:102: IOpenseaConduitController public immutable CONDUIT_CONTROLLER;
contracts/proposals/FractionalizeProposal.sol:31: IFractionalV1VaultFactory public immutable VAULT_FACTORY;
contracts/party/PartyFactory.sol:18: IGlobals public immutable GLOBALS;
contracts/utils/Implementation.sol:9: address public immutable IMPL;
contracts/utils/Proxy.sol:12: Implementation public immutable IMPL;
contracts/distribution/TokenDistributor.sol:59: IGlobals public immutable GLOBALS;
<x> += <y>
costs more gas than <x> = <x> + <y>
for state variablescontracts/party/PartyGovernance.sol:595: values.votes += votingPower;
contracts/crowdfund/Crowdfund.sol:243: ethContributed += contributions[i].amount; contracts/crowdfund/Crowdfund.sol:352: ethOwed += c.amount; contracts/crowdfund/Crowdfund.sol:355: ethUsed += c.amount; contracts/crowdfund/Crowdfund.sol:359: ethUsed += partialEthUsed; contracts/crowdfund/Crowdfund.sol:374: votingPower += (splitBps_ * totalEthUsed + (1e4 - 1)) / 1e4; // round up contracts/crowdfund/Crowdfund.sol:411: totalContributions += amount; contracts/crowdfund/Crowdfund.sol:427: lastContribution.amount += amount;
Use a solidity version of at least 0.8.2 to get simple compiler automatic inlining Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads 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
contracts/vendor/markets/IZoraAuctionHouse.sol:2:pragma solidity ^0.8; contracts/gatekeepers/IGateKeeper.sol:2:pragma solidity ^0.8; contracts/proposals/ArbitraryCallsProposal.sol:2:pragma solidity ^0.8; contracts/proposals/ProposalExecutionEngine.sol:2:pragma solidity ^0.8; contracts/proposals/vendor/FractionalV1.sol:2:pragma solidity ^0.8; contracts/proposals/vendor/IOpenseaConduitController.sol:2:pragma solidity ^0.8; contracts/proposals/vendor/IOpenseaExchange.sol:2:pragma solidity ^0.8; contracts/proposals/ListOnZoraProposal.sol:2:pragma solidity ^0.8; contracts/proposals/ProposalStorage.sol:2:pragma solidity ^0.8; contracts/proposals/LibProposal.sol:2:pragma solidity ^0.8; contracts/proposals/ListOnOpenseaProposal.sol:2:pragma solidity ^0.8; contracts/proposals/IProposalExecutionEngine.sol:2:pragma solidity ^0.8; contracts/proposals/FractionalizeProposal.sol:2:pragma solidity ^0.8; contracts/party/PartyFactory.sol:2:pragma solidity ^0.8; contracts/party/PartyGovernance.sol:2:pragma solidity ^0.8; contracts/party/IPartyFactory.sol:2:pragma solidity ^0.8; contracts/party/PartyGovernanceNFT.sol:2:pragma solidity ^0.8; contracts/party/Party.sol:2:pragma solidity ^0.8; contracts/globals/IGlobals.sol:2:pragma solidity ^0.8; contracts/globals/Globals.sol:2:pragma solidity ^0.8; contracts/crowdfund/Crowdfund.sol:2:pragma solidity ^0.8; contracts/crowdfund/BuyCrowdfund.sol:2:pragma solidity ^0.8; contracts/crowdfund/CollectionBuyCrowdfund.sol:2:pragma solidity ^0.8; contracts/crowdfund/CrowdfundFactory.sol:2:pragma solidity ^0.8; contracts/crowdfund/AuctionCrowdfund.sol:2:pragma solidity ^0.8; contracts/crowdfund/BuyCrowdfundBase.sol:2:pragma solidity ^0.8; contracts/crowdfund/CrowdfundNFT.sol:2:pragma solidity ^0.8; contracts/tokens/IERC721Receiver.sol:2:pragma solidity ^0.8; contracts/tokens/IERC1155.sol:2:pragma solidity ^0.8; contracts/tokens/ERC1155Receiver.sol:2:pragma solidity ^0.8; contracts/tokens/ERC721Receiver.sol:2:pragma solidity ^0.8; contracts/tokens/IERC20.sol:2:pragma solidity ^0.8; contracts/tokens/IERC721.sol:2:pragma solidity ^0.8; contracts/utils/ReadOnlyDelegateCall.sol:2:pragma solidity ^0.8; contracts/utils/LibSafeERC721.sol:2:pragma solidity ^0.8; contracts/utils/LibERC20Compat.sol:2:pragma solidity ^0.8; contracts/utils/LibRawResult.sol:2:pragma solidity ^0.8; contracts/utils/LibAddress.sol:2:pragma solidity ^0.8; contracts/utils/LibSafeCast.sol:2:pragma solidity ^0.8; contracts/utils/EIP165.sol:2:pragma solidity ^0.8; contracts/utils/Implementation.sol:2:pragma solidity ^0.8; contracts/utils/Proxy.sol:2:pragma solidity ^0.8; contracts/market-wrapper/IMarketWrapper.sol:2:pragma solidity ^0.8; contracts/distribution/ITokenDistributorParty.sol:2:pragma solidity ^0.8; contracts/distribution/TokenDistributor.sol:2:pragma solidity ^0.8; contracts/distribution/ITokenDistributor.sol:2:pragma solidity ^0.8;