Dexit

CertiKFiles named in the report

CertiK reviewed DeXit on 7 Sep 2022: 34 findings, 2 critical; 29 marked fixed, 5 open.

2 critical findings, fixed
Open the reportOpen DeXit’s security record

Report hosted on skynet.certik.com.

Trustblock does not review code.

Findings34

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical002
High002
Medium001
Low5024
CriticalAnyone Can Call `removeFromHighestValidatorList`Fixed

The function `removeFromHighestValidatorList()` removes a highest validator from the array `highestValidators`. However, this function is `external` and no access control is enforced. Therefore, it can be called by anyone. Since validators in `highestValidator` are responsible for producing/replaying blocks and voting/executing proposals, if a malicious user removes the validators by repeatedly calling the function, the whole network may get unstable. **Exploit Scenario** - An attacker calls `removeFromHighestValidatorList()` repeatedly to remove the highest validator from the array `highestValidators`. - Finally, there will be only one "highest validator" left. - Therefore, if the attacker is able to control the left validator, the attacker will be able to manipulate the chain and cause devastating consequences.

CriticalLack of Access Control over `punish()` functionFixed

The function `punish()` is used to record the count of the validator's missed block and jail the validator when it meets the conditions of punishment. However, this function is `external` and no access control is enforced. Therefore, it can be called by anyone. **Exploit Scenario** - An attacker repeatedly calls this function to punish all the validators multiple times, until all validators are jailed, which would cause a Denial-Of-Service.

HighNo delay in Governance tasksFixed

In the `GoveHub.sol` contract, the `voteProposal()` function allows users to participant in the governance voting. If the voting meets the threshold, the proposal will be executed automatically. However, allowing voting and executing a proposal in the same transaction could be risky since if the voting is manipulated, the proposal will be immediately executed, thus causing actual damage to the project. Therefore, considering the users' unpredictable behavior, it is recommended to introduce a certain time of delay when performing governance actions. For example, in the case that the private keys of multiple governors are compromised, attackers could immediately perform the following actions to execute malicious proposals immediately: 1. Create a Malicious Proposal (or Global Proposal), 2. Cast the Vote, 3. Execute a Malicious proposal.

HighPotential Reentrancy AttackFixed

A reentrancy attack can occur when the contract creates a function that makes an external call to another untrusted contract before resolving any effects. If the attacker can control the untrusted contract, they can make a recursive call back to the original function, repeating interactions that would have otherwise not run after the external call resolved the effects. Two functions are vulnerable to re-entrancy attack: - `claimValidatorReward()` - `claimDelegatorReward()` In both cases, if `oldClaimTime` is different from 0, a re-entrancy will occur. \ Below is a summary of the performed calls for `claimDelegatorReward()` : ```solidity function claimDelegatorReward(address validator) (...) address payable delegator = msg.sender; (...) uint256 oldClaimTime = rewardClaimRecords[delegator]; (...) uint256 staking = stakeInfo.income; (...) if (oldClaimTime == 0) { (...) } else { uint256 endTime = oldClaimTime + RewardClaimingPeriod; require(block.number >= endTime, "Claim after 24 hours"); delegator.transfer(staking); rewardClaimRecords[delegator] = block.number; stakeInfo.income = 0; ``` If `delegator` is a contract, it could implement a fallback function that re-enters through `BSCValidatorSet.claimDelegatorReward()`, which would allow the `delegator` to siphon all funds from the `BSCValidatorSet` contract. \ The issue is tied to the fact that the value of `rewardClaimRecords[delegator]` and `stakeInfo.income` are updated after the function `delegator.transfer()` is called. This violates the check-effect-interaction pattern and allows a dangerous re-entrancy. **Exploit Scenario** - An attacker uses a contract to stake tokens to become a validator/delegator and get the block reward recorded in the `validatorInfo`/`stakeInfo`. - Then the attacker claims the reward once when the `oldClaimTime` is 0. - After this, in the next call to get rewards, the attacker can make a recursive call back to the original function, and siphon all funds from the `BSCValidatorSet` contract.

MediumInconsistency in size of `highestValidators`Fixed

According to the `unstakeValidator()` function, the `highestValidators` array needs to have at least 4 elements. ```solidity require( highestValidators.length > 3 && this.isActiveValidator(staker), "Can't Unstake, Validator List Empty" ); ``` This means that, for Dexit to work correctly, at least 4 validators are required. However, the `removeFromHighestValidatorList()`, which deletes validators from the `highestValidators` array, does not check the same condition, and instead check that the `highestValidators` has at least 1 element. \ Indeed, if there are 2 validators (n=2), then the loop will be entered and one of the validators removed, leaving only one validator. ```solidity=823 function removeFromHighestValidatorList(address val) external override { uint256 n = highestValidators.length; for (uint256 k = 0; k < n && n > 1; k++) { ``` In the same way, the `removeFromCurrentValidatorList()` function does not ensure that there are at least 3 validators remaining: ```solidity=837 function removeFromCurrentValidatorList(address val) private { uint256 n = currentValidators.length; for (uint256 i = 0; i < n && n > 1; i++) ```

LowDeclaration Naming ConventionFixed

"The linked declarations do not conform to the [Solidity style guide](https://solidity.readthedocs.io/en/v0.7.3/style-guide.html#naming-conventions) with regards to its naming convention. Particularly: - `camelCase`: Should be applied to function names, argument names, local and state variable names, modifiers - `UPPER_CASE`: Should be applied to `constant` variables - `CapWords`: Should be applied to contract names, struct names, event names and enums"

LowDiscussion of the function `unstakeValidator()`Fixed

The function `unstakeValidator()` is used for the validator to unstake their position. According to the following code, the condition will ensure that the last three validators can't unstake their position. Besides, the comment for this condition check is `Can't Unstake, Validator List Empty`, which is mismatch with the condition check. ```solidity require( highestValidators.length > 3 && this.isActiveValidator(staker), "Can't Unstake, Validator List Empty" ); ```

LowDiscussion on `deposit` FunctionFixed

The function `deposit()` is used by `Coinbase` to distribute block rewards to the validator `valAddr` and the contracts `_contractArray`. ```solidity=145 if(valInfo.validator == address(0x0) && highestValidators.length == 0){ valInfo.validator = valAddr; valInfo.status = IBSCValidatorSet.Status.NotExist; valInfo.income = valInfo.income.add(value); valInfo.TotalIncome = valInfo.TotalIncome.add(value); return; } ``` Consider the following cases: * **Case 1**: highestValidators.length == 0 In this case, there is no data in the `highestValidators` and `currentValidators` arrays. As a result, for the first call of the function with the parameter address `valAddr`, the `valInfo.validator` should be address(0x0). \ This will distribute rewards to `valAddr`, all the block rewards will be recorded in the `valInfo` structure, and the current status of `valInfo` will be `NotExist`. \ Since `claimValidatorReward()` will check whether the status is `NotExist`, the call of function `claimValidatorReward()` by the `valInfo.validator` will fail and the block rewards can't be claimed. As a result, the information recorded in the `valInfo` seems to be useless unless the `valInfo.validator` stakes a sufficient amount to become a normal validator. ```solidity function claimValidatorReward() external zeroAddress returns (bool) {//@audit-ok (...) require( valInfo.status != Status.NotExist && valInfo.status != Status.Jailed ); // Check for Not Exist Or Jailed require(valInfo.income > 0, "No incomes yet."); ``` Besides, if the function with the same parameter address `valAddr` is called again before the `valInfo.validator` become a normal validator, the `valInfo.validator` has been the parameter address `valAddr`, part of the block rewards will still be accumulated to this `valInfo` in the subsequent logic, the `valInfo` is still useless like before. * **Case 2**: highestValidators.length > 0 When the `valInfo.validator` is not `address(0x0)`, it will be a normal call. However, when the `valInfo.validator` is `address(0x0)`, the function will perform multiple calls like in **Case 1**.

LowDiscussion on the status of `valInfo`Fixed

When a user stakes tokens to become a validator for the first time, the amount of `valInfo` equals 0 and the status is `NotExist`, The `valInfo`'s status is updated to `Created`. But the `if` statement at L257 checks whether `valInfo.status` is `Staked` and if not, update it to `Staked`. ```solidity=257 if (valInfo.status != IBSCValidatorSet.Status.Staked) { valInfo.status = IBSCValidatorSet.Status.Staked; } ``` The status `Created` seems to be a temporary status and won't take any effect. Is that designed as expected?

LowDivision Truncation in the function `deposit()`Fixed

According to the following code, the variable `percentageToTransfer` is the percentage of validator's stake amount in total stake. As the multiplier used is 100, the value of `percentageToTransfer` will be 0 to 100. When less than one percent, the value will be 0 due to division truncation。 ```solidity uint256 percentageToTransfer = valInfo.amount.mul(100).div(UpdatedCoins); ``` As a result, the reward for validator will be zero. We would like to confirm with the client if the current implementation aligns with the original project design.

Show 10 more
LowFunction Should Be Declared ExternalFixed

The functions which are never called internally within the contract should have external visibility for gas optimization. ```solidity=197 function getElibelDel() public view returns(address[] memory) { ``` --- ```solidity=967 function getStakingInfo(address staker, address val) ``` --- ```solidity=993 function getPunishValidators() public view returns (address[] memory) { ``` --- ```solidity=997 function getPunishInfo(address validator) ``` --- ```solidity=1011 function getBlockNumber() public view returns (uint256) { ``` --- ```solidity=1015 function getHighestValidators() public view returns (address[] memory) { ``` --- ```solidity=1019 function getContractBalance() public view returns (uint256) { ``` --- ```solidity=109 function chcekProposal() public view returns (bytes32[] memory) { ``` --- ```solidity=248 function currentValue(string memory vari_name) ``` --- ```solidity=342 function getActiveProposal(bytes32 _id) ```

LowIncorrect Function NamingFixed

The following codes contain a modifier and a function with incorrect names. ```solidity modifier zeroAddress() { _zeroAddress(); _; } ``` ```solidity function _zeroAddress() internal view { require(msg.sender != address(0), "Zero Address"); } ```

LowLack of Access Control over `updateParam` FunctioFixed

The `updateParam` function is used to update the public state variables `misdemeanorThreshold` and `felonyThreshold`. \ In commit [1c2d45f56eab6a62d0416daf9ac012c79f61e1a7](https://github.com/Dexit-Network/contract/commit/1c2d45f56eab6a62d0416daf9ac012c79f61e1a7#diff-b46875f9d322aef5238a6659b8686dec9a540ff38ff32491bbad90170bd014ecL149), the `onlyGov` modifier is removed from `updateParam` Function. ```solidity function updateParam(string calldata key, bytes calldata value) external override onlyInit { //... } ``` As a result, Anyone can call `updateParam` to change `misdemeanorThreshold` and `felonyThreshold` referenced by an unused function `clean()` from the current contract. Is that designed as expected?

LowLack of Necessary Restrictions in the function `uFixed

According to the following code, the function `unJailed()` is used for the jailed validators to unjail themselves after the `unjailingPeriod`. However, the only restriction in the function is the check of `endTime` but lack of checking `jailedTime > 0` or `status == Jailed`. ```solidity address requester = msg.sender; // validator address Validator storage valInfo = validatorInfo[requester]; PunishRecord storage punishInfo = punishRecord[requester]; // Get The Punish Record uint256 endTime = punishInfo.jailedTime + unjailingPeriod; // Get The End Time require(block.timestamp >= endTime, "WaitToUnjail"); require(msg.value == 1 ether, "Pay 1DXT"); // Need to Submit Only 1 DXT To Unjailed // Change Status To Staked valInfo.status = Status.Staked; ``` When the requester is not a jailed validator even not a validator, the `punishInfo.jailedTime` will be default value 0 and the restriction will be useless. As a result, the `valInfo.status` will be changed to `Status.Staked` and the requester may be pushed to the variable `highestValidators` incorrectly.

LowMerge Redundant `For` statementsFixed

The linked `For` statements are used to check whether `vari_name` change proposals created by the user have expired. ```solidity=157 for (uint256 i = 0; i < UserProposal.length; i++) { if ( keccak256( bytes(proposals[UserProposal[i]].variable_name) ) == keccak256(bytes("minimumStakeAmount")) && (block.timestamp < proposals[UserProposal[i]].createTime + proposalLastingPeriod) ) { bool isexist = false; require(isexist == true, "proposal created before"); } } ``` ```solidity=188 for (uint256 i = 0; i < UserProposal.length; i++) { if ( keccak256( bytes(proposals[UserProposal[i]].variable_name) ) == keccak256(bytes("MaxValidators")) && (block.timestamp < proposals[UserProposal[i]].createTime + proposalLastingPeriod) ) { bool isexist = false; require(isexist == true, "proposal created before"); } } ``` Those two statements have roughly the same logic and can be merged. \ Also, the function already validates that the `vari_name` can only have the 2 values `MaxValidators` or `minimumStakeAmount`. ```solidity keccak256(bytes(vari_name)) == keccak256(bytes("MaxValidators")) || keccak256(bytes(vari_name)) == keccak256(bytes("minimumStakeAmount")), "MaxValidators & minimumStakeAmount Proposal Only" ); ```

LowMerge Redundant If Conditional BranchesFixed

The following `if` conditional branch at L121 can be merged into else branch in L115. ```solidity=115 if ( keccak256(bytes(proposals[id].variable_name)) == keccak256(bytes("minimumStakeAmount")) ) { ibsc.updateVotingValues(proposals[id].variable_name, proposals[id].variable_value); } if ( keccak256(bytes(proposals[id].variable_name)) == keccak256(bytes("MaxValidators")) ) { ibsc.updateVotingValues(proposals[id].variable_name, proposals[id].variable_value); } ```

LowMismatch Between Code and CommentFixed

In `GovHub`, the constant state variable `proposalLastingPeriod` is 3 days, which means that a validator can create another proposal after an interval of 3 days, while in the comment at L156 the interval time is 7 days. In `GovHub`, consider this case: `proposals[id].votePowerOfAgree` is 10.00001 ether and `totalVotePower` is 20 ether, although the voting power doesn't reach 51% of total voting power, the condition at L322 is true, which is inconsistent with the comment at L321 mentioning that the voting must be agreed by 51% calculating `votingPower`. In `BSCValidatorSet`, the `Max_Validators`, which represents the max length of `highestValidators` is 5 (L62), while in the comment (L44) states that the max length of `highestValidators` is 21.

LowMissing Zero Address Validation in `deposit()` fuFixed

The `Coinbase` will distribute block rewards to the input address `valAddr`, if `valAddr` is zero, the rewards distributed to zero address will be locked in current contract forever.

LowPotential Integer Overflow and UnderflowFixed

Integer overflow and underflow might happen in integer operations if the Solidity version is lower than 0.8.0. Although `SafeMath` has applied to `uint256` at L11, but no function from `SafeMath` is taken for mathematical operations. ```solidity contract RewardRegister is System, IRewardRegister{ using SafeMath for uint256; //.... } ``` ```solidity=66 rewardAmountOwner[eligibleOwners[i]] += rewardPerOwner; ``` ```solidity=84 uint256 endTime = oldClaimTime + RewardClaimingPeriod; ```

LowPotential Logic Flaw in `unJailed()`Fixed

The `unJailed()` function is used to un-jail a validator. If the length of `highestValidators` doesn't reach `MaxValidators`, the requester will be pushed into `highestValidators`, and the function returns directly, leaving the requester in the array `punishValidator` and keeping the state of `punishInfo`. ```solidity=750 if ( highestValidators.length < MaxValidators && requester != address(0) ) { //console.log("this is push 766 : ",MaxValidators, minimumStakeAmount); highestValidators.push(requester); } else { //... // Reset Punish Record punishInfo.jailedTime = 0; punishInfo.index = 0; punishInfo.isPunished = false; removeFromPunishValidator(requester); // Remove From Punish Record } } ``` This will cause the requester to be both in `highestValidators` and `punishValidator`. Is that designed as expected?

Show 9 more
LowPotential rewards not fully distributedFixed

According to the following code, the variable `percentageToTransfer` is the percentage of one delegator's stake amount in total stake. As the multiplier used is 100 and the division truncates, the sum value of `percentageToTransfer` for delegators will likely be much less than 100。 ```solidity uint256 percentageToTransfer = stakeamount.mul(100).div(totalCoins); ``` As a result, the rewards are not fully distributed and remain in the contract.

LowRedundant ChecksFixed

The statement at L300 ensures that `stakeamount` is greater than 0 and the statement at 302 ensures that `valInfo.status` equals `Staked`. The `if` statement at L304 and the condition `valInfo.status == Status.Staked` at L308 are redundant. ```solidity=300 require(stakeamount > 0, "Can't stake 0 DXT"); require(this.isActiveValidator(validator), "Validator Not Exist"); require(Status.Staked == valInfo.status, "Validator Not Staked"); if (stakeamount <= 0) { return false; } if (valInfo.status == Status.Staked && stakeInfo.amount == 0) { ``` The condition at L328 will check whether the validator is already in the `highestValidators` or not, so there is no need to check it again at L337. ```solidity=326 } else if ( highestValidators.length >= MaxValidators && !this.isTopValidator(validator) ) { // Find The Lowest Coins Address & Index in HighestValidators List uint256 lowCoin; uint256 lowIdx; address lowAddress; (lowCoin, lowIdx, lowAddress) = this.lowestCoinsInHighestValidator(); if (valInfo.coins > lowCoin) { if (!this.isTopValidator(validator)) { highestValidators[lowIdx] = validator; } } ``` The statement at L611 ensures that `stakeInfo.income` is greater than zero so the `if` at L614 is redundant. ```solidity=611 require(stakeInfo.income > 0, "No incomes yet."); uint256 staking = stakeInfo.income; if (stakeInfo.income <= 0) { return false; // return if income is zero } ``` The function `highestCoinsInCurrentValidatorsNotInTopValidator` ensures that the returned `highestValidatorAddress`'s status is `Staked`. ```solidity=891 function highestCoinsInCurrentValidatorsNotInTopValidator() { //... for (uint256 k = 0; k < currentValidators.length; k++) { if ( validatorInfo[currentValidators[k]].coins > highCoins && !this.isTopValidator(currentValidators[k]) && validatorInfo[currentValidators[k]].status == Status.Staked ) { highCoins = validatorInfo[currentValidators[k]].coins; highIndex = k; highestValidatorAddress = currentValidators[k]; } } return (highCoins, highIndex, highestValidatorAddress); ``` As a result, there is no need to check whether the `highestValidatorAddress`'s status is `Jailed` or not at L707. ```solidity=702 ) = this.highestCoinsInCurrentValidatorsNotInTopValidator(); if ( highestValidators.length < MaxValidators && addValAddress != address(0) && validatorInfo[addValAddress].status != Status.Jailed ```

LowTypoFixed

`then` is supposed to be `than` and `creat` is supposed to be `create`.

LowUnnecessary `payable` Address TypeFixed

In the `registerContract` function, the address `rewardAddr` has a `payable` attribute, but no ether is transferred in or out from that address. In addition, the state variable `rewardAddresses` stores values as `address`. ```solidity=14 mapping(address => address) public rewardAddresses; ```

LowUnnecessary Condition CheckingFixed

The `onlyInit` modifier makes sure that the `init` function is called, so the `burnRatioInitialized` is true and `burnRatio` assigned as `INIT_BURN_RATIO` is greater than 0. The `noEmptyDeposit` checks whether `msg.sender` is greater than 0. Therefore, Conditions at L134 and L137 are always true and the checks here are unnecessary.

LowUnused Functions in `SlashIndicator`Fixed

The `clean()` function annotated by `onlyValidatorContract` can only be called by `BSCValidatorSet` contract and the `updateParam` annotated by `onlyGov` can only be called by `GovHub` contract, but no calls come from both contracts. In addition, the `encodeSlashPackage` is an internal function but never used.

LowUnused Import FilesFixed

The linked `import` statements include a contract or library which is not used in the file that imported such files.

LowUnused private functionFixed

The function `isDelegatorsExist()` is declared as private and never be called in the contract.

LowUser-Defined GettersFixed

The linked functions are equivalent to the compiler-generated getter functions for the respective variables.

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (23)

  • GovHub.sol
  • interface/IRelayerHub.sol
  • interface/ILightClient.sol
  • RewardRegister.sol
  • lib/BytesToTypes.sol
  • System.sol
  • BSCValidatorSet.sol
  • interface/ICrossChain.sol
  • lib/BytesLib.sol
  • lib/Memory.sol
  • lib/SafeMath.sol
  • interface/ISlashIndicator.sol
  • interface/IBSCValidatorSet.sol
  • lib/RLPDecode.sol
  • lib/CmnPkg.sol
  • interface/ISystemReward.sol
  • lib/SizeOf.sol
  • interface/IRewardRegister.sol
  • lib/RLPEncode.sol
  • lib/TypesToBytes.sol
  • interface/IParamSubscriber.sol
  • interface/IApplication.sol
  • SlashIndicator.sol

Part of DeXit’s security record

DeXit
Audits1 audit by 1 firmLast audit Sep 2022, by CertiK.
Critical findingsNo open critical findingsIn that report, 2 critical findings were reported. Each is marked fixed by the firm that reported it.
Open DeXit’s security record

Security firm

CertiK

1,044 projects reviewed · 1,375 reports · latest May 2025

See CertiK’s audits

Something 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