Neo Tokyo contest - btk's results

A staking contract for the crypto gaming illuminati.

General Information

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

Neo Tokyo

Findings Distribution

Researcher Performance

Rank: 23/123

Findings: 1

Award: $235.24

QA:
grade-a

🌟 Selected for report: 0

🚀 Solo Findings: 0

Total Low issues
RiskIssues DetailsNumber
[L-01]Loss of precision due to rounding8
[L-02]Missing checks for address(0)2
[L-03]Array lengths not checked4
[L-04]Solidity compiler optimizations can be problematic1
[L-05]Value is not validated to be different than the existing one2
[L-06]Use immutable instead of constant for values such as a call to keccak256()7
Total Non-Critical issues
RiskIssues DetailsNumber
[NC-01]Include return parameters in NatSpec comments1
[NC-02]Non-usage of specific imports9
[NC-03]Lack of event emit2
[NC-04]Function writing does not comply with the Solidity Style Guide1
[NC-05]Add address(0) check for the critical changes1
[NC-06]Use a more recent version of OpenZeppelin dependencies1
[NC-07]Constants in comparisons should appear on the left side11
[NC-08]Contracts should have full test coverageAll Contracts
[NC-09]Need Fuzzing testAll Contracts
[NC-10]Generate perfect code headers every timeAll Contracts
[NC-11]Lock pragmas to specific compiler version2
[NC-12]Consider using delete rather than assigning zero to clear values8
[NC-13]For functions, follow Solidity standard naming conventionsAll Contracts
[NC-14]Add NatSpec Mapping comment12
[NC-15]Use SMTChecker

[L-01] Loss of precision due to rounding

Description

Loss of precision due to the nature of arithmetics and rounding errors.

Lines of code
			citizenStatus.points =
				identityPoints * vaultMultiplier * timelockMultiplier /
				_DIVISOR / _DIVISOR;
			citizenStatus.points = 100 * timelockMultiplier / _DIVISOR;
				uint256 bonusPoints = (amount * 100 / _BYTES_PER_POINT);
				uint256 bonusPoints = (amount * 100 / _BYTES_PER_POINT);
			uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR;
				uint256 share = points * _PRECISION / pool.totalPoints * totalReward;
				uint256 daoShare = share * pool.daoTax / (100 * _DIVISOR);
			uint256 points = amount * 100 / 1e18 * lpPosition.multiplier / _DIVISOR;

[L-02] Missing checks for address(0)

Description

Check of address(0) to protect the code from (0x0000000000000000000000000000000000000000) address problem just in case. This is best practice or instead of suggesting that they verify _address != address(0), you could add some good NatSpec comments explaining what is valid and what is invalid and what are the implications of accidentally using an invalid address.

Lines of code
	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;
		VAULT_CAP = _vaultCap;
		NO_VAULT_CAP = _noVaultCap;
	}

Add checks for address(0) when assigning values to address state variables.

[L-03] Array lengths not checked

Description

If one of the arrays is a shorter length than the other arrays:

  • The additional values in the other arrays may be ignored.
  • The users operations may not be fully executed.
  • This could lead to transfers with unexpected results, which would be better served by reverting.
Lines of code
	function configureTimelockOptions (
		AssetType _assetType,
		uint256[] memory _timelockIds,
		uint256[] memory _encodedSettings
	) external hasValidPermit(bytes32(uint256(_assetType)), CONFIGURE_TIMELOCKS) {
		for (uint256 i; i < _timelockIds.length; ) {
			timelockOptions[_assetType][_timelockIds[i]] = _encodedSettings[i];
			unchecked { ++i; }
		}
	}
	function configureIdentityCreditYields (
		uint256[] memory _citizenRewardRates, 
		string[] memory _vaultRewardRates,
		string[] memory _identityCreditYields
	) hasValidPermit(UNIVERSAL, CONFIGURE_CREDITS) external {
		for (uint256 i; i < _citizenRewardRates.length; ) {
			identityCreditYield[
				_citizenRewardRates[i]
			][
				_vaultRewardRates[i]
			] = _identityCreditYields[i];
			unchecked { ++i; }
		}
	}
	function configureIdentityCreditPoints (
		string[] memory _identityCreditYields,
		uint256[] memory _points
	) hasValidPermit(UNIVERSAL, CONFIGURE_CREDITS) external {
		for (uint256 i; i < _identityCreditYields.length; ) {
			identityCreditPoints[_identityCreditYields[i]] = _points[i];
			unchecked { ++i; }
		}
	}
	function configureVaultCreditMultipliers (
		string[] memory _vaultCreditMultipliers,
		uint256[] memory _multipliers
	) hasValidPermit(UNIVERSAL, CONFIGURE_CREDITS) external {
		for (uint256 i; i < _vaultCreditMultipliers.length; ) {
			vaultCreditMultiplier[_vaultCreditMultipliers[i]] = _multipliers[i];
			unchecked { ++i; }
		}
	}

Check that the arrays length are the same.

[L-04] Solidity compiler optimizations can be problematic

Description

