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: 15/123
Findings: 2
Award: $385.13
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xSmartContract
Also found by: 0x1f8b, 0x6980, 0xAgro, 0xSolus, 0xhacksmithh, 0xkazim, ABA, BPZ, BowTiedOriole, ChainReview, DadeKuma, DeFiHackLabs, Deathstore, DevABDee, Diana, Dravee, Dug, Englave, Go-Langer, Haipls, IceBear, Inspex, Jeiwan, Kek, Kresh, Madalad, MatricksDeCoder, MyFDsYours, RaymondFam, Rolezn, SAAJ, Sathish9098, Taloner, Udsen, Viktor_Cortess, atharvasama, ayden, brgltd, btk, carlitox477, catellatech, chaduke, codeislight, deadrxsezzz, descharre, erictee, fatherOfBlocks, favelanky, glcanvas, handsomegiraffe, jasonxiale, jekapi, joestakey, lemonr, luxartvinsec, martin, matrix_0wl, minhquanym, mrpathfindr, nadin, oyc_109, parsely, peanuts, pfedprog, rbserver, rokso, saian, santipu_, scokaf, slvDev, tsvetanovv, ubl4nk, ulqiorra, yamapyblack, zaskoh
235.2398 USDC - $235.24
Issue | Contexts | |
---|---|---|
LOW‑1 | Add to blacklist function | 1 |
LOW‑2 | Function can risk gas exhaustion on large receipt calls due to multiple mandatory loops | 2 |
LOW‑3 | Possible rounding issue | 10 |
LOW‑4 | Contracts are not using their OZ Upgradeable counterparts | 3 |
LOW‑5 | Unbounded loop | 2 |
LOW‑6 | Upgrade OpenZeppelin Contract Dependency | 2 |
LOW‑7 | Missing Non Reentrancy Modifiers | 1 |
Total: 21 contexts over 7 issues
Issue | Contexts | |
---|---|---|
NC‑1 | Add a timelock to critical functions | 2 |
NC‑2 | Avoid Floating Pragmas: The Version Should Be Locked | 2 |
NC‑3 | Variable names that consist of all capital letters should be reserved for constant /immutable variables | 5 |
NC‑4 | Variable Names That Consist Of All Capital Letters Should Be Reserved For Const/immutable Variables | 3 |
NC‑5 | Constants Should Be Defined Rather Than Using Magic Numbers | 6 |
NC‑6 | Constants in comparisons should appear on the left side | 5 |
NC‑7 | Duplicated require() /revert() Checks Should Be Refactored To A Modifier Or Function | 2 |
NC‑8 | Function writing that does not comply with the Solidity Style Guide | 2 |
NC‑9 | Use delete to Clear Variables | 8 |
NC‑10 | NatSpec return parameters should be included in contracts | 1 |
NC‑11 | NatSpec comments should be increased in contracts | 1 |
NC‑12 | Non-usage of specific imports | 6 |
NC‑13 | Empty blocks should be removed or emit something | 8 |
NC‑14 | Use private constant consistently | 5 |
Total: 56 contexts over 14 issues
blacklist
functionNFT thefts have increased recently, so with the addition of hacked NFTs to the platform, NFTs can be converted into liquidity. To prevent this, I recommend adding the blacklist function.
Marketplaces such as Opensea have a blacklist feature that will not list NFTs that have been reported theft, NFT projects such as Manifold have blacklist functions in their smart contracts.
Here is the project example; Manifold
Manifold Contract https://etherscan.io/address/0xe4e4003afe3765aca8149a82fc064c0b125b9e5a#code
modifier nonBlacklistRequired(address extension) { require(!_blacklistedExtensions.contains(extension), "Extension blacklisted"); _; }
Add to Blacklist function and modifier.
The function contains loops over arrays that the user cannot influence. In any call for the function, users spend gas to iterate over the array. There are loops in the following functions that iterate all array values. Which could risk gas exhaustion on large array calls.
This risk is small, but could be lessened somewhat by allowing separate calls for small loops. Consider allowing partial calls via array arguments, or detecting expensive call bundles and only partially iterating over them. Since the logic already filters out calls, this would make the later loops smaller.
1459: function _withdrawS1Citizen () private { ... uint256[] storage oldPosition = _stakerS1Position[msg.sender]; for (uint256 stakedIndex; stakedIndex < oldPosition.length; ) { if (citizenId == oldPosition[stakedIndex]) { if (stakedIndex != oldPosition.length - 1) { oldPosition[stakedIndex] = oldPosition[oldPosition.length - 1]; } oldPosition.pop(); break; } unchecked { stakedIndex++; } } ... }
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1459
Division by large numbers may result in the result not rounding properlly, due to solidity not supporting fractions. Consider requiring a minimum amount for the numerator to ensure that it is always larger than the denominator
152: treasuryShare = _amount * 2 / 3;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/BYTES2.sol#L152
The non-upgradeable standard version of OpenZeppelin’s library are inherited / used by the contracts. It would be safer to use the upgradeable versions of the library contracts to avoid unexpected behaviour.
4: import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5: import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/BYTES2.sol#L4-L5
4: import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L4
Where applicable, use the contracts from @openzeppelin/contracts-upgradeable
instead of @openzeppelin/contracts
.
See https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/tree/master/contracts for list of available upgradeable contracts
An outdated OZ version is used (which has known vulnerabilities, see: https://github.com/OpenZeppelin/openzeppelin-contracts/security/advisories).
Require dependencies to be at least version of 4.8.1 to include critical vulnerability patches.
"@openzeppelin/contracts-upgradeable": "^4.4.2"
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/../package.json#L9
"@openzeppelin/contracts": "^4.3.1"
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/../package.json#L23
Update OpenZeppelin Contracts Usage in package.json and require at least version of 4.8.1 via >=4.8.1
The claimReward
is missing nonrenonReentrantentrant
modifier although some other public/external functions does use the nonReentrant
modifer such as stake
and withdraw
. It seems like the claimReward
function should have the nonReentrant
modifier as the other functions have it as well.
function claimReward ( address _recipient ) external returns (uint256, uint256) {
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1409
Avoid floating pragmas for non-library contracts.
While floating pragmas make sense for libraries to allow them to be included with multiple different versions of applications, it may be a security risk for application implementations.
A known vulnerable compiler version may accidentally be selected or security tools might fall-back to an older compiler version ending up checking a different EVM compilation that is ultimately deployed on the blockchain.
It is recommended to pin to a concrete compiler version.
Found usage of floating pragmas ^0.8.19 of Solidity in [BYTES2.sol]
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/BYTES2.sol#L2
Found usage of floating pragmas ^0.8.19 of Solidity in [NeoTokyoStaker.sol]
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L2
constant
/immutable
variablesVariable names with all capital letters should be restricted to be used only with constant
or immutable
variables.
If the variable needs to be different based on which class it comes from, a view
/pure
function should be used instead (e.g. like <a href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/76eee35971c2541585e05cbf258510dda7b2fbc6/contracts/token/ERC20/extensions/draft-IERC20Permit.sol#L59">this</a>).
49: address public STAKER;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/BYTES2.sol#L49
52: address public TREASURY;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/BYTES2.sol#L52
232: address public LP;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L232
249: uint256 public VAULT_CAP;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L249
255: uint256 public NO_VAULT_CAP;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L255
1022: citizenStatus.points = 100 * timelockMultiplier / _DIVISOR;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1022
1155: uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1205 ```solidity 1623: uint256 points = amount * 100 / 1e18 * lpPosition.multiplier / _DIVISOR;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1623
Doing so will prevent <a href="https://www.moserware.com/2008/01/constants-on-left-are-better-but-this.html">typo bugs</a>
633: if (rewardRate == 0) {
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L633
1144: if (stakerLPPosition[msg.sender].multiplier == 0) {
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1144
1221: if (timelockOption == 0) {
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1221
1472: if (stakedCitizen.timelockEndTime == 0) {
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1472
1547: if (stakedCitizen.timelockEndTime == 0) {
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1547
require()
/revert()
Checks Should Be Refactored To A Modifier Or FunctionSaves deployment costs
784: revert(string(data)); 812: revert(string(data));
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L784
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L812
Order of Functions; ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier. But there are contracts in the project that do not comply with this.
https://docs.soliditylang.org/en/v0.8.17/style-guide.html
Functions should be grouped according to their visibility and ordered:
delete
to Clear Variablesdelete a
assigns the initial value for the type to a
. i.e. for integers it is equivalent to a = 0
, but it can also be used on arrays, where it assigns a dynamic array of length zero or a static array of the same length with all elements reset. For structs, it assigns a struct with all members reset. Similarly, it can also be used to set an address to zero address. It has no effect on whole mappings though (as the keys of mappings may be arbitrary and are generally unknown). However, individual keys and what they map to can be deleted: If a
is a mapping, then delete a[x]
will delete the value stored at x
.
The delete
key better conveys the intention and is also more idiomatic. Consider replacing assignments of zero with delete
statements.
1517: stakedCitizen.stakedBytes = 0; 1518: stakedCitizen.timelockEndTime = 0; 1519: stakedCitizen.points = 0; 1521: stakedCitizen.stakedVaultId = 0;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1517
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1518
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1519
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1521
1582: stakedCitizen.stakedBytes = 0; 1583: stakedCitizen.timelockEndTime = 0; 1584: stakedCitizen.points = 0;
1635: lpPosition.multiplier = 0;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1635
It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.
https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
Include return parameters in NatSpec comments
Recommendation Code Style: (from Uniswap3)
/// @notice Adds liquidity for the given recipient/tickLower/tickUpper position /// @dev The caller of this method receives a callback in the form of IUniswapV3MintCallback#uniswapV3MintCallback /// in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends /// on tickLower, tickUpper, the amount of liquidity, and the current price. /// @param recipient The address for which the liquidity will be created /// @param tickLower The lower tick of the position in which to add liquidity /// @param tickUpper The upper tick of the position in which to add liquidity /// @param amount The amount of liquidity to mint /// @param data Any data that should be passed through to the callback /// @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in the callback /// @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in the callback function mint( address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data ) external returns (uint256 amount0, uint256 amount1);
For example see NeoTokyoStaker.getPoolReward()
/** This function supports retrieving the reward and tax earned by a particular `_recipient` on a specific pool of type `_assetType`. @param _assetType The type of the asset to calculate rewards for. @param _recipient The recipient of the reward. */ function getPoolReward ( AssetType _assetType, address _recipient ) public view returns (uint256, uint256) {
It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability. https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
For example see NeoTokyoStaker._stakeBytes()
/** A private function for managing the staking of BYTES into a Citizen. */ function _stakeBytes ( uint256 ) private {
NatSpec comments should be increased in contracts
The current form of relative path import is not recommended for use because it can unpredictably pollute the namespace. Instead, the Solidity docs recommend specifying imported symbols explicitly. https://docs.soliditylang.org/en/v0.8.15/layout-of-source-files.html#importing-other-source-files
A good example:
import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {SafeCastLib} from "solmate/utils/SafeCastLib.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {IProducer} from "src/interfaces/IProducer.sol"; import {GlobalState, UserState} from "src/Common.sol";
7: import "../access/PermitControl.sol"; 8: import "../interfaces/IByteContract.sol"; 9: import "../interfaces/IStaker.sol";
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/BYTES2.sol#L7-L9
6: import "../access/PermitControl.sol"; 7: import "../interfaces/IByteContract.sol"; 8: import "../interfaces/IGenericGetter.sol";
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L6-L8
Use specific imports syntax per solidity docs recommendation.
193: ) external { } 207: ) external { }
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/BYTES2.sol#L193
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/BYTES2.sol#L207
1250: default {} 1693: default {}
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1250
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1693
private constant
consistentlyReplace constant private
with private constant
.
191: bytes4 constant private _TRANSFER_FROM_SELECTOR = 0x23b872dd; 194: bytes4 constant private _TRANSFER_SELECTOR = 0xa9059cbb; 197: uint256 constant private _PRECISION = 1e12; 200: uint256 constant private _DIVISOR = 100; 203: uint256 constant private _BYTES_PER_POINT = 200 * 1e18;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L191
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L194
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L197
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L200
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L203
#0 - c4-judge
2023-03-17T03:20:53Z
hansfriese marked the issue as grade-a
🌟 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
Issue | Contexts | Estimated Gas Saved | |
---|---|---|---|
GAS‑1 | Use calldata instead of memory for function parameters | 1 | 300 |
GAS‑2 | Do not calculate constants | 1 | - |
GAS‑3 | Duplicated require() /revert() Checks Should Be Refactored To A Modifier Or Function | 2 | 56 |
GAS‑4 | Empty Blocks Should Be Removed Or Emit Something | 2 | - |
GAS‑5 | Using delete statement can save gas | 8 | - |
GAS‑6 | Use assembly to write address storage values | 3 | - |
GAS‑7 | Use hardcoded address instead address(this) | 8 | - |
GAS‑8 | Optimize names to save gas | 2 | 44 |
GAS‑9 | <x> += <y> Costs More Gas Than <x> = <x> + <y> For State Variables | 8 | - |
GAS‑10 | Structs can be packed into fewer storage slots by editing time variables | 5~8 | 10000~16000 |
GAS‑11 | Public Functions To External | 3 | - |
GAS‑12 | Save gas with the use of specific import statements | 6 | - |
GAS‑13 | Help The Optimizer By Saving A Storage Variable’s Reference Instead Of Repeatedly Fetching It | 1 | - |
GAS‑14 | Use v4.8.1 OpenZeppelin contracts | 2 | - |
Total: 52 contexts over 14 issues
In some cases, having function arguments in calldata instead of memory is more optimal.
Consider the following generic example:
contract C { function add(uint[] memory arr) external returns (uint sum) { uint length = arr.length; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } } }
In the above example, the dynamic array arr has the storage location memory. When the function gets called externally, the array values are kept in calldata and copied to memory during ABI decoding (using the opcode calldataload and mstore). And during the for loop, arr[i] accesses the value in memory using a mload. However, for the above example this is inefficient. Consider the following snippet instead:
contract C { function add(uint[] calldata arr) external returns (uint sum) { uint length = arr.length; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } } }
In the above snippet, instead of going via memory, the value is directly read from calldata using calldataload. That is, there are no intermediate memory operations that carries this value.
Gas savings: In the former example, the ABI decoding begins with copying value from calldata to memory in a for loop. Each iteration would cost at least 60 gas. In the latter example, this can be completely avoided. This will also reduce the number of instructions and therefore reduces the deploy time cost of the contract.
In short, use calldata instead of memory if the function argument is only read.
Note that in older Solidity versions, changing some function arguments from memory to calldata may cause "unimplemented feature error". This can be avoided by using a newer (0.8.*) Solidity compiler.
Examples Note: The following pattern is prevalent in the codebase:
function f(bytes memory data) external { (...) = abi.decode(data, (..., types, ...)); }
Here, changing to bytes calldata will decrease the gas. The total savings for this change across all such uses would be quite significant.
function getStakerPosition ( address _staker, AssetType _assetType ) external view returns (uint256[] memory) {
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L689
Due to how constant variables are implemented (replacements at compile-time), an expression assigned to a constant variable is recomputed each time that the variable is used, which wastes some gas.
203: uint256 constant private _BYTES_PER_POINT = 200 * 1e18;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L203
require()
/revert()
Checks Should Be Refactored To A Modifier Or FunctionSaves deployment costs
784: revert(string(data)); 812: revert(string(data));
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L784
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L812
The code should be refactored such that they no longer exist, or the block should do something useful, such as emitting an event or reverting. If the contract is meant to be extended, the contract should be abstract and the function signatures be added without any default implementation. If the block is an empty if-statement block to avoid doing subsequent checks in the else-if/else conditions, the else-if/else conditions should be nested under the negation of the if-statement, because they involve different classes of checks, which may lead to the introduction of errors when the code is later modified (if(x){}else if(y){...}else{...} => if(!x){if(y){...}else{...}})
1250: default {}
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1250
1693: default {}
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1693
delete
statement can save gas1517: stakedCitizen.stakedBytes = 0; 1518: stakedCitizen.timelockEndTime = 0; 1519: stakedCitizen.points = 0; 1521: stakedCitizen.stakedVaultId = 0;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1517
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1518
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1519
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1521
1582: stakedCitizen.stakedBytes = 0; 1583: stakedCitizen.timelockEndTime = 0; 1584: stakedCitizen.points = 0;
1635: lpPosition.multiplier = 0;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1635
assembly
to write address storage values1231: _b = _stakeBytes;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1231
1232: _lp = _stakeLP;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1232
1678: _lp = _withdrawLP;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1678
address(this)
Instead of using address(this)
, it is more gas-efficient to pre-calculate and use the hardcoded address
. Foundry's script.sol and solmate's LibRlp.sol
contracts can help achieve this.
References: https://book.getfoundry.sh/reference/forge-std/compute-create-address
https://twitter.com/transmissions11/status/1518507047943245824
897: _assetTransferFrom(S1_CITIZEN, msg.sender, address(this), citizenId); 927: _assetTransferFrom(VAULT, msg.sender, address(this), vaultId);
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L897
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L927
1010: _assetTransferFrom(S2_CITIZEN, msg.sender, address(this), citizenId);
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1010
1057: _assetTransferFrom(BYTES, msg.sender, address(this), amount);
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1057
1137: _assetTransferFrom(LP, msg.sender, address(this), amount);
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1137
1485: address(this), 1492: _assetTransferFrom(S1_CITIZEN, address(this), msg.sender, citizenId);
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1485
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1492
1557: _assetTransferFrom(S2_CITIZEN, address(this), msg.sender, citizenId);
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1557
Use hardcoded address
Contracts most called functions could simply save gas by function ordering via Method ID. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions.
See more <a href="https://medium.com/joyso/solidity-how-does-function-name-affect-gas-consumption-in-smart-contract-47d270d8ac92">here</a>
All in-scope contracts
Find a lower method ID name for the most called functions for example Call() vs. Call1() is cheaper by 22 gas For example, the function IDs in the Gauge.sol contract will be the most used; A lower method ID may be given.
<x> += <y>
Costs More Gas Than <x> = <x> + <y>
For State Variables977: pool.totalPoints += citizenStatus.points;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L977
1029: pool.totalPoints += citizenStatus.points;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1029
1078: citizenStatus.stakedBytes += amount; 1079: citizenStatus.points += bonusPoints; 1080: pool.totalPoints += bonusPoints; 1078: citizenStatus.stakedBytes += amount; 1079: citizenStatus.points += bonusPoints; 1080: pool.totalPoints += bonusPoints;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1078
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1079
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1080
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1078
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1079
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1080
1160: stakerLPPosition[msg.sender].amount += amount; 1161: stakerLPPosition[msg.sender].points += points; 1164: pool.totalPoints += points;
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1160
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1161
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1164
The following structs contain time variables that are unlikely to ever reach max uint256 then it is possible to set them as uint64, saving gas slots. In total, it is possible to save around 5-8 storage slots
359: struct StakedS1Citizen { uint256 stakedBytes; uint256 timelockEndTime; uint256 points; uint256 stakedVaultId; bool hasVault; }
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L359
Can save one storage slot by editing timelockEndTime
to uint64
and changing to:
359: struct StakedS1Citizen { uint256 stakedBytes; uint256 points; uint256 stakedVaultId; bool hasVault; uint64 timelockEndTime; }
The same applies to:
462: struct StakedS1CitizenOutput { uint256 citizenId; uint256 stakedBytes; uint256 timelockEndTime; uint256 points; uint256 stakedVaultId; bool hasVault; }
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L462
For the following, each struct can save between 1-2 storage slot by changing timelockEndTime
to uint64
and the rest to a smaller uint such as uint128
if uint256
if the value is unlikely to go over max uin128
394: struct StakedS2Citizen { uint256 stakedBytes; uint256 timelockEndTime; uint256 points; } 423: struct LPPosition { uint256 amount; uint256 timelockEndTime; uint256 points; uint256 multiplier; } 486: struct StakedS2CitizenOutput { uint256 citizenId; uint256 stakedBytes; uint256 timelockEndTime; uint256 points; }
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L394 https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L423 https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L486 ### <a href="#Summary">[GAS‑11]</a><a name="GAS‑11"> Public Functions To External The following functions could be set external to save gas and improve code quality. External call cost is less expensive than of public functions. #### <ins>Proof Of Concept</ins> ```solidity function getCreditYield ( uint256 _citizenId, uint256 _vaultId ) public view returns (string memory) {
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L625
function getConfiguredVaultMultiplier ( uint256 _vaultId ) public view returns (uint256) {
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L659
function getPoolReward ( AssetType _assetType, address _recipient ) public view returns (uint256, uint256) {
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1264
The current form of relative path import is not recommended for use because it can unpredictably pollute the namespace. Instead, the Solidity docs recommend specifying imported symbols explicitly. https://docs.soliditylang.org/en/v0.8.15/layout-of-source-files.html#importing-other-source-files
A good example:
import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {SafeCastLib} from "solmate/utils/SafeCastLib.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {IProducer} from "src/interfaces/IProducer.sol"; import {GlobalState, UserState} from "src/Common.sol";
7: import "../access/PermitControl.sol"; 8: import "../interfaces/IByteContract.sol"; 9: import "../interfaces/IStaker.sol";
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/BYTES2.sol#L7-L9
6: import "../access/PermitControl.sol"; 7: import "../interfaces/IByteContract.sol"; 8: import "../interfaces/IGenericGetter.sol";
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L6-L8
Use specific imports syntax per solidity docs recommendation.
To help the optimizer, declare a storage type variable and use it instead of repeatedly fetching the reference in a map or an array. The effect can be quite significant. As an example, instead of repeatedly calling someMap[someIndex], save its reference like this: SomeStruct storage someStruct = someMap[someIndex] and use it.
1502: if (citizenId == oldPosition[stakedIndex]) {
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/staking/NeoTokyoStaker.sol#L1502
The upcoming v4.8.1 version of OpenZeppelin provides many small gas optimizations. https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v4.8.1
"@openzeppelin/contracts-upgradeable": "^4.4.2"
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/../package.json#L9
"@openzeppelin/contracts": "^4.3.1"
https://github.com/code-423n4/2023-03-neotokyo/tree/main/contracts/../package.json#L23
Update OpenZeppelin Contracts Usage in package.json and require at least version of 4.8.1
#0 - c4-judge
2023-03-17T04:33:44Z
hansfriese marked the issue as grade-a