Platform: Code4rena
Start Date: 13/12/2023
Pot Size: $36,500 USDC
Total HM: 18
Participants: 110
Period: 8 days
Judge: 0xTheC0der
Id: 311
League: ETH
Rank: 88/110
Findings: 1
Award: $20.84
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: MrPotatoMagic
Also found by: 0x11singh99, 0xAnah, IllIllI, JCK, Raihan, SAQ, Sathish9098, SovaSlava, c3phas, donkicha, fnanni, hunter_w3b, lsaudit, naman1778, pavankv, sivanesh_808
20.8446 USDC - $20.84
Number | Issue | nstances |
---|---|---|
[G-01] | Avoid Unnecessary Public Variables | 44 |
[G-02] | State variables that are used multiple times in a function should be cached in stack variables | 13 |
[G-03] | Use assembly to write address storage values | 7 |
[G-04] | Increments can be unchecked in for-loops | 9 |
[G-05] | Use named returns for local variables of pure functions where it is possible | 3 |
[G-06] | Use uint256(1)/uint256(2) instead for true and false boolean states | 10 |
[G-07] | require() statements that check input arguments should be at the top of the function | 2 |
[G-08] | Redundant state variable getters | 3 |
[G-09] | CultureIndex._verifyVoteSignature() function can be optimized | 1 |
[G-10] | Don’t cache calls/variables if using them once | 2 |
[G-11] | Using storage instead of memory for structs/arrays saves gas | 1 |
[G-12] | (Proposal) Variables that should be constant/immutable | 10 |
[G-13] | For same condition checks use modifiers | 4 |
[G-14] | Switching between 1 and 2 instead of 0 and 1 (or false and true) is more gas efficient | 5 |
[G-15] | Counting down in for statements is more gas efficient | 8 |
[G-16] | Use assembly to perform efficient back-to-back calls | 1 |
[G-17] | Declare the variables outside the loop | 1 |
[G-18] | Explicitly assigning a default value in storage wastes gas | 4 |
Public storage variables increase the contract's size due to the implicit generation of public getter functions. This makes the contract larger and could increase deployment and interaction costs.
If you do not require other contracts to read these variables, consider making them private or internal.
file: main/packages/revolution/src/AuctionHouse.sol 48 IVerbsToken public verbs; // The ERC20 governance token IERC20TokenEmitter public erc20TokenEmitter; // The address of the WETH contract address public WETH; // The minimum amount of time left in an auction after a new bid is created uint256 public timeBuffer; // The minimum price accepted in an auction uint256 public reservePrice; // The minimum percentage difference between the last bid amount and the current bid uint8 public minBidIncrementPercentage; // The split of the winning bid that is reserved for the creator of the Verb in basis points uint256 public creatorRateBps; // The all time minimum split of the winning bid that is reserved for the creator of the Verb in basis points uint256 public minCreatorRateBps; // The split of (auction proceeds * creatorRate) that is sent to the creator as ether in basis points uint256 public entropyRateBps; // The duration of a single auction uint256 public duration; // The active auction IAuctionHouse.Auction public auction;
file: main/packages/revolution/src/CultureIndex.sol 36 MaxHeap public maxHeap; // The ERC20 token used for voting ERC20VotesUpgradeable public erc20VotingToken; // The ERC721 token used for voting ERC721CheckpointableUpgradeable public erc721VotingToken; // The weight of the 721 voting token uint256 public erc721VotingTokenWeight; /// @notice The maximum settable quorum votes basis points uint256 public constant MAX_QUORUM_VOTES_BPS = 6_000; // 6,000 basis points or 60% /// @notice The minimum vote weight required in order to vote uint256 public minVoteWeight; /// @notice The basis point number of votes in support of a art piece required in order for a quorum to be reached and for an art piece to be dropped. uint256 public quorumVotesBPS; /// @notice The name of the culture index string public name; /// @notice A description of the culture index - can include rules or guidelines string public description; // The list of all pieces mapping(uint256 => ArtPiece) public pieces; // The internal piece ID tracker uint256 public _currentPieceId; // The mapping of all votes for a piece mapping(uint256 => mapping(address => Vote)) public votes; // The total voting weight for a piece mapping(uint256 => uint256) public totalVoteWeights; // Constant for max number of creators uint256 public constant MAX_NUM_CREATORS = 100; // The address that is allowed to drop art pieces address public dropperAdmin;
file: main/packages/revolution/src/ERC20TokenEmitter.sol 25 address public treasury; // The token that is being minted. NontransferableERC20Votes public token; // The VRGDA contract VRGDAC public vrgdac; // solhint-disable-next-line not-rely-on-time uint256 public startTime; /** * @notice A running total of the amount of tokens emitted. */ int256 public emittedTokenWad; // The split of the purchase that is reserved for the creator of the Verb in basis points uint256 public creatorRateBps; // The split of (purchase proceeds * creatorRate) that is sent to the creator as ether in basis points uint256 public entropyRateBps; // The account or contract to pay the creator reward to address public creatorsAddress;
file: main/packages/revolution/src/VerbsToken.sol 41 // An address who has permissions to mint Verbs address public minter; // The Verbs token URI descriptor IDescriptorMinimal public descriptor; // The CultureIndex contract ICultureIndex public cultureIndex; // Whether the minter can be updated bool public isMinterLocked; // Whether the CultureIndex can be updated bool public isCultureIndexLocked; // Whether the descriptor can be updated bool public isDescriptorLocked; // The internal verb ID tracker uint256 private _currentVerbId; // IPFS content hash of contract-level metadata string private _contractURIHash = "QmQzDwaZ7yQxHHs7sQQenJVB89riTSacSGcJRv9jtHPuz5"; // The Verb art pieces mapping(uint256 => ICultureIndex.ArtPiece) public artPieces;
By caching state variables in stack variables, we reduce the need to frequently access storage, thereby saving gas.
file: main/packages/revolution/src/AuctionHouse.sol 336 function _settleAuction() internal { IAuctionHouse.Auction memory _auction = auction; require(_auction.startTime != 0, "Auction hasn't begun"); require(!_auction.settled, "Auction has already been settled"); //slither-disable-next-line timestamp require(block.timestamp >= _auction.endTime, "Auction hasn't completed"); auction.settled = true; uint256 creatorTokensEmitted = 0; // Check if contract balance is greater than reserve price if (address(this).balance < reservePrice) { // If contract balance is less than reserve price, refund to the last bidder if (_auction.bidder != address(0)) { _safeTransferETHWithFallback(_auction.bidder, _auction.amount); } // And then burn the Noun verbs.burn(_auction.verbId); } else { //If no one has bid, burn the Verb if (_auction.bidder == address(0)) verbs.burn(_auction.verbId); //If someone has bid, transfer the Verb to the winning bidder else verbs.transferFrom(address(this), _auction.bidder, _auction.verbId); if (_auction.amount > 0) { // Ether going to owner of the auction uint256 auctioneerPayment = (_auction.amount * (10_000 - creatorRateBps)) / 10_000; //Total amount of ether going to creator uint256 creatorsShare = _auction.amount - auctioneerPayment; uint256 numCreators = verbs.getArtPieceById(_auction.verbId).creators.length; address deployer = verbs.getArtPieceById(_auction.verbId).sponsor; //Build arrays for erc20TokenEmitter.buyToken uint256[] memory vrgdaSplits = new uint256[](numCreators); address[] memory vrgdaReceivers = new address[](numCreators); //Transfer auction amount to the DAO treasury _safeTransferETHWithFallback(owner(), auctioneerPayment); uint256 ethPaidToCreators = 0; //Transfer creator's share to the creator, for each creator, and build arrays for erc20TokenEmitter.buyToken if (creatorsShare > 0 && entropyRateBps > 0) { for (uint256 i = 0; i < numCreators; i++) { ICultureIndex.CreatorBps memory creator = verbs.getArtPieceById(_auction.verbId).creators[i];
file: main/packages/revolution/src/AuctionHouse.sol 171 function createBid(uint256 verbId, address bidder) external payable override nonReentrant { IAuctionHouse.Auction memory _auction = auction; //require bidder is valid address require(bidder != address(0), "Bidder cannot be zero address"); require(_auction.verbId == verbId, "Verb not up for auction"); //slither-disable-next-line timestamp require(block.timestamp < _auction.endTime, "Auction expired"); require(msg.value >= reservePrice, "Must send at least reservePrice"); require( msg.value >= _auction.amount + ((_auction.amount * minBidIncrementPercentage) / 100), "Must send more than last bid by minBidIncrementPercentage amount" ); address payable lastBidder = _auction.bidder; auction.amount = msg.value; auction.bidder = payable(bidder); // Extend the auction if the bid was received within `timeBuffer` of the auction end time bool extended = _auction.endTime - block.timestamp < timeBuffer; if (extended) auction.endTime = _auction.endTime = block.timestamp + timeBuffer; // Refund the last bidder, if applicable if (lastBidder != address(0)) _safeTransferETHWithFallback(lastBidder, _auction.amount); emit AuctionBid(_auction.verbId, bidder, msg.sender, msg.value, extended); if (extended) emit AuctionExtended(_auction.verbId, _auction.endTime); } 336 function _settleAuction() internal { IAuctionHouse.Auction memory _auction = auction; require(_auction.startTime != 0, "Auction hasn't begun"); require(!_auction.settled, "Auction has already been settled"); //slither-disable-next-line timestamp require(block.timestamp >= _auction.endTime, "Auction hasn't completed"); auction.settled = true; uint256 creatorTokensEmitted = 0; // Check if contract balance is greater than reserve price if (address(this).balance < reservePrice) { // If contract balance is less than reserve price, refund to the last bidder if (_auction.bidder != address(0)) { _safeTransferETHWithFallback(_auction.bidder, _auction.amount); } // And then burn the Noun verbs.burn(_auction.verbId); } else { //If no one has bid, burn the Verb if (_auction.bidder == address(0)) verbs.burn(_auction.verbId); //If someone has bid, transfer the Verb to the winning bidder else verbs.transferFrom(address(this), _auction.bidder, _auction.verbId); if (_auction.amount > 0) { // Ether going to owner of the auction uint256 auctioneerPayment = (_auction.amount * (10_000 - creatorRateBps)) / 10_000; //Total amount of ether going to creator uint256 creatorsShare = _auction.amount - auctioneerPayment; uint256 numCreators = verbs.getArtPieceById(_auction.verbId).creators.length; address deployer = verbs.getArtPieceById(_auction.verbId).sponsor; //Build arrays for erc20TokenEmitter.buyToken uint256[] memory vrgdaSplits = new uint256[](numCreators); address[] memory vrgdaReceivers = new address[](numCreators); //Transfer auction amount to the DAO treasury _safeTransferETHWithFallback(owner(), auctioneerPayment); uint256 ethPaidToCreators = 0; //Transfer creator's share to the creator, for each creator, and build arrays for erc20TokenEmitter.buyToken if (creatorsShare > 0 && entropyRateBps > 0) { for (uint256 i = 0; i < numCreators; i++) { ICultureIndex.CreatorBps memory creator = verbs.getArtPieceById(_auction.verbId).creators[i]; vrgdaReceivers[i] = creator.creator; vrgdaSplits[i] = creator.bps; //Calculate paymentAmount for specific creator based on BPS splits - same as multiplying by creatorDirectPayment uint256 paymentAmount = (creatorsShare * entropyRateBps * creator.bps) / (10_000 * 10_000); ethPaidToCreators += paymentAmount; //Transfer creator's share to the creator _safeTransferETHWithFallback(creator.creator, paymentAmount); } } //Buy token from ERC20TokenEmitter for all the creators if (creatorsShare > ethPaidToCreators) { creatorTokensEmitted = erc20TokenEmitter.buyToken{ value: creatorsShare - ethPaidToCreators }( vrgdaReceivers, vrgdaSplits, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: deployer }) ); } } } emit AuctionSettled(_auction.verbId, _auction.bidder, _auction.amount, creatorTokensEmitted); }
file: main/packages/revolution/src/AuctionHouse.sol 113 function initialize( address _erc721Token, address _erc20TokenEmitter, address _initialOwner, address _weth, IRevolutionBuilder.AuctionParams calldata _auctionParams ) external initializer { require(msg.sender == address(manager), "Only manager can initialize"); require(_weth != address(0), "WETH cannot be zero address"); __Pausable_init(); __ReentrancyGuard_init(); __Ownable_init(_initialOwner); _pause(); require( _auctionParams.creatorRateBps >= _auctionParams.minCreatorRateBps, "Creator rate must be greater than or equal to the creator rate" ); verbs = IVerbsToken(_erc721Token); erc20TokenEmitter = IERC20TokenEmitter(_erc20TokenEmitter); timeBuffer = _auctionParams.timeBuffer; reservePrice = _auctionParams.reservePrice; minBidIncrementPercentage = _auctionParams.minBidIncrementPercentage; duration = _auctionParams.duration; creatorRateBps = _auctionParams.creatorRateBps; entropyRateBps = _auctionParams.entropyRateBps; minCreatorRateBps = _auctionParams.minCreatorRateBps; WETH = _weth; } 171 function createBid(uint256 verbId, address bidder) external payable override nonReentrant { IAuctionHouse.Auction memory _auction = auction; //require bidder is valid address require(bidder != address(0), "Bidder cannot be zero address"); require(_auction.verbId == verbId, "Verb not up for auction"); //slither-disable-next-line timestamp require(block.timestamp < _auction.endTime, "Auction expired"); require(msg.value >= reservePrice, "Must send at least reservePrice"); require( msg.value >= _auction.amount + ((_auction.amount * minBidIncrementPercentage) / 100), "Must send more than last bid by minBidIncrementPercentage amount" ); address payable lastBidder = _auction.bidder; auction.amount = msg.value; auction.bidder = payable(bidder); // Extend the auction if the bid was received within `timeBuffer` of the auction end time bool extended = _auction.endTime - block.timestamp < timeBuffer; if (extended) auction.endTime = _auction.endTime = block.timestamp + timeBuffer; // Refund the last bidder, if applicable if (lastBidder != address(0)) _safeTransferETHWithFallback(lastBidder, _auction.amount); emit AuctionBid(_auction.verbId, bidder, msg.sender, msg.value, extended); if (extended) emit AuctionExtended(_auction.verbId, _auction.endTime); }
file: main/packages/revolution/src/AuctionHouse.sol 137 reservePrice = _auctionParams.reservePrice;
file: main/packages/revolution/src/AuctionHouse.sol 138 minBidIncrementPercentage = _auctionParams.minBidIncrementPercentage;
file: main/packages/revolution/src/AuctionHouse.sol 142 minCreatorRateBps = _auctionParams.minCreatorRateBps;
file: main/packages/revolution/src/CultureIndex.sol 119 require(_cultureIndexParams.quorumVotesBPS <= MAX_QUORUM_VOTES_BPS, "invalid quorum bps"); require(_cultureIndexParams.erc721VotingTokenWeight > 0, "invalid erc721 voting token weight"); require(_erc721VotingToken != address(0), "invalid erc721 voting token"); require(_erc20VotingToken != address(0), "invalid erc20 voting token"); // Setup ownable __Ownable_init(_initialOwner); // Initialize EIP-712 support __EIP712_init(string.concat(_cultureIndexParams.name, " CultureIndex"), "1"); __ReentrancyGuard_init(); erc20VotingToken = ERC20VotesUpgradeable(_erc20VotingToken); erc721VotingToken = ERC721CheckpointableUpgradeable(_erc721VotingToken); erc721VotingTokenWeight = _cultureIndexParams.erc721VotingTokenWeight;
file: main/packages/revolution/src/CultureIndex.sol 490 function _setQuorumVotesBPS(uint256 newQuorumVotesBPS) external onlyOwner { require(newQuorumVotesBPS <= MAX_QUORUM_VOTES_BPS, "CultureIndex::_setQuorumVotesBPS: invalid quorum bps"); emit QuorumVotesBPSSet(quorumVotesBPS, newQuorumVotesBPS); quorumVotesBPS = newQuorumVotesBPS; }
file: main/packages/revolution/src/CultureIndex.sol 109 function initialize( address _erc20VotingToken, address _erc721VotingToken, address _initialOwner, address _maxHeap, address _dropperAdmin, IRevolutionBuilder.CultureIndexParams memory _cultureIndexParams ) external initializer { require(msg.sender == address(manager), "Only manager can initialize"); require(_cultureIndexParams.quorumVotesBPS <= MAX_QUORUM_VOTES_BPS, "invalid quorum bps"); require(_cultureIndexParams.erc721VotingTokenWeight > 0, "invalid erc721 voting token weight"); require(_erc721VotingToken != address(0), "invalid erc721 voting token"); require(_erc20VotingToken != address(0), "invalid erc20 voting token"); // Setup ownable __Ownable_init(_initialOwner); // Initialize EIP-712 support __EIP712_init(string.concat(_cultureIndexParams.name, " CultureIndex"), "1"); __ReentrancyGuard_init(); erc20VotingToken = ERC20VotesUpgradeable(_erc20VotingToken); erc721VotingToken = ERC721CheckpointableUpgradeable(_erc721VotingToken); erc721VotingTokenWeight = _cultureIndexParams.erc721VotingTokenWeight; name = _cultureIndexParams.name; description = _cultureIndexParams.description; quorumVotesBPS = _cultureIndexParams.quorumVotesBPS; minVoteWeight = _cultureIndexParams.minVoteWeight; dropperAdmin = _dropperAdmin; emit QuorumVotesBPSSet(quorumVotesBPS, _cultureIndexParams.quorumVotesBPS); // Create maxHeap maxHeap = MaxHeap(_maxHeap); }
file: main/packages/revolution/src/CultureIndex.sol 307 function _vote(uint256 pieceId, address voter) internal { require(pieceId < _currentPieceId, "Invalid piece ID"); require(voter != address(0), "Invalid voter address"); require(!pieces[pieceId].isDropped, "Piece has already been dropped"); require(!(votes[pieceId][voter].voterAddress != address(0)), "Already voted"); uint256 weight = _getPastVotes(voter, pieces[pieceId].creationBlock); require(weight > minVoteWeight, "Weight must be greater than minVoteWeight"); votes[pieceId][voter] = Vote(voter, weight); totalVoteWeights[pieceId] += weight; uint256 totalWeight = totalVoteWeights[pieceId]; // TODO add security consideration here based on block created to prevent flash attacks on drops? maxHeap.updateValue(pieceId, totalWeight); emit VoteCast(pieceId, voter, weight, totalWeight); }
file: main/packages/revolution/src/ERC20TokenEmitter.sol 195 if (creatorDirectPayment > 0) { (success, ) = creatorsAddress.call{ value: creatorDirectPayment }(new bytes(0)); require(success, "Transfer failed."); } //Mint tokens for creators if (totalTokensForCreators > 0 && creatorsAddress != address(0)) { _mint(creatorsAddress, uint256(totalTokensForCreators)); }
file: main/packages/revolution/src/ERC20TokenEmitter.sol 101 treasury = _treasury; 102 creatorsAddress = _creatorsAddress; 103 vrgdac = VRGDAC(_vrgdac); 104 token = NontransferableERC20Votes(_erc20Token);
file: main/packages/revolution/src/VerbsToken.sol 153 minter = _minter; 154 descriptor = IDescriptorMinimal(_descriptor); 155 cultureIndex = ICultureIndex(_cultureIndex);
This one isn’t covered by c4udit
In Solidity 0.8+, there’s a default overflow check on unsigned integers. It’s possible to uncheck this in for-loops and save some gas at each iteration, but at the cost of some code readability, as this uncheck cannot be made inline.
ethereum/solidity#10695
Consider wrapping with an unchecked block here (around 25 gas saved per instance):
file: main/packages/revolution/src/AuctionHouse.sol 384 for (uint256 i = 0; i < numCreators; i++) {
file: main/packages/revolution/src/CultureIndex.sol 185 for (uint i; i < creatorArrayLength; i++) { 236 for (uint i; i < creatorArrayLength; i++) { 243 for (uint i; i < creatorArrayLength; i++) { 355 for (uint256 i; i < len; i++) { 403 for (uint256 i; i < len; i++) { 407 for (uint256 i; i < len; i++) {
file: main/packages/revolution/src/ERC20TokenEmitter.sol 209 for (uint256 i = 0; i < addresses.length; i++) {
file: main/packages/revolution/src/VerbsToken.sol 306 for (uint i = 0; i < artPiece.creators.length; i++) {
file: main/packages/revolution/src/MaxHeap.sol 78 function parent(uint256 pos) private pure returns (uint256) {
file: main/packages/protocol-rewards/src/abstract/RewardSplits.sol 40 function computeTotalReward(uint256 paymentAmountWei) public pure returns (uint256) {
file: main/packages/revolution/src/CultureIndex.sol 179 function validateCreatorsArray(CreatorBps[] calldata creatorArray) internal pure returns (uint256) {
Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas), and to avoid Gsset (20000 gas) when changing from ‘false’ to ‘true’, after having been ‘true’ in the past. see source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/58f635312aa21f947cae5f8578638a85aa2519f5/contracts/security/ReentrancyGuard.sol#L23-L27
file: main/packages/revolution/src/VerbsToken.sol 51 bool public isMinterLocked; 54 bool public isCultureIndexLocked; 57 bool public isDescriptorLocked;
file: main/packages/revolution/src/AuctionHouse.sol 191 bool extended 424 bool success; 438 bool wethSuccess
file: main/packages/revolution/src/AuctionHouse.sol 344 auction.settled = true;
file: main/packages/revolution/src/VerbsToken.sol 221 isMinterLocked = true; 243 isDescriptorLocked = true; 263 isCultureIndexLocked = true;
Checks that involve constants should come before checks that involve state variables, function calls, and calculations. By doing these checks first, the function is able to revert before wasting a SLOAD (2100 gas for the 1st one) in a function that may ultimately revert in the unhappy case.
file: main/packages/revolution/src/AuctionHouse.sol 123 __Pausable_init(); __ReentrancyGuard_init(); __Ownable_init(_initialOwner); _pause(); require( _auctionParams.creatorRateBps >= _auctionParams.minCreatorRateBps, "Creator rate must be greater than or equal to the creator rate" );
file: require( _auctionParams.creatorRateBps >= _auctionParams.minCreatorRateBps, "Creator rate must be greater than or equal to the creator rate" ); __Pausable_init(); __ReentrancyGuard_init(); __Ownable_init(_initialOwner); _pause();
file: main/packages/revolution/src/ERC20TokenEmitter.sol 93 __Pausable_init(); __ReentrancyGuard_init(); require(_treasury != address(0), "Invalid treasury address");
require(_treasury != address(0), "Invalid treasury address"); __Pausable_init(); __ReentrancyGuard_init();
Getters for public state variables are automatically generated by the solidity compiler so there is no need to code them manually as this increases deployment cost.
file: main/packages/revolution/src/CultureIndex.sol 265 function getVotes(address account) external view override returns (uint256) { return _getVotes(account); }
The solidity compiler would automatically create a getter function for the _getVotes mapping above since it is declared as a public variable. However, a getter function getVotes() was also declared in the contract for the same variable; thereby, making two getter functions for the same variable in the contract. We could rectify this issue by making the _getVotes variable private or internal (if the variable is to be inherited). The diff below shows how the code could be refactored:
file: main/packages/revolution/src/VerbsToken.sol 273 function getArtPieceById(uint256 verbId) public view returns (ICultureIndex.ArtPiece memory) { require(verbId <= _currentVerbId, "Invalid piece ID"); return artPieces[verbId]; }
file: main/packages/revolution/src/CultureIndex.sol 256 function hasVoted(uint256 pieceId, address voter) external view returns (bool) { return votes[pieceId][voter].voterAddress != address(0); }
By moving condition checks up in the execution flow, we save on computational steps and improve the gas efficiency of the _verifyVoteSignature () function.
file: main/packages/revolution/src/CultureIndex.sol 429 bytes32 voteHash; voteHash = keccak256(abi.encode(VOTE_TYPEHASH, from, pieceIds, nonces[from]++, deadline)); bytes32 digest = _hashTypedDataV4(voteHash); address recoveredAddress = ecrecover(digest, v, r, s); // Ensure to address is not 0 if (from == address(0)) revert ADDRESS_ZERO();
No need to cache _hashTypedDataV4(voteHash)
file: main/packages/revolution/src/CultureIndex.sol 433 bytes32 digest = _hashTypedDataV4(voteHash);
No need to cache _verifyVoteSignature(from, pieceIds, deadline, v, r, s);
file: main/packages/revolution/src/CultureIndex.sol 375 bool success = _verifyVoteSignature(from, pieceIds, deadline, v, r, s);
When fetching data from a storage location, assigning the data to a memory variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declearing the variable with the memory keyword, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory, or if the array/struct is being read from another memory array/struct
There is 1 instance of this issue:
file: main/packages/protocol-rewards/src/abstract/RewardSplits.sol 72 (RewardsSettings memory settings, uint256 totalReward) = computePurchaseRewards(paymentAmountWei);
There are some variables that are very unlikely to change
file: main/packages/revolution/src/ERC20TokenEmitter.sol 28 NontransferableERC20Votes public token; 31 VRGDAC public vrgdac;
file: main/packages/revolution/src/VerbsToken.sol 45 IDescriptorMinimal public descriptor; 48 ICultureIndex public cultureIndex;
file: main/packages/revolution/src/AuctionHouse.sol 48 IVerbsToken public verbs; 51 IERC20TokenEmitter public erc20TokenEmitter; 78 IAuctionHouse.Auction public auction;
file: main/packages/revolution/src/CultureIndex.sol 36 MaxHeap public maxHeap; 39 ERC20VotesUpgradeable public erc20VotingToken; 42 ERC721CheckpointableUpgradeable public erc721VotingToken;
file: main/packages/revolution/src/CultureIndex.sol 308 require(pieceId < _currentPieceId, "Invalid piece ID"); 452 require(pieceId < _currentPieceId, "Invalid piece ID"); 462 require(pieceId < _currentPieceId, "Invalid piece ID");
file: packages/revolution/src/ERC20TokenEmitter.sol 192 require(success, "Transfer failed."); 197 require(success, "Transfer failed.");
file: main/packages/revolution/src/MaxHeap.sol 157 require(size > 0, "Heap is empty"); 170 require(size > 0, "Heap is empty");
file: main/packages/revolution/src/VerbsToken.sol 139 require(_minter != address(0), "Minter cannot be zero address"); 210 require(_minter != address(0), "Minter cannot be zero address");
SSTORE from 0 to 1 (or any non-zero value) costs 20000 gas. SSTORE from 1 to 2 (or any other non-zero value) costs 5000 gas.
By storing the original value once again, a refund is triggered (https://eips.ethereum.org/EIPS/eip-2200).
Since refunds are capped to a percentage of the total transaction’s gas, it is best to keep them low, to increase the likelihood of the full refund coming into effect.
Therefore, switching between 1, 2 instead of 0, 1 will be more gas efficient.
file: main/packages/revolution/src/AuctionHouse.sol 344 auction.settled = true;
file: main/packages/revolution/src/CultureIndex.sol 526 pieces[piece.pieceId].isDropped = true;
file: main/packages/revolution/src/VerbsToken.sol 221 isMinterLocked = true; 243 isDescriptorLocked = true; 263 isCultureIndexLocked = true;
Counting down is more gas efficient than counting up because neither we are making zero variable to non-zero variable and also we will get gas refund in the last transaction when making non-zero to zero variable.
by changing this logic you can save 12171 gas per one for loop
Tools used Remix
file: main/packages/revolution/src/AuctionHouse.sol 384 for (uint256 i = 0; i < numCreators; i++) {
file: main/packages/revolution/src/CultureIndex.sol 185 for (uint i; i < creatorArrayLength; i++) { 236 for (uint i; i < creatorArrayLength; i++) { 243 for (uint i; i < creatorArrayLength; i++) { 355 for (uint256 i; i < len; i++) { 403 for (uint256 i; i < len; i++) { 407 for (uint256 i; i < len; i++) {
file: main/packages/revolution/src/ERC20TokenEmitter.sol 209 for (uint256 i = 0; i < addresses.length; i++) {
contract GasTest is DSTest { Contract0 c0; Contract1 c1; function setUp() public { c0 = new Contract0(); c1 = new Contract1(); } function testGas() public { c0.AddNum(); c1.AddNum(); } } contract Contract0 { uint256 num = 3; function AddNum() public { uint256 _num = num; for(uint i=0;i<=9;i++){ _num = _num +1; } num = _num; } } contract Contract1 { uint256 num = 3; function AddNum() public { uint256 _num = num; for(uint i=9;i>=0;i--){ _num = _num +1; } num = _num; } }
file: main/packages/revolution/src/AuctionHouse.sol 370 uint256 numCreators = verbs.getArtPieceById(_auction.verbId).creators.length; 371 address deployer = verbs.getArtPieceById(_auction.verbId).sponsor;
Per iterations saves 26 GAS
file: main/packages/revolution/src/AuctionHouse.sol 390 uint256 paymentAmount = (creatorsShare * entropyRateBps * creator.bps) / (10_000 * 10_000);
This is a useless storage writing as it assigns the default value:
file: main/packages/revolution/src/MaxHeap.sol 67 uint256 public size = 0;
file: main/packages/revolution/src/AuctionHouse.sol 346 uint256 creatorTokensEmitted = 0; 380 uint256 ethPaidToCreators = 0;
file: main/packages/revolution/src/ERC20TokenEmitter.sol 205 uint256 bpsSum = 0;
#0 - c4-pre-sort
2023-12-24T01:39:27Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2024-01-07T13:25:26Z
MarioPoneder marked the issue as grade-b