Platform: Code4rena
Start Date: 08/03/2023
Pot Size: $60,500 USDC
Total HM: 2
Participants: 123
Period: 7 days
Judge: hansfriese
Id: 220
League: ETH
Rank: 54/123
Findings: 1
Award: $149.89
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: JCN
Also found by: 0x1f8b, 0xSmartContract, 0xSolus, 0xhacksmithh, 0xnev, Angry_Mustache_Man, Aymen0909, Diana, Flora, Inspex, Madalad, MatricksDeCoder, MiniGlome, R-Nemes, RaymondFam, ReyAdmirado, Rolezn, SAAJ, Sathish9098, Shubham, Udsen, Viktor_Cortess, arialblack14, atharvasama, ayden, c3phas, carlitox477, descharre, dharma09, durianSausage, fatherOfBlocks, ginlee, glcanvas, hunter_w3b, leopoldjoy, matrix_0wl, mrpathfindr, nadin, oyc_109, pipoca, schrodinger, slvDev, ulqiorra, volodya
149.8945 USDC - $149.89
NB: Some functions have been truncated where necessary to just show affected parts of the code Through out the report some places might be denoted with audit tags to show the actual place affected.
I've tried to give the exact amount of gas being saved from running the included tests. Whenever the function is within the test coverage, the average gas before and after will be included, and often a diff of the code will also accompany this.
Some functions are not covered by the test cases or are internal/private functions. In this case, the gas can be estimated by looking at the opcodes involved.
Special attention should be paid on the use calldata instead of memory
as the average gas saved from the tests included seems to be too much
Here, the storage variables can be tightly packed
File: /contracts/staking/NeoTokyoStaker.sol 232: address public LP; 249: uint256 public VAULT_CAP; 510: /// Whether or not setting the LP token contract address is locked. 511: bool public lpLocked;
diff --git a/contracts/staking/NeoTokyoStaker.sol b/contracts/staking/NeoTokyoStaker.sol index a54d218..09710a4 100644 --- a/contracts/staking/NeoTokyoStaker.sol +++ b/contracts/staking/NeoTokyoStaker.sol @@ -231,6 +231,9 @@ contract NeoTokyoStaker is PermitControl, ReentrancyGuard { /// The address of the LP token contract. address public LP; + /// Whether or not setting the LP token contract address is locked. + bool public lpLocked; - /// Whether or not setting the LP token contract address is locked. - bool public lpLocked;
Some gas saved seems too much to be true, but included as it is from running the included tests If a reference type function parameter is read-only, it is cheaper in gas to use calldata instead of memory. Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory.
Gas benchmarks
Min | Max | Avg | |
---|---|---|---|
Before | 139971 | 162925 | 145710 |
After | 138817 | 161625 | 144519 |
File: /contracts/staking/NeoTokyoStaker.sol 1737: function configureTimelockOptions ( 1738: AssetType _assetType, 1739: uint256[] memory _timelockIds, 1740: uint256[] memory _encodedSettings 1741: ) external hasValidPermit(bytes32(uint256(_assetType)), CONFIGURE_TIMELOCKS) { 1742: for (uint256 i; i < _timelockIds.length; ) { 1743: timelockOptions[_assetType][_timelockIds[i]] = _encodedSettings[i]; 1744: unchecked { ++i; } 1745: } 1746: }
diff --git a/contracts/staking/NeoTokyoStaker.sol b/contracts/staking/NeoTokyoStaker.sol index a54d218..485c14f 100644 --- a/contracts/staking/NeoTokyoStaker.sol +++ b/contracts/staking/NeoTokyoStaker.sol @@ -1736,8 +1736,8 @@ contract NeoTokyoStaker is PermitControl, ReentrancyGuard { */ function configureTimelockOptions ( AssetType _assetType, - uint256[] memory _timelockIds, - uint256[] memory _encodedSettings + uint256[] calldata _timelockIds, + uint256[] calldata _encodedSettings ) external hasValidPermit(bytes32(uint256(_assetType)), CONFIGURE_TIMELOCKS) {
Gas benchmarks
Min | Max | Avg | |
---|---|---|---|
Before | - | - | 581101 |
After | - | - | 561317 |
File: /contracts/staking/NeoTokyoStaker.sol 1760: function configureIdentityCreditYields ( 1761: uint256[] memory _citizenRewardRates, 1762: string[] memory _vaultRewardRates, 1763: string[] memory _identityCreditYields 1764: ) hasValidPermit(UNIVERSAL, CONFIGURE_CREDITS) external { 1765: for (uint256 i; i < _citizenRewardRates.length; ) { 1766: identityCreditYield[ 1767: _citizenRewardRates[i] 1768: ][ 1769: _vaultRewardRates[i] 1770: ] = _identityCreditYields[i]; 1771: unchecked { ++i; } 1772: } 1773: }
diff --git a/contracts/staking/NeoTokyoStaker.sol b/contracts/staking/NeoTokyoStaker.sol index a54d218..97fff89 100644 --- a/contracts/staking/NeoTokyoStaker.sol +++ b/contracts/staking/NeoTokyoStaker.sol @@ -1758,9 +1758,9 @@ contract NeoTokyoStaker is PermitControl, ReentrancyGuard { `_vaultRewardRates`. */ function configureIdentityCreditYields ( - uint256[] memory _citizenRewardRates, - string[] memory _vaultRewardRates, - string[] memory _identityCreditYields + uint256[] calldata _citizenRewardRates, + string[] calldata _vaultRewardRates, + string[] calldata _identityCreditYields ) hasValidPermit(UNIVERSAL, CONFIGURE_CREDITS) external {
Gas benchmarks
Min | Max | Avg | |
---|---|---|---|
Before | - | - | 96450 |
After | - | - | 94385 |
File: /contracts/staking/NeoTokyoStaker.sol 1783: function configureIdentityCreditPoints ( 1784: string[] memory _identityCreditYields, 1785: uint256[] memory _points 1786: ) hasValidPermit(UNIVERSAL, CONFIGURE_CREDITS) external { 1787: for (uint256 i; i < _identityCreditYields.length; ) { 1788: identityCreditPoints[_identityCreditYields[i]] = _points[i]; 1789: unchecked { ++i; } 1790: } 1791: }
diff --git a/contracts/staking/NeoTokyoStaker.sol b/contracts/staking/NeoTokyoStaker.sol index a54d218..98b9f86 100644 --- a/contracts/staking/NeoTokyoStaker.sol +++ b/contracts/staking/NeoTokyoStaker.sol @@ -1781,8 +1781,8 @@ contract NeoTokyoStaker is PermitControl, ReentrancyGuard { `_identityCreditYields`. */ function configureIdentityCreditPoints ( - string[] memory _identityCreditYields, - uint256[] memory _points + string[] calldata _identityCreditYields, + uint256[] calldata _points ) hasValidPermit(UNIVERSAL, CONFIGURE_CREDITS) external {
Gas benchmarks
Min | Max | Avg | |
---|---|---|---|
Before | - | - | 167875 |
After | - | - | 164179 |
File: /contracts/staking/NeoTokyoStaker.sol 1802: function configureVaultCreditMultipliers ( 1803: string[] memory _vaultCreditMultipliers, 1804: uint256[] memory _multipliers 1805: ) hasValidPermit(UNIVERSAL, CONFIGURE_CREDITS) external { 1806: for (uint256 i; i < _vaultCreditMultipliers.length; ) { 1807: vaultCreditMultiplier[_vaultCreditMultipliers[i]] = _multipliers[i]; 1808: unchecked { ++i; } 1809: } 1810: }
diff --git a/contracts/staking/NeoTokyoStaker.sol b/contracts/staking/NeoTokyoStaker.sol index a54d218..8dc58e9 100644 --- a/contracts/staking/NeoTokyoStaker.sol +++ b/contracts/staking/NeoTokyoStaker.sol @@ -1800,8 +1800,8 @@ contract NeoTokyoStaker is PermitControl, ReentrancyGuard { value in `_vaultCreditMultipliers`. */ function configureVaultCreditMultipliers ( - string[] memory _vaultCreditMultipliers, - uint256[] memory _multipliers + string[] calldata _vaultCreditMultipliers, + uint256[] calldata _multipliers ) hasValidPermit(UNIVERSAL, CONFIGURE_CREDITS) external { for (uint256 i; i < _vaultCreditMultipliers.length; ) { vaultCreditMultiplier[_vaultCreditMultipliers[i]] = _multipliers[i];
Checks that involve constants should come before checks that involve state variables, function calls, and calculations. By doing these checks first, the function is able to revert before wasting a Gcoldsload (2100 gas) in a function that may ultimately revert in the unhappy case.
File: /contracts/staking/NeoTokyoStaker.sol 875: function _stakeS1Citizen ( 876: uint256 _timelock 877: ) private { 899: // Retrieve storage for tracking the staking state of this S1 Citizen. 900: StakedS1Citizen storage citizenStatus = stakedS1[msg.sender][citizenId]; 902: // Attach a getter to the S1 Citizen and check for a component Vault. 903: IGenericGetter citizen = IGenericGetter(S1_CITIZEN); 904: uint256 citizenVaultId = citizen.getVaultIdOfTokenId(citizenId); 910: if (citizenVaultId != 0 && vaultId != 0) { 911: revert CitizenAlreadyHasVault(citizenVaultId, vaultId); 917: } else if (citizenVaultId != 0 && vaultId == 0) { 918: citizenStatus.hasVault = true; 919: vaultId = citizenVaultId; 926: } else if (citizenVaultId == 0 && vaultId != 0) { 927: _assetTransferFrom(VAULT, msg.sender, address(this), vaultId); 928: citizenStatus.hasVault = true; 929: citizenStatus.stakedVaultId = vaultId; 930: }
On line 900 we read from storage , on line 911, we have a revert statement if citizenVaultId != 0 && vaultId != 0
If we end up reverting, we waste the gas used on reading from storage. We can refactor the code, to fail early and cheaply ie not wasting gas reading from storage.
As the revert is part of an if... else if
we can refactor that to use solely if
diff --git a/contracts/staking/NeoTokyoStaker.sol b/contracts/staking/NeoTokyoStaker.sol index a54d218..a791c2d 100644 --- a/contracts/staking/NeoTokyoStaker.sol +++ b/contracts/staking/NeoTokyoStaker.sol @@ -896,9 +896,6 @@ contract NeoTokyoStaker is PermitControl, ReentrancyGuard { */ _assetTransferFrom(S1_CITIZEN, msg.sender, address(this), citizenId); - // Retrieve storage for tracking the staking state of this S1 Citizen. - StakedS1Citizen storage citizenStatus = stakedS1[msg.sender][citizenId]; - // Attach a getter to the S1 Citizen and check for a component Vault. IGenericGetter citizen = IGenericGetter(S1_CITIZEN); uint256 citizenVaultId = citizen.getVaultIdOfTokenId(citizenId); @@ -914,7 +911,11 @@ contract NeoTokyoStaker is PermitControl, ReentrancyGuard { If no optional vault is provided, and the S1 Citizen being staked already has an existing Vault, override the provided `vaultId`. */ - } else if (citizenVaultId != 0 && vaultId == 0) { + } + // Retrieve storage for tracking the staking state of this S1 Citizen. + StakedS1Citizen storage citizenStatus = stakedS1[msg.sender][citizenId]; + + if (citizenVaultId != 0 && vaultId == 0) { citizenStatus.hasVault = true; vaultId = citizenVaultId; @@ -923,7 +924,8 @@ contract NeoTokyoStaker is PermitControl, ReentrancyGuard { Vault is staked and the S1 Citizen is recorded as carrying an optional, separately-attached vault. */ - } else if (citizenVaultId == 0 && vaultId != 0) { + } + if (citizenVaultId == 0 && vaultId != 0) { _assetTransferFrom(VAULT, msg.sender, address(this), vaultId); citizenStatus.hasVault = true; citizenStatus.stakedVaultId = vaultId;
File: /contracts/staking/NeoTokyoStaker.sol 625: function getCreditYield ( 626: uint256 _citizenId, 627: uint256 _vaultId 628: ) public view returns (string memory) { 630: // Retrieve the total reward rate of this S1 Citizen. 631: IGenericGetter citizen = IGenericGetter(S1_CITIZEN); 632: uint256 rewardRate = citizen.getRewardRateOfTokenId(_citizenId); 633: if (rewardRate == 0) { 634: revert CitizenDoesNotExist(_citizenId); 635: } 637: // Retrieve the credit rate multiplier of any associated Vault. 638: IGenericGetter vault = IGenericGetter(VAULT); 639: string memory vaultMultiplier = (_vaultId != 0) 640: ? vault.getCreditMultiplier(_vaultId) 641: : ""; 643: // Deduce the original Identity credit yield. 644: return identityCreditYield[rewardRate][vaultMultiplier]; 645: }
@@ -628,16 +629,14 @@ contract NeoTokyoStaker is PermitControl, ReentrancyGuard { ) public view returns (string memory) { // Retrieve the total reward rate of this S1 Citizen. - IGenericGetter citizen = IGenericGetter(S1_CITIZEN); - uint256 rewardRate = citizen.getRewardRateOfTokenId(_citizenId); + uint256 rewardRate = IGenericGetter(S1_CITIZEN).getRewardRateOfTokenId(_citizenId); if (rewardRate == 0) { revert CitizenDoesNotExist(_citizenId); } // Retrieve the credit rate multiplier of any associated Vault. - IGenericGetter vault = IGenericGetter(VAULT); string memory vaultMultiplier = (_vaultId != 0) - ? vault.getCreditMultiplier(_vaultId) + ? IGenericGetter(VAULT).getCreditMultiplier(_vaultId) : ""; // Deduce the original Identity credit yield.
File: /contracts/staking/NeoTokyoStaker.sol 659: function getConfiguredVaultMultiplier ( 660: uint256 _vaultId 661: ) public view returns (uint256) { 663: // Retrieve the credit rate multiplier of the Vault. 664: IGenericGetter vault = IGenericGetter(VAULT); 665: string memory vaultMultiplier = (_vaultId != 0) 666: ? vault.getCreditMultiplier(_vaultId) 667: : ""; 669: // Deduce the configured Vault multiplier. 670: return vaultCreditMultiplier[vaultMultiplier]; 671: }
diff --git a/contracts/staking/NeoTokyoStaker.sol b/contracts/staking/NeoTokyoStaker.sol index a54d218..f72bf08 100644 --- a/contracts/staking/NeoTokyoStaker.sol +++ b/contracts/staking/NeoTokyoStaker.sol @@ -661,9 +661,8 @@ contract NeoTokyoStaker is PermitControl, ReentrancyGuard { ) public view returns (uint256) { // Retrieve the credit rate multiplier of the Vault. - IGenericGetter vault = IGenericGetter(VAULT); string memory vaultMultiplier = (_vaultId != 0) - ? vault.getCreditMultiplier(_vaultId) + ? IGenericGetter(VAULT).getCreditMultiplier(_vaultId) : "";
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/staking/NeoTokyoStaker.sol function _stakeLP ( if (stakerLPPosition[msg.sender].multiplier == 0) { //@audit: 1st access - happy path stakerLPPosition[msg.sender].multiplier = timelockMultiplier;//@audit: 2nd access - happy path } else if (stakerLPPosition[msg.sender].multiplier != timelockMultiplier) { revert MismatchedTimelock(); } unchecked { uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR; // Update the caller's LP token stake. stakerLPPosition[msg.sender].timelockEndTime = block.timestamp + timelockDuration;//@audit: 3rd access - happy path stakerLPPosition[msg.sender].amount += amount;//@audit: 4th access - happy path stakerLPPosition[msg.sender].points += points;//@audit: 5th access - happy path
File: /contracts/staking/NeoTokyoStaker.sol 1409: function claimReward ( 1432: // Record the current time as the beginning time for checking rewards. 1433: lastRewardTime[_recipient][AssetType.S1_CITIZEN] = block.timestamp;//@audit: 1st access 1434: lastRewardTime[_recipient][AssetType.S2_CITIZEN] = block.timestamp;//@audit: 2nd access 1435: lastRewardTime[_recipient][AssetType.LP] = block.timestamp;//@audit: 3rd access
The following are not in scope, Calling them out as we can pack with no precision loss
As the solidity EVM works with 32 bytes, variables less than 32 bytes should be packed inside a struct so that they can be stored in the same slot, this saves gas when writing to storage ~20000 gas
As uint256 timelockEndTime is a timestamp, we can reduce the size from uint256 to maybe uint128 with no side effects. This would still be safe against overflows
File: /contracts/staking/NeoTokyoStaker.sol 359: struct StakedS1Citizen { 360: uint256 stakedBytes; 361: uint256 timelockEndTime; 362: uint256 points; 363: uint256 stakedVaultId; 364: bool hasVault; 365: }
Reduce the size of the variable timelockEndTime
and pack it with hasVault
diff --git a/contracts/staking/NeoTokyoStaker.sol b/contracts/staking/NeoTokyoStaker.sol index a54d218..b9ea61e 100644 --- a/contracts/staking/NeoTokyoStaker.sol +++ b/contracts/staking/NeoTokyoStaker.sol @@ -358,10 +358,10 @@ contract NeoTokyoStaker is PermitControl, ReentrancyGuard { */ struct StakedS1Citizen { uint256 stakedBytes; - uint256 timelockEndTime; + uint128 timelockEndTime; + bool hasVault; uint256 points; uint256 stakedVaultId; - bool hasVault; } /**
As uint256 timelockEndTime is a timestamp, we can reduce the size from uint256 to maybe uint128 with no side effects. This would still be safe against overflows
File: /contracts/staking/NeoTokyoStaker.sol 462: struct StakedS1CitizenOutput { 463: uint256 citizenId; 464: uint256 stakedBytes; 465: uint256 timelockEndTime; 466: uint256 points; 467: uint256 stakedVaultId; 468: bool hasVault; 469: }
diff --git a/contracts/staking/NeoTokyoStaker.sol b/contracts/staking/NeoTokyoStaker.sol index a54d218..8de274f 100644 --- a/contracts/staking/NeoTokyoStaker.sol +++ b/contracts/staking/NeoTokyoStaker.sol @@ -462,10 +462,11 @@ contract NeoTokyoStaker is PermitControl, ReentrancyGuard { struct StakedS1CitizenOutput { uint256 citizenId; uint256 stakedBytes; - uint256 timelockEndTime; + uint128 timelockEndTime; + bool hasVault; uint256 points; uint256 stakedVaultId; - bool hasVault; + }
#0 - c4-judge
2023-03-17T03:44:10Z
hansfriese marked the issue as grade-a
#1 - TimTinkers
2023-03-21T01:40:29Z
Great report, thank you.
#2 - c4-sponsor
2023-03-21T01:40:36Z
TimTinkers marked the issue as sponsor confirmed