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: 85/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
if
conditions instead of one with multiple of ORsif (_protocolRewards == address(0) || _revolutionRewardRecipient == address(0)) revert("Invalid Address Zero")
should be changed to:
if (_protocolRewards == address(0)) revert("Invalid Address Zero"); if (_revolutionRewardRecipient == address(0)) revert("Invalid Address Zero");
if (paymentAmountWei <= minPurchaseAmount || paymentAmountWei >= maxPurchaseAmount) revert INVALID_ETH_AMOUNT();
should be changed to:
if (paymentAmountWei <= minPurchaseAmount) revert INVALID_ETH_AMOUNT(); if (paymentAmountWei >= maxPurchaseAmount) revert INVALID_ETH_AMOUNT();
msgValue - _depositPurchaseRewards(msgValue, builderReferral, purchaseReferral, deployer)
in TokenEmitterRewards.sol
can be uncheckedif (msgValue < computeTotalReward(msgValue)) revert INVALID_ETH_AMOUNT(); return msgValue - _depositPurchaseRewards(msgValue, builderReferral, purchaseReferral, deployer);
Line 18 defines that msgValue
has to be greater or equal to computeTotalReward(msgValue)
.
When we look at _depositPurchaseRewards()
implementation, we can spot that it returns computePurchaseRewards()
:
function _depositPurchaseRewards( uint256 paymentAmountWei, [...] (RewardsSettings memory settings, uint256 totalReward) = computePurchaseRewards(paymentAmountWei); [...] return totalReward;
Now, when we check computePurchaseRewards()
implementation, we can see that it returns computeTotalReward()
.
function computePurchaseRewards(uint256 paymentAmountWei) public pure returns (RewardsSettings memory, uint256) { return ( RewardsSettings({ [...] }), computeTotalReward(paymentAmountWei) ); }
Since Line 18 already states that msgValue
has to be greater or equal to computeTotalReward(msgValue)
. (otherwise, it will revert with INVALID_ETH_AMOUNT()
), we can be sure that msgValue - _depositPurchaseRewards(msgValue, builderReferral, purchaseReferral, deployer)
won't underflow - thus it can be unchecked.
uint256 verbId = _currentVerbId++;
can be changed to uint256 verbId = ++_currentVerbId;
uint256 pieceId = _currentPieceId++;
can be changed to uint256 pieceId = ++_currentPieceId;
require
statements on top of the function__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" );
Above require
can be moved on top, above __Pausable_init();
. If condition won't be met, function will revert immediately, instead of wasting gas on performing other operations.
__Pausable_init(); __ReentrancyGuard_init(); require(_treasury != address(0), "Invalid treasury address");
Above require
can be moved on top, above __Pausable_init();
. If condition won't be met, function will revert immediately, instead of wasting gas on performing other operations.
createBid()
in AuctionHouse.sol
can be optimizedWe can merge two if
condition into one. That way we can achieve two optimizations:
extended
variable declarationif
conditionbool 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);
Variable extended
is used only twice, but it's used in if
condition. Since the second if
emits only an event, we can merge two conditions into one and get rid of extended
declaration:
if (_auction.endTime - block.timestamp < timeBuffer) { auction.endTime = _auction.endTime = block.timestamp + timeBuffer; emit AuctionExtended(_auction.verbId, _auction.endTime); } // Refund the last bidder, if applicable if (lastBidder != address(0)) _safeTransferETHWithFallback(lastBidder, _auction.amount); emit AuctionBid(_auction.verbId, bidder, msg.sender, msg.value, extended);
require
statements which uses less gas first in AuctionHouse.sol
function setCreatorRateBps(uint256 _creatorRateBps) external onlyOwner { 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");
First condition reads state variable minCreatorRateBps
, while the second condition reads constant 10 000
. The 2nd condition uses less gas which means it should be first.
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");
First condition reads state variable creatorRateBps
, while the second condition reads constant 10 000
. The 2nd condition uses less gas which means it should be first.
_auction.amount - auctioneerPayment
in AuctionHouse
can be uncheckeduint256 auctioneerPayment = (_auction.amount * (10_000 - creatorRateBps)) / 10_000; //Total amount of ether going to creator uint256 creatorsShare = _auction.amount - auctioneerPayment;
Since we firstly multiple _auction.amount
by 10_000 - creatorRateBps
and then divide by 10_000
, we can be sure, that auctioneerPayment <= _auction.amount
.
This proves, that uint256 creatorsShare = _auction.amount - auctioneerPayment;
won't underflow and can be unchecked.
length
of array multiple of times in ERC20TokenEmitter.sol
The bot-race result reported that addresses.length
should not be used inside loop and it should be cached in the local variable before line 209.
However addresses.length
should be cached earlier. addresses.length
is being used in line 162, which means that it should be cached before that line. That way, we won't be calculating the length of addresses
two times:
162: require(addresses.length == basisPointSplits.length, "Parallel arrays required"); [...] 209: for (uint256 i = 0; i < addresses.length; i++) {
should be changed to:
uint256 addessesLength = addresses.length; require(addessesLength == basisPointSplits.length, "Parallel arrays required"); [...] for (uint256 i = 0; i < addessesLength; i++) {
toPayTreasury
in ERC20TokenEmitter.sol
can be uncheckedBelow issue describes multiple of steps which proves that toPayTreasury
can be unchecked.
10_000 - creatorRateBps
can be uncheckeduint256 toPayTreasury = (msgValueRemaining * (10_000 - creatorRateBps)) / 10_000;
uint256 toPayTreasury = (msgValueRemaining * (10_000 - creatorRateBps)) / 10_000;
Function setCreatorRateBps()
sets creatorRateBps
variable. According to its restriction: require(_creatorRateBps <= 10_000, "Creator rate must be less than or equal to 10_000");
(line 300), we can be sure, that creatorRateBps <= 10_000
. This implies, that 10_000 - creatorRateBps
will never underflow thus can be unchecked.
msgValueRemaining * (10_000 - creatorRateBps)
can be uncheckedSince msgValueRemaining
is returned by _handleRewardsAndGetValueToSend()
- we can be sure, that its value is smaller than msg.data
. Since the total ETH Supply is less than 121 000 000, we can safely assume, that msgValueRemaining * (10_000 - creatorRateBps)
will never overflow uint256
- thus it can be unchecked.
(msgValueRemaining * (10_000 - creatorRateBps)) / 10_000
Division can be always unchecked
if
condition to avoid reading state variable twice in ERC20TokenEmitter.sol
emittedTokenWad += totalTokensForBuyers; if (totalTokensForCreators > 0) emittedTokenWad += totalTokensForCreators;
Current implementation reads emittedTokenWad
(state variable) twice. Firstly, to add totalTokensForBuyers
, then (when totalTokensForCreators > 0
) to add totalTokensForCreators
.
To reduce the number of reading of emittedTokenWad
, we can rewrite above code to:
if (totalTokensForCreators > 0) { emittedTokenWad += totalTokensForBuyers + totalTokensForCreators; } else { emittedTokenWad += totalTokensForBuyers; }
address(0)
check in initialize()
in ERC20TokenEmitter.sol
to avoid checking for address(0)
in buyToken()
Because of the initilizer
modifier, function initialize()
can be called only once. This function sets the creatorsAddress
variable.
Our recommendation is to add additional condition in initialize()
: require(creatorsAddress != address(0), "Cannot be address(0)")
.
This will guarantee, that creatorsAddress
won't be address(0)
. Moreover, function setCreatorsAddress()
which updates that variable also performs that check:
function setCreatorsAddress(address _creatorsAddress) external override onlyOwner nonReentrant { require(_creatorsAddress != address(0), "Invalid address");
Those two conditions guarantee that creatorsAddress
will never be address(0)
. This implies, that condition in function buyToken()
:
if (totalTokensForCreators > 0 && creatorsAddress != address(0)) {
can be reduced to:
if (totalTokensForCreators > 0) {
This implements a significant improvement. We add just one address(0)
check in the initialize()
(which can be called only once), and removed that check from buyToken()
(which can be called multiple of times).
for (uint256 i = 0; i < addresses.length; i++) {
for (uint i; i < creatorArrayLength; i++) {
for (uint i; i < creatorArrayLength; i++) {
for (uint i; i < creatorArrayLength; i++) {
for (uint256 i; i < len; i++) {
for (uint256 i; i < len; i++) {
for (uint256 i; i < len; i++) {
Above loops can be rewritten to do-while
loops. Especially, that they implement just a simple iterations, which can easily be re-implemented to a do-while loops.
require
in CultureIndex.sol
require(uint8(metadata.mediaType) > 0 && uint8(metadata.mediaType) <= 5, "Invalid media type"); if (metadata.mediaType == MediaType.IMAGE) require(bytes(metadata.image).length > 0, "Image URL must be provided"); else if (metadata.mediaType == MediaType.ANIMATION) require(bytes(metadata.animationUrl).length > 0, "Animation URL must be provided"); else if (metadata.mediaType == MediaType.TEXT) require(bytes(metadata.text).length > 0, "Text must be provided");
The first require
statement verifies if metadata.mediaType
is in a (1, 5) range.
Then, if
/else
condition verifies what MediaType it is. However, please notice, that when metadata.mediaType
is MediaType.IMAGE
or MediaType.ANIMATION
or MediaType.TEXT
, then metadata.mediaType
is already in that range - thus checking that is redundant. Above code can be rewritten to:
if (metadata.mediaType == MediaType.IMAGE) require(bytes(metadata.image).length > 0, "Image URL must be provided"); else if (metadata.mediaType == MediaType.ANIMATION) require(bytes(metadata.animationUrl).length > 0, "Animation URL must be provided"); else if (metadata.mediaType == MediaType.TEXT) require(bytes(metadata.text).length > 0, "Text must be provided"); else require(uint8(metadata.mediaType) > 0 && uint8(metadata.mediaType) <= 5, "Invalid media type");
CultureIndex.sol
for (uint i; i < creatorArrayLength; i++) { newPiece.creators.push(creatorArray[i]); } emit PieceCreated(pieceId, msg.sender, metadata, newPiece.quorumVotes, newPiece.totalVotesSupply); // Emit an event for each creator for (uint i; i < creatorArrayLength; i++) { emit PieceCreatorAdded(pieceId, creatorArray[i].creator, msg.sender, creatorArray[i].bps); }
Those two loops are iterating over the same indexes. The first one adds new creators, the second one emits an event. This means, that it can be done in the same loop, instead of iterating twice.
for (uint i; i < creatorArrayLength; i++) { newPiece.creators.push(creatorArray[i]); emit PieceCreatorAdded(pieceId, creatorArray[i].creator, msg.sender, creatorArray[i].bps); } emit PieceCreated(pieceId, msg.sender, metadata, newPiece.quorumVotes, newPiece.totalVotesSupply);
_verifyVoteSignature()
in CultureIndex.sol
if (from == address(0)) revert ADDRESS_ZERO(); // Ensure signature is valid if (recoveredAddress == address(0) || recoveredAddress != from) revert INVALID_SIGNATURE();
ecrecover
returns signer address or address(0)
when signature is not valid.
Let's consider every possible scenario:
from: 0xAAA
, recoveredAddress: 0xAAA
(signature VALID).
In that case, if (recoveredAddress == address(0) || recoveredAddress != from) revert INVALID_SIGNATURE();
won't revert, since recoveredAddress
is not address(0)
and recoveredAddress == from
.
from: 0xAAA
, recoveredAddress: 0x0
(signature INVALID).
In that case, if (recoveredAddress == address(0) || recoveredAddress != from) revert INVALID_SIGNATURE();
will revert, since recoveredAddress == address(0)
.
from: 0x0
, recoveredAddress: 0x0
(signature AMBIGUOUS).
In that case, if (recoveredAddress == address(0) || recoveredAddress != from) revert INVALID_SIGNATURE();
will revert, since recoveredAddress == address(0)
.
These conditions fulfill all scenarios. This implies that if (from == address(0)) revert ADDRESS_ZERO();
is redundant and can be removed, since if (recoveredAddress == address(0) || recoveredAddress != from)
already checks all possible conditions.
MaxHeap.sol
can be uncheckedrequire(size > 0, "Heap is empty"); uint256 popped = heap[0]; heap[0] = heap[--size];
Because of the require
, size > 0
, thus we know that heap[--size]
won't underflow and can be unchecked.
parent()
calculation in MaxHeap
can be optimizedfunction parent(uint256 pos) private pure returns (uint256) { require(pos != 0, "Position should not be zero"); return (pos - 1) / 2; }
There are 2 ways to re-implement parent()
so it will use less gas.
pos > 0
, (pos - 1) / 2
can be unchecked (pos - 1
won't underflow and division can be unchecked)require
statemens in MaxHeap.sol
Solidity uses short-circuiting when evaluating boolean expressions. E.g. for A and B
expression, when A == false
, B
won't be evaluated (because since A
is already false
, A AND B
also will be false
no matter what value B
has). This behavior suggests, that it's better to use expressions which uses less gas first.
if (pos >= (size / 2) && pos <= size) return;
pos <= size
uses less gas than pos >= (size / 2)
(<=
operator vs >=
and division by 2). This impies, that above line of code should be rewritten to:
if (pos <= size && pos >= (size / 2)) return;
require(!(votes[pieceId][voter].voterAddress != address(0)), "Already voted");
Above code can be simplified to:
require(votes[pieceId][voter].voterAddress == address(0), "Already voted");
address(0)
check in VerbsToken.sol
require(_initialOwner != address(0), "Initial owner cannot be zero address");
Above check is redundant and can be removed. _initialOwner
is being passed to Open Zeppelin's __Ownable_init()
. This function calls __Ownable_init_unchained()
which already performs address(0)
check and reverts with OwnableInvalidOwner()
when _initialOwner == address(0)
.
MaxHeap.sol
function extractMax() external onlyAdmin returns (uint256, uint256) { require(size > 0, "Heap is empty"); uint256 popped = heap[0]; heap[0] = heap[--size]; maxHeapify(0); return (popped, valueMapping[popped]); }
There is no need to waste gas on reading size
variable in line: require(size > 0, "Heap is empty");
. If size == 0
, function will revert in line heap[0] = heap[--size];
, because of the underflow. Please notice, that implementing this recommendation will save gas (there will be no need to read size
variable) - but - in case of revert, we won't get the "Heap is empty"
revert message.
#0 - c4-pre-sort
2023-12-24T01:46:35Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2024-01-07T13:29:55Z
MarioPoneder marked the issue as grade-b