Platform: Code4rena
Start Date: 15/12/2022
Pot Size: $128,000 USDC
Total HM: 28
Participants: 111
Period: 19 days
Judge: GalloDaSballo
Total Solo HM: 1
Id: 194
League: ETH
Rank: 64/111
Findings: 1
Award: $82.32
๐ Selected for report: 0
๐ Solo Findings: 0
๐ Selected for report: NoamYakov
Also found by: AkshaySrivastav, IllIllI, ast3ros, betweenETHlines, c3phas, camdengrieh, chaduke, fatherOfBlocks, kartkhira, latt1ce, shark
82.3208 USDC - $82.32
Table of Contents
keccak256()
should only need to be called on a specific string literal onceNB: Some functions have been truncated where necessary to just show affected parts of the code Throughout the report some places might be denoted with audit tags to show the actual place affected.
Use immutable if you want to assign a permanent value at construction. Use constants if you already know the permanent value. Both get directly embedded in bytecode, saving SLOAD.The compiler does not reserve a storage slot for these variables, and every occurrence is replaced by the respective value.
File: /contracts/contract/tokens/TokenggAVAX.sol 44: uint32 public rewardsCycleLength;
The variable rewardsCycleLength
has an hard coded value of 14 days
defined on Line 76
As this value is never modified we could save a lot of gas by declaring it as a constant
diff --git a/contracts/contract/tokens/TokenggAVAX.sol b/contracts/contract/tokens/TokenggAVAX.sol index dc3948d..c44c4f6 100644 --- a/contracts/contract/tokens/TokenggAVAX.sol +++ b/contracts/contract/tokens/TokenggAVAX.sol @@ -41,7 +41,7 @@ contract TokenggAVAX is Initializable, ERC4626Upgradeable, UUPSUpgradeable, Base uint32 public lastSync; /// @notice the maximum length of a rewards cycle - uint32 public rewardsCycleLength; + uint32 public constant rewardsCycleLength = 14 days; /// @notice the end of the current cycle. Will always be evenly divisible by `rewardsCycleLength`. uint32 public rewardsCycleEnd; @@ -73,7 +73,7 @@ contract TokenggAVAX is Initializable, ERC4626Upgradeable, UUPSUpgradeable, Base __ERC4626Upgradeable_init(asset, "GoGoPool Liquid Staking Token", "ggAVAX"); __BaseUpgradeable_init(storageAddress); - rewardsCycleLength = 14 days; // Ensure it will be evenly divisible by `rewardsCycleLength`. rewardsCycleEnd = (block.timestamp.safeCastTo32() / rewardsCycleLength) * rewardsCycleLength; }
Not inlining costs 20 to 40 gas because of two extra JUMP instructions and additional stack operations needed for function calls.
Affected code:
File: /contracts/contract/RewardsPool.sol 110: function increaseRewardsCycleCount() internal {
File: /contracts/contract/RewardsPool.sol 203: function distributeMultisigAllotment( 204: uint256 allotment, 205: Vault vault, 206: TokenGGP ggp 207: ) internal {
File: /contracts/contract/Staking.sol 87: function increaseGGPStake(address stakerAddr, uint256 amount) internal {
Caching a mapping's value in a local storage or calldata variable when the value is accessed multiple times saves ~42 gas per access due to not having to perform the same offset calculation every time. 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.
As an example, instead of repeatedly calling someMap[someIndex]
, save its reference like this: SomeStruct storage someStruct = someMap[someIndex]
and use it.
File: /contracts/contract/Vault.sol 61: function withdrawAVAX(uint256 amount) external onlyRegisteredNetworkContract nonReentrant { 71: if (avaxBalances[contractName] < amount) { //@audit: Initial access 72: revert InsufficientContractBalance(); 73: } 74: // Update balance 75: avaxBalances[contractName] = avaxBalances[contractName] - amount;//@audit: 2nd and 3rd access
File: /contracts/contract/Vault.sol 84: function transferAVAX(string memory toContractName, uint256 amount) external onlyRegisteredNetworkContract { 95: if (avaxBalances[fromContractName] < amount) { //@audit: Initial access 96: revert InsufficientContractBalance(); 97: } 98: // Update balances 99: avaxBalances[fromContractName] = avaxBalances[fromContractName] - amount;//@audit: 2nd and 3rd access
File: /contracts/contract/Vault.sol 137: function withdrawToken( 138: address withdrawalAddress, 139: ERC20 tokenAddress, 140: uint256 amount 141: ) external onlyRegisteredNetworkContract nonReentrant { 151: if (tokenBalances[contractKey] < amount) { //@audit: Initial access 152: revert InsufficientContractBalance(); 153: } 154: // Update balances 155: tokenBalances[contractKey] = tokenBalances[contractKey] - amount;//@audit: 2nd and 3rd access
File: /contracts/contract/Vault.sol 166: function transferToken( 167: string memory networkContractName, 168: ERC20 tokenAddress, 169: uint256 amount 170: ) external onlyRegisteredNetworkContract { 183: if (tokenBalances[contractKeyFrom] < amount) { //@audit: Initial access 184: revert InsufficientContractBalance(); 185: } 186: // Update Balances 187: tokenBalances[contractKeyFrom] = tokenBalances[contractKeyFrom] - amount;//@audit: 2nd and 3rd access
Saves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.
File: /contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol 35: mapping(address => uint256) public balanceOf; 37: mapping(address => mapping(address => uint256)) public allowance;
Instead of using the && operator in a single require statement to check multiple conditions,using multiple require statements with 1 condition per require statement will save 8 GAS per && The gas difference would only be realized if the revert condition is realized(met).
File: /contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol 154: require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");
diff --git a/contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol b/contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol index 2ea27d6..e53ae5f 100644 --- a/contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol +++ b/contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol @@ -151,7 +151,8 @@ abstract contract ERC20Upgradeable is Initializable { s ); - require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); + require(recoveredAddress != address(0) , "INVALID_SIGNER"); + require(recoveredAddress == owner, "INVALID_SIGNER"); allowance[recoveredAddress][spender] = value; }
A division/multiplication by any number x being a power of 2 can be calculated by shifting log2(x) to the right/left.
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.
File: /contracts/contract/MinipoolManager.sol 413: uint256 avaxHalfRewards = avaxTotalRewardAmt / 2;
File: /contracts/contract/tokens/TokenggAVAX.sol 149: stakingTotalAssets -= baseAmt;
diff --git a/contracts/contract/tokens/TokenggAVAX.sol b/contracts/contract/tokens/TokenggAVAX.sol index dc3948d..7aa769a 100644 --- a/contracts/contract/tokens/TokenggAVAX.sol +++ b/contracts/contract/tokens/TokenggAVAX.sol @@ -146,7 +146,7 @@ contract TokenggAVAX is Initializable, ERC4626Upgradeable, UUPSUpgradeable, Base } emit DepositedFromStaking(msg.sender, baseAmt, rewardAmt); - stakingTotalAssets -= baseAmt; + stakingTotalAssets = stakingTotalAssets - baseAmt; IWAVAX(address(asset)).deposit{value: totalAmt}(); }
File: /contracts/contract/tokens/TokenggAVAX.sol 160: stakingTotalAssets += assets;
File: /contracts/contract/tokens/TokenggAVAX.sol 245: totalReleasedAssets -= amount;
File: /contracts/contract/tokens/TokenggAVAX.sol 252: totalReleasedAssets += amount;
File: /contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol 184: totalSupply += amount;
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 see resource
File: /contracts/contract/ClaimNodeOp.sol 102: uint256 restakeAmt = ggpRewards - claimAmt;
The operation ggpRewards - claimAmt
cannot underflow due to the check on Line 95 which ensurs that ggpRewards
is greater than claimAmt
before perfoming the operation
We can rewrite the above as follows.
diff --git a/contracts/contract/ClaimNodeOp.sol b/contracts/contract/ClaimNodeOp.sol index efcce70..0e9b116 100644 --- a/contracts/contract/ClaimNodeOp.sol +++ b/contracts/contract/ClaimNodeOp.sol @@ -99,7 +99,10 @@ contract ClaimNodeOp is Base { staking.decreaseGGPRewards(msg.sender, ggpRewards); Vault vault = Vault(getContractAddress("Vault")); - uint256 restakeAmt = ggpRewards - claimAmt; + uint256 restakeAmt; + unchecked { + restakeAmt = ggpRewards - claimAmt; + }
File: /contracts/contract/Vault.sol 75: avaxBalances[contractName] = avaxBalances[contractName] - amount;
The operation avaxBalances[contractName] - amount
cannot underflow due to the check on Line 71 that ensures that amount
is less than avaxBalances[contractName]
File:/contracts/contract/Vault.sol 99: avaxBalances[fromContractName] = avaxBalances[fromContractName] - amount;
The operation avaxBalances[fromContractName] - amount
cannot underflow due to the check on Line 95 that ensures that amount
is less than avaxBalances[fromContractName]
File: contracts/contract/Vault.sol 155: tokenBalances[contractKey] = tokenBalances[contractKey] - amount;
The operation tokenBalances[contractKey] - amount
cannot underflow due to the check on Line 151 that ensures that amount
is less than tokenBalances[contractKey]
File:/contracts/contract/Vault.sol 187: tokenBalances[contractKeyFrom] = tokenBalances[contractKeyFrom] - amount;
The operation tokenBalances[contractKeyFrom] - amount
cannot underflow due to the check on Line 183 that ensures that amount
is less than tokenBalances[contractKeyFrom]
The majority of Solidity for loops increment a uint256 variable that starts at 0. These increment operations never need to be checked for over/underflow because the variable will never reach the max number of uint256 (will run out of gas long before that happens). The default over/underflow check wastes gas in every iteration of virtually every for loop . eg.
e.g Let's work with a sample loop below.
for(uint256 i; i < 10; i++){ //doSomething }
can be written as shown below.
for(uint256 i; i < 10;) { // loop logic unchecked { i++; } }
We can also write it as an inlined function like below.
function inc(i) internal pure returns (uint256) { unchecked { return i + 1; } } for(uint256 i; i < 10; i = inc(i)) { // doSomething }
File: /contracts/contract/MinipoolManager.sol 619: for (uint256 i = offset; i < max; i++) { 620: Minipool memory mp = getMinipool(int256(i)); 621: if (mp.status == uint256(status)) { 621: minipools[total] = mp; 622: total++; 623: } 624: }
The above should be modified to:
diff --git a/contracts/contract/MinipoolManager.sol b/contracts/contract/MinipoolManager.sol index 8563580..30f203c 100644 --- a/contracts/contract/MinipoolManager.sol +++ b/contracts/contract/MinipoolManager.sol @@ -616,12 +616,15 @@ contract MinipoolManager is Base, ReentrancyGuard, IWithdrawer { } minipools = new Minipool[](max - offset); uint256 total = 0; - for (uint256 i = offset; i < max; i++) { + for (uint256 i = offset; i < max;) { Minipool memory mp = getMinipool(int256(i)); if (mp.status == uint256(status)) { minipools[total] = mp; total++; } + unchecked { + ++i; + } }
Other Instances to modify
File: /contracts/contract/MultisigManager.sol 84: for (uint256 i = 0; i < total; i++) {
File: /contracts/contract/Ocyticus.sol 61: for (uint256 i = 0; i < count; i++) {
File: /contracts/contract/RewardsPool.sol 74: for (uint256 i = 0; i < inflationIntervalsElapsed; i++) { 215: for (uint256 i = 0; i < count; i++) { 230: for (uint256 i = 0; i < enabledMultisigs.length; i++) {
File:/contracts/contract/Staking.sol 428: for (uint256 i = offset; i < max; i++) {
keccak256()
should only need to be called on a specific string literal onceIt should be saved to an immutable variable, and the variable used instead. If the hash is being used as a part of a function selector, the cast to bytes4
should also only be done once
File: /contracts/contract/ClaimNodeOp.sol 36: return getUint(keccak256("NOPClaim.RewardsCycleTotal"));
File: /contracts/contract/ClaimNodeOp.sol 41: setUint(keccak256("NOPClaim.RewardsCycleTotal"), amount);
File: /contracts/contract/MinipoolManager.sol 248: minipoolIndex = int256(getUint(keccak256("minipool.count"))); 252: addUint(keccak256("minipool.count"), 1); 433: subUint(keccak256("MinipoolManager.TotalAVAXLiquidStakerAmt"), avaxLiquidStakerAmt); 512: subUint(keccak256("MinipoolManager.TotalAVAXLiquidStakerAmt"), avaxLiquidStakerAmt); 542: return getUint(keccak256("MinipoolManager.TotalAVAXLiquidStakerAmt")); 612: uint256 totalMinipools = getUint(keccak256("minipool.count")); 635: return getUint(keccak256("minipool.count"));
File: /contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol 139: keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), 170: keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 172: keccak256("1"),
Comparing to a constant (true or false) is a bit more expensive than directly checking the returned boolean value. I suggest using if(directValue) instead of if(directValue == true) and if(!directValue) instead of if(directValue == false) here:
File: /contracts/contract/BaseAbstract.sol 25: if (getBool(keccak256(abi.encodePacked("contract.exists", msg.sender))) == false) {
File: /contracts/contract/BaseAbstract.sol 74: if (enabled == false) { 75: revert MustBeMultisig(); 76: }
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) Custom errors save ~50 gas each time theyโre hit by avoiding having to allocate and store the revert string. Not defining the strings also save deployment gas
Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries). see Source
File: /contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol 127: require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); 154: require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");
File: /contracts/contract/tokens/upgradeable/ERC4626Upgradeable.sol 44: require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); 103: require((assets = previewRedeem(shares)) != 0, "ZERO_ASSETS");
The code can be optimized by minimizing the number of SLOADs.
SLOADs are expensive (100 gas after the 1st one) compared to MLOADs/MSTOREs (3 gas each). Storage values read multiple times should instead be cached in memory the first time (costing 1 SLOAD) and then read from this cache to avoid multiple SLOADs.
File: /contracts/contract/tokens/TokenggAVAX.sol 142: function depositFromStaking(uint256 baseAmt, uint256 rewardAmt) public payable onlySpecificRegisteredContract("MinipoolManager", msg.sender) { 144: if (totalAmt != (baseAmt + rewardAmt) || baseAmt > stakingTotalAssets) { 145: revert InvalidStakingDeposit(); 146: } 149: stakingTotalAssets -= baseAmt;
diff --git a/contracts/contract/tokens/TokenggAVAX.sol b/contracts/contract/tokens/TokenggAVAX.sol index dc3948d..9f131f6 100644 --- a/contracts/contract/tokens/TokenggAVAX.sol +++ b/contracts/contract/tokens/TokenggAVAX.sol @@ -141,12 +141,13 @@ contract TokenggAVAX is Initializable, ERC4626Upgradeable, UUPSUpgradeable, Base function depositFromStaking(uint256 baseAmt, uint256 rewardAmt) public payable onlySpecificRegisteredContract("MinipoolManager", msg.sender) { uint256 totalAmt = msg.value; - if (totalAmt != (baseAmt + rewardAmt) || baseAmt > stakingTotalAssets) { + uint256 _stakingTotalAssets = stakingTotalAssets; + if (totalAmt != (baseAmt + rewardAmt) || baseAmt > _stakingTotalAssets) { revert InvalidStakingDeposit(); } emit DepositedFromStaking(msg.sender, baseAmt, rewardAmt); - stakingTotalAssets -= baseAmt; + stakingTotalAssets = _stakingTotalAssets - baseAmt; IWAVAX(address(asset)).deposit{value: totalAmt}(); }
#0 - GalloDaSballo
2023-02-03T18:06:50Z
44: uint32 public rewardsCycleLength; 2.1k
800 from caching SLOADs
300 rest
3200
#1 - c4-judge
2023-02-03T19:12:38Z
GalloDaSballo marked the issue as grade-b