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: 35/110
Findings: 1
Award: $201.70
🌟 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
201.6999 USDC - $201.70
Highlighted below are optimizations exclusively targeting state-mutating functions and view/pure functions invoked by state-mutating functions. In the discussion that follows, only runtime gas is emphasized, given its inevitable dominance over deployment gas costs throughout the protocol's lifetime.
Please be aware that some code snippets may be shortened to conserve space, and certain code snippets may include @audit tags in comments to facilitate issue explanations."
Caching the length of calldata increases gas cost instead of reducing it
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract CacheCallDataLength { function calculateTotal(uint256[5] memory numbers) public pure returns(uint256 total) { uint256 len = numbers.length; for(uint i; i < len; ++i) { total += numbers[i]; } } }
test for test/CacheCallDataLength.t.sol:CacheCallDataLengthTest [PASS] test_calculateTotal() (gas: 1633)
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract NoCacheCallDataLength { function calculateTotal(uint256[5] memory numbers) public pure returns(uint256 total) { for(uint i; i < numbers.length; ++i) { total += numbers[i]; } } }
test for test/NoCacheCallDataLength.t.sol:NoCacheCallDataLengthTest [PASS] test_calculateTotal() (gas: 1628)
CultureIndex.validateCreatorsArray()
function by not caching the length of calldata.file: revolution/src/CultureIndex.sol 179: function validateCreatorsArray(CreatorBps[] calldata creatorArray) internal pure returns (uint256) { 180: uint256 creatorArrayLength = creatorArray.length; 181: //Require that creatorArray is not more than MAX_NUM_CREATORS to prevent gas limit issues 182: require(creatorArrayLength <= MAX_NUM_CREATORS, "Creator array must not be > MAX_NUM_CREATORS"); 183: 184: uint256 totalBps; 185: for (uint i; i < creatorArrayLength; i++) { 186: require(creatorArray[i].creator != address(0), "Invalid creator address"); 187: totalBps += creatorArray[i].bps; 188: } 189: 190: require(totalBps == 10_000, "Total BPS must sum up to 10,000"); 191: 192: return creatorArrayLength; 193: }
git diff diff --git a/packages/revolution/src/CultureIndex.sol b/packages/revolution/src/CultureIndex.sol index 88f3338..68be7b8 100644 --- a/packages/revolution/src/CultureIndex.sol +++ b/packages/revolution/src/CultureIndex.sol @@ -177,19 +177,18 @@ contract CultureIndex is * - The function will return the length of the `creatorArray`. */ function validateCreatorsArray(CreatorBps[] calldata creatorArray) internal pure returns (uint256) { - uint256 creatorArrayLength = creatorArray.length; //Require that creatorArray is not more than MAX_NUM_CREATORS to prevent gas limit issues require(creatorArrayLength <= MAX_NUM_CREATORS, "Creator array must not be > MAX_NUM_CREATORS"); uint256 totalBps; - for (uint i; i < creatorArrayLength; i++) { + for (uint i; i < creatorArray.length; i++) { require(creatorArray[i].creator != address(0), "Invalid creator address"); totalBps += creatorArray[i].bps; } require(totalBps == 10_000, "Total BPS must sum up to 10,000"); - return creatorArrayLength; + return creatorArray.length; } /**
CultureIndex._voteForMany()
function by not caching the length of calldata.file: revolution/src/CultureIndex.sol 353: function _voteForMany(uint256[] calldata pieceIds, address from) internal { 354: uint256 len = pieceIds.length; 355: for (uint256 i; i < len; i++) { 356: _vote(pieceIds[i], from); 357: } 358: }
diff --git a/packages/revolution/src/CultureIndex.sol b/packages/revolution/src/CultureIndex.sol index 88f3338..4dd3186 100644 --- a/packages/revolution/src/CultureIndex.sol +++ b/packages/revolution/src/CultureIndex.sol @@ -351,8 +351,7 @@ contract CultureIndex is * Emits a series of VoteCast event upon successful execution. */ function _voteForMany(uint256[] calldata pieceIds, address from) internal { - uint256 len = pieceIds.length; - for (uint256 i; i < len; i++) { + for (uint256 i; i < pieceIds.length; i++) { _vote(pieceIds[i], from); } }
VerbsToken.setMinter()
functionThe VerbsToken.setMinter()
function has a modifier whenMinterNotLocked
which reads state variable isMinterLocked
. If the check does not pass we have a revert. Using this modifier in our function means we run the check first and revert if the check does not pass. If the check passes, the VerbsToken.setMinter()
function has one more check require(_minter != address(0), "Minter cannot be zero address")
which basically checks that the parameter _minter
address is not zero. This is way cheaper compared to the check inside the modifier if this check does not pass, it means we would also revert, now suppose the first check(modifier) passes but the non zero fails, the gas consumed inside the modifier doing the state read be wasted. We can do this check more efficiently by prioritizing cheaper checks first but for this to work, we would need to inline our modifier as shown in the diff below:
file: revolution/src/VerbsToken.sol 209: function setMinter(address _minter) external override onlyOwner nonReentrant whenMinterNotLocked { 210: require(_minter != address(0), "Minter cannot be zero address"); 211: minter = _minter; 212: 213: emit MinterUpdated(_minter); 214: }
diff --git a/packages/revolution/src/VerbsToken.sol b/packages/revolution/src/VerbsToken.sol index 7bd9527..6726c4d 100644 --- a/packages/revolution/src/VerbsToken.sol +++ b/packages/revolution/src/VerbsToken.sol @@ -206,8 +206,9 @@ contract VerbsToken is * @notice Set the token minter. * @dev Only callable by the owner when not locked. */ - function setMinter(address _minter) external override onlyOwner nonReentrant whenMinterNotLocked { + function setMinter(address _minter) external override onlyOwner nonReentrant { require(_minter != address(0), "Minter cannot be zero address"); + require(!isMinterLocked, "Minter is locked"); minter = _minter; emit MinterUpdated(_minter);
Estimated gas saved: 2100 gas units
ERC20TokenEmitter.buyToken()
function to avoid making the same checks on every loop iteration.Having to perform the same check if (totalTokensForBuyers > 0)
on every iteration of the loop is gas consuming, redundant and unnecessary since the check is not dependent on the loop iteration. We can reduce the gas cost of the ERC20TokenEmitter.buyToken()
function if we move the check outside of the loop and then nest the loop within the check (the if statement). The diff below shows how the code could be refactored:
file: revolution/src/ERC20TokenEmitter.sol 152: function buyToken( 153: address[] calldata addresses, 154: uint[] calldata basisPointSplits, 155: ProtocolRewardAddresses calldata protocolRewardsRecipients 156: ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { . . . 183: // Tokens to emit to buyers 184: int totalTokensForBuyers = toPayTreasury > 0 ? getTokenQuoteForEther(toPayTreasury) : int(0); 185: 186: //Transfer ETH to treasury and update emitted 187: emittedTokenWad += totalTokensForBuyers; 188: if (totalTokensForCreators > 0) emittedTokenWad += totalTokensForCreators; 189: 190: //Deposit funds to treasury 191: (bool success, ) = treasury.call{ value: toPayTreasury }(new bytes(0)); 192: require(success, "Transfer failed."); 193: 194: //Transfer ETH to creators 195: if (creatorDirectPayment > 0) { 196: (success, ) = creatorsAddress.call{ value: creatorDirectPayment }(new bytes(0)); 197: require(success, "Transfer failed."); 198: } 199: 200: //Mint tokens for creators 201: if (totalTokensForCreators > 0 && creatorsAddress != address(0)) { 202: _mint(creatorsAddress, uint256(totalTokensForCreators)); 203: } 204: 205: uint256 bpsSum = 0; 206: 207: //Mint tokens to buyers 208: 209: for (uint256 i = 0; i < addresses.length; i++) { 210: if (totalTokensForBuyers > 0) { @audit same check on every loop iterations 211: // transfer tokens to address 212: _mint(addresses[i], uint256((totalTokensForBuyers * int(basisPointSplits[i])) / 10_000)); 213: } 214: bpsSum += basisPointSplits[i]; 215: } . . . 230: }
diff --git a/packages/revolution/src/ERC20TokenEmitter.sol b/packages/revolution/src/ERC20TokenEmitter.sol index e6f7d46..5c3b6b2 100644 --- a/packages/revolution/src/ERC20TokenEmitter.sol +++ b/packages/revolution/src/ERC20TokenEmitter.sol @@ -206,12 +206,13 @@ contract ERC20TokenEmitter is //Mint tokens to buyers - for (uint256 i = 0; i < addresses.length; i++) { - if (totalTokensForBuyers > 0) { + if (totalTokensForBuyers > 0) { + for (uint256 i = 0; i < addresses.length; i++) { // transfer tokens to address _mint(addresses[i], uint256((totalTokensForBuyers * int(basisPointSplits[i])) / 10_000)); + bpsSum += basisPointSplits[i]; } - bpsSum += basisPointSplits[i]; + } require(bpsSum == 10_000, "bps must add up to 10_000");
In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive computations and returning the cached result when the same inputs occur again.
Rather than having to perform the calculations msgValueRemaining - toPayTreasury
and ((msgValueRemaining - toPayTreasury) - creatorDirectPayment)
multiple times in the function we should memoize the result of the calculations (i.e cache the result of the calculation the first time) then use the cache result subsequently rather than having to re-perform the calculations. The diff below shows how the code could be refcatored:
file: revolution/src/ERC20TokenEmitter.sol 152: function buyToken( 153: address[] calldata addresses, 154: uint[] calldata basisPointSplits, 155: ProtocolRewardAddresses calldata protocolRewardsRecipients 156: ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { . . . 172: //Share of purchase amount to send to treasury 173: uint256 toPayTreasury = (msgValueRemaining * (10_000 - creatorRateBps)) / 10_000; 174: 175: //Share of purchase amount to reserve for creators 176: //Ether directly sent to creators 177: uint256 creatorDirectPayment = ((msgValueRemaining - toPayTreasury) * entropyRateBps) / 10_000; 178: //Tokens to emit to creators 179: int totalTokensForCreators = ((msgValueRemaining - toPayTreasury) - creatorDirectPayment) > 0 180: ? getTokenQuoteForEther((msgValueRemaining - toPayTreasury) - creatorDirectPayment) 181: : int(0); . . . 230: }
diff --git a/packages/revolution/src/ERC20TokenEmitter.sol b/packages/revolution/src/ERC20TokenEmitter.sol index e6f7d46..5fed0a4 100644 --- a/packages/revolution/src/ERC20TokenEmitter.sol +++ b/packages/revolution/src/ERC20TokenEmitter.sol @@ -174,10 +174,12 @@ contract ERC20TokenEmitter is //Share of purchase amount to reserve for creators //Ether directly sent to creators - uint256 creatorDirectPayment = ((msgValueRemaining - toPayTreasury) * entropyRateBps) / 10_000; + uint256 _creatorDirectPayment = msgValueRemaining - toPayTreasury; + uint256 creatorDirectPayment = ((_creatorDirectPayment) * entropyRateBps) / 10_000; //Tokens to emit to creators - int totalTokensForCreators = ((msgValueRemaining - toPayTreasury) - creatorDirectPayment) > 0 - ? getTokenQuoteForEther((msgValueRemaining - toPayTreasury) - creatorDirectPayment) + uint256 _totalTokensForCreators = _creatorDirectPayment - creatorDirectPayment; + int totalTokensForCreators = _totalTokensForCreators > 0 + ? getTokenQuoteForEther(_totalTokensForCreators) : int(0);
Erc20TokenEmitter.buyTokens()
function to avoid storage read and write in some scenariosIn the Erc20TokenEmitter.buyTokens()
function as shown below the ternary statement on [Line 184](- https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/ERC20TokenEmitter.sol#L184-#L187) show that in some scenarios the value of totalTokensForBuyers
could be 0 therefore in such scenarios we should avoid incrementing the
value of emittedTokenWad
by 0 as it makes no sense as this would cost 2100
for SLOAD
and 2900
for SSTORE
rather we could refactor the function such that the value of emittedTokenWad
is only incremented when the value of totalTokensForBuyers
is not zero. The diff below shows how we could refactor the code:
file: revolution/src/ERC20TokenEmitter.sol 152: function buyToken( 153: address[] calldata addresses, 154: uint[] calldata basisPointSplits, 155: ProtocolRewardAddresses calldata protocolRewardsRecipients 156: ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { . . . 183: // Tokens to emit to buyers 184: int totalTokensForBuyers = toPayTreasury > 0 ? getTokenQuoteForEther(toPayTreasury) : int(0); 185: 186: //Transfer ETH to treasury and update emitted 187: emittedTokenWad += totalTokensForBuyers; 188: if (totalTokensForCreators > 0) emittedTokenWad += totalTokensForCreators; . . . 230: }
diff --git a/packages/revolution/src/ERC20TokenEmitter.sol b/packages/revolution/src/ERC20TokenEmitter.sol index e6f7d46..2e66b8e 100644 --- a/packages/revolution/src/ERC20TokenEmitter.sol +++ b/packages/revolution/src/ERC20TokenEmitter.sol @@ -181,10 +181,17 @@ contract ERC20TokenEmitter is : int(0); // Tokens to emit to buyers - int totalTokensForBuyers = toPayTreasury > 0 ? getTokenQuoteForEther(toPayTreasury) : int(0); - - //Transfer ETH to treasury and update emitted - emittedTokenWad += totalTokensForBuyers; + int totalTokensForBuyers; + + if (toPayTreasury > 0) { + totalTokensForBuyers = getTokenQuoteForEther(toPayTreasury); + //Transfer ETH to treasury and update emitted + emittedTokenWad += totalTokensForBuyers; + }else{ + totalTokensForBuyers = int(0); + } + + if (totalTokensForCreators > 0) emittedTokenWad += totalTokensForCreators;
Estimated Gas saved: 5000 gas units
AuctionHouse._settleAuction()
function to avoid unnecessary external callsIn the AuctionHouse._settleAuction()
function as shown below rather than having to perform the external call verbs.getArtPieceById(_auction.verbId)
on multiple occasions in the function (even in a loop) we can make the external call once, cache the returned struct in to a memory variable then use then read from the memory variable subsequently. In implementing this change we would save above 100
gas unit for every subsequent external call.
file: revolution/src/AuctionHouse.sol 336: function _settleAuction() internal { . . . 363: if (_auction.amount > 0) { 364: // Ether going to owner of the auction 365: uint256 auctioneerPayment = (_auction.amount * (10_000 - creatorRateBps)) / 10_000; 366: 367: //Total amount of ether going to creator 368: uint256 creatorsShare = _auction.amount - auctioneerPayment; 369: 370: uint256 numCreators = verbs.getArtPieceById(_auction.verbId).creators.length; @audit external call unncessary 371: address deployer = verbs.getArtPieceById(_auction.verbId).sponsor; @audit external call unncessary 372: 373: //Build arrays for erc20TokenEmitter.buyToken 374: uint256[] memory vrgdaSplits = new uint256[](numCreators); 375: address[] memory vrgdaReceivers = new address[](numCreators); 376: 377: //Transfer auction amount to the DAO treasury 378: _safeTransferETHWithFallback(owner(), auctioneerPayment); 379: 380: uint256 ethPaidToCreators = 0; 381: 382: //Transfer creator's share to the creator, for each creator, and build arrays for erc20TokenEmitter.buyToken 383: if (creatorsShare > 0 && entropyRateBps > 0) { 384: for (uint256 i = 0; i < numCreators; i++) { 385: ICultureIndex.CreatorBps memory creator = verbs.getArtPieceById(_auction.verbId).creators[i]; @audit external call unncessary 386: vrgdaReceivers[i] = creator.creator; 387: vrgdaSplits[i] = creator.bps; 388: 389: //Calculate paymentAmount for specific creator based on BPS splits - same as multiplying by creatorDirectPayment 390: uint256 paymentAmount = (creatorsShare * entropyRateBps * creator.bps) / (10_000 * 10_000); 391: ethPaidToCreators += paymentAmount; 392: 393: //Transfer creator's share to the creator 394: _safeTransferETHWithFallback(creator.creator, paymentAmount); 395: } 396: } 397: . . . 414 }
diff --git a/packages/revolution/src/AuctionHouse.sol b/packages/revolution/src/AuctionHouse.sol index 55e55a1..5a4fc36 100644 --- a/packages/revolution/src/AuctionHouse.sol +++ b/packages/revolution/src/AuctionHouse.sol @@ -367,8 +367,10 @@ contract AuctionHouse is //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; + ICultureIndex.ArtPiece memory artPiece = verbs.getArtPieceById(_auction.verbId); + + uint256 numCreators = artPiece.creators.length; + address deployer = artPiece.sponsor; //Build arrays for erc20TokenEmitter.buyToken uint256[] memory vrgdaSplits = new uint256[](numCreators); @@ -382,7 +384,7 @@ contract AuctionHouse is //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]; + ICultureIndex.CreatorBps memory creator = artPiece.creators[i]; vrgdaReceivers[i] = creator.creator; vrgdaSplits[i] = creator.bps;
CultureIndex.createPiece()
function to avoid unnecessary external callsIn the CultureIndex.createPiece()
function as shown below rather than having to perform the external call erc20VotingToken.totalSupply()
on twice we can make the external call once, cache the returned value into a stack variable then use then read from the stack variable subsequently. In implementing this change we would save above 100
gas unit for second external call.
file: revolution/src/CultureIndex.sol 209 function createPiece( 210: ArtPieceMetadata calldata metadata, 211: CreatorBps[] calldata creatorArray 212: ) public returns (uint256) { . . . 225: newPiece.pieceId = pieceId; 226: newPiece.totalVotesSupply = _calculateVoteWeight( 227: erc20VotingToken.totalSupply(), 228: erc721VotingToken.totalSupply() 229: ); 230: newPiece.totalERC20Supply = erc20VotingToken.totalSupply(); 231: newPiece.metadata = metadata; 232: newPiece.sponsor = msg.sender; 233: newPiece.creationBlock = block.number; 234: newPiece.quorumVotes = (quorumVotesBPS * newPiece.totalVotesSupply) / 10_000; . . . 248: }
diff --git a/packages/revolution/src/CultureIndex.sol b/packages/revolution/src/CultureIndex.sol index 88f3338..7e7079a 100644 --- a/packages/revolution/src/CultureIndex.sol +++ b/packages/revolution/src/CultureIndex.sol @@ -221,13 +221,13 @@ contract CultureIndex is maxHeap.insert(pieceId, 0); ArtPiece storage newPiece = pieces[pieceId]; - + uint256 _erc20VotingTokenTotalSupply = erc20VotingToken.totalSupply(); newPiece.pieceId = pieceId; newPiece.totalVotesSupply = _calculateVoteWeight( - erc20VotingToken.totalSupply(), + _erc20VotingTokenTotalSupply, erc721VotingToken.totalSupply() ); - newPiece.totalERC20Supply = erc20VotingToken.totalSupply(); + newPiece.totalERC20Supply = _erc20VotingTokenTotalSupply; newPiece.metadata = metadata; newPiece.sponsor = msg.sender; newPiece.creationBlock = block.number;
Estimated Gas saved: 97 gas units
CultureIndex.createPiece()
to avoid unnecessary SLOADIn the CultureIndex.createPiece()
function as shown below we can cache the calculation _calculateVoteWeight(erc20VotingToken.totalSupply(), erc721VotingToken.totalSupply())
in to a stack variable, then assign the stack variable to newPiece.totalVotesSupply
then use the stack variable in place of newPiece.totalVotesSupply
subsequently. Also we can cache the calculation (quorumVotesBPS * newPiece.totalVotesSupply) / 10_000
into a stack variable then assign the stack variable to newPiece.quorumVotes
then use the stack variable in place of newPiece.quorumVotes
subsequently.
file: revolution/src/CultureIndex.sol 209: function createPiece( 210: ArtPieceMetadata calldata metadata, 211: CreatorBps[] calldata creatorArray 212: ) public returns (uint256) { 213: uint256 creatorArrayLength = validateCreatorsArray(creatorArray); 214: 215: // Validate the media type and associated data 216: validateMediaType(metadata); 217: 218: uint256 pieceId = _currentPieceId++; 219: 220: /// @dev Insert the new piece into the max heap 221: maxHeap.insert(pieceId, 0); 222: 223: ArtPiece storage newPiece = pieces[pieceId]; 224: 225: newPiece.pieceId = pieceId; 226: newPiece.totalVotesSupply = _calculateVoteWeight( 227: erc20VotingToken.totalSupply(), 228: erc721VotingToken.totalSupply() 229: ); 230: newPiece.totalERC20Supply = erc20VotingToken.totalSupply(); 231: newPiece.metadata = metadata; 232: newPiece.sponsor = msg.sender; 233: newPiece.creationBlock = block.number; 234: newPiece.quorumVotes = (quorumVotesBPS * newPiece.totalVotesSupply) / 10_000; 235: 236: for (uint i; i < creatorArrayLength; i++) { 237: newPiece.creators.push(creatorArray[i]); 238: } 239 240: emit PieceCreated(pieceId, msg.sender, metadata, newPiece.quorumVotes, newPiece.totalVotesSupply); 241: 242: // Emit an event for each creator 243: for (uint i; i < creatorArrayLength; i++) { 244: emit PieceCreatorAdded(pieceId, creatorArray[i].creator, msg.sender, creatorArray[i].bps); 245: } 246: 247: return newPiece.pieceId; 248: }
diff --git a/packages/revolution/src/CultureIndex.sol b/packages/revolution/src/CultureIndex.sol index 88f3338..c760096 100644 --- a/packages/revolution/src/CultureIndex.sol +++ b/packages/revolution/src/CultureIndex.sol @@ -222,22 +222,26 @@ contract CultureIndex is ArtPiece storage newPiece = pieces[pieceId]; - newPiece.pieceId = pieceId; - newPiece.totalVotesSupply = _calculateVoteWeight( + uint256 _voteWeight = _calculateVoteWeight( erc20VotingToken.totalSupply(), erc721VotingToken.totalSupply() ); + + uint256 _quorumVotes = (quorumVotesBPS * _voteWeight) / 10_000; + + newPiece.pieceId = pieceId; + newPiece.totalVotesSupply = _voteWeight; newPiece.totalERC20Supply = erc20VotingToken.totalSupply(); newPiece.metadata = metadata; newPiece.sponsor = msg.sender; newPiece.creationBlock = block.number; - newPiece.quorumVotes = (quorumVotesBPS * newPiece.totalVotesSupply) / 10_000; + newPiece.quorumVotes = _quorumVotes; for (uint i; i < creatorArrayLength; i++) { newPiece.creators.push(creatorArray[i]); } - emit PieceCreated(pieceId, msg.sender, metadata, newPiece.quorumVotes, newPiece.totalVotesSupply); + emit PieceCreated(pieceId, msg.sender, metadata, _quorumVotes, _voteWeight); // Emit an event for each creator for (uint i; i < creatorArrayLength; i++) {
ERC20TokenEmitter.buyToken()
to avoid unnecessary SLOADIn the ERC20TokenEmitter.buyToken()
we can avoid unnecessary SLOAD
by caching the creatorsAddress
and treasury
state variables into stack variables and using the stack variables in place the state variables for subsequent reads.
file: revolution/src/ERC20TokenEmitter.sol 152: function buyToken( 153: address[] calldata addresses, 154: uint[] calldata basisPointSplits, 155: ProtocolRewardAddresses calldata protocolRewardsRecipients 156: ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { 157: //prevent treasury from paying itself 158: require(msg.sender != treasury && msg.sender != creatorsAddress, "Funds recipient cannot buy tokens"); 159: 160: require(msg.value > 0, "Must send ether"); 161: // ensure the same number of addresses and bps 162: require(addresses.length == basisPointSplits.length, "Parallel arrays required"); 163: 164: // Get value left after protocol rewards 165: uint256 msgValueRemaining = _handleRewardsAndGetValueToSend( 166: msg.value, 167: protocolRewardsRecipients.builder, 168: protocolRewardsRecipients.purchaseReferral, 169: protocolRewardsRecipients.deployer 170: ); 171: 172: //Share of purchase amount to send to treasury 173: uint256 toPayTreasury = (msgValueRemaining * (10_000 - creatorRateBps)) / 10_000; 174: 175: //Share of purchase amount to reserve for creators 176: //Ether directly sent to creators 177: uint256 creatorDirectPayment = ((msgValueRemaining - toPayTreasury) * entropyRateBps) / 10_000; 178: //Tokens to emit to creators 179: int totalTokensForCreators = ((msgValueRemaining - toPayTreasury) - creatorDirectPayment) > 0 180: ? getTokenQuoteForEther((msgValueRemaining - toPayTreasury) - creatorDirectPayment) 181: : int(0); 182: 183: // Tokens to emit to buyers 184: int totalTokensForBuyers = toPayTreasury > 0 ? getTokenQuoteForEther(toPayTreasury) : int(0); 185: 186: //Transfer ETH to treasury and update emitted 187: emittedTokenWad += totalTokensForBuyers; 188: if (totalTokensForCreators > 0) emittedTokenWad += totalTokensForCreators; 189: 190: //Deposit funds to treasury 191: (bool success, ) = treasury.call{ value: toPayTreasury }(new bytes(0)); 192: require(success, "Transfer failed."); 193: 194: //Transfer ETH to creators 195: if (creatorDirectPayment > 0) { 196: (success, ) = creatorsAddress.call{ value: creatorDirectPayment }(new bytes(0)); 197: require(success, "Transfer failed."); 198: } 199: 200: //Mint tokens for creators 201: if (totalTokensForCreators > 0 && creatorsAddress != address(0)) { 202: _mint(creatorsAddress, uint256(totalTokensForCreators)); 203: } 204: 205 uint256 bpsSum = 0; 206: 207: //Mint tokens to buyers 208: 209 for (uint256 i = 0; i < addresses.length; i++) { 210: if (totalTokensForBuyers > 0) { 211: // transfer tokens to address 212: _mint(addresses[i], uint256((totalTokensForBuyers * int(basisPointSplits[i])) / 10_000)); 213: } 214: bpsSum += basisPointSplits[i]; 215: } 216: 217: require(bpsSum == 10_000, "bps must add up to 10_000"); 218: 219: emit PurchaseFinalized( 220: msg.sender, 221: msg.value, 222: toPayTreasury, 223: msg.value - msgValueRemaining, 224: uint256(totalTokensForBuyers), 225: uint256(totalTokensForCreators), 226: creatorDirectPayment 227: ); 228: 229: return uint256(totalTokensForBuyers); 230 }
diff --git a/packages/revolution/src/ERC20TokenEmitter.sol b/packages/revolution/src/ERC20TokenEmitter.sol index e6f7d46..2c64cbc 100644 --- a/packages/revolution/src/ERC20TokenEmitter.sol +++ b/packages/revolution/src/ERC20TokenEmitter.sol @@ -155,7 +155,9 @@ contract ERC20TokenEmitter is ProtocolRewardAddresses calldata protocolRewardsRecipients ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { //prevent treasury from paying itself - require(msg.sender != treasury && msg.sender != creatorsAddress, "Funds recipient cannot buy tokens"); + address _treasury = treasury; + address _creatorsAddress = creatorsAddress; + require(msg.sender != _treasury && msg.sender != _creatorsAddress, "Funds recipient cannot buy tokens"); require(msg.value > 0, "Must send ether"); // ensure the same number of addresses and bps @@ -188,18 +190,18 @@ contract ERC20TokenEmitter is if (totalTokensForCreators > 0) emittedTokenWad += totalTokensForCreators; //Deposit funds to treasury - (bool success, ) = treasury.call{ value: toPayTreasury }(new bytes(0)); + (bool success, ) = _treasury.call{ value: toPayTreasury }(new bytes(0)); require(success, "Transfer failed."); //Transfer ETH to creators if (creatorDirectPayment > 0) { - (success, ) = creatorsAddress.call{ value: creatorDirectPayment }(new bytes(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)); + if (totalTokensForCreators > 0 && _creatorsAddress != address(0)) { + _mint(_creatorsAddress, uint256(totalTokensForCreators)); } uint256 bpsSum = 0;
AuctionHouse._settleAuction()
to avoid unnecessary SLOADfile: revolution/src/AuctionHouse.sol 336: function _settleAuction() internal { 337: IAuctionHouse.Auction memory _auction = auction; 338: 339: require(_auction.startTime != 0, "Auction hasn't begun"); 340: require(!_auction.settled, "Auction has already been settled"); 341: //slither-disable-next-line timestamp 342: require(block.timestamp >= _auction.endTime, "Auction hasn't completed"); 343: 344: auction.settled = true; 345: 346: uint256 creatorTokensEmitted = 0; 347: // Check if contract balance is greater than reserve price 348: if (address(this).balance < reservePrice) { 349: // If contract balance is less than reserve price, refund to the last bidder 350: if (_auction.bidder != address(0)) { 351: _safeTransferETHWithFallback(_auction.bidder, _auction.amount); 352: } 353: 354: // And then burn the Noun 355: verbs.burn(_auction.verbId); 356: } else { 357: //If no one has bid, burn the Verb 358: if (_auction.bidder == address(0)) 359: verbs.burn(_auction.verbId); 360: //If someone has bid, transfer the Verb to the winning bidder 361: else verbs.transferFrom(address(this), _auction.bidder, _auction.verbId); 362: 363: if (_auction.amount > 0) { 364: // Ether going to owner of the auction 365: uint256 auctioneerPayment = (_auction.amount * (10_000 - creatorRateBps)) / 10_000; 366: 367: //Total amount of ether going to creator 368: uint256 creatorsShare = _auction.amount - auctioneerPayment; 369: 370: uint256 numCreators = verbs.getArtPieceById(_auction.verbId).creators.length; 371: address deployer = verbs.getArtPieceById(_auction.verbId).sponsor; 372: 373: //Build arrays for erc20TokenEmitter.buyToken 374: uint256[] memory vrgdaSplits = new uint256[](numCreators); 375: address[] memory vrgdaReceivers = new address[](numCreators); 376: 377: //Transfer auction amount to the DAO treasury 378: _safeTransferETHWithFallback(owner(), auctioneerPayment); 379: 380: uint256 ethPaidToCreators = 0; 381: 382: //Transfer creator's share to the creator, for each creator, and build arrays for erc20TokenEmitter.buyToken 383: if (creatorsShare > 0 && entropyRateBps > 0) { 384: for (uint256 i = 0; i < numCreators; i++) { 385: ICultureIndex.CreatorBps memory creator = verbs.getArtPieceById(_auction.verbId).creators[i]; 386: vrgdaReceivers[i] = creator.creator; 387: vrgdaSplits[i] = creator.bps; 388: 389: //Calculate paymentAmount for specific creator based on BPS splits - same as multiplying by creatorDirectPayment 390: uint256 paymentAmount = (creatorsShare * entropyRateBps * creator.bps) / (10_000 * 10_000); 391: ethPaidToCreators += paymentAmount; 392: 393: //Transfer creator's share to the creator 394: _safeTransferETHWithFallback(creator.creator, paymentAmount); 395: } 396: } 397: 398: //Buy token from ERC20TokenEmitter for all the creators 399: if (creatorsShare > ethPaidToCreators) { 400: creatorTokensEmitted = erc20TokenEmitter.buyToken{ value: creatorsShare - ethPaidToCreators }( 401: vrgdaReceivers, 402: vrgdaSplits, 403: IERC20TokenEmitter.ProtocolRewardAddresses({ 404: builder: address(0), 405: purchaseReferral: address(0), 406: deployer: deployer 407: }) 408: ); 409: } 410: } 411: } 412: 413: emit AuctionSettled(_auction.verbId, _auction.bidder, _auction.amount, creatorTokensEmitted); 414: }
VerbsToken._mintTo()
to avoid unnecessary SLOADIn the VerbsToken._mintTo()
we can avoid unnecessary SLOAD
by caching the cultureIndex
state variables into stack variables and using the stack variables in place the state variables for subsequent reads.
file: revolution/src/VerbsToken.sol 281: function _mintTo(address to) internal returns (uint256) { 282: ICultureIndex.ArtPiece memory artPiece = cultureIndex.getTopVotedPiece(); 283: 284: // Check-Effects-Interactions Pattern 285: // Perform all checks 286: require( 287: artPiece.creators.length <= cultureIndex.MAX_NUM_CREATORS(), 288: "Creator array must not be > MAX_NUM_CREATORS" 289: ); 290: 291: // Use try/catch to handle potential failure 292: try cultureIndex.dropTopVotedPiece() returns (ICultureIndex.ArtPiece memory _artPiece) { 293: artPiece = _artPiece; 294: uint256 verbId = _currentVerbId++; 295: . . . 319: }
diff --git a/packages/revolution/src/VerbsToken.sol b/packages/revolution/src/VerbsToken.sol index 7bd9527..e4b139f 100644 --- a/packages/revolution/src/VerbsToken.sol +++ b/packages/revolution/src/VerbsToken.sol @@ -279,17 +279,18 @@ contract VerbsToken is * @notice Mint a Verb with `verbId` to the provided `to` address. Pulls the top voted art piece from the CultureIndex. */ function _mintTo(address to) internal returns (uint256) { - ICultureIndex.ArtPiece memory artPiece = cultureIndex.getTopVotedPiece(); + ICultureIndex _cultureIndex = cultureIndex; + ICultureIndex.ArtPiece memory artPiece = _cultureIndex.getTopVotedPiece(); // Check-Effects-Interactions Pattern // Perform all checks require( - artPiece.creators.length <= cultureIndex.MAX_NUM_CREATORS(), + artPiece.creators.length <= _cultureIndex.MAX_NUM_CREATORS(), "Creator array must not be > MAX_NUM_CREATORS" ); // Use try/catch to handle potential failure - try cultureIndex.dropTopVotedPiece() returns (ICultureIndex.ArtPiece memory _artPiece) { + try _cultureIndex.dropTopVotedPiece() returns (ICultureIndex.ArtPiece memory _artPiece) { artPiece = _artPiece; uint256 verbId = _currentVerbId++;
Estimated gas saved: 194 gas units
pieceId
in place of newPiece.pieceId
We can save 1 SLOAD
2100
gas units if we use pieceId
in place of newPiece.pieceId
since their values are equal
file: 209: function createPiece( 210: ArtPieceMetadata calldata metadata, 211: CreatorBps[] calldata creatorArray 212: ) public returns (uint256) { 213: uint256 creatorArrayLength = validateCreatorsArray(creatorArray); 214: 215: // Validate the media type and associated data 216: validateMediaType(metadata); 217: 218: uint256 pieceId = _currentPieceId++; 219: 220: /// @dev Insert the new piece into the max heap 221: maxHeap.insert(pieceId, 0); 222: 223: ArtPiece storage newPiece = pieces[pieceId]; 224: 225: newPiece.pieceId = pieceId; 226: newPiece.totalVotesSupply = _calculateVoteWeight( 227: erc20VotingToken.totalSupply(), 228: erc721VotingToken.totalSupply() 229: ); 230: newPiece.totalERC20Supply = erc20VotingToken.totalSupply(); 231: newPiece.metadata = metadata; 232: newPiece.sponsor = msg.sender; 233: newPiece.creationBlock = block.number; 234: newPiece.quorumVotes = (quorumVotesBPS * newPiece.totalVotesSupply) / 10_000; 235: 236: for (uint i; i < creatorArrayLength; i++) { 237: newPiece.creators.push(creatorArray[i]); 238: } 239: 240: emit PieceCreated(pieceId, msg.sender, metadata, newPiece.quorumVotes, newPiece.totalVotesSupply); 241: 242: // Emit an event for each creator 243: for (uint i; i < creatorArrayLength; i++) { 244: emit PieceCreatorAdded(pieceId, creatorArray[i].creator, msg.sender, creatorArray[i].bps); 245: } 246: 247: return newPiece.pieceId; @audit use pieceId to avoid SLOAD 248: }
diff --git a/packages/revolution/src/CultureIndex.sol b/packages/revolution/src/CultureIndex.sol index 88f3338..a6b0034 100644 --- a/packages/revolution/src/CultureIndex.sol +++ b/packages/revolution/src/CultureIndex.sol @@ -221,13 +221,13 @@ contract CultureIndex is @@ -244,7 +244,7 @@ contract CultureIndex is emit PieceCreatorAdded(pieceId, creatorArray[i].creator, msg.sender, creatorArray[i].bps); } - return newPiece.pieceId; + return pieceId; } /**
Estimated Gas saved: 2100 gas units.
Solidity does not recognize null as a value, so uint variables are initialized to zero. Setting a uint variable to zero is redundant and can waste gas.
Using a simple Remix test
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract InitializeDefaultValue { uint256 num = 0; }
Deployment gas cost: 14669
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract NoInitializeDefaultValue { uint256 num; }
Deployment gas cost: 12464
file: revolution/src/MaxHeap.sol 67: uint256 public size = 0;
diff --git a/packages/revolution/src/MaxHeap.sol b/packages/revolution/src/MaxHeap.sol index 5dfb81c..5fb9125 100644 --- a/packages/revolution/src/MaxHeap.sol +++ b/packages/revolution/src/MaxHeap.sol @@ -64,7 +64,7 @@ contract MaxHeap is VersionedContract, UUPS, Ownable2StepUpgradeable, Reentrancy /// @notice Struct to represent an item in the heap by it's ID mapping(uint256 => uint256) public heap; - uint256 public size = 0; + uint256 public size; /// @notice Mapping to keep track of the value of an item in the heap mapping(uint256 => uint256) public valueMapping;
file: revolution/src/ERC20TokenEmitter.sol 152: function buyToken( 153: address[] calldata addresses, 154: uint[] calldata basisPointSplits, 155: ProtocolRewardAddresses calldata protocolRewardsRecipients 156: ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { . . . 190: //Deposit funds to treasury 191: (bool success, ) = treasury.call{ value: toPayTreasury }(new bytes(0)); //@audit `` is cheaper than new bytes(0) 192: require(success, "Transfer failed."); 193: 194: //Transfer ETH to creators 195: if (creatorDirectPayment > 0) { 196: (success, ) = creatorsAddress.call{ value: creatorDirectPayment }(new bytes(0)); //@audit `` is cheaper than new bytes(0) 197: require(success, "Transfer failed."); 198: } . . . 230: }
diff --git a/packages/revolution/src/ERC20TokenEmitter.sol b/packages/revolution/src/ERC20TokenEmitter.sol index e6f7d46..958cbc2 100644 --- a/packages/revolution/src/ERC20TokenEmitter.sol +++ b/packages/revolution/src/ERC20TokenEmitter.sol @@ -188,12 +188,12 @@ contract ERC20TokenEmitter is if (totalTokensForCreators > 0) emittedTokenWad += totalTokensForCreators; //Deposit funds to treasury - (bool success, ) = treasury.call{ value: toPayTreasury }(new bytes(0)); + (bool success, ) = treasury.call{ value: toPayTreasury }(""); require(success, "Transfer failed."); //Transfer ETH to creators if (creatorDirectPayment > 0) { - (success, ) = creatorsAddress.call{ value: creatorDirectPayment }(new bytes(0)); + (success, ) = creatorsAddress.call{ value: creatorDirectPayment }(""); require(success, "Transfer failed."); }
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 alot of gas in a function that may ultimately revert in the unhappy case.
The require statements of the CultureIndex._vote()
function should be re-orderd the statement require(voter != address(0), "Invalid voter address")
should move to the top of the top function as its the less gas consuming statement of the bunch. So that in scenarios in which it fails the EVM would waste gas executing require(pieceId < _currentPieceId, "Invalid piece ID")
which involves an SLOAD
. The diff below shows how the code should be refactored:
file: revolution/src/CultureIndex.sol 307: function _vote(uint256 pieceId, address voter) internal { 308: require(pieceId < _currentPieceId, "Invalid piece ID"); 309: require(voter != address(0), "Invalid voter address"); //@audit cheaper require statement should move to function top 310: require(!pieces[pieceId].isDropped, "Piece has already been dropped"); 311: require(!(votes[pieceId][voter].voterAddress != address(0)), "Already voted"); 312: 313: uint256 weight = _getPastVotes(voter, pieces[pieceId].creationBlock); 314: require(weight > minVoteWeight, "Weight must be greater than minVoteWeight"); 315: 316: votes[pieceId][voter] = Vote(voter, weight); 317: totalVoteWeights[pieceId] += weight; 318: 319: uint256 totalWeight = totalVoteWeights[pieceId]; 320: 321: // TODO add security consideration here based on block created to prevent flash attacks on drops? 322: maxHeap.updateValue(pieceId, totalWeight); 323: emit VoteCast(pieceId, voter, weight, totalWeight); 324: }
diff --git a/packages/revolution/src/CultureIndex.sol b/packages/revolution/src/CultureIndex.sol index 88f3338..beb7bc3 100644 --- a/packages/revolution/src/CultureIndex.sol +++ b/packages/revolution/src/CultureIndex.sol @@ -305,8 +305,8 @@ contract CultureIndex is * Emits a VoteCast event upon successful execution. */ function _vote(uint256 pieceId, address voter) internal { - require(pieceId < _currentPieceId, "Invalid piece ID"); require(voter != address(0), "Invalid voter address"); + require(pieceId < _currentPieceId, "Invalid piece ID"); require(!pieces[pieceId].isDropped, "Piece has already been dropped"); require(!(votes[pieceId][voter].voterAddress != address(0)), "Already voted");
Estimated Gas saved: 2100 gas units
The require statements of the ERC20TokenEmitter.ERC20TokenEmitter()
function should be re-orderd the statement require(msg.value > 0, "Must send ether")
should move to the top of the top function as its the less gas consuming statement of the bunch. So that in scenarios in which it fails the EVM would waste gas executing require(msg.sender != treasury && msg.sender != creatorsAddress, "Funds recipient cannot buy tokens")
which involves 2 SLOAD
. The diff below shows how the code should be refactored:
file: revolution/src/ERC20TokenEmitter.sol 152: function buyToken( 153: address[] calldata addresses, 154: uint[] calldata basisPointSplits, 155: ProtocolRewardAddresses calldata protocolRewardsRecipients 156: ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { 157: //prevent treasury from paying itself 158: require(msg.sender != treasury && msg.sender != creatorsAddress, "Funds recipient cannot buy tokens"); 159: 160: require(msg.value > 0, "Must send ether"); //@audit should move to function top 161: // ensure the same number of addresses and bps 162: require(addresses.length == basisPointSplits.length, "Parallel arrays required"); . . . 230: }
diff --git a/packages/revolution/src/ERC20TokenEmitter.sol b/packages/revolution/src/ERC20TokenEmitter.sol index e6f7d46..8d777bb 100644 --- a/packages/revolution/src/ERC20TokenEmitter.sol +++ b/packages/revolution/src/ERC20TokenEmitter.sol @@ -154,12 +154,14 @@ contract ERC20TokenEmitter is uint[] calldata basisPointSplits, ProtocolRewardAddresses calldata protocolRewardsRecipients ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { + + require(msg.value > 0, "Must send ether"); + require(addresses.length == basisPointSplits.length, "Parallel arrays required"); //prevent treasury from paying itself require(msg.sender != treasury && msg.sender != creatorsAddress, "Funds recipient cannot buy tokens"); - require(msg.value > 0, "Must send ether"); // ensure the same number of addresses and bps - require(addresses.length == basisPointSplits.length, "Parallel arrays required"); + // Get value left after protocol rewards uint256 msgValueRemaining = _handleRewardsAndGetValueToSend(
The require statements of the AuctionHouse.setCreatorRateBps()
function should be re-orderd the statement require(_creatorRateBps <= 10_000, "Creator rate must be less than or equal to 10_000")
should move to the top of the top function as its the less gas consuming statement of the bunch. So that in scenarios in which it fails the EVM would waste gas executing require(_creatorRateBps >= minCreatorRateBps, "Creator rate must be greater than or equal to minCreatorRateBps")
which involves an SLOAD
. The diff below shows how the code should be refactored:
file: revolution/src/AuctionHouse.sol 217: function setCreatorRateBps(uint256 _creatorRateBps) external onlyOwner { 218: require( 219: _creatorRateBps >= minCreatorRateBps, 220: "Creator rate must be greater than or equal to minCreatorRateBps" 221: ); 222: require(_creatorRateBps <= 10_000, "Creator rate must be less than or equal to 10_000"); 223: creatorRateBps = _creatorRateBps; 224: 225: emit CreatorRateBpsUpdated(_creatorRateBps); 226: }
diff --git a/packages/revolution/src/AuctionHouse.sol b/packages/revolution/src/AuctionHouse.sol index 55e55a1..b5f03ce 100644 --- a/packages/revolution/src/AuctionHouse.sol +++ b/packages/revolution/src/AuctionHouse.sol @@ -215,11 +215,12 @@ contract AuctionHouse is * @param _creatorRateBps New creator rate in basis points. */ function setCreatorRateBps(uint256 _creatorRateBps) external onlyOwner { + require(_creatorRateBps <= 10_000, "Creator rate must be less than or equal to 10_000"); require( _creatorRateBps >= minCreatorRateBps, "Creator rate must be greater than or equal to minCreatorRateBps" ); - require(_creatorRateBps <= 10_000, "Creator rate must be less than or equal to 10_000"); + creatorRateBps = _creatorRateBps; emit CreatorRateBpsUpdated(_creatorRateBps);
Estimated gas saved: 2100 gas units
The require statements of the AuctionHouse.setMinCreatorRateBps()
function should be re-orderd the statement require(_minCreatorRateBps <= 10_000, "Min creator rate must be less than or equal to 10_000")
should move to the top of the top function as its the less gas consuming statement of the bunch. So that in scenarios in which it fails the EVM would waste gas executing require(_minCreatorRateBps <= creatorRateBps, "Min creator rate must be less than or equal to creator rate")
which involves an SLOAD
. The diff below shows how the code should be refactored:
file: revolution/src/AuctionHouse.sol 233: function setMinCreatorRateBps(uint256 _minCreatorRateBps) external onlyOwner { 234: require(_minCreatorRateBps <= creatorRateBps, "Min creator rate must be less than or equal to creator rate"); 235: require(_minCreatorRateBps <= 10_000, "Min creator rate must be less than or equal to 10_000"); 236: 237: //ensure new min rate cannot be lower than previous min rate 238: require( 239: _minCreatorRateBps > minCreatorRateBps, 240: "Min creator rate must be greater than previous minCreatorRateBps" 241: ); 242: 243: minCreatorRateBps = _minCreatorRateBps; 244: 245: emit MinCreatorRateBpsUpdated(_minCreatorRateBps); 246: }
diff --git a/packages/revolution/src/AuctionHouse.sol b/packages/revolution/src/AuctionHouse.sol index 55e55a1..2e0579e 100644 --- a/packages/revolution/src/AuctionHouse.sol +++ b/packages/revolution/src/AuctionHouse.sol @@ -231,8 +231,9 @@ contract AuctionHouse is * @param _minCreatorRateBps New minimum creator rate in basis points. */ function setMinCreatorRateBps(uint256 _minCreatorRateBps) external onlyOwner { - require(_minCreatorRateBps <= creatorRateBps, "Min creator rate must be less than or equal to creator rate"); require(_minCreatorRateBps <= 10_000, "Min creator rate must be less than or equal to 10_000"); + require(_minCreatorRateBps <= creatorRateBps, "Min creator rate must be less than or equal to creator rate"); + //ensure new min rate cannot be lower than previous min rate require(
The solidity compiler introduces some checks to avoid an underflow, but in some scenarios where it is impossible for underflow to occur we can use unchecked blocks to have some gas savings.
file: revolution/src/MaxHeap.sol 78: function parent(uint256 pos) private pure returns (uint256) { 79: require(pos != 0, "Position should not be zero"); 80: return (pos - 1) / 2; //@audit can be unchecked since (pos-1) cannot underflow 81: }
file: 156: function extractMax() external onlyAdmin returns (uint256, uint256) { 157: require(size > 0, "Heap is empty"); 158: 159: uint256 popped = heap[0]; 160: heap[0] = heap[--size]; //@audit can be unchecked since (--size) cannot underflow 161: maxHeapify(0); 162: 163: return (popped, valueMapping[popped]); 164: }
When a storage variable goes from zero to non-zero, the user must pay 22,100 gas total (20,000 gas for a zero to non-zero write and 2,100 for a cold storage access).
This is why the Openzeppelin reentrancy guard registers functions as active or not with 1 and 2 rather than 0 and 1. It only costs 5,000 gas to alter a storage variable from non-zero to non-zero.
MaxHeap
contract to avoid zero to non-zero storage writesWhen declaring the variable admin
the default value is applied since we do not assign anything, the default value of address is address(0). We then call the function initialize() and here we assign the value of _admin
to admin
. This means our value is updated from address(0)
to the value of _admin
. Since the initialize() function will always be called first, we can initialize our variable admin
with address(1)
during declaration which should avoid the zero to non zero writes. The diff below shows how the code could be refactored:
file: revolution/src/MaxHeap.sol 16: address public admin; . . . 55: function initialize(address _initialOwner, address _admin) public initializer { 56: require(msg.sender == address(manager), "Only manager can initialize"); 57: 58: admin = _admin; 59: 60: __Ownable_init(_initialOwner); 61: __ReentrancyGuard_init(); 62: }
diff --git a/packages/revolution/src/MaxHeap.sol b/packages/revolution/src/MaxHeap.sol index 5dfb81c..e4a20a8 100644 --- a/packages/revolution/src/MaxHeap.sol +++ b/packages/revolution/src/MaxHeap.sol @@ -13,7 +13,7 @@ import { VersionedContract } from "./version/VersionedContract.sol"; /// @author Written by rocketman and gpt4 contract MaxHeap is VersionedContract, UUPS, Ownable2StepUpgradeable, ReentrancyGuardUpgradeable { /// @notice The parent contract that is allowed to update the data store - address public admin; + address public admin = address(1);
CultureIndex
contract to avoid zero to non-zero storage writesERC20TokenEmitter
contract to avoid zero to non-zero storage writesAuctionHouse
contract to avoid zero to non-zero storage writesAuctionHouse
contract to avoid zero to non-zero storage writesIn the following functions a complete struct is being copied to memory to end up using only some of its attributes.
AuctionHouse.createBid()
to avoid unnecessary copy of storage struct to memoryIn the AuctionHouse.createBid()
function as shown below the storage struct auction
was copied into memory variable _auction
but only the verbId
, endtime
, amount
, bidder
attributes were used in the function thereby making this process gas expensive as some attributes (starttime
, settled
) were copied into memory but were not used in the function. We can refactor the AuctionHouse.createBid()
function to be more gas efficient by not copying the auction
storage struct into memory rather we cache only the auction
attributes needed in the function. The diff below shows how the code could be refactored:
file: revolution/src/AuctionHouse.sol 171: function createBid(uint256 verbId, address bidder) external payable override nonReentrant { 172: IAuctionHouse.Auction memory _auction = auction; 173: 174: //require bidder is valid address 175: require(bidder != address(0), "Bidder cannot be zero address"); 176: require(_auction.verbId == verbId, "Verb not up for auction"); 177: //slither-disable-next-line timestamp 178: require(block.timestamp < _auction.endTime, "Auction expired"); 179: require(msg.value >= reservePrice, "Must send at least reservePrice"); 180: require( 181: msg.value >= _auction.amount + ((_auction.amount * minBidIncrementPercentage) / 100), 182: "Must send more than last bid by minBidIncrementPercentage amount" 183: ); 184: 185: address payable lastBidder = _auction.bidder; 186: 187: auction.amount = msg.value; 188: auction.bidder = payable(bidder); 189: 190: // Extend the auction if the bid was received within `timeBuffer` of the auction end time 191: bool extended = _auction.endTime - block.timestamp < timeBuffer; 192: if (extended) auction.endTime = _auction.endTime = block.timestamp + timeBuffer; 193: 194: // Refund the last bidder, if applicable 195: if (lastBidder != address(0)) _safeTransferETHWithFallback(lastBidder, _auction.amount); 196: 197: emit AuctionBid(_auction.verbId, bidder, msg.sender, msg.value, extended); 198: 199: if (extended) emit AuctionExtended(_auction.verbId, _auction.endTime); 200: }
diff --git a/packages/revolution/src/AuctionHouse.sol b/packages/revolution/src/AuctionHouse.sol index 55e55a1..76818ae 100644 --- a/packages/revolution/src/AuctionHouse.sol +++ b/packages/revolution/src/AuctionHouse.sol @@ -169,34 +169,36 @@ contract AuctionHouse is * @param bidder The address of the bidder. */ function createBid(uint256 verbId, address bidder) external payable override nonReentrant { - IAuctionHouse.Auction memory _auction = auction; + + uint256 _auctionVerbId = auction.verbId; + uint256 _auctionEndtime = auction.endTime; + uint256 _auctionAmount = auction.amount; + address payable _auctionbidder = auction.bidder; //require bidder is valid address require(bidder != address(0), "Bidder cannot be zero address"); - require(_auction.verbId == verbId, "Verb not up for auction"); + require(_auctionVerbId == verbId, "Verb not up for auction"); //slither-disable-next-line timestamp - require(block.timestamp < _auction.endTime, "Auction expired"); + require(block.timestamp < _auctionEndtime, "Auction expired"); require(msg.value >= reservePrice, "Must send at least reservePrice"); require( - msg.value >= _auction.amount + ((_auction.amount * minBidIncrementPercentage) / 100), + msg.value >= _auctionAmount + ((_auctionAmount * 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; + bool extended = _auctionEndtime - block.timestamp < timeBuffer; + if (extended) auction.endTime = _auctionEndtime = block.timestamp + timeBuffer; // Refund the last bidder, if applicable - if (lastBidder != address(0)) _safeTransferETHWithFallback(lastBidder, _auction.amount); + if (_auctionbidder != address(0)) _safeTransferETHWithFallback(_auctionbidder, _auctionAmount); - emit AuctionBid(_auction.verbId, bidder, msg.sender, msg.value, extended); + emit AuctionBid(_auctionVerbId, bidder, msg.sender, msg.value, extended); - if (extended) emit AuctionExtended(_auction.verbId, _auction.endTime); + if (extended) emit AuctionExtended(_auctionVerbId, _auctionEndtime); }
The functions below read storage slots that are previously read in the functions that invoke them. We can refactor the external/internal functions to pass cached storage variables as stack variables and avoid the extra storage reads that would otherwise take place in the internal functions.
As you embark on incorporating the recommended optimizations, we want to emphasize the utmost importance of proceeding with vigilance and dedicating thorough efforts to comprehensive testing. It is of paramount significance to ensure that the proposed alterations do not inadvertently introduce fresh vulnerabilities, while also successfully achieving the anticipated enhancements in performance.
We strongly advise conducting a meticulous and exhaustive evaluation of the modifications made to the codebase. This rigorous scrutiny and exhaustive assessment will play a pivotal role in affirming both the security and efficacy of the refactored code. Your careful attention to detail, coupled with the implementation of a robust testing framework, will provide the necessary assurance that the refined code aligns with your security objectives and effectively fulfills the intended performance optimizations.
#0 - c4-pre-sort
2023-12-24T01:35:52Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2024-01-07T13:40:02Z
MarioPoneder marked the issue as grade-a
#2 - c4-sponsor
2024-01-09T15:39:14Z
rocketman-21 (sponsor) acknowledged