Platform: Code4rena
Start Date: 31/10/2023
Pot Size: $60,500 USDC
Total HM: 9
Participants: 65
Period: 10 days
Judge: gzeon
Total Solo HM: 2
Id: 301
League: ETH
Rank: 53/65
Findings: 1
Award: $23.81
š Selected for report: 0
š Solo Findings: 0
š Selected for report: 0xVolcano
Also found by: 0x11singh99, 0xAnah, 0xhacksmithh, 0xhex, 0xta, DavidGiladi, K42, Topmark, arjun16, dharma09, hunter_w3b, lsaudit, pavankv, tabriz, tala7985, ybansal2403
23.8054 USDC - $23.81
IERC2981
and IERC1271
library instead of openzeppelin librarysendFndingSplit()
function for better gas efficiencycall
Ā can be optimized with assemblyFile: contracts/crowdfund/InitialETHCrowdfund.sol 204: function batchContribute( BatchContributeArgs calldata args ) external payable onlyDelegateCall returns (uint96[] memory votingPowers) { uint256 numContributions = args.tokenIds.length; votingPowers = new uint96[](numContributions); uint256 ethAvailable = msg.value; //@audit Do Not Cache global variable for (uint256 i; i < numContributions; ++i) { ethAvailable -= args.values[i]; votingPowers[i] = _contribute( payable(msg.sender), args.delegate, args.values[i], args.tokenIds[i], args.gateDatas[i] ); } // Refund any unused ETH. if (ethAvailable > 0) payable(msg.sender).transfer(ethAvailable); }
File: contracts/crowdfund/InitialETHCrowdfund.sol 204: function batchContribute( BatchContributeArgs calldata args ) external payable onlyDelegateCall returns (uint96[] memory votingPowers) { uint256 numContributions = args.tokenIds.length; votingPowers = new uint96[](numContributions); - uint256 ethAvailable = msg.value; + uint256 ethAvailable; for (uint256 i; i < numContributions; ++i) { - ethAvailable -= args.values[i]; + ethAvailable = msg.value - args.values[i] votingPowers[i] = _contribute( payable(msg.sender), args.delegate, args.values[i], args.tokenIds[i], args.gateDatas[i] ); } // Refund any unused ETH. - if (ethAvailable > 0) payable(msg.sender).transfer(ethAvailable); + if (msg.value > 0) payable(msg.sender).transfer(msg.value); }
File: contracts/party/PartyGovernanceNFT.sol 208: function increaseVotingPower(uint256 tokenId, uint96 votingPower) external { _assertAuthority(); uint96 mintedVotingPower_ = mintedVotingPower; uint96 totalVotingPower = _getSharedProposalStorage().governanceValues.totalVotingPower; // Cap voting power to remaining unminted voting power supply. Allow // minting past total voting power if minting party cards for initial // crowdfund when there is no total voting power. if (totalVotingPower != 0 && totalVotingPower - mintedVotingPower_ < votingPower) { unchecked { votingPower = totalVotingPower - mintedVotingPower_; } } // Update state. mintedVotingPower += votingPower; //@audit Use cache value uint256 newIntrinsicVotingPower = votingPowerByTokenId[tokenId] + votingPower; votingPowerByTokenId[tokenId] = newIntrinsicVotingPower; emit PartyCardIntrinsicVotingPowerSet(tokenId, newIntrinsicVotingPower); _adjustVotingPower(ownerOf(tokenId), votingPower.safeCastUint96ToInt192(), address(0)); }
File: contracts/party/PartyGovernanceNFT.sol 208: function increaseVotingPower(uint256 tokenId, uint96 votingPower) external { _assertAuthority(); uint96 mintedVotingPower_ = mintedVotingPower; uint96 totalVotingPower = _getSharedProposalStorage().governanceValues.totalVotingPower; // Cap voting power to remaining unminted voting power supply. Allow // minting past total voting power if minting party cards for initial // crowdfund when there is no total voting power. if (totalVotingPower != 0 && totalVotingPower - mintedVotingPower_ < votingPower) { unchecked { votingPower = totalVotingPower - mintedVotingPower_; } } // Update state. - mintedVotingPower += votingPower; + mintedVotingPower = mintedVotingPower + votingPower; uint256 newIntrinsicVotingPower = votingPowerByTokenId[tokenId] + votingPower; votingPowerByTokenId[tokenId] = newIntrinsicVotingPower; emit PartyCardIntrinsicVotingPowerSet(tokenId, newIntrinsicVotingPower); _adjustVotingPower(ownerOf(tokenId), votingPower.safeCastUint96ToInt192(), address(0)); }
The variablesĀ tokenId
,Ā owner
,Ā token
, andĀ amount
are all declared inside the loop. This means that a new instance of each variable will be created for each iteration of the loop. SavesĀ 200-300 Gas
Ā per iteration
File: contracts/party/PartyGovernanceNFT.sol 273: uint256 tokenId = tokenIds[i]; //@audit 274: address owner = ownerOf(tokenId);
File: contracts/party/PartyGovernanceNFT.sol 409: IERC20 token = withdrawTokens[i]; //@audit 410: uint256 amount = withdrawAmounts[i];
File: contracts/party/PartyGovernanceNFT.sol 318: function setRageQuit(uint40 newRageQuitTimestamp) external { _assertHost(); // Prevent disabling ragequit after initialization. if (newRageQuitTimestamp == DISABLE_RAGEQUIT_PERMANENTLY) { revert CannotDisableRageQuitAfterInitializationError(); } uint40 oldRageQuitTimestamp = rageQuitTimestamp; // Prevent setting timestamp if it is permanently enabled/disabled. if ( oldRageQuitTimestamp == ENABLE_RAGEQUIT_PERMANENTLY || oldRageQuitTimestamp == DISABLE_RAGEQUIT_PERMANENTLY ) { revert FixedRageQuitTimestampError(oldRageQuitTimestamp); } emit RageQuitSet(oldRageQuitTimestamp, rageQuitTimestamp = newRageQuitTimestamp); //@audit rageQuitTimestamp }
File: contracts/party/PartyGovernanceNFT.sol 318: function setRageQuit(uint40 newRageQuitTimestamp) external { _assertHost(); // Prevent disabling ragequit after initialization. if (newRageQuitTimestamp == DISABLE_RAGEQUIT_PERMANENTLY) { revert CannotDisableRageQuitAfterInitializationError(); } uint40 oldRageQuitTimestamp = rageQuitTimestamp; rageQuitTimestamp = newRageQuitTimestamp // Prevent setting timestamp if it is permanently enabled/disabled. if ( oldRageQuitTimestamp == ENABLE_RAGEQUIT_PERMANENTLY || oldRageQuitTimestamp == DISABLE_RAGEQUIT_PERMANENTLY ) { revert FixedRageQuitTimestampError(oldRageQuitTimestamp); } - - emit RageQuitSet(oldRageQuitTimestamp, rageQuitTimestamp = newRageQuitTimestamp); + emit RageQuitSet(oldRageQuitTimestamp, newRageQuitTimestamp); }
IERC2981
and IERC1271
library instead of openzeppelin librarySoladyāsĀ IERC2981Ā implementation is more gas efficient than OZ's version. I recommend to switch to that library instead
File: contracts/party/PartyGovernanceNFT.sol 6: import "openzeppelin/contracts/interfaces/IERC2981.sol"; //@audit Use solady library's
File: contracts/proposals/ProposalExecutionEngine.sol 7: import { IERC1271 } from "openzeppelin/contracts/interfaces/IERC1271.sol"; //@audit Use solady library's
File: contracts/signature-validators/OffChainSignatureValidator.sol 4: import { IERC1271 } from "openzeppelin/contracts/interfaces/IERC1271.sol"; //@audit Use solady library's
If function use variable only once then it doesnāt make sense to cache that variable in memory it just waste of gas.Recommeded avoid such allocation.
File: contracts/crowdfund/ETHCrowdfundBase.sol 261: address payable fundingSplitRecipient_ = fundingSplitRecipient; //@audit uint16 fundingSplitBps_ = fundingSplitBps; if (fundingSplitRecipient_ != address(0) && fundingSplitBps_ > 0) { // Removes funding split from contribution amount in a way that // avoids rounding errors for very small contributions <1e4 wei. amount = (amount * (1e4 - fundingSplitBps_)) / 1e4; }
File: contracts/crowdfund/ETHCrowdfundBase.sol -261: address payable fundingSplitRecipient_ = fundingSplitRecipient; //@audit uint16 fundingSplitBps_ = fundingSplitBps; - if (fundingSplitRecipient_ != address(0) && fundingSplitBps_ > 0) { + if (fundingSplitRecipient != address(0) && fundingSplitBps_ > 0) { // Removes funding split from contribution amount in a way that // avoids rounding errors for very small contributions <1e4 wei. amount = (amount * (1e4 - fundingSplitBps_)) / 1e4; }
File: contracts/crowdfund/ETHCrowdfundBase.sol 322: address payable fundingSplitRecipient_ = fundingSplitRecipient; //@audit uint16 fundingSplitBps_ = fundingSplitBps; if (fundingSplitRecipient_ != address(0) && fundingSplitBps_ > 0) { totalContributions_ -= (totalContributions_ * fundingSplitBps_) / 1e4; }
File: contracts/crowdfund/ETHCrowdfundBase.sol -322: address payable fundingSplitRecipient_ = fundingSplitRecipient; //@audit uint16 fundingSplitBps_ = fundingSplitBps; - if (fundingSplitRecipient_ != address(0) && fundingSplitBps_ > 0) { + if (fundingSplitRecipient != address(0) && fundingSplitBps_ > 0) { totalContributions_ -= (totalContributions_ * fundingSplitBps_) / 1e4; }
sendFndingSplit()
function for better gas efficiencyFile: contracts/crowdfund/ETHCrowdfundBase.sol 339: function sendFundingSplit() external returns (uint96 splitAmount) { // Check that the crowdfund is finalized. CrowdfundLifecycle lc = getCrowdfundLifecycle(); if (lc != CrowdfundLifecycle.Finalized) revert WrongLifecycleError(lc); if (fundingSplitPaid) revert FundingSplitAlreadyPaidError(); //@audit Revert first address payable fundingSplitRecipient_ = fundingSplitRecipient; uint16 fundingSplitBps_ = fundingSplitBps; if (fundingSplitRecipient_ == address(0) || fundingSplitBps_ == 0) { revert FundingSplitNotConfiguredError(); } fundingSplitPaid = true; // Transfer funding split to recipient. splitAmount = (totalContributions * fundingSplitBps_) / 1e4; payable(fundingSplitRecipient_).transferEth(splitAmount); emit FundingSplitSent(fundingSplitRecipient_, splitAmount); }
File: contracts/crowdfund/ETHCrowdfundBase.sol 339: function sendFundingSplit() external returns (uint96 splitAmount) { // Check that the crowdfund is finalized. + if (fundingSplitPaid) revert FundingSplitAlreadyPaidError(); CrowdfundLifecycle lc = getCrowdfundLifecycle(); if (lc != CrowdfundLifecycle.Finalized) revert WrongLifecycleError(lc); - if (fundingSplitPaid) revert FundingSplitAlreadyPaidError(); address payable fundingSplitRecipient_ = fundingSplitRecipient; uint16 fundingSplitBps_ = fundingSplitBps; if (fundingSplitRecipient_ == address(0) || fundingSplitBps_ == 0) { revert FundingSplitNotConfiguredError(); } fundingSplitPaid = true; // Transfer funding split to recipient. splitAmount = (totalContributions * fundingSplitBps_) / 1e4; payable(fundingSplitRecipient_).transferEth(splitAmount); emit FundingSplitSent(fundingSplitRecipient_, splitAmount); }
call
Ā can be optimized with assemblyWhen using low-level calls, theĀ returnData
Ā is copied to memory even if the variable is not utilized. The proper way to handle this is through a low level assembly call. For example:
(bool success,) = payable(receiver).call{gas: gas, value: value}("");
can be optimized to:
bool success; assembly { success := call(gas, receiver, value, 0, 0, 0, 0) }
File: contracts/crowdfund/ETHCrowdfundBase.sol 379: (bool success, bytes memory res) = targetAddress.call{ value: amountEth }(targetCallData); //@audit if (!success) { res.rawRevert(); }0
File: contracts/crowdfund/InitialETHCrowdfund.sol 358: (bool s, bytes memory r) = address(this).call( //@audit abi.encodeCall(this.refund, (tokenIds[i])) );
File: ontracts/party/PartyGovernance.sol 846: (bool success, bytes memory res) = targetAddress.call{ value: amountEth }(targetCallData); //@audit
#0 - c4-pre-sort
2023-11-13T06:56:04Z
ydspa marked the issue as sufficient quality report
#1 - c4-judge
2023-11-19T18:26:38Z
gzeon-c4 marked the issue as grade-b