CertiK reviewed DeXit on 7 Sep 2022: 34 findings, 2 critical; 29 marked fixed, 5 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 | 2 |
| High | 0 | 0 | 2 |
| Medium | 0 | 0 | 1 |
| Low | 5 | 0 | 24 |
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.
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.
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.
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.
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++) ```
"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"
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" ); ```
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**.
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?
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.
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) ```
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"); } ```
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?
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.
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" ); ```
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); } ```
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.
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.
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; ```
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?
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.
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 ```
`then` is supposed to be `than` and `creat` is supposed to be `create`.
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; ```
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.
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.
The linked `import` statements include a contract or library which is not used in the file that imported such files.
The function `isDelegatorsExist()` is declared as private and never be called in the contract.
The linked functions are equivalent to the compiler-generated getter functions for the respective variables.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (23)
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