CertiK reviewed Next Level Game on 18 Nov 2022: 7 findings, 0 critical; 4 marked fixed, 3 open.
Report hosted on skynet.certik.com.
Trustblock does not review code.
Statuses are the security firm’s own, from its report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 0 |
| Medium | 1 | 0 | 0 |
| Low | 2 | 0 | 4 |
The functions which are never called internally within the contract should have external visibility for gas optimization. #### CompanyVault.sol ```solidity=20 function getMainToken() public view returns (address) { ``` #### Ownable.sol ```solidity=44 function renounceOwnership() public virtual onlyOwner { ``` --- ```solidity=53 function transferOwnership(address newOwner) public virtual onlyOwner { ```
The events `FirstJoin/JoinNextLevel` are used to record the user's join information. The parameter `toTableIdJoin` in the events records the `tableId` that the user joined within the call. In the function call `joinRefZeroTable()`, there is an internal call to function `joinRefLevel()` used to add the user to the corresponding table of the given `tableId`, and records the given `tableId` in the event. However, within function calls `joinZeroLevel()` and `toNextLevel()`, the internal call to function `join()` adds the user to the table corresponding to the business logic, and records the `toTableJoinId` (which represents the table user joined) in the event. #### joinZeroLevel() Function `joinZeroLevel()` calls `join()`. ```sol function joinZeroLevel() external { ... bool isFirstJoinToLevel = join(levels[nextLevel]); uint toTableJoinId = levels[nextLevel].actualTableId; emit FirstJoin(client, levels[nextLevel].tablesSize - 1, block.timestamp, toTableJoinId); ``` ```sol function join(NextLevelDTOs.Level storage level) internal returns (bool) { ... bool isFirstJoinToLevel = joinActualTable(level); createNewLevelJoin(level); return isFirstJoinToLevel; } ``` Function `join()` calls `joinActualTable()`. In the case that the logic within the `if` statement (L66-73) is triggered, the function will update the record to reflect that the user joined the table under a `level.actualTableId` value that will have been increased by 1. ```sol function joinActualTable(NextLevelDTOs.Level storage level) private returns (bool) { ... if (table.joinId == tableSize) { // Close actual table table.state = NextLevelDTOs.TableState.CLOSED; table.closedDate = block.timestamp; // Move level actual pointer level.actualTableId++; } return false; } ``` For the function `joinZeroLevel()`, the value of `levels[nextLevel].actualTableId` is used after the call of `join()`, in the above mentioned case, therefore the `toTableJoinId` recorded in the event `FirstJoin` might be larger than the actual `tableId` assigned to the joining user. ```sol function joinZeroLevel() external { ... bool isFirstJoinToLevel = join(levels[nextLevel]); uint toTableJoinId = levels[nextLevel].actualTableId; emit FirstJoin(client, levels[nextLevel].tablesSize - 1, block.timestamp, toTableJoinId); ``` #### toNextLevel() For the function `toNextLevel()`, the value used in the events `FirstJoin/JoinNextLevel` was stored prior to the running of the function `joinNextLevel()`. ```sol=48 function toNextLevel() external { ... uint toTableJoinId = levels[nextLevel].actualTableId; if (nextLevel == 0) { isFirstJoinToLevel = join(levels[nextLevel]); emit FirstJoin(client, levels[nextLevel].tablesSize - 1, block.timestamp, toTableJoinId); } else { uint tableId = joins[client].tablesRefByLevel[nextLevel - 1]; isFirstJoinToLevel = joinNextLevel(levels[nextLevel - 1], tableId, levels[nextLevel]); emit JoinNextLevel(client, nextLevel - 1, tableId, levels[nextLevel].tablesSize - 1, block.timestamp, toTableJoinId); } ``` Function `toNextLevel()` calls `joinNextLevel()`. ```sol function joinNextLevel(NextLevelDTOs.Level storage level, uint tableToNextLevelId, NextLevelDTOs.Level storage nextLevel) internal returns (bool) { ... return join(nextLevel); } ``` Function `joinNextLevel()` calls function `join()`. ```sol function join(NextLevelDTOs.Level storage level) internal returns (bool) { ... bool isFirstJoinToLevel = joinActualTable(level); createNewLevelJoin(level); return isFirstJoinToLevel; } ``` The function `joinActualTable()` calls `findActualTable()`. ```sol function joinActualTable(NextLevelDTOs.Level storage level) private returns (bool) { uint actualTableId = findActualTable(level); ``` The function `findActualTable()` may call itself (L84-89) to make the value of `level.actualTableId` match the table user will join. Several calls of the function `joinRefZeroTable()` may cause `level.actualTableId` to not match with the table that the user will join before the call of the function `findActualTable()`. As a result, the `tableId` user joined may be larger than the value stored before in the events. ```sol=78 function findActualTable(NextLevelDTOs.Level storage level) private returns (uint) { NextLevelDTOs.Table storage table = level.tables[level.actualTableId]; if (table.owner == address(0)) { return level.actualTableId; } if (table.state != NextLevelDTOs.TableState.OPENED) { // Found closed level.actualTableId++; // search next return findActualTable(level);
The function `toNextLevel()` is called by the user to upgrade and join the next level when the user's current table is closed in one day. However, according to the constructor function, the level seems to be restricted to values between `0` to `15`. ```sol constructor(address mainToken, uint companyFee, uint joinPrice) LevelProcessing(mainToken, companyFee, joinPrice) { for (uint i = 0; i < 16; ++i) { levels[i].level = i; } } ``` In the function `toNextLevel()`, the `nextLevel` is not checked. As a result, the user can potentially create a new table in a level that is above the value `15`. ```sol if (nextLevel == 0) { isFirstJoinToLevel = join(levels[nextLevel]); emit FirstJoin(client, levels[nextLevel].tablesSize - 1, block.timestamp, toTableJoinId); } else { uint tableId = joins[client].tablesRefByLevel[nextLevel - 1]; isFirstJoinToLevel = joinNextLevel(levels[nextLevel - 1], tableId, levels[nextLevel]); emit JoinNextLevel(client, nextLevel - 1, tableId, levels[nextLevel].tablesSize - 1, block.timestamp, toTableJoinId); } ```
The linked variables assigned in the constructor can be declared as `immutable`. Immutable state variables can be assigned during contract creation but will remain constant throughout the lifetime of a deployed contract. An advantage of immutable variables is that reading them is significantly cheaper than reading from regular state variables since they will not be stored in storage.
The report names a repository or files. Open the report to see its scope.
1,044 projects reviewed · 1,375 reports · latest May 2025
See CertiK’s auditsSomething wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.
Tell us