Platform: Code4rena
Start Date: 21/07/2023
Pot Size: $90,500 USDC
Total HM: 8
Participants: 60
Period: 7 days
Judge: 0xean
Total Solo HM: 2
Id: 264
League: ETH
Rank: 38/60
Findings: 1
Award: $313.63
🌟 Selected for report: 0
🚀 Solo Findings: 0
313.6275 USDC - $313.63
bytes32 public constant ADMIN_ROLE = 0xdf8b4c520ffe197c5343c6f5aec59570151ef9a492f2c624fd45ddde6135ec42; bytes32 public constant BADGE_MANAGER_ROLE = 0x9605363ac419df002bafe0cc1dcbfb19cf2a2e2afa1c7428b115d0be2ecd78e5; bytes32 public constant RESOURCE_MANAGER_ROLE = 0xa08df8e9779f89161e9d4aa6eaffa8e1f95bfbc9b9812ee5dec3c3b19090625d;
i
instantiation
here we can optimize this for loop by rewriting it like thisfor(uint i; i< _claimData.length; ) { // Logic unchecked { ++i; } }
uint256
so turning this into a uint48 actually costs extra gas as the EVM has to down-size for every interaction. Another instance hereClaimData
variables
Here we should avoid down-sizing these values to save gas, due to the fact this data is only read in publishRoots
its less efficient to pack these variables since we are calling from a calldata array, the extra computation to down-size outweighs leaving claimExpiration
at uint256
struct ClaimData { uint208 tokenId; uint48 claimExpiration; bytes32 claimRoot; uint256 mintPrice; }
Method | Avg Cost before changes | Avg Cost after changes | Gas savings |
---|---|---|---|
publishRoots | 138021 | 137077 | 944 |
address public minter; uint96 public mintingAllowedAfter;
as mintingAllowedAfter
is a timestamp variable we can safely pack it into a uint96 as this will not overflow for thousands of years.
function gscSpend( address token, uint256 amount, address destination ) external onlyRole(GSC_CORE_VOTING_ROLE) { if (destination == address(0)) revert T_ZeroAddress("destination"); if (amount == 0) revert T_ZeroAmount(); // Will underflow if amount is greater than remaining allowance gscAllowance[token] -= amount; _spend(token, amount, destination, spendThresholds[token].small); }
The same can be done for the below functions that call _spend()
as well.
Grant
struct into fewer slots to save gas
Here we can pack some of these smaller variables to save gas here is a recommended example of how to pack them:struct Grant { uint128 allocation; uint128 cliffAmount; uint128 withdrawn; uint128 created; uint256 latestVotingPower; address delegatee; uint48 expiration; uint48 cliff; }
This change packs the struct into 4 slots where it was 5 before, we can do this as expiration
and cliff
are timestamp variables, which means they will not feasibly overflow at this sizing.
grant.withdrawn
just to set it to zero at the end of the call, this can be refactored to not waste SSTORE callsfunction revokeGrant(address who) external virtual onlyManager { // Logic unchecked { uint256 cachedWithdrawn = grant.withdrawn; uint256 withdrawable = _getWithdrawableAmount(grant); token.safeTransfer(who, withdrawable); // transfer the remaining tokens to the vesting manager uint256 remaining = grant.allocation - (cachedWithdrawn + withdrawable); token.safeTransfer(msg.sender, remaining); grant.withdrawn = uint128(cachedWithdrawn + remaining + withdrawable); } // Remaining logic }
Due to the nature of how grant.allocation
will always be greater then (grant.withdrawn + withdrawable)
we can safely wrap it all in an unchecked block
x > y
and then we do x-y
since we confirm that x is always greater then y x-y
will never underflow so we can safely wrap it in an unchecked block.if (unassigned.data < amount) revert AVV_InsufficientBalance(unassigned.data); unchecked { unassigned.data -= amount; }
Keep constant variables in the default 256 bit size Here we convert these constant variables to uint128 unnecessarily, we can keep them as uint256 to save some gas. uint256 is the default size so it costs extra to down-size, in this instance it does not benefit us to do so.
Make internal functions use memory to compute instead of a storage pointer to avoid unnecessary storage calls These three functions can be refactored to pass down variables and use memory instead of doing all the math with a storage pointer. This will significantly reduce the amount of SLOAD calls we make
Example Refactors:
function _syncVotingPower(address who, uint128 latestVotingPower, uint128 amount, uint128 withdrawn, address tokenAddress, uint128 tokenId, address delegatee) internal returns(uint128) { History.HistoricalBalances memory votingPower = _votingPower(); uint256 delegateeVotes = votingPower.loadTop(delegatee); uint256 newVotingPower = _currentVotingPower(amount, withdrawn, tokenAddress, tokenId); // get the change in voting power. Negative if the voting power is reduced int256 change = int256(newVotingPower) - int256(uint256(latestVotingPower)); // do nothing if there is no change if (change == 0) return latestVotingPower; if (change > 0) { votingPower.push(delegatee, delegateeVotes + uint256(change)); } else { // if the change is negative, we multiply by -1 to avoid underflow when casting votingPower.push(delegatee, delegateeVotes - uint256(change * -1)); } latestVotingPower = uint128(newVotingPower); emit VoteChange(who, delegatee, change); return latestVotingPower; } function _getWithdrawableAmount( uint128 withdraw, uint128 amount ) internal pure returns (uint256) { if (withdrawn == amount) { return 0; } return amount - withdrawn; } function _currentVotingPower( uint128 amount, uint128 withdrawn, address tokenAddress, uint128 tokenId ) internal view virtual returns (uint256) { uint128 locked = amount - withdrawn; if (tokenAddress != address(0) && tokenId != 0) { return (locked * getMultiplier(tokenAddress, tokenId)) / MULTIPLIER_DENOMINATOR; } return locked; }
This applies to a lot of internal functions as well.
Another refactor we can make to _syncVotingPower
is to not pass down the who
variable as its only used to emit the event, instead we can emit the event in the external function which would call _syncVotingPower
.
_withdrawNft()
call is wasting gas
Here the _withdrawNft()
call also wastes a lot of gas because of the interactions with storage we can safely remove this whole code snippet to save gas, instead we can replace it with thisif (registration.tokenAddress != address(0) && registration.tokenId != 0) { // withdraw the current ERC1155 from the registration IERC1155(registration.tokenAddress).safeTransferFrom( address(this), msg.sender, registration.tokenId, 1, bytes("") ); }
This is because _withdrawNft
makes a lot of unnecessary state changes for this function.
Method | Avg Cost before changes | Avg Cost after changes | Gas savings |
---|---|---|---|
updateNft | 155654 | 147743 | 7911 |
Redundant check inside _withdrawNft
can be removed to save gas
Here we check for the same conditions that _withdrawNft
already checks for. So we can remove the check inside _withdrawNft
to not have a redundant check. We also need to update to the check inside the external function withdrawNft
here so it is still safe.
balance
is an unnecessary storage variable
The ERC20 standard already saves user balances and is tracking the balance of the contract so whenever a user is depositing/withdrawing tokens we are calling extra SSTORE/SLOADS for no reason I am referencing this.
As a note this is not completely unnecessary because if a user sends tokens to the contract manually we will not be able to track which are deposited through function calls of the vault and which were sent manually, alternatively we can add a function like claimDust()
or getDust()
to see if there are tokens that were sent be accident, as we do not have this functionality currently, it is safe to remove the storage variable.
Cache storage data when reading from it multiple times to save gas by reducing SLOAD calls
For example here by caching registration.delegatee
and registration.latestVotingPower
we can save a lot of gas because of the repetitive SLOAD calls, here is an example refactor:
function delegate(address to) external override { if (to == address(0)) revert NBV_ZeroAddress("to"); NFTBoostVaultStorage.Registration storage registration = _getRegistrations()[msg.sender]; uint256 _latestVotingPower = registration.latestVotingPower; address _oldDelegate = registration.delegatee; // If to address is already the delegate, don't send the tx if (to == _oldDelegate) revert NBV_AlreadyDelegated(); History.HistoricalBalances memory votingPower = _votingPower(); uint256 oldDelegateeVotes = votingPower.loadTop(_oldDelegate); // Remove voting power from old delegatee and emit event votingPower.push(_oldDelegate, oldDelegateeVotes - _latestVotingPower); emit VoteChange(msg.sender, _oldDelegate, -1 * int256(uint256(_latestVotingPower))); // Note - It is important that this is loaded here and not before the previous state change because if // to == registration.delegatee and re-delegation was allowed we could be working with out of date state uint256 newDelegateeVotes = votingPower.loadTop(to); // return the current voting power of the Registration. Varies based on the multiplier associated with the // user's ERC1155 token at the time of txn uint256 addedVotingPower = _currentVotingPower(registration.amount, registration.withdrawn, registration.tokenAddress, registration.tokenId); // add voting power to the target delegatee and emit event votingPower.push(to, newDelegateeVotes + addedVotingPower); // update registration properties registration.latestVotingPower = uint128(addedVotingPower); registration.delegatee = to; emit VoteChange(msg.sender, to, int256(addedVotingPower)); }
Method | Avg Cost before changes | Avg Cost after changes | Gas savings |
---|---|---|---|
delegate | 118171 | 117692 | 479 |
#0 - c4-pre-sort
2023-08-01T14:36:34Z
141345 marked the issue as high quality report
#1 - c4-sponsor
2023-08-02T20:37:06Z
PowVT marked the issue as sponsor confirmed
#2 - c4-judge
2023-08-11T16:44:36Z
0xean marked the issue as grade-a