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: 2/123
Findings: 3
Award: $3,209.67
🌟 Selected for report: 0
🚀 Solo Findings: 0
2819.6915 USDC - $2,819.69
https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/staking/NeoTokyoStaker.sol#L1044-L1116 https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/staking/NeoTokyoStaker.sol#L1124-L1174 https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/staking/NeoTokyoStaker.sol#L1264-L1396
When calling the following NeoTokyoStaker._stakeBytes
and NeoTokyoStaker._stakeLP
functions, the higher the specified amount
to be staked is, the higher the pool.totalPoints
is increased by.
function _stakeBytes ( uint256 ) private { uint256 amount; uint256 citizenId; uint256 seasonId; assembly{ amount := calldataload(0x44) citizenId := calldataload(0x64) seasonId := calldataload(0x84) } ... _assetTransferFrom(BYTES, msg.sender, address(this), amount); ... if (seasonId == 1) { ... PoolData storage pool = _pools[AssetType.S1_CITIZEN]; unchecked { uint256 bonusPoints = (amount * 100 / _BYTES_PER_POINT); citizenStatus.stakedBytes += amount; citizenStatus.points += bonusPoints; pool.totalPoints += bonusPoints; } ... } else if (seasonId == 2) { ... PoolData storage pool = _pools[AssetType.S2_CITIZEN]; unchecked { uint256 bonusPoints = (amount * 100 / _BYTES_PER_POINT); citizenStatus.stakedBytes += amount; citizenStatus.points += bonusPoints; pool.totalPoints += bonusPoints; } ... } else { revert InvalidSeasonId(seasonId); } ... }
function _stakeLP ( uint256 _timelock ) private { uint256 amount; assembly{ amount := calldataload(0x44) } ... _assetTransferFrom(LP, msg.sender, address(this), amount); ... uint256 timelockDuration = _timelock >> 128; uint256 timelockMultiplier = _timelock & type(uint128).max; ... PoolData storage pool = _pools[AssetType.LP]; unchecked { uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR; // Update the caller's LP token stake. stakerLPPosition[msg.sender].timelockEndTime = block.timestamp + timelockDuration; stakerLPPosition[msg.sender].amount += amount; stakerLPPosition[msg.sender].points += points; // Update the pool point weights for rewards. pool.totalPoints += points; } ... }
When Staker A, who has staked some tokens already, calls the following NeoTokyoStaker.getPoolReward
function, the share
to be minted to this staker is calculated by executing uint256 share = points * _PRECISION / pool.totalPoints * totalReward
and share /= _PRECISION
. When such staker's NeoTokyoStaker.stake
or NeoTokyoStaker.withdraw
transaction, which calls the NeoTokyoStaker.getPoolReward
function, is in the mempool, Staker B, who has many BYTES tokens or LP tokens, can maliciously frontrun such transaction by calling the NeoTokyoStaker.stake
function with a gas price that is higher than such transaction's, which can increase the pool.totalPoints
by a lot. When this happens, the pool.totalPoints
could have been increased to an extent in which executing uint256 share = points * _PRECISION / pool.totalPoints * totalReward
would cause share
to be smaller than _PRECISION
when Staker A's transaction is executed. As a result, 0 share
will be minted to Staker A and 0 daoShare
will be minted to the DAO's treasury while both Staker A and the DAO should deserve some reward shares; essentially, Staker A and the DAO lose these reward shares that they are entitled to.
function getPoolReward ( AssetType _assetType, address _recipient ) public view returns (uint256, uint256) { ... PoolData storage pool = _pools[_assetType]; if (pool.totalPoints != 0) { ... // Return final shares. unchecked { uint256 share = points * _PRECISION / pool.totalPoints * totalReward; uint256 daoShare = share * pool.daoTax / (100 * _DIVISOR); share /= _PRECISION; daoShare /= _PRECISION; return ((share - daoShare), daoShare); } } return (0, 0); }
The following steps can occur for the described scenario.
stakerLPPosition
's points
and the pool.totalPoints
to 100.NeoTokyoStaker.stake
function, which further calls the NeoTokyoStaker.getPoolReward
function, accordingly.NeoTokyoStaker.stake
function to stake many LP tokens so the pool.totalPoints
is increased to 100500.totalReward
can be 1000 at this moment. When calling the NeoTokyoStaker.getPoolReward
function, uint256 share = points * _PRECISION / pool.totalPoints * totalReward
is evaluated to share = 100 * 1e12 / 100500 * 1000 = 995024875000
, and share /= _PRECISION
is evaluated to share = 995024875000 / 1e12 = 0
.VSCode
Flashbots can be used to keep the NeoTokyoStaker.stake
and NeoTokyoStaker.withdraw
transactions, which call the NeoTokyoStaker.getPoolReward
function, away from the mempool for counteracting frontrunning.
#0 - c4-judge
2023-03-16T09:02:56Z
hansfriese marked the issue as satisfactory
#1 - c4-judge
2023-03-16T09:03:11Z
hansfriese marked the issue as duplicate of #423
#2 - c4-judge
2023-03-29T00:21:36Z
hansfriese marked the issue as not a duplicate
#3 - c4-judge
2023-03-29T00:21:46Z
hansfriese changed the severity to 3 (High Risk)
#4 - c4-judge
2023-03-29T00:22:09Z
hansfriese marked the issue as duplicate of #423
🌟 Selected for report: rokso
Also found by: 0x52, 0xnev, ABA, BPZ, DadeKuma, Haipls, J4de, Jeiwan, Josiah, Krace, LegendFenGuin, Lirios, MadWookie, RaymondFam, Ruhum, Toshii, UdarTeam, aashar, ak1, anodaram, auditor0517, carlitox477, cccz, jekapi, juancito, kaden, kenzo, minhquanym, nobody2018, rbserver, rokso, ulqiorra
154.74 USDC - $154.74
https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/staking/NeoTokyoStaker.sol#L1597-L1644 https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/staking/NeoTokyoStaker.sol#L1264-L1396
When withdrawing the staked LP tokens, the staker can divide the total staked token amount into smaller amounts and call the NeoTokyoStaker.withdraw
function, which further calls the following NeoTokyoStaker._withdrawLP
function, to withdraw each of such smaller token amounts. When such token amount is small enough, executing uint256 points = amount * 100 / 1e18 * lpPosition.multiplier / _DIVISOR
in the NeoTokyoStaker._withdrawLP
function can cause points
to equal 0, which does not decrease the staker's lpPosition.points
at all. In this situation, given some time between two NeoTokyoStaker.withdraw
function calls, the staker can benefit from the unchanged lpPosition.points
in each subsequent NeoTokyoStaker.withdraw
function call, which eventually calls the lpPosition.getPoolReward
function below. When executing uint256 share = points * _PRECISION / pool.totalPoints * totalReward
in each lpPosition.getPoolReward
function call, the unchanged lpPosition.points
would cause a positive share
to be minted to the staker while the staker should deserve 0 extra reward shares if it has withdrawn the staked LP tokens all at once and reduced its lpPosition.points
to 0. As a result, the total number of reward shares minted to the staker becomes more than the staker is entitled to.
function _withdrawLP () private { uint256 amount; assembly{ amount := calldataload(0x24) } ... _assetTransfer(LP, msg.sender, amount); // Update caller staking information and asset data. PoolData storage pool = _pools[AssetType.LP]; unchecked { uint256 points = amount * 100 / 1e18 * lpPosition.multiplier / _DIVISOR; // Update the caller's LP token stake. lpPosition.amount -= amount; lpPosition.points -= points; // Update the pool point weights for rewards. pool.totalPoints -= points; } ... }
function getPoolReward ( AssetType _assetType, address _recipient ) public view returns (uint256, uint256) { ... PoolData storage pool = _pools[_assetType]; if (pool.totalPoints != 0) { // Calculate the total number of points accrued to the `_recipient`. uint256 points; if (_assetType == AssetType.S1_CITIZEN) { ... } else if (_assetType == AssetType.S2_CITIZEN) { ... } else if (_assetType == AssetType.LP) { unchecked { points += stakerLPPosition[_recipient].points; } } else { revert InvalidAssetType(uint256(_assetType)); } ... // Return final shares. unchecked { uint256 share = points * _PRECISION / pool.totalPoints * totalReward; uint256 daoShare = share * pool.daoTax / (100 * _DIVISOR); share /= _PRECISION; daoShare /= _PRECISION; return ((share - daoShare), daoShare); } } return (0, 0); }
The following steps can occur for the described scenario.
NeoTokyoStaker.withdraw
function is called to withdraw 0.9e16 LP tokens, the lpPosition.getPoolReward
function is eventually called. share
that is (points * _PRECISION / pool.totalPoints * totalReward) / _PRECISION
is minted to the staker.NeoTokyoStaker.withdraw
function calls the NeoTokyoStaker._withdrawLP
function, lpPosition.multiplier
can be 100, and uint256 points = amount * 100 / 1e18 * lpPosition.multiplier / _DIVISOR
is evaluated to points = 0.9e16 * 100 / 1e18 * 100 / 100 = 0
. Therefore, the staker's lpPosition.points
is not decreased at all.NeoTokyoStaker.withdraw
function is called again to withdraw another 0.9e16 LP tokens, the lpPosition.getPoolReward
function is called again, and a positive share
, which is (points * _PRECISION / pool.totalPoints * totalReward) / _PRECISION
, is minted to the staker because the staker's lpPosition.points
did not change and was not reduced to 0.lpPosition.points
would be reduced to 0 when the NeoTokyoStaker._withdrawLP
function is called, and any subsequent lpPosition.getPoolReward
function call would calculate the share
to be 0 so no extra reward shares would be minted to the staker. However, such extra reward shares that the staker should not deserve are minted to the staker for the described scenario.VSCode
The NeoTokyoStaker._withdrawLP
function can be updated to revert when the calculated points
is 0 to ensure that the staker must withdraw enough staked LP tokens to reduce its lpPosition.points
properly.
#0 - c4-judge
2023-03-16T04:24:17Z
hansfriese marked the issue as satisfactory
#1 - c4-judge
2023-03-16T04:25:12Z
hansfriese marked the issue as duplicate of #348
#2 - c4-judge
2023-03-21T09:20:36Z
hansfriese marked the issue as duplicate of #261
🌟 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 | |
---|---|
[01] | NeoTokyoStaker.stake AND NeoTokyoStaker.withdraw FUNCTIONS SHOULD CHECK uint8(_assetType) > 3) INSTEAD OF uint8(_assetType) > 4) |
[02] | STAKING FUNCTIONALITY IS UNAVAILABLE WHEN block.timestamp AND _pools[_assetType].rewardWindows[0].startTime ARE SAME |
[03] | unchecked USAGE CAN EVENTUALLY BECOME UNSAFE |
[04] | BYTES 2.0 AND BYTES 1.0 TOKENS USE SAME NAME AND SYMBOL |
[05] | ADDITIONAL LP TOKENS CANNOT BE STAKED WITH DIFFERENT timelockMultiplier , WHICH CAN BE INCONVENIENT TO USERS |
[06] | USERS ARE UNABLE TO WITHDRAW SPECIFIED BYTES TOKEN AMOUNTS OVER TIME |
[07] | NeoTokyoStaker.stake FUNCTION'S COMMENT IS NOT COMPLETE FOR SITUATION WHERE ASSET BEING STAKED IS BYTES |
[08] | FUNCTIONS IN NeoTokyoStaker CONTRACT DO NOT FOLLOW CHECKS-EFFECTS-INTERACTIONS PATTERN |
[09] | MISSING address(0) CHECKS FOR CRITICAL ADDRESS INPUTS |
[10] | VULNERABILITIES IN VERSION 4.4.2 OF @openzeppelin/contracts-upgradeable AND VERSION 4.3.1 OF @openzeppelin/contracts |
[11] | CONSTANTS CAN BE USED INSTEAD OF MAGIC NUMBERS |
[12] | HARDCODED STRINGS THAT HAVE SPECIAL MEANINGS CAN BE REPLACED WITH CONSTANTS |
[13] | FLOATING PRAGMAS |
[14] | WORD TYPING TYPO |
[15] | INCOMPLETE NATSPEC COMMENTS |
[16] | MISSING NATSPEC COMMENTS |
NeoTokyoStaker.stake
AND NeoTokyoStaker.withdraw
FUNCTIONS SHOULD CHECK uint8(_assetType) > 3)
INSTEAD OF uint8(_assetType) > 4)
The uint8
representation of the following AssetType
enum's maximum is 3. Yet, calling the NeoTokyoStaker.stake
and NeoTokyoStaker.withdraw
functions below would revert if uint8(_assetType)
is more than 4. In other words, if uint8(_assetType)
equals 4, calling these functions would not revert. This is inconsistent with the AssetType
enum in which its maximum cannot be 4. To improve these sanity checks, please consider replacing uint8(_assetType) > 4)
with uint8(_assetType) > 3)
in these functions.
enum AssetType { S1_CITIZEN, S2_CITIZEN, BYTES, LP }
function stake ( AssetType _assetType, uint256 _timelockId, uint256, uint256, uint256 ) external nonReentrant { // Validate that the asset being staked is of a valid type. if (uint8(_assetType) > 4) { revert InvalidAssetType(uint256(_assetType)); } ... }
function withdraw ( AssetType _assetType, uint256 ) external nonReentrant { /* Validate that the asset being withdrawn is of a valid type. BYTES may not be withdrawn independently of the Citizen that they are staked into. */ if (uint8(_assetType) == 2 || uint8(_assetType) > 4) { revert InvalidAssetType(uint256(_assetType)); } ... }
block.timestamp
AND _pools[_assetType].rewardWindows[0].startTime
ARE SAMEThe comment of the following RewardWindow
struct indicates that its startTime
is "the time at which the daily reward activated". Yet, calling the NeoTokyoStaker.stake
function below reverts when block.timestamp
and _pools[_assetType].rewardWindows[0].startTime
are the same though the pool would be considered as active at _pools[_assetType].rewardWindows[0].startTime
. This means that the staking functionality becomes unavailable to users when block.timestamp
and _pools[_assetType].rewardWindows[0].startTime
are the same.
As a mitigation, please consider updating the NeoTokyoStaker.stake
function to check _pools[_assetType].rewardWindows[0].startTime > block.timestamp
instead of _pools[_assetType].rewardWindows[0].startTime >= block.timestamp
.
@param startTime The time at which the daily reward activated. @param reward The reward emission rate beginning at `startTime`. */ struct RewardWindow { uint128 startTime; uint128 reward; }
function stake ( AssetType _assetType, uint256 _timelockId, uint256, uint256, uint256 ) external nonReentrant { ... // Validate that the asset being staked matches an active pool. if (_pools[_assetType].rewardWindows[0].startTime >= block.timestamp) { revert InactivePool(uint256(_assetType)); } ... }
unchecked
USAGE CAN EVENTUALLY BECOME UNSAFEunchecked
is used in multiple places of the codebase but can eventually become unsafe. For example, the following NeoTokyoStaker.claimReward
function executes totalReward = (s1Reward + s2Reward + lpReward)
in an unchecked
block. If the corresponding tokens have been staked for a very long time, calling the NeoTokyoStaker.getPoolReward
function can return very large numbers of reward shares; then executing s1Reward + s2Reward + lpReward
in the NeoTokyoStaker.claimReward
function can potentially overflow to an amount that is much smaller than it should be without reverting. In this case, such small number of BYTES token are minted to the user when the user actually is entitled to a much bigger number of BYTES token for the rewards.
As a mitigation, please consider not using unchecked
in the relevant code. For example, the NeoTokyoStaker.claimReward
function can be updated to not wrap totalReward = (s1Reward + s2Reward + lpReward)
and totalTax = (s1Tax + s2Tax + lpTax)
in the unchecked
block. In addition, another function can be added and used for claiming a pre-determined maximum number of reward shares if executing s1Reward + s2Reward + lpReward
ever overflows to revert in the NeoTokyoStaker.claimReward
function.
function claimReward ( address _recipient ) external returns (uint256, uint256) { ... // Retrieve the `_recipient` reward share from each pool. (uint256 s1Reward, uint256 s1Tax) = getPoolReward( AssetType.S1_CITIZEN, _recipient ); (uint256 s2Reward, uint256 s2Tax) = getPoolReward( AssetType.S2_CITIZEN, _recipient ); (uint256 lpReward, uint256 lpTax) = getPoolReward( AssetType.LP, _recipient ); ... // Calculate total reward and tax. uint256 totalReward; uint256 totalTax; unchecked { totalReward = (s1Reward + s2Reward + lpReward); totalTax = (s1Tax + s2Tax + lpTax); } ... }
As shown by the following code, the name and symbol are BYTES
for the BYTES 2.0 token, which are the same for the BYTES 1.0 token. This can cause confusion to users because tools like a scanner can display the same meta information for both tokens. Also, this can introduce difficulties for other protocols when integrating with this protocol.
As a mitigation, please consider updating the BYTES 2.0 token to use name and symbol that are different than BYTES
.
https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/staking/BYTES2.sol#L34
contract BYTES2 is PermitControl, ERC20("BYTES", "BYTES") {
https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/s1/BYTESContract.sol#L798
contract BYTESContract is Ownable, ERC20("BYTES", "BYTES") {
timelockMultiplier
, WHICH CAN BE INCONVENIENT TO USERSCalling the following NeoTokyoStaker._stakeLP
function reverts if stakerLPPosition[msg.sender].multiplier != timelockMultiplier
is true. This means that after staking some LP tokens with a timelockMultiplier
, the user cannot stake additional LP tokens with a different timelockMultiplier
. In order to stake additional LP tokens with a different timelockMultiplier
, the user has to wait until the timelock expires for the previously staked LP tokens and withdraw them, which can be very inconvenient. To improve the user experience, please consider adding a mechanism for staking additional LP tokens with a different timelockMultiplier
.
function _stakeLP ( uint256 _timelock ) private { ... // Decode the timelock option's duration and multiplier. uint256 timelockDuration = _timelock >> 128; uint256 timelockMultiplier = _timelock & type(uint128).max; // If this is a new stake of this asset, initialize the multiplier details. if (stakerLPPosition[msg.sender].multiplier == 0) { stakerLPPosition[msg.sender].multiplier = timelockMultiplier; // If a multiplier exists already, we must match it. } else if (stakerLPPosition[msg.sender].multiplier != timelockMultiplier) { revert MismatchedTimelock(); } ... }
When staking the BYTES tokens, users can stake various BYTES token amounts over time; however, they have to withdraw the staked BYTES tokens all at once because only the following NeoTokyoStaker._withdrawS1Citizen
and NeoTokyoStaker._withdrawS2Citizen
functions, which would transfer all staked BYTES tokens to msg.sender
, are available. To improve the user experience, please consider adding a mechanism to support the use case for withdrawing specified BYTES token amounts over time.
function _withdrawS1Citizen () private { ... // Return any staked BYTES. if (stakedCitizen.stakedBytes > 0) { _assetTransfer(BYTES, msg.sender, stakedCitizen.stakedBytes); } ... stakedCitizen.stakedBytes = 0; ... }
function _withdrawS2Citizen () private { ... // Return any staked BYTES. if (stakedCitizen.stakedBytes > 0) { _assetTransfer(BYTES, msg.sender, stakedCitizen.stakedBytes); } ... stakedCitizen.stakedBytes = 0; ... }
NeoTokyoStaker.stake
FUNCTION'S COMMENT IS NOT COMPLETE FOR SITUATION WHERE ASSET BEING STAKED IS BYTESThe following comment for the NeoTokyoStaker.stake
function only describes the input parameter for the situation where the asset being staked is an S1 Citizen. Yet, this input parameter is also used as the corresponding citizenId
when the asset being staked is BYTES. This can confuse and mislead users for calling this function. To avoid such confusion, please consider completing this comment to explain the situation where the asset being staked is BYTES.
@custom:param If the asset being staked is an S1 Citizen, this is the ID of a Vault to attempt to optionally attach.
NeoTokyoStaker
CONTRACT DO NOT FOLLOW CHECKS-EFFECTS-INTERACTIONS PATTERNThe NeoTokyoStaker._stakeS1Citizen
, NeoTokyoStaker._stakeS2Citizen
, NeoTokyoStaker._stakeBytes
, NeoTokyoStaker._stakeLP
, NeoTokyoStaker._withdrawS1Citizen
, NeoTokyoStaker._withdrawS2Citizen
, and NeoTokyoStaker._withdrawLP
functions execute the following code to transfer the corresponding tokens before updating the relevant states, which do not follow the checks-effects-interactions pattern. To reduce the potential attack surface and increase the level of security, please consider updating these functions to follow the checks-effects-interactions pattern.
contracts\staking\NeoTokyoStaker.sol 897: _assetTransferFrom(S1_CITIZEN, msg.sender, address(this), citizenId); 1010: _assetTransferFrom(S2_CITIZEN, msg.sender, address(this), citizenId); 1057: _assetTransferFrom(BYTES, msg.sender, address(this), amount); 1137: _assetTransferFrom(LP, msg.sender, address(this), amount); 1492: _assetTransferFrom(S1_CITIZEN, address(this), msg.sender, citizenId); 1557: _assetTransferFrom(S2_CITIZEN, address(this), msg.sender, citizenId); 1618: _assetTransfer(LP, msg.sender, amount);
address(0)
CHECKS FOR CRITICAL ADDRESS INPUTSTo prevent unintended behaviors, critical address inputs should be checked against address(0)
. address(0)
checks are missing for the address
input variables in the following constructors. Please consider checking them.
https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/staking/BYTES2.sol#L75-L85
constructor ( address _bytes, address _s1Citizen, address _staker, address _treasury ) { BYTES1 = _bytes; S1_CITIZEN = _s1Citizen; STAKER = _staker; TREASURY = _treasury; }
constructor ( address _bytes, address _s1Citizen, address _s2Citizen, address _lpToken, address _identity, address _vault, uint256 _vaultCap, uint256 _noVaultCap ) { BYTES = _bytes; S1_CITIZEN = _s1Citizen; S2_CITIZEN = _s2Citizen; LP = _lpToken; IDENTITY = _identity; VAULT = _vault; ... }
@openzeppelin/contracts-upgradeable
AND VERSION 4.3.1 OF @openzeppelin/contracts
As shown in the following code in package.json
, version 4.4.2 of @openzeppelin/contracts-upgradeable
and version 4.3.1 of @openzeppelin/contracts
can be used. As described in https://security.snyk.io/package/npm/@openzeppelin%2Fcontracts-upgradeable/4.4.2 and https://security.snyk.io/package/npm/@openzeppelin%2Fcontracts/4.3.1, these versions have vulnerabilities that are related to ECDSA.recover
, initializer
, etc. To reduce the potential attack surface and be more future-proofed, please consider upgrading these packages to at least version 4.7.3.
https://github.com/code-423n4/2023-03-neotokyo/blob/main/package.json#L9-L23
"@openzeppelin/contracts-upgradeable": "^4.4.2", ... "@openzeppelin/contracts": "^4.3.1",
To improve readability and maintainability, a constant can be used instead of the magic number. Please consider replacing the magic numbers, such as 100
, used in the following code with constants.
contracts\staking\NeoTokyoStaker.sol 946: uint256 vaultMultiplier = 100; 962: uint256 timelockDuration = _timelock >> 128; 1016: uint256 timelockDuration = _timelock >> 128; 1022: citizenStatus.points = 100 * timelockMultiplier / _DIVISOR; 1077: uint256 bonusPoints = (amount * 100 / _BYTES_PER_POINT); 1098: uint256 bonusPoints = (amount * 100 / _BYTES_PER_POINT); 1113: (seasonId << 128) + citizenId, 1140: uint256 timelockDuration = _timelock >> 128; 1155: uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR; 1389: uint256 daoShare = share * pool.daoTax / (100 * _DIVISOR);
For better maintainability, the following Hand of Citadel
and ?
that are hardcoded and have special meanings can be replaced with constants.
if (_stringEquals(class, "Hand of Citadel")) { vaultMultiplier = vaultCreditMultiplier["?"];
It is a best practice to lock pragmas instead of using floating pragmas to ensure that contracts are tested and deployed with the intended compiler version. Accidentally deploying contracts with different compiler versions can lead to unexpected risks and undiscovered bugs. Please consider locking pragmas for the following files.
contracts\staking\BYTES2.sol 2: pragma solidity ^0.8.19; contracts\staking\NeoTokyoStaker.sol 2: pragma solidity ^0.8.19;
( Please note that the following instance is not found in https://gist.github.com/Picodes/01427c59b07c651699136589541159a7#nc-1-typos. )
ctiizen
can be changed to citizen
in the following comment.
https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/staking/NeoTokyoStaker.sol#L533-L535
@param timelockOption Data encoding the parameters surrounding the timelock option used in staking the particular asset. Alternatively, this encodes ctiizen information for BYTES staking.
NatSpec comments provide rich code documentation. The following function misses the @return
comments. Please consider completing the NatSpec comments for this function.
contracts\staking\NeoTokyoStaker.sol 1264: function getPoolReward (
NatSpec comments provide rich code documentation. The following function misses NatSpec comments. Please consider adding NatSpec comments for this function.
contracts\staking\NeoTokyoStaker.sol 1044: function _stakeBytes (
#0 - c4-judge
2023-03-17T02:38:14Z
hansfriese marked the issue as grade-a
#1 - c4-sponsor
2023-03-21T02:12:55Z
TimTinkers marked the issue as sponsor acknowledged