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: 12/60
Findings: 2
Award: $726.93
🌟 Selected for report: 1
🚀 Solo Findings: 0
407.7158 USDC - $407.72
General Optimization =
Possible Optimization 1 =
Here is the optimized code snippet:
function _spend(address token, uint256 amount, address destination, uint256 limit) internal { uint256 spentThisBlock = blockExpenditure[block.number] + amount; if (spentThisBlock > limit) revert T_BlockSpendLimit(); blockExpenditure[block.number] = spentThisBlock; if (address(token) == ETH_CONSTANT) { payable(destination).transfer(amount); } else { IERC20(token).safeTransfer(destination, amount); } emit TreasuryTransfer(token, destination, amount); } function _approve(address token, address spender, uint256 amount, uint256 limit) internal { uint256 spentThisBlock = blockExpenditure[block.number] + amount; if (spentThisBlock > limit) revert T_BlockSpendLimit(); blockExpenditure[block.number] = spentThisBlock; IERC20(token).approve(spender, amount); emit TreasuryApproval(token, spender, amount); }
Possible Optimization 2 =
Here is the optimized code:
function batchCalls( address[] memory targets, bytes[] calldata calldatas ) external onlyRole(ADMIN_ROLE) nonReentrant { if (targets.length != calldatas.length) revert T_ArrayLengthMismatch(); for (uint256 i = 0; i < targets.length; ++i) { (bool success, ) = targets[i].call(calldatas[i]); if (!success) revert T_CallFailed(); } }
General Optimization =
onlyManager
and onlyTimelock
modifiers in multiple functions. These modifiers check if the caller is the manager
or the timelock
. However, these checks could be optimized by creating a new modifier that directly checks the msg.sender
against the manager
or timelock
, saving the gas used by the function call.Possible Optimization 1 =
staleBlockLag
. This could be optimized by only clearing entries when the number of entries exceeds a certain limit. This would reduce the number of storage operations, which are expensive in terms of gas.After Optimization:
function queryVotePower(address user, uint256 blockNumber, bytes calldata) external override returns (uint256) { // Get our reference to historical data History.HistoricalBalances memory votingPower = _votingPower(); // Find the historical data and clear everything more than 'staleBlockLag' into the past // only if the number of entries exceeds a certain limit (e.g., 100) if (votingPower.numEntries(user) > 100) { return votingPower.findAndClear(user, blockNumber, block.number - staleBlockLag); } else { return votingPower.find(user, blockNumber); } }
votingPower
mapping for a user. If there are more than 100 entries, this optimization could save approximately 20,000 gas per extra entry.Possible Optimization 2 =
After Optimization:
function setTimelock(address timelock_) external onlyTimelock { if (timelock_ == address(0)) revert BVV_ZeroAddress("timelock"); if (timelock_ != timelock()) { Storage.set(Storage.addressPtr("timelock"), timelock_); } } function setManager(address manager_) external onlyTimelock { if (manager_ == address(0)) revert BVV_ZeroAddress("manager"); if (manager_ != manager()) { Storage.set(Storage.addressPtr("manager"), manager_); } }
Possible Optimization 1 =
After Optimization:
function addGrantAndDelegate( address who, uint128 amount, uint128 cliffAmount, uint128 startTime, uint128 expiration, uint128 cliff, address delegatee ) external onlyManager { Storage.Uint256 storage unassigned = _unassigned(); ARCDVestingVaultStorage.Grant storage grant = _grants()[who]; // input validation if (who == address(0)) revert AVV_ZeroAddress("who"); if (amount == 0) revert AVV_InvalidAmount(); // if no custom start time is needed we use this block. if (startTime == 0) { startTime = uint128(block.number); } // grant schedule check if (cliff >= expiration || cliff < startTime) revert AVV_InvalidSchedule(); // cliff check if (cliffAmount >= amount) revert AVV_InvalidCliffAmount(); if (unassigned.data < amount) revert AVV_InsufficientBalance(unassigned.data); // if this address already has a grant, a different address must be provided // topping up or editing active grants is not supported. if (grant.allocation != 0) revert AVV_HasGrant(); // load the delegate. Defaults to the grant owner delegatee = delegatee == address(0) ? who : delegatee; // calculate the voting power. Assumes all voting power is initially locked. uint128 newVotingPower = amount; // set the new grant grant.allocation = amount; grant.cliffAmount = cliffAmount; grant.withdrawn = 0; grant.created = startTime; grant.expiration = expiration; grant.cliff = cliff; grant.latestVotingPower = newVotingPower; grant.delegatee = delegatee; // update the amount of unassigned tokens unassigned.data -= amount; // update the delegatee's voting power History.HistoricalBalances memory votingPower = _votingPower(); uint256 delegateeVotes = votingPower.loadTop(grant.delegatee); votingPower.push(grant.delegatee, delegateeVotes + newVotingPower); emit VoteChange(grant.delegatee, who, int256(uint256(newVotingPower))); }
Possible Optimization 2 =
After Optimization:
function claim(uint256 amount) external override nonReentrant { ARCDVestingVaultStorage.Grant storage grant = _grants()[msg.sender]; if (amount == 0) revert AVV_InvalidAmount(); if (grant.allocation == 0) revert AVV_NoGrantSet(); if (grant.cliff > block.number) revert AVV_CliffNotReached(grant.cliff); // get the withdrawable amount uint256 withdrawable = _getWithdrawableAmount(grant); if (amount > withdrawable) revert AVV_InsufficientBalance(withdrawable); // update the grant's withdrawn amount if (amount == withdrawable) { grant.withdrawn += uint128(withdrawable); } else { grant.withdrawn += uint128(amount); withdrawable = amount; } // update the user's voting power _syncVotingPower(msg.sender, grant); // transfer the available amount token.safeTransfer(msg.sender, withdrawable); }
Possible Optimization 3 =
Before:
// if the grant has already been removed or no grant available, revert if (grant.allocation == 0) revert AVV_NoGrantSet();
After:
// if the grant has already been removed or no grant available, revert if (grant.allocation == 0) { revert AVV_NoGrantSet(); return; }
Possible Optimization 1 =
After Optimization:
function addNftAndDelegate( uint128 amount, uint128 tokenId, address tokenAddress, address delegatee ) external override nonReentrant { _registerAndDelegate(msg.sender, amount, tokenId, tokenAddress, delegatee); if (amount == 0) revert NBV_ZeroAmount(); _lockTokens(msg.sender, uint256(amount), tokenAddress, tokenId, 1); }
Possible Optimization 2 =
After Optimization:
if (_tokenAddress != address(0) && _tokenId != 0 && IERC1155(_tokenAddress).balanceOf(user, _tokenId) == 0) { revert NBV_DoesNotOwn(); } uint128 multiplier = getMultiplier(_tokenAddress, _tokenId); if (multiplier == 0) revert NBV_NoMultiplierSet();
getMultiplier
function call.Possible Optimization 3 =
After Optimization:
if (newVotingPower == registration.latestVotingPower) return; int256 change = int256(newVotingPower) - int256(uint256(registration.latestVotingPower));
Possible Optimization =
After Optimization:
uint256 mintCapAmount = (totalSupply() * MINT_CAP) / PERCENT_DENOMINATOR; if (_amount > mintCapAmount) { revert AT_MintingCapExceeded(totalSupply(), mintCapAmount, _amount); } mintingAllowedAfter = block.timestamp + MIN_TIME_BETWEEN_MINTS;
Possible Optimization =
uint256
(or uint8
for even more gas savings) with each bit representing a different distribution
. This would save gas by reducing the number of storage slots used.After Optimization:
// Combine distribution flags into a single uint8 uint8 public distributionsSent; function toTreasury(address _treasury) external onlyOwner { // Check if the treasury distribution has not been sent (first bit of distributionsSent is 0) // and if the _treasury address is not zero require((distributionsSent & 1 == 0) && _treasury != address(0), "AT_AlreadySent or zero address"); // Set the first bit of distributionsSent to 1 (indicating that the treasury distribution has been sent) distributionsSent |= 1; // Transfer the treasury amount of tokens to the _treasury address arcadeToken.safeTransfer(_treasury, treasuryAmount); // Emit a Distribute event emit Distribute(address(arcadeToken), _treasury, treasuryAmount); }
SSTORE
opcode, which is used to update storage, is one of the most expensive operations in terms of gas cost. By reducing the number of storage slots used, we can save a significant amount of gas.Possible Optimization 1 =
After Optimization:
uint48 claimExpiration = claimExpirations[tokenId]; if (block.timestamp > claimExpiration) revert RB_ClaimingExpired(claimExpiration, uint48(block.timestamp)); if (!_verifyClaim(recipient, tokenId, totalClaimable, merkleProof)) revert RB_InvalidMerkleProof(); if (amountClaimed[recipient][tokenId] + amount > totalClaimable) { revert RB_InvalidClaimAmount(amount, totalClaimable); } uint256 mintPrice = mintPrices[tokenId] * amount; if (msg.value < mintPrice) revert RB_InvalidMintFee(mintPrice, msg.value); // increment amount claimed amountClaimed[recipient][tokenId] += amount; // mint to recipient _mint(recipient, tokenId, amount, "");
Possible Optimization 2 =
After Optimization:
function publishRoots(ClaimData[] calldata _claimData) external onlyRole(BADGE_MANAGER_ROLE) { if (_claimData.length == 0) revert RB_NoClaimData(); if (_claimData.length > 50) revert RB_ArrayTooLarge(); for (uint256 i = 0; i < _claimData.length; i++) { // uniqueness check if (claimRoots[_claimData[i].tokenId] != bytes32(0)) revert RB_TokenIdNotUnique(); // expiration check if (_claimData[i].claimExpiration <= block.timestamp) { revert RB_InvalidExpiration(_claimData[i].claimRoot, _claimData[i].tokenId); } claimRoots[_claimData[i].tokenId] = _claimData[i].claimRoot; claimExpirations[_claimData[i].tokenId] = _claimData[i].claimExpiration; mintPrices[_claimData[i].tokenId] = _claimData[i].mintPrice; } emit RootsPublished(_claimData); }
tokenIds
that are not unique. This is because the SSTORE
opcode, which is used to update storage, is one of the most expensive operations in terms of gas cost. By reducing the number of unnecessary SSTORE
operations, we can save a significant amount of gas.#0 - c4-pre-sort
2023-07-31T15:42:02Z
141345 marked the issue as high quality report
#1 - c4-sponsor
2023-08-02T22:51:54Z
PowVT marked the issue as sponsor acknowledged
#2 - PowVT
2023-08-02T22:52:54Z
A couple of these optimizations will cause security issues. Marking as ack
#3 - c4-judge
2023-08-11T16:48:41Z
0xean marked the issue as selected for report
#4 - c4-judge
2023-08-11T16:48:47Z
0xean marked the issue as grade-a
🌟 Selected for report: Sathish9098
Also found by: 0x3b, 0xnev, 3agle, BugBusters, DadeKuma, K42, Udsen, foxb868, ktg, kutugu, oakcobalt, peanuts, squeaky_cactus
While the current architecture is robust, there are a few recommendations that could further enhance the platform:
The platform has been designed with decentralization in mind. However, there are a few areas where centralization risks may arise:
16 hours
#0 - c4-pre-sort
2023-07-31T16:40:04Z
141345 marked the issue as high quality report
#1 - liveactionllama
2023-08-02T17:38:13Z
After discussion with the lookout, removing the high quality
label here, simply to focus usage of that label on the top 2 QA reports.
#2 - c4-judge
2023-08-10T23:00:01Z
0xean marked the issue as grade-a