Platform: Code4rena
Start Date: 11/05/2022
Pot Size: $150,000 USDC
Total HM: 23
Participants: 93
Period: 14 days
Judge: LSDan
Total Solo HM: 18
Id: 123
League: ETH
Rank: 47/93
Findings: 2
Award: $233.30
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0xNazgul, 0xNineDec, 0xf15ers, 0xkatana, 242, AlleyCat, BouSalman, BowTiedWardens, CertoraInc, Chom, Cityscape, FSchmoede, Funen, GimelSec, Hawkeye, JC, JDeryl, Kaiziron, Kthere, Kumpa, MaratCerby, MiloTruck, Nethermind, NoamYakov, PPrieditis, QuantumBrief, Rolezn, Ruhum, SmartSek, SooYa, Tadashi, TerrierLover, WatchPug, Waze, _Adam, asutorufos, berndartmueller, bobirichman, c3phas, catchup, cccz, ch13fd357r0y3r, cryptphi, csanuragjain, cthulhu_cult, defsec, delfin454000, ellahi, fatherOfBlocks, hansfriese, hubble, hyh, jayjonah8, joestakey, kenta, kenzo, kirk-baird, mics, oyc_109, p_crypt0, reassor, robee, sach1r0, samruna, sashik_eth, sikorico, simon135, sorrynotsorry, sseefried, tintin, unforgiven, z3s, zmj
150.0334 USDC - $150.03
The convention for constants in Solidity is to use capital letters, see Solidity style guide.
Examples of constants not using this convention can be found in AuraLocker.sol
L73, L81, L83, L104, L105, L107, L109 and L119.
This means that variables like these:
uint256 public constant denominator = 10000; address public immutable cvxcrvStaking;
Should be named to:
uint256 public constant DENOMINATOR= 10000; address public immutable CVXCRV_STAKING;
The usage of Solidity’s time units can increase the readability of the code. For example in the AuraLocker.sol
contract at L81-L83:
uint256 public constant rewardsDuration = 86400 * 7; // Duration of lock/earned penalty period uint256 public constant lockDuration = rewardsDuration * 17;
Can be rewritten into:
uint256 public constant rewardsDuration = 1 weeks; // Duration of lock/earned penalty period uint256 public constant lockDuration = rewardsDuration * 17;
Which makes it much more clear that rewardsDuration
is 1 week and lockDuration
is 17 weeks.
In some contracts the order of the methods seems to be more or less random, which makes the contract hard to read through. An example of such contract could be AuraBalRewardPool.sol
. The Solidity docs have a style guide which gives some good guidelines on the order of which methods should be declared to improve readability, see the style guide.
#0 - 0xMaharishi
2022-05-26T10:43:19Z
The function layout of convex-platform
and any contracts forked from there (like AuraBalRewardPool) have been kept in tact to ensure the diffs are easy to parse. That said, i agree with your reports
🌟 Selected for report: IllIllI
Also found by: 0v3rf10w, 0x1f8b, 0x4non, 0xKitsune, 0xNazgul, 0xf15ers, 0xkatana, BowTiedWardens, CertoraInc, DavidGialdi, FSchmoede, Fitraldys, Funen, GimelSec, Hawkeye, JC, Kaiziron, Kthere, MaratCerby, MiloTruck, NoamYakov, QuantumBrief, Randyyy, Ruhum, SmartSek, SooYa, Tadashi, TerrierLover, Tomio, UnusualTurtle, WatchPug, Waze, _Adam, antonttc, asutorufos, bobirichman, c3phas, catchup, csanuragjain, cthulhu_cult, defsec, delfin454000, ellahi, fatherOfBlocks, hansfriese, hyh, jayjonah8, joestakey, kenta, marcopaladin, mics, minhquanym, orion, oyc_109, reassor, rfa, robee, sach1r0, samruna, sashik_eth, sikorico, simon135, unforgiven, z3s, zmj
83.2689 USDC - $83.27
There is no need to initialize a uint to 0, since this will be done by default when declaring the variable. In fact it will cost a small amount of gas to initialize the value “twice”. An example can be found in the contract at AuraLocker.sol
L72.
The initialization should instead look like this:
uint256 public queuedCvxCrvRewards;
Variables initialized to 0 can also be found several times in AuraBalRewardPool.sol
and AuraMerkleDrop.sol
.
The loops found in e.g. AuraLocker.sol
can be optimized to save gas. They could look like this found at L173-L185:
uint256 rewardTokensLength = rewardTokens.length; for (uint256 i = 0; i < rewardTokensLength; i++) { address token = rewardTokens[i]; uint256 newRewardPerToken = _rewardPerToken(token); rewardData[token].rewardPerTokenStored = newRewardPerToken.to96(); rewardData[token].lastUpdateTime = _lastTimeRewardApplicable(rewardData[token].periodFinish).to32(); if (_account != address(0)) { userData[_account][token] = UserData({ rewardPerTokenPaid: newRewardPerToken.to128(), rewards: _earned(_account, token, userBalance.locked).to128() }); } }
3 modifications can be done to this loop to safe gas:
i
to 0, since this is automatically done when declaring a uint
(the same argument as G-01).++i
is cheaper than postincrement i++
, since the latter will make use of a temporary variable to store the returned value in before incrementing it. This can be saved by doing a preincrement, which increments the value and then returns it.i
.With the above suggestions the for-loop above can be rewritten into:
uint256 rewardTokensLength = rewardTokens.length; for (uint256 i; i < rewardTokensLength) { address token = rewardTokens[i]; uint256 newRewardPerToken = _rewardPerToken(token); rewardData[token].rewardPerTokenStored = newRewardPerToken.to96(); rewardData[token].lastUpdateTime = _lastTimeRewardApplicable(rewardData[token].periodFinish).to32(); if (_account != address(0)) { userData[_account][token] = UserData({ rewardPerTokenPaid: newRewardPerToken.to128(), rewards: _earned(_account, token, userBalance.locked).to128() }); } unchecked { ++i; } }
Throughout the contract AuraLocker.sol
there are multiple places where the block timestamp is divided and multiplied by the same constant. Division immediately followed by a multiplication (or vice versa) with the same number equals each other out, and hence both operations are redundant. An example can be seen at L162:
uint256 currentEpoch = block.timestamp.div(rewardsDuration).mul(rewardsDuration);
Where the div() and mul() will be a waste of gas since they use the same constant, and thus can be rewritten to:
uint256 currentEpoch = block.timestamp
The state variable minterMinted
in the contract Aura.sol
is initialized with the value of type(uint256).max
however, the variable is set to 0 in the contract’s init()
function. The variable is used in two other places, which required the init()
method to have been called first. Thus the initial value of type(uint256).max
is never used, and is hence a waste of gas. The variable is declared at L33 and should instead be declared as:
uint256 private minterMinted;
This also means that the assignment of 0 to the variable in the init()
method can be removed to save further gas. Hence the line L74 can be deleted, so the init()
method looks like this:
function init( address _to, uint256 _amount, address _minter ) external { require(msg.sender == operator, "Only operator"); require(totalSupply() == 0, "Only once"); require(_amount > 0, "Must mint something"); require(_minter != address(0), "Invalid minter"); _mint(_to, _amount); updateOperator(); minter = _minter; emit Initialised(); }
In the method claim()
in the contract AuraMerkleDrop.sol
the variable merkleRoot
can be cached to save some gas. The state variable is read 2 times in the method, and by caching it in memory it is possible to switch one of the expensive SLOAD
operations to a MLOAD
/MSTORE
operation which is way cheaper.
The code L119-L126:
require(merkleRoot != bytes32(0), "!root"); // SLOAD1 require(block.timestamp > startTime, "!started"); require(block.timestamp < expiryTime, "!active"); require(_amount > 0, "!amount"); require(hasClaimed[msg.sender] == false, "already claimed"); bytes32 leaf = keccak256(abi.encodePacked(msg.sender, _amount)); require(MerkleProof.verify(_proof, merkleRoot, leaf), "invalid proof"); //SLOAD2
Can be changed into:
bytes32 cachedMerkleRoot = merkleRoot; // SLOAD + MSTORE require(cachedMerkleRoot != bytes32(0), "!root"); // MLOAD1 require(block.timestamp > startTime, "!started"); require(block.timestamp < expiryTime, "!active"); require(_amount > 0, "!amount"); require(hasClaimed[msg.sender] == false, "already claimed"); bytes32 leaf = keccak256(abi.encodePacked(msg.sender, _amount)); require(MerkleProof.verify(_proof, cachedMerkleRoot, leaf), "invalid proof"); // MLOAD2
In the method claim()
in the contract AuraMerkleDrop.sol
the state variable auraLocker
is read 2 times inside the if clause, and by caching it in memory it is possible to switch one of the expensive SLOAD
operations to a MLOAD
/MSTORE
operation which is way cheaper.
There is no reason to cache auraLocker
outside of the if-statement, since the else clause only reads the variable once. If we cache it outside the if-statement we would make the else part a bit more expensive.
The code L130-L139:
if (_lock) { aura.safeApprove(address(auraLocker), 0); // SLOAD1 aura.safeApprove(address(auraLocker), _amount); // SLOAD2 auraLocker.lock(msg.sender, _amount); } else { // If there is an address for auraLocker, and not locking, apply 20% penalty uint256 penalty = address(auraLocker) == address(0) ? 0 : (_amount * 2) / 10; pendingPenalty += penalty; aura.safeTransfer(msg.sender, _amount - penalty); }
Can be changed into:
if (_lock) { IAuraLocker cachedAuraLocker = auraLocker; // SLAOD1 aura.safeApprove(address(cachedAuraLocker), 0); // MLOAD1 aura.safeApprove(address(cachedAuraLocker), _amount); // MLOAD2 auraLocker.lock(msg.sender, _amount); } else { // If there is an address for auraLocker, and not locking, apply 20% penalty uint256 penalty = address(auraLocker) == address(0) ? 0 : (_amount * 2) / 10; pendingPenalty += penalty; aura.safeTransfer(msg.sender, _amount - penalty); }
#0 - 0xMaharishi
2022-05-26T10:44:29Z
G-3 and G-4 are invalid, otherwise these are fair reports