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: 16/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
It is important to always check for the value address(0x0) (also known as address(0)) when assigning values to address state variables. This is because address(0x0) represents the Ethereum zero address, which has a special meaning and can cause unexpected behavior if not properly handled
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
81: BYTES1 = _bytes; 82: S1_CITIZEN = _s1Citizen; 83: STAKER = _staker; 84: TREASURY = _treasury;
function changeStakingContractAddress ( address _staker ) hasValidPermit(UNIVERSAL, ADMIN) external { STAKER = _staker; }
function changeTreasuryContractAddress ( address _treasury ) hasValidPermit(UNIVERSAL, ADMIN) external { TREASURY = _treasury; }
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
598: BYTES = _bytes; 599: S1_CITIZEN = _s1Citizen; 600: S2_CITIZEN = _s2Citizen; 601: LP = _lpToken; 602: IDENTITY = _identity; 603: VAULT = _vault;
Recommended Mitigation:
Add address(0) checks before assigning value to state variables
The safeMint() function is typically used in contracts that implement the ERC20 or ERC721 standards, which are widely adopted standards for creating fungible and non-fungible tokens on the Ethereum blockchain. The safeMint() function is designed to prevent potential security vulnerabilities in these token contracts by including additional checks and safeguards to prevent certain types of attacks, such as integer overflows or reentrancy attacks
OpenZeppelin recommendation is to use the safe variant of _mint.
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
102 : _mint(msg.sender, _amount);
124: _mint(_to, reward);
127 : _mint(TREASURY, daoCommision);
154: _mint(TREASURY, treasuryShare);
Recommended Mitigation:
Replace _mint() with _safeMint()
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
604: VAULT_CAP = _vaultCap; 605: NO_VAULT_CAP = _noVaultCap;
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
_amountis not checked to be != 0 before calling the _burn()
function burn (address _from,uint256 _amount) hasValidPermit(UNIVERSAL, BURN) external {
_burn(_from, _amount);
The critical procedures should be two step process.
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
function changeStakingContractAddress ( address _staker ) hasValidPermit(UNIVERSAL, ADMIN) external { STAKER = _staker; }
function changeTreasuryContractAddress ( address _treasury ) hasValidPermit(UNIVERSAL, ADMIN) external { TREASURY = _treasury; }
function configureLP ( address _lp ) external hasValidPermit(UNIVERSAL, CONFIGURE_LP) { if (lpLocked) { revert LockedConfigurationOfLP(); } LP = _lp; }
Rounding is a process of approximating a number by replacing it with a nearby value that has fewer digits. In computing, rounding is often used to reduce the number of decimal places or significant digits in a calculation to a level that is appropriate for the context in which the result will be used. However, rounding can lead to a loss of precision in the calculation, which can be a problem in some situations
For example, suppose we want to calculate the average of two numbers, 1.23456789 and 9.87654321, to two decimal places. The exact answer is 5.55555555, but if we round each number to two decimal places before averaging, we get 1.23 and 9.88, which give an average of 5.555. This answer is less precise than the exact answer, and the error introduced by rounding can accumulate in more complex calculations
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
968: citizenStatus.points = identityPoints * vaultMultiplier * timelockMultiplier /_DIVISOR / _DIVISOR;
1022: citizenStatus.points = 100 * timelockMultiplier / _DIVISOR;
1077: uint256 bonusPoints = (amount * 100 / _BYTES_PER_POINT);
1155: uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR;
1388: uint256 share = points * _PRECISION / pool.totalPoints * totalReward; 1389: uint256 daoShare = share * pool.daoTax / (100 * _DIVISOR);
1623: uint256 points = amount * 100 / 1e18 * lpPosition.multiplier / _DIVISOR;
The SafeCast library provides a set of functions for safely casting between integer types in Solidity. These functions perform various checks to prevent integer overflows and underflows that can occur when performing type conversions
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
1205: if (uint8(_assetType) > 4) { //@audit uint8 safecast
1206: revert InvalidAssetType(uint256(_assetType)); //@audit uint256 safecast
1211: revert UnconfiguredPool(uint256(_assetType));
1216: revert InactivePool(uint256(_assetType));
1222: revert InvalidTimelockOption(uint256(_assetType), _timelockId);
1307: revert InvalidAssetType(uint256(_assetType));
1668: if (uint8(_assetType) == 2 || uint8(_assetType) > 4) { 1669: revert InvalidAssetType(uint256(_assetType));
Recommended Mitigation:
Use OpenZeppelin safeCast library
return data (bool success,) has to be stored due to EVM architecture, if in a usage like below, ‘out’ and ‘outsize’ values are given (0,0) . Thus, this storage disappears and may come from external contracts a possible Gas griefing/theft problem is avoided
(https://twitter.com/pashovkrum/status/1607024043718316032?t=xs30iD6ORWtE2bTTYsCFIQ&s=19)
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
(bool success, bytes memory data) = _asset.call( abi.encodeWithSelector( _TRANSFER_FROM_SELECTOR, _from, _to, _idOrAmount ) );
Adding events for critical parameter changes will facilitate offchain monitoring and indexing
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
function changeStakingContractAddress ( address _staker ) hasValidPermit(UNIVERSAL, ADMIN) external { STAKER = _staker; }
function changeTreasuryContractAddress ( address _treasury ) hasValidPermit(UNIVERSAL, ADMIN) external { TREASURY = _treasury; }
function configureLP ( address _lp ) external hasValidPermit(UNIVERSAL, CONFIGURE_LP) { if (lpLocked) { revert LockedConfigurationOfLP(); } LP = _lp; }
Recommended Mitigation Steps :
Add events for every critical changes happen in protocol with old and new values
It can’t be recovered if the tokens accidentally arrive at the contract address, which has happened to many popular projects, so I recommend adding a recovery code to your critical contracts.
Recommended Mitigation Steps
Add this code:
/**
*/ function rescueERC20(address account, address externalToken, uint256 amount) public onlyOwner returns (bool) { IERC20(externalToken).transfer(account, amount); return true; } }
"@openzeppelin/contracts-upgradeable": "^4.4.2" version used in this protocol
Recommended mitigation:
Use safest openzeppelin version 4.8.2
It’s possible to name the imports to improve code readability. E.g. import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; can be rewritten as import {IERC20} from “import “@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol”;
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../access/PermitControl.sol"; import "../interfaces/IByteContract.sol"; import "../interfaces/IStaker.sol";
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "../access/PermitControl.sol"; import "../interfaces/IByteContract.sol"; import "../interfaces/IGenericGetter.sol";
While it doesn’t save any gas because the compiler knows that developers often make this mistake, it’s still best to use the right tool for the task at hand. There is a difference between constant variables and immutable variables, and they should each be used in their appropriate contexts. constants should be used for literal values written into the code, and immutable variables should be used for expressions, or values calculated in, or passed into the constructor.
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
37: bytes32 public constant BURN = keccak256("BURN"); 40: bytes32 public constant ADMIN = keccak256("ADMIN");
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
206: bytes32 public constant CONFIGURE_LP = keccak256("CONFIGURE_LP");
209: bytes32 public constant CONFIGURE_TIMELOCKS = keccak256( "CONFIGURE_TIMELOCKS" );
214: bytes32 public constant CONFIGURE_CREDITS = keccak256("CONFIGURE_CREDITS");
217: bytes32 public constant CONFIGURE_POOLS = keccak256("CONFIGURE_POOLS");
220: bytes32 public constant CONFIGURE_CAPS = keccak256("CONFIGURE_CAPS");
in Solidity, the order of modifiers in a variable declaration matters. The public modifier should come before the immutable modifier.
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
43: address immutable public BYTES1;
46: address immutable public S1_CITIZEN;
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
223: address immutable public BYTES; 226: address immutable public S1_CITIZEN; 229: address immutable public S2_CITIZEN;
239: address immutable public IDENTITY;
242: address immutable public VAULT;
it is common practice to use lowercase letters for variable names, and to use capital letters for contract names and event names Using capital letters for state variable names may cause confusion or make the code harder to read, especially if the convention is not consistent throughout the codebase. Therefore, it is generally recommended to use lowercase letters for state variable names.
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
49: address public STAKER;
52: address public TREASURY;
(https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/BYTES2.sol#L49-L52) FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
232: address public LP;
CONTEXT: ALL FUNCTIONS
When this function is compiled, the Natspec comments containing the @notice and @param tags are included in the contract's metadata
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
/** Allow holders of the old BYTES contract to change them for BYTES 2.0; old BYTES tokens will be burnt. @param _amount The amount of old BYTES tokens to exchange. */ function upgradeBytes (
/** This function is called by the S1 Citizen contract to emit BYTES to callers based on their state from the staker contract. @param _to The reward address to mint BYTES to. */ function getReward ( address _to ) external {
(https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/BYTES2.sol#L131-L140) (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/BYTES2.sol#L157-L162) (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/BYTES2.sol#L169-L173) (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/BYTES2.sol#L179-L189)
If the _to is zero address the overall functions executions waste of work
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
function getReward ( address _to ) external { (uint256 reward,uint256 daoCommision) = IStaker(STAKER).claimReward(_to);
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
151: unchecked { treasuryShare = _amount * 2 / 3; }
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
728 : unchecked { i++; }
743: unchecked { i++; }
unchecked { citizenStatus.points = identityPoints * vaultMultiplier * timelockMultiplier / _DIVISOR / _DIVISOR; citizenStatus.timelockEndTime = block.timestamp + timelockDuration;
// Record the caller's staked S1 Citizen. _stakerS1Position[msg.sender].push(citizenId); // Update the pool point weights for rewards pool.totalPoints += citizenStatus.points; }
unchecked { citizenStatus.points = 100 * timelockMultiplier / _DIVISOR; citizenStatus.timelockEndTime = block.timestamp + timelockDuration; // Record the caller's staked S2 Citizen. _stakerS2Position[msg.sender].push(citizenId); // Update the pool point weights for rewards pool.totalPoints += citizenStatus.points; }
unchecked { uint256 bonusPoints = (amount * 100 / _BYTES_PER_POINT); citizenStatus.stakedBytes += amount; citizenStatus.points += bonusPoints; pool.totalPoints += bonusPoints; }
unchecked { points += s1Citizen.points; i++; }
1291 : unchecked {
1297: unchecked {
1330 : unchecked {
1341: unchecked {
1354: unchecked {
1369 : unchecked { j++; }
1377: unchecked {
1383 : unchecked { i++; }
1387: unchecked {
1509 : unchecked { stakedIndex++; }
Throughout the codebase, events are generally emitted when sensitive changes are made to the contracts. However, some events are missing important parameters
The events should include the new value and old value where possible
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
function changeStakingContractAddress ( address _staker ) hasValidPermit(UNIVERSAL, ADMIN) external { STAKER = _staker; }
function changeTreasuryContractAddress ( address _treasury ) hasValidPermit(UNIVERSAL, ADMIN) external { TREASURY = _treasury; }
Add an additional check to ensure that the input is not the same as the previously used value, thus preventing same value inputs in a more robust way
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
function changeStakingContractAddress ( address _staker ) hasValidPermit(UNIVERSAL, ADMIN) external { STAKER = _staker; }
function changeTreasuryContractAddress ( address _treasury ) hasValidPermit(UNIVERSAL, ADMIN) external { TREASURY = _treasury; }
function configureLP ( address _lp ) external hasValidPermit(UNIVERSAL, CONFIGURE_LP) { if (lpLocked) { revert LockedConfigurationOfLP(); } LP = _lp; }
Recommended Mitigation:
require(oldAddress!= _newAddress, "Same Address");
Even assembly can benefit from using readable constants instead of hex/numeric literals
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
203 : uint256 constant private _BYTES_PER_POINT = 200 * 1e18;
in Solidity, the order of modifiers in a variable declaration matters. The private modifier should come before the immutable modifier.
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
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;
It is generally considered best practice to specify a fixed compiler version in non-library/interface files in Solidity, rather than using a floating version. This is because different versions of the Solidity compiler can introduce changes to the language syntax or behavior, which may cause compatibility issues or unexpected behavior in your code
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
2: pragma solidity ^0.8.19;
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
2: pragma solidity ^0.8.19;
Description I recommend using header for Solidity code layout and readability
(https://github.com/transmissions11/headers)
Another recommendation is to declare internal functions below external functions.
Functions should be grouped according to their visibility and ordered:
constructor
receive function (if exists)
fallback function (if exists)
external
public
internal
private
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
The instances below highlights private above external. If possible, consider adding private functions below external functions for the contract layout
It is generally recommended to use constants instead of hardcoding numbers directly in your code. This can help make your code more readable and maintainable, as well as avoid errors that may arise from accidentally mistyping a number
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
962: uint256 timelockDuration = _timelock >> 128;
1016: uint256 timelockDuration = _timelock >> 128;
1022: citizenStatus.points = 100 * timelockMultiplier / _DIVISOR;
1113: (seasonId << 128) + citizenId,
1140: uint256 timelockDuration = _timelock >> 128;
1205: if (uint8(_assetType) > 4) {
1668: if (uint8(_assetType) == 2 || uint8(_assetType) > 4) {
There are many addresses and constants used in the system. It is recommended to put the most used ones in one file (for example constants.sol, use inheritance to access these values).
This will help with readability and easier maintenance for future changes. This also helps with any issues, as some of these hard-coded values are admin addresses
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
bytes4 constant private _TRANSFER_FROM_SELECTOR = 0x23b872dd; bytes4 constant private _TRANSFER_SELECTOR = 0xa9059cbb; uint256 constant private _PRECISION = 1e12; uint256 constant private _DIVISOR = 100; uint256 constant private _BYTES_PER_POINT = 200 * 1e18; bytes32 public constant CONFIGURE_LP = keccak256("CONFIGURE_LP"); bytes32 public constant CONFIGURE_TIMELOCKS = keccak256( "CONFIGURE_TIMELOCKS" ); bytes32 public constant CONFIGURE_CREDITS = keccak256("CONFIGURE_CREDITS"); bytes32 public constant CONFIGURE_POOLS = keccak256("CONFIGURE_POOLS"); bytes32 public constant CONFIGURE_CAPS = keccak256("CONFIGURE_CAPS");
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
bytes32 public constant BURN = keccak256("BURN"); bytes32 public constant ADMIN = keccak256("ADMIN");
Since this is a low level language that is more difficult to parse by readers, include extensive documentation, comments on the rationale behind its use, clearly explaining what each assembly instruction does.
This will make it easier for users to trust the code, for reviewers to validate the code, and for developers to build on or update the code.
Note that using Assembly removes several important security features of Solidity, which can make the code more insecure and more error-prone.
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
833: assembly {
886: assembly {
1001: assembly {
1050: assembly{
1128: assembly{
1236: assembly {
1461: assembly {
1536: assembly {
1599: assembly{
1682: assembly {
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
693: if (_assetType == AssetType.S1_CITIZEN) { 1278: if (_assetType == AssetType.S1_CITIZEN) {
NeoTokyoStaker.sol#L693,NeoTokyoStaker.sol#L1278
1287: } else if (_assetType == AssetType.S2_CITIZEN) { 695: } else if (_assetType == AssetType.S2_CITIZEN) {
NeoTokyoStaker.sol#L1287,NeoTokyoStaker.sol#L695
1071: if (citizenStatus.timelockEndTime == 0) { 1092: if (citizenStatus.timelockEndTime == 0) {
NeoTokyoStaker.sol#L1071,NeoTokyoStaker.sol#L1092
1472: if (stakedCitizen.timelockEndTime == 0) { 1547: if (stakedCitizen.timelockEndTime == 0) {
NeoTokyoStaker.sol#L1472,NeoTokyoStaker.sol#L1547
1477: if (stakedCitizen.stakedBytes > 0) { 1552: if (stakedCitizen.stakedBytes > 0) {
NeoTokyoStaker.sol#L1477,NeoTokyoStaker.sol#L1552
1502: if (citizenId == oldPosition[stakedIndex]) { 1567: if (citizenId == oldPosition[stakedIndex]) {
#0 - c4-judge
2023-03-17T03:18:09Z
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
public/external function names and public member variable names can be optimized to save gas. Below are the interfaces/abstract contracts that can be optimized so that the most frequently-called functions use the least amount of gas possible during method lookup. Method IDs that have two leading zero bytes can save 128 gas each during deployment, and renaming functions to have lower method IDs will save 22 gas per call, per sorted position shifted
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
/// @audit upgradeBytes(),getReward(),burn(),changeStakingContractAddress(),updateReward(),updateRewardOnMint() 34: contract BYTES2 is PermitControl, ERC20("BYTES", "BYTES") {
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
/// @audit getCreditYield(),getConfiguredVaultMultiplier(),getStakerPosition(),getStakerPositions(),stake(), getPoolReward(),claimReward(),withdraw(),configureLP(),lockLP(),configureTimelockOptions(), configureIdentityCreditYields(),configureIdentityCreditPoints()configureVaultCreditMultipliers(), configurePools(),configureCaps 188: contract NeoTokyoStaker is PermitControl, ReentrancyGuard {
INSTANCES(2):
APROXIMATE GAS SAVED : 26 GAS
ou can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. Making the constructor payable eliminates the need for an initial check of msg.value == 0 and saves 13 gas on deployment with no security risks
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
75 : constructor (
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
588: constructor (
Using immutable variables instead of constant variables for expressions that evaluate to constant values, such as a call to keccak256(), can potentially save gas in smart contracts
The reason for this is that immutable variables are evaluated at compile time and the resulting values are hardcoded into the bytecode of the contract. This means that the gas cost of evaluating the expression is incurred only once, during contract compilation, and not at runtime
In contrast, constant variables are evaluated at runtime, which can result in additional gas costs. Specifically, when a constant variable is accessed, the EVM must perform a lookup to retrieve the value of the variable from storage, which can add to the gas cost of executing the contract
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
37: bytes32 public constant BURN = keccak256("BURN"); 40: bytes32 public constant ADMIN = keccak256("ADMIN");
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
206: bytes32 public constant CONFIGURE_LP = keccak256("CONFIGURE_LP"); 209: bytes32 public constant CONFIGURE_TIMELOCKS = keccak256( "CONFIGURE_TIMELOCKS" ); 214: bytes32 public constant CONFIGURE_CREDITS = keccak256("CONFIGURE_CREDITS"); 217: bytes32 public constant CONFIGURE_POOLS = keccak256("CONFIGURE_POOLS"); 220: bytes32 public constant CONFIGURE_CAPS = keccak256("CONFIGURE_CAPS");
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 : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
316: mapping ( AssetType => PoolData ) private _pools; 319: mapping ( address => mapping ( AssetType => uint256 )) public lastRewardTime; 326: mapping ( uint256 => mapping ( string => string )) public identityCreditYield; 329: mapping ( string => uint256 ) public identityCreditPoints; 332: mapping ( string => uint256 ) public vaultCreditMultiplier;
372 : mapping ( address => mapping( uint256 => StakedS1Citizen )) public stakedS1; 378: mapping ( address => uint256[] ) private _stakerS1Position;
405: mapping ( address => mapping( uint256 => StakedS2Citizen )) public stakedS2; 411: mapping ( address => uint256[] ) private _stakerS2Position;
434: mapping ( address => LPPosition ) public stakerLPPosition;
INSTANCES(10):
APROXIMATE GAS SAVED : 90 GAS
In Solidity, declaring a variable inside a loop can result in higher gas costs compared to declaring it outside the loop. This is because every time the loop runs, a new instance of the variable is created, which can lead to unnecessary memory allocation and increased gas costs
On the other hand, if you declare a variable outside the loop, the variable is only initialized once, and you can reuse the same instance of the variable inside the loop. This approach can save gas and optimize the execution of your code
GAS SAMPLE TEST IN remix ide(Without gas optimizations) :
Before Mitigation :
function testGas() public view { for(uint256 i = 0; i < 10; i++) { address collateral = msg.sender; address collateral1 = msg.sender; }
The execution Cost : 2176
After Mitigation :
function testGas() public view { address collateral; address collateral1; for(uint256 i = 0; i < 10; i++) { collateral = msg.sender; collateral1 = msg.sender; }
The execution Cost : 2086
Hence clearly after mitigation the gas cost is reduced. Approximately its possible to save 9 gas for every iterations
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
for (uint256 i; i < _stakerS1Position[_staker].length; ) { uint256 citizenId = _stakerS1Position[_staker][i]; StakedS1Citizen memory citizenDetails = stakedS1[_staker][citizenId];
for (uint256 i; i < _stakerS2Position[_staker].length; ) { uint256 citizenId = _stakerS2Position[_staker][i]; StakedS2Citizen memory citizenDetails = stakedS2[_staker][citizenId];
for (uint256 i; i < _stakerS1Position[_recipient].length; ) { uint256 citizenId = _stakerS1Position[_recipient][i]; StakedS1Citizen memory s1Citizen = stakedS1[_recipient][citizenId];
NeoTokyoStaker.sol#L1279-L1281
for (uint256 i; i < _stakerS2Position[_recipient].length; ) { uint256 citizenId = _stakerS2Position[_recipient][i]; StakedS2Citizen memory s2Citizen = stakedS2[_recipient][citizenId];
NeoTokyoStaker.sol#L1288-L1290
for (uint256 i; i < windowCount; ) { RewardWindow memory window = pool.rewardWindows[i];
NeoTokyoStaker.sol#L1312-L1313
1320 : uint256 currentRewardRate = pool.rewardWindows[i - 1].reward;
1331: uint256 timeSinceReward = block.timestamp - lastPoolRewardTime;
1342: uint256 timeSinceReward = window.startTime - lastPoolRewardTime;
For each && split can save 10-20 gas
INSTANCES(10):
APROXIMATE GAS SAVED : 100 GAS
Using nested is cheaper than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
910: if (citizenVaultId != 0 && vaultId != 0) {
[NeoTokyoStaker.sol#L910] (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L910)
917: } else if (citizenVaultId != 0 && vaultId == 0) {
926: } else if (citizenVaultId == 0 && vaultId != 0) {
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
37: bytes32 public constant BURN = keccak256("BURN"); 40: bytes32 public constant ADMIN = keccak256("ADMIN");
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
206: bytes32 public constant CONFIGURE_LP = keccak256("CONFIGURE_LP"); 209: bytes32 public constant CONFIGURE_TIMELOCKS = keccak256( "CONFIGURE_TIMELOCKS" ); 214: bytes32 public constant CONFIGURE_CREDITS = keccak256("CONFIGURE_CREDITS"); 217: bytes32 public constant CONFIGURE_POOLS = keccak256("CONFIGURE_POOLS"); 220: bytes32 public constant CONFIGURE_CAPS = keccak256("CONFIGURE_CAPS");
Using the addition/Subtraction operator instead of plus-equals/minus-equals saves 113 gas
INSTANCES(13):
APROXIMATE GAS SAVED : 1469 GAS
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
977: pool.totalPoints += citizenStatus.points;
1029 : pool.totalPoints += citizenStatus.points;
1078: citizenStatus.stakedBytes += amount; 1079: citizenStatus.points += bonusPoints; 1080: pool.totalPoints += bonusPoints;
NeoTokyoStaker.sol#L1078-L1080
1160: stakerLPPosition[msg.sender].amount += amount; 1161: stakerLPPosition[msg.sender].points += points; 1164: pool.totalPoints += points;
NeoTokyoStaker.sol#L1160-L1164
1519 : pool.totalPoints -= stakedCitizen.points;
1580 : pool.totalPoints -= stakedCitizen.points;
1626: lpPosition.amount -= amount; 1627: lpPosition.points -= points; 1630: pool.totalPoints -= points;
NeoTokyoStaker.sol#L1626-L1627
INSTANCES(3):
Using constants instead of type(uintx).max can save gas in some cases because constants are precomputed at compile-time and can be reused throughout the code, whereas type(uintx).max requires computation at runtime
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
963: uint256 timelockMultiplier = _timelock & type(uint128).max;
1017 : uint256 timelockMultiplier = _timelock & type(uint128).max;
1141 : uint256 timelockMultiplier = _timelock & type(uint128).max;
INSTANCES(10):
598: BYTES = _bytes; 599: S1_CITIZEN = _s1Citizen; 600: S2_CITIZEN = _s2Citizen; 601: LP = _lpToken; 602: IDENTITY = _identity; 603: VAULT = _vault;
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
81: BYTES1 = _bytes; 82: S1_CITIZEN = _s1Citizen; 83: STAKER = _staker; 84: TREASURY = _treasury;
INSTANCES(2):
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
824: function _stringEquals ( string memory _a, string memory _b ) private pure returns (bool) {
INSTANCES(2):
While the following two critical VAULT_CAP,NO_VAULT_CAP values are assigned in the constructor, there is no zero value control. This means that if both state variables are started with a possible value of 0, the contract must be deployed again. This possibility means gas consumption.
Zero value control is the most error-prone value control since zero value is assigned in case of no value entry due to EVM design.
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
604: VAULT_CAP = _vaultCap; 605: NO_VAULT_CAP = _noVaultCap;
Recommendation:
It is recommended to perform a zero value check for critical value assignments.
INSTANCES(8):
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
1517: stakedCitizen.stakedBytes = 0; 1518: stakedCitizen.timelockEndTime = 0; 1519: stakedCitizen.points = 0; 1520: stakedCitizen.hasVault = false; 1521: stakedCitizen.stakedVaultId = 0;
NeoTokyoStaker.sol#L1517-L1521
1582: stakedCitizen.stakedBytes = 0; 1583: stakedCitizen.timelockEndTime = 0; 1584: stakedCitizen.points = 0;
NeoTokyoStaker.sol#L1582-L1584
Each iteration of the cycle requires a gas flow. A moment may come when more gas is required than it is allocated to record one block. In this case, all iterations of the loop will fail
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
(https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L717-L728) (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L734-L743) (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1279-L1302) (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1312-L1383) (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1499-L1509) (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1564-L1574) (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1742-L1745) (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1765-L1772) (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1787-L1791) (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1806-L1809) (https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1822-L1840)Reco
Recommendations:
Check the maxProcessingLengt allowed before start iterating the loops
require(Varibale.length < maxProcessingLengt, "max length");
INSTANCES(8):
Description:
Solidity code is also cleaner in another way that might not be noticeable: the struct Point. We were importing it previously with global import but not using it. The Point struct polluted the source code with an unnecessary object we were not using because we did not need it.
This was breaking the rule of modularity and modular programming: only import what you need Specific imports with curly braces allow us to apply this rule better.
Recommendation:
import {contract1 , contract2} from "filename.sol";
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
4: import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
5: import "../access/PermitControl.sol"; 6: import "../interfaces/IByteContract.sol"; 7: import "../interfaces/IGenericGetter.sol";
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../access/PermitControl.sol"; import "../interfaces/IByteContract.sol"; import "../interfaces/IStaker.sol";
INSTANCES(5):
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
( uint256 reward, uint256 daoCommision ) = IStaker(STAKER).claimReward(_to); 96: if (IERC20(BYTES1).balanceOf(msg.sender) < _amount) {
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
632: uint256 rewardRate = citizen.getRewardRateOfTokenId(_citizenId);
904: uint256 citizenVaultId = citizen.getVaultIdOfTokenId(citizenId); 948: uint256 identityId = citizen.getIdentityIdOfTokenId(citizenId);
INSTANCES(4):
It’s the most gas efficient to make up to 3 event parameters indexed. If there are less than 3 parameters, you need to make all parameters indexed.
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
The same extra indexed parameter can be applied to BytesUpgraded()
event BytesUpgraded ( address indexed caller, uint256 amount );
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
The same extra indexed parameter can be applied to Stake()
event Stake ( address indexed staker, address indexed asset, uint256 timelockOption, uint256 amountOrTokenId );
The same extra indexed parameter can be applied to Claim()
event Claim ( address indexed recipient, uint256 reward, uint256 tax );
The same extra indexed parameter can be applied to Withdraw()
event Withdraw ( address indexed caller, address indexed asset, uint256 amountOrTokenId );
INSTANCES(12):
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
693: if (_assetType == AssetType.S1_CITIZEN) { 1278: if (_assetType == AssetType.S1_CITIZEN) {
NeoTokyoStaker.sol#L693,NeoTokyoStaker.sol#L1278
1287: } else if (_assetType == AssetType.S2_CITIZEN) { 695: } else if (_assetType == AssetType.S2_CITIZEN) {
NeoTokyoStaker.sol#L1287,NeoTokyoStaker.sol#L695
1071: if (citizenStatus.timelockEndTime == 0) { 1092: if (citizenStatus.timelockEndTime == 0) {
NeoTokyoStaker.sol#L1071,NeoTokyoStaker.sol#L1092
1472: if (stakedCitizen.timelockEndTime == 0) { 1547: if (stakedCitizen.timelockEndTime == 0) {
NeoTokyoStaker.sol#L1472,NeoTokyoStaker.sol#L1547
1477: if (stakedCitizen.stakedBytes > 0) { 1552: if (stakedCitizen.stakedBytes > 0) {
NeoTokyoStaker.sol#L1477,NeoTokyoStaker.sol#L1552
1502: if (citizenId == oldPosition[stakedIndex]) { 1567: if (citizenId == oldPosition[stakedIndex]) {
NeoTokyoStaker.sol#L1502,NeoTokyoStaker.sol#L1567
INSTANCES(2):
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
638: IGenericGetter vault = IGenericGetter(VAULT);
664: IGenericGetter vault = IGenericGetter(VAULT);
NeoTokyoStaker.sol#L638,NeoTokyoStaker.sol#L664
631: IGenericGetter citizen = IGenericGetter(S1_CITIZEN); 903: IGenericGetter citizen = IGenericGetter(S1_CITIZEN);
NeoTokyoStaker.sol#L631,NeoTokyoStaker.sol#L903
PROOF OF CONCEPT (REMIX WITHOUT OPTIMIZATIONS) :
pragma solidity ^0.8.7;
contract MappingTest {
address public immutable A=0xE0f5206BBD039e7b0592d8918820024e2a7437b9; // execution cost 195 gas function withoutImmutableCache() public view { if(A==0xE0f5206BBD039e7b0592d8918820024e2a7437b9){ if(A==0xE0f5206BBD039e7b0592d8918820024e2a7437b9){
} }
} // execution cost 135 gas function withImmutableCache() public view {
address a=A; if(a==0xE0f5206BBD039e7b0592d8918820024e2a7437b9){ if(a==0xE0f5206BBD039e7b0592d8918820024e2a7437b9){ } }
}
}
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
897: _assetTransferFrom(S1_CITIZEN, msg.sender, address(this), citizenId);
903: IGenericGetter citizen = IGenericGetter(S1_CITIZEN);
emit Stake( msg.sender, S1_CITIZEN, _timelock, citizenId );
NeoTokyoStaker.sol#L897,NeoTokyoStaker.sol#L903,NeoTokyoStaker.sol#L983
S2_CITIZEN should be cached with local address variable
1010: _assetTransferFrom(S2_CITIZEN, msg.sender, address(this), citizenId);
emit Stake( msg.sender, S2_CITIZEN, _timelock, citizenId );
NeoTokyoStaker.sol#L1010,NeoTokyoStaker.sol#L1035
PROOF OF CONCEPT (REMIX) :
pragma solidity ^0.8.7;
contract MappingTest { uint256 public constant A=100; // execution cost 802 gas function withoutContantCache() public pure { uint256 b=200;
b+=A; b/=A; b*=A; } // execution cost 793 gas function withConstantCache() public pure { uint256 b=200; uint256 c=A; b+=c; b/=c; b*=c; } }
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
1388: uint256 share = points * _PRECISION / pool.totalPoints * totalReward; 1390: share /= _PRECISION; 1391: daoShare /= _PRECISION;
NeoTokyoStaker.sol#L1388-L1391
APROXIMATE GAS SAVED : 40000 (2 SLOTS)
Each slot saved can avoid an extra Gsset (20000 gas) for the first setting of the struct. Subsequent reads as well as writes have smaller gas savings
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
struct StakedS1Citizen { uint256 stakedBytes; //32-SLOT 1 uint256 timelockEndTime; //32- SLOT 2 uint256 points; //32 SLOT-3 uint256 stakedVaultId; //32 SLOT-4 bool hasVault; //1 SLOT-5 }
Total slots 5
We can declare timelockEndTime uint128 instead of uint256. Using uint128 we can store 10,774,146,971,163 years this is more than enough for timelockEndTime . So we can save 1 slot and saves 20000 gas
Recommended Mitigation:
/// @audit Variable ordering with 4 slots instead of the current 5: struct StakedS1Citizen { uint256 stakedBytes; //32-SLOT 1 - uint256 timelockEndTime; uint256 points; //32 SLOT-2 uint256 stakedVaultId; //32 SLOT-3
struct StakedS1CitizenOutput { uint256 citizenId; //32 -SLOT1 uint256 stakedBytes; //32 -SLOT2 uint256 timelockEndTime; //32 -SLOT3 uint256 points; //32 -SLOT4 uint256 stakedVaultId; //32 -SLOT5 bool hasVault; //1 -SLOT6 }
Total slots 6
Recommended Mitigation:
/// @audit Variable ordering with 5 slots instead of the current 6: struct StakedS1CitizenOutput { uint256 citizenId; //32 -SLOT1 uint256 stakedBytes; //32 -SLOT2 - uint256 timelockEndTime; uint256 points; //32 -SLOT3 uint256 stakedVaultId; //32 -SLOT4 + uint128 timelockEndTime; //16 -SLOT5 bool hasVault; //1 -SLOT5 }
INSTANCES(1):
APROXIMATE GAS SAVED : 20000 (1 SLOT)
If variables occupying the same slot are both written the same function or by the constructor, avoids a separate Gsset (20000 gas). Reads of the variables can also be cheaper
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
232: address public LP; //20 bytes 511: bool public lpLocked; //1byte
NeoTokyoStaker.sol#L232,NeoTokyoStaker.sol#L511
Move the (bool public lpLocked) bellow the (address public LP) this wills save the one slot
232: address public LP; //20 bytes
Once moved possible to pack both variable in single slot instead of 2 slots . So possible to save 20000 gas
INSTANCES(5):
APROXIMATE GAS SAVED : 200 GAS
Not inlining costs 20 to 40 gas because of two extra JUMP instructions and additional stack operations needed for function calls.
FILE : 2023-03-neotokyo/contracts/staking/NeoTokyoStaker.sol
function _stringEquals ( string memory _a, string memory _b ) private pure returns (bool) {
function _stakeS1Citizen ( uint256 _timelock ) private {
function _stakeS2Citizen ( uint256 _timelock ) private {
function _stakeBytes ( uint256 ) private {
NeoTokyoStaker.sol#L1044-L1046
function _stakeLP ( uint256 _timelock ) private {
NeoTokyoStaker.sol#L1124-L1126
INSTANCES(2):
FILE : 2023-03-neotokyo/contracts/staking/BYTES2.sol
function updateRewardOnMint ( address, uint256 ) external { }
function updateReward ( address, address, uint256 ) external { }
INSTANCES(6):
windowCount - 1 is not possible to overflow . We already checked the value in for loop
1353: if (j == windowCount - 1) {
1376: } else if (i == windowCount - 1) {
1503: if (stakedIndex != oldPosition.length - 1) { 1504: oldPosition[stakedIndex] = oldPosition[oldPosition.length - 1];
1568: if (stakedIndex != oldPosition.length - 1) { 1569: oldPosition[stakedIndex] = oldPosition[oldPosition.length - 1];
#0 - c4-judge
2023-03-17T04:30:49Z
hansfriese marked the issue as grade-a