Protocol has enabled optional compiler optimizations in Solidity. There have been several optimization bugs with security implications. Moreover, optimizations are actively being developed. Solidity compiler optimizations are disabled by default, and it is unclear how many contracts in the wild actually use them.

Therefore, it is unclear how well they are being tested and exercised. High-severity security issues due to optimization bugs have occurred in the past. A high-severity bug in the emscripten-generated solc-js compiler used by Truffle and Remix persisted until late 2018. The fix for this bug was not reported in the Solidity CHANGELOG.

Another high-severity optimization bug resulting in incorrect bit shift results was patched in Solidity 0.5.6. More recently, another bug due to the incorrect caching of keccak256 was reported. A compiler audit of Solidity from November 2018 concluded that the optional optimizations may not be safe. It is likely that there are latent bugs related to optimization and that new bugs will be introduced due to future optimizations.

Exploit Scenario A latent or future bug in Solidity compiler optimizations—or in the Emscripten transpilation to solc-js—causes a security vulnerability in the contracts.

Lines of code
				settings: {
					optimizer: {
						enabled: true,
						runs: 200, 
						details: {
							yul: true 
						}
					}

Short term, measure the gas savings from optimizations and carefully weigh them against the possibility of an optimization-related bug. Long term, monitor the development and adoption of Solidity compiler optimizations to assess their maturity.

[L-05] Value is not validated to be different than the existing one

Description

While the value is validated to be in the constant bounds, it is not validated to be different than the existing one. Queueing the same value will cause multiple abnormal events to be emitted, will ultimately result in a no-op situation.

Lines of code
	function changeStakingContractAddress (
		address _staker
	) hasValidPermit(UNIVERSAL, ADMIN) external {
		STAKER = _staker;
	}
	function changeTreasuryContractAddress (
		address _treasury
	) hasValidPermit(UNIVERSAL, ADMIN) external {
		TREASURY = _treasury;
	}

Add a require() statement to check that the new value is different than the current one.

[L-06] Use immutable instead of constant for values such as a call to keccak256()

Description

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.

Lines of code
	bytes32 public constant BURN = keccak256("BURN");
	bytes32 public constant ADMIN = keccak256("ADMIN");
	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");

Use immutable instead of constants

[NC-01] Include return parameters in NatSpec comments

Description

If Return parameters are declared, you must prefix them with /// @return. Some code analysis programs do analysis by reading NatSpec details, if they can't see the @return tag, they do incomplete analysis.

Lines of code
	) public view returns (uint256, uint256) {

Include @return parameters in NatSpec comments

[NC-02] Non-usage of specific imports

Description

The current form of relative path import is not recommended for use because it can unpredictably pollute the namespace. Instead, the Solidity docs recommend specifying imported symbols explicitly.

Lines of code
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";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../access/PermitControl.sol";
import "../interfaces/IByteContract.sol";
import "../interfaces/IGenericGetter.sol";

Use specific imports syntax per solidity docs recommendation.

[NC-03] Lack of event emit

Description

The below methods do not emit an event when the state changes, something that it's very important for dApps and users.

Lines of code
	function changeStakingContractAddress (
		address _staker
	) hasValidPermit(UNIVERSAL, ADMIN) external {
		STAKER = _staker;
	}
	function changeTreasuryContractAddress (
		address _treasury
	) hasValidPermit(UNIVERSAL, ADMIN) external {
		TREASURY = _treasury;
	}

Emit event.

[NC-04] Function writing does not comply with the Solidity Style Guide

Description

Ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier. But there are contracts in the project that do not comply with this.

Functions should be grouped according to their visibility and ordered:

  • constructor()
  • receive()
  • fallback()
  • external / public / internal / private
  • view / pure
Lines of code

Follow Solidity Style Guide.

[NC-05] Add address(0) check for the critical changes

Description

Check of the address for the critical changes to protect the code from address(0) problem in case of mistakes.

Lines of code
	function changeStakingContractAddress (
		address _staker
	) hasValidPermit(UNIVERSAL, ADMIN) external {
		STAKER = _staker;
	}
	function changeTreasuryContractAddress (
		address _treasury
	) hasValidPermit(UNIVERSAL, ADMIN) external {
		TREASURY = _treasury;
	}

Add address(0) check.

[NC-06] Use a more recent version of OpenZeppelin dependencies

Description

For security, it is best practice to use the latest OpenZeppelin version.

Lines of code
    "@openzeppelin/contracts-upgradeable": "^4.4.2",

Old version of OpenZeppelin is used (4.4.2), newer version can be used (4.8.0).

[NC-07] Constants in comparisons should appear on the left side

Description

Constants in comparisons should appear on the left side, doing so will prevent typo bug.

		if (stakerLPPosition[msg.sender].multiplier == 0) {
Lines of code
		if (rewardRate == 0) {
		} else if (citizenVaultId != 0 && vaultId == 0) {
		} else if (citizenVaultId == 0 && vaultId != 0) {
			if (citizenStatus.timelockEndTime == 0) {
			if (citizenStatus.timelockEndTime == 0) {
		if (stakerLPPosition[msg.sender].multiplier == 0) {
		if (_pools[_assetType].rewardCount == 0) {
		if (timelockOption == 0) {
		if (stakedCitizen.timelockEndTime == 0) {
		if (stakedCitizen.timelockEndTime == 0) {
		if (lpPosition.amount == 0) {

Constants should appear on the left side:

		if (0 == stakerLPPosition[msg.sender].multiplier) {

[NC-08] Contracts should have full test coverage

Description

While 100% code coverage does not guarantee that there are no bugs, it often will catch easy-to-find bugs, and will ensure that there are fewer regressions when the code invariably has to be modified. Furthermore, in order to get full coverage, code authors will often have to re-organize their code so that it is more modular, so that each component can be tested separately, which reduces interdependencies between modules and layers, and makes for code that is easier to reason about and audit.

- What is the overall line coverage percentage provided by your tests?: 80

Line coverage percentage should be 100%.

[NC-09] Need Fuzzing test

Description

In total 2 contracts, 30 unchecked{} are used, the functions used are critical. For this reason, there must be fuzzing tests in the tests of the project. Not seen in tests.

Lines of code

Use should fuzzing test like Echidna. As Alberto Cuesta Canada said: Fuzzing is not easy, the tools are rough, and the math is hard, but it is worth it. Fuzzing gives me a level of confidence in my smart contracts that I didn’t have before. Relying just on unit testing anymore and poking around in a testnet seems reckless now.

Ref: https://medium.com/coinmonks/smart-contract-fuzzing-d9b88e0b0a05

[NC-10] Generate perfect code headers every time

Description

I recommend using header for Solidity code layout and readability

Ref: https://github.com/transmissions11/headers

Lines of code
/*////////////////////////////////////////////////////////////// TESTING 123 //////////////////////////////////////////////////////////////*/

[NC-11] Lock pragmas to specific compiler version

Description

Pragma statements can be allowed to float when a contract is intended for consumption by other developers, as in the case with contracts in a library or EthPM package. Otherwise, the developer would need to manually update the pragma in order to compile locally.

Ref: https://swcregistry.io/docs/SWC-103

Lines of code
pragma solidity ^0.8.19;
pragma solidity ^0.8.19;

Ethereum Smart Contract Best Practices: Lock pragmas to specific compiler version.

[NC-12] Consider using delete rather than assigning zero to clear values

Description

The delete keyword more closely matches the semantics of what is being done, and draws more attention to the changing of state, which may lead to a more thorough audit of its associated logic.

Lines of code
		stakedCitizen.stakedBytes = 0;
		stakedCitizen.timelockEndTime = 0;
		stakedCitizen.points = 0;
		stakedCitizen.stakedVaultId = 0;
		stakedCitizen.stakedBytes = 0;
		stakedCitizen.timelockEndTime = 0;
		stakedCitizen.points = 0;
			lpPosition.multiplier = 0;

Use the delete keyword.

[NC-13] For functions, follow Solidity standard naming conventions

Description

The protocol don't follow solidity standard naming convention.

Ref: https://docs.soliditylang.org/en/v0.8.17/style-guide.html#naming-conventions

Lines of code

Follow solidity standard naming convention.

[NC-14] Add NatSpec Mapping comment

Description

Add NatSpec comments describing mapping keys and values.

	mapping ( address => mapping ( AssetType => uint256 )) public lastRewardTime;
Lines of code
	mapping ( AssetType => mapping ( uint256 => uint256 )) public	timelockOptions;
		mapping ( uint256 => RewardWindow ) rewardWindows;
	mapping ( AssetType => PoolData ) private _pools;
	mapping ( address => mapping ( AssetType => uint256 )) public lastRewardTime;
	mapping ( uint256 => mapping ( string => string )) public identityCreditYield;
	mapping ( string => uint256 ) public identityCreditPoints;
	mapping ( string => uint256 ) public vaultCreditMultiplier;
	mapping ( address => mapping( uint256 => StakedS1Citizen )) public stakedS1;
	mapping ( address => uint256[] ) private _stakerS1Position;
	mapping ( address => mapping( uint256 => StakedS2Citizen )) public stakedS2;
	mapping ( address => uint256[] ) private _stakerS2Position;
	mapping ( address => LPPosition ) public stakerLPPosition;
/// @dev address(caller) => AssetType(Asset) => uint256(Time)
	mapping ( address => mapping ( AssetType => uint256 )) public lastRewardTime;

[NC-15] Use SMTChecker

Description

The highest tier of smart contract behavior assurance is formal mathematical verification. All assertions that are made are guaranteed to be true across all inputs → The quality of your asserts is the quality of your verification.

Ref: https://twitter.com/0xOwenThurm/status/1614359896350425088?t=dbG9gHFigBX85Rv29lOjIQ&s=19

#0 - c4-judge

2023-03-17T02:42:30Z

hansfriese marked the issue as grade-a

#1 - TimTinkers

2023-03-21T05:36:26Z

Will follow some of the recommendations, stylistic advice decidedly out-of-scope.

#2 - c4-sponsor

2023-03-21T05:36:30Z

TimTinkers marked the issue as sponsor acknowledged

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter