You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The code is annotated at multiple places with //@audit comments to pinpoint the issues. Please, pay attention to them for more details.
Findings
Storage
Help the optimizer by saving a storage variable's reference instead of repeatedly fetching it
To help the optimizer, declare a storage type variable and use it instead of repeatedly fetching the reference in a map or an array.
The effect can be quite significant.
As an example, instead of repeatedly calling someMap[someIndex], save its reference like this: SomeStruct storage someStruct = someMap[someIndex] and use it.
Instances include (check the @audit tags):
contracts/HolyPaladinToken.sol:
351: uint256 previousLockAmount = userLocks[msg.sender][currentUserLockIndex].amount;//@audit gas: should've declared "UserLock storage currentUserLock = userLocks[msg.sender][currentUserLockIndex];" and use it to access amount359: if(duration == userLocks[msg.sender][currentUserLockIndex].duration) { //@audit gas: should've used suggested "UserLock storage currentUserLock = userLocks[msg.sender][currentUserLockIndex];" to access duration506: if (totalLocks[nbCheckpoints -1].fromBlock <= blockNumber) { //@audit gas: should've declared "TotalLock storage totalLock = totalLocks[nbCheckpoints - 1];" and use it to access fromBlock507: return totalLocks[nbCheckpoints -1]; //@audit gas: should've used suggested "TotalLock storage totalLock = totalLocks[nbCheckpoints - 1];" instead520: if (totalLocks[mid].fromBlock == blockNumber) { //@audit gas: should've declared "TotalLock storage totalLockMid = totalLocks[mid];" and use it to cache in memory the fromBlock value521: return totalLocks[mid]; //@audit gas: should've used suggested "TotalLock storage totalLockMid = totalLocks[mid];" instead571: uint256(userLocks[user][lastUserLockIndex].amount), //@audit gas: should've cached userLocks[user][lastUserLockIndex].amount in memory632: uint256 lockAmount = userLocks[user][nbLocks -1].amount; //@audit gas: should've declared "UserLock storage userLock = userLocks[user][nbLocks - 1];" and use it to access amount633: uint256 bonusVotes = delegates[user] == user && userLocks[user][nbLocks -1].duration >= ONE_YEAR ? (lockAmount * bonusLockVoteRatio) / UNIT : 0; //@audit gas: should've used suggested "UserLock storage userLock = userLocks[user][nbLocks - 1];" and use it to access duration678: if (delegateCheckpoints[account][nbCheckpoints -1].fromBlock <= blockNumber) { //@audit gas: should've used "DelegateCheckpoint storage delegateCheckpoint = delegateCheckpoints[account][nbCheckpoints - 1];"679: return delegateCheckpoints[account][nbCheckpoints -1].delegate; //@audit gas: should've used "DelegateCheckpoint storage delegateCheckpoint = delegateCheckpoints[account][nbCheckpoints - 1];"692: if (delegateCheckpoints[account][mid].fromBlock == blockNumber) { //@audit gas: should've used "DelegateCheckpoint storage delegatecheckpointMid" and cache fromBlock693: return delegateCheckpoints[account][mid].delegate; //@audit gas: should've used "DelegateCheckpoint storage delegatecheckpointMid"816: userLockedBalance =uint256(userLocks[user][vars.lastUserLockIndex].amount); //@audit gas: should've declared "UserLock storage userLock = userLocks[user][vars.lastUserLockIndex];" and use it to access amount819: if(userLockedBalance >0&& userLocks[user][vars.lastUserLockIndex].duration !=0){ //@audit gas: should've used suggested "UserLock storage userLock = userLocks[user][vars.lastUserLockIndex];" and use it to access duration935: if (userLocks[account][nbCheckpoints -1].fromBlock <= blockNumber) { //@audit gas: should've declared "UserLock storage userLock = userLocks[account][nbCheckpoints - 1];" and use it to cache in memory the fromBlock value936: return userLocks[account][nbCheckpoints -1]; //@audit gas: should've used suggested "UserLock storage userLock = userLocks[account][nbCheckpoints - 1];" instead949: if (userLocks[account][mid].fromBlock == blockNumber) { //@audit gas: should've declared "UserLock storage userLockMid = userLocks[account][mid];" and use it to cache in memory the fromBlock value950: return userLocks[account][mid]; //@audit gas: should've used suggested "UserLock storage userLockMid = userLocks[account][mid];"969: if (checkpoints[account][nbCheckpoints -1].fromBlock <= blockNumber) { //@audit gas: should've used "Checkpoint storage checkpointMid"970: return checkpoints[account][nbCheckpoints -1].votes; //@audit gas: should've used "Checkpoint storage checkpointMid"981: if (checkpoints[account][mid].fromBlock == blockNumber) { //@audit gas: should've used "Checkpoint storage checkpointMid" and cache fromBlock982: return checkpoints[account][mid].votes;//@audit gas: should've used "Checkpoint storage checkpointMid"1001: if (delegateCheckpoints[account][nbCheckpoints -1].fromBlock <= blockNumber) { //@audit gas: should've used "DelegateCheckpoint storage delegateCheckpoint = delegateCheckpoints[account][nbCheckpoints - 1];" and cache fromBlock1002: return delegateCheckpoints[account][nbCheckpoints -1].delegate; //@audit gas: should've used "DelegateCheckpoint storage delegateCheckpoint = delegateCheckpoints[account][nbCheckpoints - 1];"1013: if (delegateCheckpoints[account][mid].fromBlock == blockNumber) { //@audit gas: should've used "DelegateCheckpoint storage delegatecheckpointMid = delegateCheckpoints[account][mid];" and cache fromBlock1014: return delegateCheckpoints[account][mid].delegate;//@audit gas: should've used "DelegateCheckpoint storage delegatecheckpointMid = delegateCheckpoints[account][mid];"1051: if (pos >0&& checkpoints[delegatee][pos -1].fromBlock ==block.number) { //@audit gas: should've used "Checkpoint storage checkpoint"1052: checkpoints[delegatee][pos -1].votes =safe224(newVotes); //@audit gas: should've used "Checkpoint storage checkpoint"
This is a known practice in the same contract, as several UserLock storage currentUserLock are declared and used instead of repeatedly fetching the struct's reference.
Caching storage values in memory
The code can be optimized by minimising the number of SLOADs. SLOADs are expensive (100 gas) compared to MLOADs/MSTOREs (3 gas). Here, storage values should get cached in memory (see the @audit tags for further details):
contracts/HolyPaladinToken.sol:
520: if (totalLocks[mid].fromBlock == blockNumber) { //@audit gas: should've declared "TotalLock storage totalLockMid = totalLocks[mid];" and use it to cache in memory the fromBlock value523: if (totalLocks[mid].fromBlock > blockNumber) { //@audit gas: should've used suggested cached fromBlock value instead571: uint256(userLocks[user][lastUserLockIndex].amount), //@audit gas: should've cached userLocks[user][lastUserLockIndex].amount in memory572: balanceOf(user) -uint256(userLocks[user][lastUserLockIndex].amount) //@audit gas: should've used suggested cached userLocks[user][lastUserLockIndex].amount in memory683: if (delegateCheckpoints[account][0].fromBlock > blockNumber) { //@audit should've used suggested cached fromBlock692: if (delegateCheckpoints[account][mid].fromBlock == blockNumber) { //@audit gas: should've used "DelegateCheckpoint storage delegatecheckpointMid" and cache fromBlock695: if (delegateCheckpoints[account][mid].fromBlock > blockNumber) { //@audit should've used suggested cached fromBlock935: if (userLocks[account][nbCheckpoints -1].fromBlock <= blockNumber) { //@audit gas: should've declared "UserLock storage userLock = userLocks[account][nbCheckpoints - 1];" and use it to cache in memory the fromBlock value949: if (userLocks[account][mid].fromBlock == blockNumber) { //@audit gas: should've declared "UserLock storage userLockMid = userLocks[account][mid];" and use it to cache in memory the fromBlock value952: if (userLocks[account][mid].fromBlock > blockNumber) { //@audit gas: should've used the suggested cached fromBlock value981: if (checkpoints[account][mid].fromBlock == blockNumber) { //@audit gas: should've used "Checkpoint storage checkpointMid" and cache fromBlock984: if (checkpoints[account][mid].fromBlock > blockNumber) { //@audit gas: should've used suggested cached fromBlock1001: if (delegateCheckpoints[account][nbCheckpoints -1].fromBlock <= blockNumber) { //@audit gas: should've used "DelegateCheckpoint storage delegateCheckpoint = delegateCheckpoints[account][nbCheckpoints - 1];" and cache fromBlock1006: if (delegateCheckpoints[account][0].fromBlock > blockNumber) returnaddress(0); //@audit gas: should've used suggested cached fromBlock1013: if (delegateCheckpoints[account][mid].fromBlock == blockNumber) { //@audit gas: should've used "DelegateCheckpoint storage delegatecheckpointMid = delegateCheckpoints[account][mid];" and cache fromBlock1016: if (delegateCheckpoints[account][mid].fromBlock > blockNumber) { //@audit gas: should've used suggested cached fromBlock
Variables
No need to explicitly initialize variables with default values
If a variable is not set/initialized, it is assumed to have the default value (0 for uint, false for bool, address(0) for address...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
I suggest removing explicit initializations for default values.
Variable that should be constant
This variable is never updated. It should be declared as constant:
HolyPaladinToken.bonusLockVoteRatio (contracts/HolyPaladinToken.sol#100) should be constant
Comparisons
> 0 is less efficient than != 0 for unsigned integers (with proof)
!= 0 costs less gas compared to > 0 for unsigned integers in require statements with the optimizer enabled (6 gas)
Proof: While it may seem that > 0 is cheaper than !=, this is only true without the optimizer enabled and outside a require statement. If you enable the optimizer at 10k AND you're in a require statement, this will save gas. You can see this tweet for more proofs: https://twitter.com/gzeon/status/1485428085885640706
I suggest wrapping with an unchecked block here (see @audit tags for more details):
contracts/HolyPaladinToken.sol:
270require(userLocks[msg.sender].length!=0, "hPAL: No Lock");
271// Find the current Lock272: uint256 currentUserLockIndex = userLocks[msg.sender].length-1; //@audit gas: should be unchecked (can't underflow due to L270)286require(userLocks[msg.sender].length!=0, "hPAL: No Lock");
287// Find the current Lock288: uint256 currentUserLockIndex = userLocks[msg.sender].length-1; //@audit gas: should be unchecked (can't underflow due to L286)348require(userLocks[msg.sender].length!=0, "hPAL: No Lock");
349// Find the current Lock350: uint256 currentUserLockIndex = userLocks[msg.sender].length-1; //@audit gas: should be unchecked (can't underflow due to L348)388uint256 claimAmount = amount < claimableRewards[msg.sender] ? amount : claimableRewards[msg.sender];
389390// remove the claimed amount from the claimable mapping for the user, 391// and transfer the PAL from the rewardsVault to the user392: claimableRewards[msg.sender] -= claimAmount; //@audit gas: should be unchecked (can't underflow due to L388)455if(emergency || userLocks[user].length==0) returnUserLock(0, 0, 0, 0);
456: uint256 lastUserLockIndex = userLocks[user].length-1; //@audit gas: should be unchecked (can't underflow due to L455)568: uint256 lastUserLockIndex = userLocks[user].length-1; //@audit gas: should be unchecked (can't underflow due to L556)629if(nbLocks ==0) return currentVotes;
630631// and if there is a lock, and user self-delegate, add the bonus voting power 632: uint256 lockAmount = userLocks[user][nbLocks -1].amount; //@audit gas: should be unchecked (can't underflow due to L629)675if (nbCheckpoints ==0) returnaddress(0);
676677// last checkpoint check678: if (delegateCheckpoints[account][nbCheckpoints -1].fromBlock <= blockNumber) { //@audit gas: should be unchecked (can't underflow due to L675)679: return delegateCheckpoints[account][nbCheckpoints -1].delegate; //@audit gas: should be unchecked (can't underflow due to L675)687: uint256 high = nbCheckpoints -1; // last checkpoint already checked //@audit gas: should be unchecked (can't underflow due to L675)701: return high ==0?address(0) : delegateCheckpoints[account][high -1].delegate; //@audit gas: should be unchecked (can't underflow here)708if(userLocks[user].length==0) returnbalanceOf(user);
709: uint256 lastUserLockIndex = userLocks[user].length-1; //@audit gas: should be unchecked (can't underflow due to L708)727if(block.timestamp< lastDropUpdate + MONTH) return currentDropPerSecond; // Update it once a month728729uint256 dropDecreasePerMonth = (startDropPerSecond - endDropPerSecond) / (dropDecreaseDuration / MONTH);
730: uint256 nbMonthEllapsed = (block.timestamp- lastDropUpdate) / MONTH; //@audit gas: should be unchecked (can't underflow due to L727)809if(userLocks[user].length>0){
810 UserLockRewardVars memory vars;
811812// and if an user has a lock, calculate the locked rewards813: vars.lastUserLockIndex = userLocks[user].length-1; //@audit gas: should be unchecked (can't underflow due to L809)828: newBonusRatio = vars.bonusRatioDecrease >= vars.previousBonusRatio ?0 : vars.previousBonusRatio - vars.bonusRatioDecrease; //@audit gas: should be unchecked (can't underflow here)932if (nbCheckpoints ==0) return emptyLock;
933934// last checkpoint check935: if (userLocks[account][nbCheckpoints -1].fromBlock <= blockNumber) { //@audit gas: should be unchecked (can't underflow due to L932)936: return userLocks[account][nbCheckpoints -1]; //@audit gas: should be unchecked (can't underflow due to L932)944: uint256 high = nbCheckpoints -1; // last checkpoint already checked //@audit gas: should be unchecked (can't underflow due to L932)958: return high ==0? emptyLock : userLocks[account][high -1]; //@audit gas: should be unchecked (can't underflow here)966if (nbCheckpoints ==0) return0;
967968// last checkpoint check969: if (checkpoints[account][nbCheckpoints -1].fromBlock <= blockNumber) { //@audit gas: should be unchecked (can't underflow due to L966)970: return checkpoints[account][nbCheckpoints -1].votes;//@audit gas: should be unchecked (can't underflow due to L966)976: uint256 high = nbCheckpoints -1; // last checkpoint already checked//@audit gas: should be unchecked (can't underflow due to L966)990: return high ==0?0 : checkpoints[account][high -1].votes;//@audit gas: should be unchecked (can't underflow here)998if (nbCheckpoints ==0) returnaddress(0);
9991000// last checkpoint check1001: if (delegateCheckpoints[account][nbCheckpoints -1].fromBlock <= blockNumber) { //@audit gas: should be unchecked (can't underflow due to L998)1002: return delegateCheckpoints[account][nbCheckpoints -1].delegate; //@audit gas: should be unchecked (can't underflow due to L998)1008: uint256 high = nbCheckpoints -1; // last checkpoint already checked //@audit gas: should be unchecked (can't underflow due to L998)1022: return high ==0?address(0) : delegateCheckpoints[account][high -1].delegate; //@audit gas: should be unchecked (can't underflow here)1030: uint256 oldVotes = nbCheckpoints ==0?0 : checkpoints[from][nbCheckpoints -1].votes; //@audit gas: should be unchecked (can't underflow here)1039: uint256 oldVotes = nbCheckpoints ==0?0 : checkpoints[to][nbCheckpoints -1].votes; //@audit gas: should be unchecked (can't underflow here)1051: if (pos >0&& checkpoints[delegatee][pos -1].fromBlock ==block.number) { //@audit gas: should be unchecked (can't underflow here)1052: checkpoints[delegatee][pos -1].votes =safe224(newVotes); //@audit gas: should be unchecked (can't underflow here)1083require(block.timestamp> (userCooldown + COOLDOWN_PERIOD), "hPAL: Insufficient cooldown");
1084: require(block.timestamp- (userCooldown + COOLDOWN_PERIOD) <= UNSTAKE_PERIOD, "hPAL: unstake period expired"); //@audit gas: should be unchecked (can't underflow due to L1083)1173: uint256 currentUserLockIndex = userLocks[user].length-1; //@audit gas: should be unchecked (can't underflow due to L1145 and being in the else clause)1237require(userLocks[user].length>0, "hPAL: No Lock");
12381239// Get the user current Lock1240// And calculate the end of the Lock1241: uint256 currentUserLockIndex = userLocks[user].length-1; //@audit gas: should be unchecked (can't underflow due to L1237)1272require(userLocks[user].length>0, "hPAL: No Lock");
12731274// Get the user to kick current Lock1275// and calculate the end of the Lock1276: uint256 currentUserLockIndex = userLocks[user].length-1; //@audit gas: should be unchecked (can't underflow due to L1272)1345if(userLocks[msg.sender].length!=0){
1346// Check if the user has a Lock, and if so, fetch it1347: uint256 currentUserLockIndex = userLocks[msg.sender].length-1; //@audit gas: should be unchecked (can't underflow due to L1345)
Shift Right instead of Dividing by 2
A division by 2 can be calculated by shifting one to the right.
While the DIV opcode uses 5 gas, the SHR opcode only uses 3 gas. Furthermore, Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting.
Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.
Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).
QA & gas optimizations changes are done in the PR: PaladinFinance/Paladin-Tokenomics#6
(some changes/tips were implemented, others are noted but won't be applied)
Gas Report
Table of Contents:
> 0
is less efficient than!= 0
for unsigned integers (with proof)Foreword
@audit
tagsFindings
Storage
Help the optimizer by saving a storage variable's reference instead of repeatedly fetching it
To help the optimizer, declare a
storage
type variable and use it instead of repeatedly fetching the reference in a map or an array.The effect can be quite significant.
As an example, instead of repeatedly calling
someMap[someIndex]
, save its reference like this:SomeStruct storage someStruct = someMap[someIndex]
and use it.Instances include (check the
@audit
tags):This is a known practice in the same contract, as several
UserLock storage currentUserLock
are declared and used instead of repeatedly fetching the struct's reference.Caching storage values in memory
The code can be optimized by minimising the number of SLOADs. SLOADs are expensive (100 gas) compared to MLOADs/MSTOREs (3 gas). Here, storage values should get cached in memory (see the
@audit
tags for further details):Variables
No need to explicitly initialize variables with default values
If a variable is not set/initialized, it is assumed to have the default value (
0
foruint
,false
forbool
,address(0)
for address...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.Instances include:
I suggest removing explicit initializations for default values.
Variable that should be constant
This variable is never updated. It should be declared as constant:
Comparisons
> 0
is less efficient than!= 0
for unsigned integers (with proof)!= 0
costs less gas compared to> 0
for unsigned integers inrequire
statements with the optimizer enabled (6 gas)Proof: While it may seem that
> 0
is cheaper than!=
, this is only true without the optimizer enabled and outside a require statement. If you enable the optimizer at 10k AND you're in arequire
statement, this will save gas. You can see this tweet for more proofs: https://twitter.com/gzeon/status/1485428085885640706I suggest changing
> 0
with!= 0
here:Also, please enable the Optimizer.
Arithmetics
Unchecking arithmetics operations that can't underflow/overflow
Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn't possible (as an example, when a comparison is made before the arithmetic operation), some gas can be saved by using an
unchecked
block: https://docs.soliditylang.org/en/v0.8.10/control-structures.html#checked-or-unchecked-arithmeticI suggest wrapping with an
unchecked
block here (see@audit
tags for more details):Shift Right instead of Dividing by 2
A division by 2 can be calculated by shifting one to the right.
While the
DIV
opcode uses 5 gas, theSHR
opcode only uses 3 gas. Furthermore, Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting.I suggest replacing
/ 2
with>> 1
here:Errors
Use Custom Errors instead of Revert Strings to save Gas
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)
Source: https://blog.soliditylang.org/2021/04/21/custom-errors/:
Custom errors are defined using the
error
statement, which can be used inside and outside of contracts (including interfaces and libraries).Instances include:
I suggest replacing revert strings with custom errors. They are already used in the solution but not extensively.
The text was updated successfully, but these errors were encountered: