Be Chain - ValidatorSmartContract

CertiKFiles named in the report

CertiK reviewed BEBE (Be Chain - ValidatorSmartContract) on 19 Mar 2025: 20 findings, 0 critical; 15 marked fixed, 5 open.

No open critical findings
Open the reportOpen BEBE’s security record

Report hosted on skynet.certik.com.

Trustblock does not review code.

Findings20

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

SeverityOpenAcknowledgedFixed
Critical000
High101
Medium003
Low4011
HighPotential Underflow/Cast Issue in `_requireValidSFixed

Within the `ValidatorSmartContract`, the `_requireValidSignatures` function contains a logic bug involving the line: ```solidity uint16 validatorBit = uint16(1 << info.index); ``` This line attempts to track which root validators have signed a particular action using a 16-bit bitmap. The contract stores a list of validators in the `validators` array. Each validator has an associated `ValidatorInfo` struct, which includes a `uint16` index. This index is simply the validator’s position in the `validators` array. When a new root validator is added (via `addRootValidator`), `info.index` is set to the new `validators.length - 1`. This value can be between 0 and 254. Because a validator’s `info.index` can exceed 15, shifting `1 << info.index` and then casting to `uint16` truncates values above 15 bits to 0. Consequently, potential double-signature from two different root validators both above index 15 could map to 0 in the mask, so the code might not detect that they are distinct signers. (Though in practice it sees 0 for both and just checks `signersBitmap & 0 == 0`, so the second signature is also “accepted” but increments approvals incorrectly—leading to under- or over-counting.) This issue directly compromises the core security guarantee of multi-signature authorization among root validators. An action may fail to be authorized, or the contract may count incorrect signers.

Medium`queueAction` Can Exhaust Other SignatureFixed

The `queueAction` function allows the root validator to initiate `TimedAction` data for subsequent `executeAction`. The `actionType` in `queueAction` can be set any value, which is different from other functions, such as `APPROVE_VALIDATOR` in `approveValidator`. ```solidity=373 function queueAction( string memory actionId, string memory actionType, uint256 timeoutBlock, address targetAddress, ContractState newState, bytes[] memory signatures, bytes calldata _data ) external nonReentrant onlyRootValidator { if (_usedActionIds[actionId]) revert DuplicateActionError(); _usedActionIds[actionId] = true; _requireValidSignatures( actionId, actionType, timeoutBlock, targetAddress, newState, signatures ); ... } ``` If a one root validator disagrees with an `actionId` for functions such as `approveValidator` or `removeRootValidator`, that validator can front-run the function and consume the `actionId` in the `queueAction`. Consequently, functions like `approveValidator` or `removeRootValidator` will be reverted. This allows the root validator to veto an `actionId` easily.

MediumMissing Critical Parameters in Signature MessageFixed

In the `ValidatorSmartContract`, the functions that require multiple signatures (e.g., `approveValidator`, `disapproveValidator`, `addRootValidator`, `removeRootValidator`, `revokeValidator`, and `changeState`) only include `actionId` and `actionType` in the signed message. They do not include critical parameters such as the target `validator` address or the new contract `state`. This allows anyone who holds valid signatures for a certain (`actionId`, `actionType`) pair to reuse those signatures to carry out the same action type but on any arbitrary parameter.

MediumMissing Validation to `_data` in `queueAction`Fixed

In the `queueAction` function, the `_data` parameter can be set to any value without validation, which may threaten the system in the subsequent `executeAction`. For example, the `data` for `changeState` can be replaced by the `executeEmergencyWithdraw`.

LowCheck Effect Interaction Pattern ViolatedFixed

The following codes in the function `executeAction()` do not meet the Checks-Effects-Interactions pattern. And there also exits a limited reentrancy issue. ```solidity=476 _timelockActionExecuting = true; (bool success, bytes memory returnData) = address(this).call( action.data ); _timelockActionExecuting = false; ... action.executed = true; ``` One edge case could be when the `EMERGENCY_WALLET` is a contract and `address(this).call` is used in the `executeEmergencyWithdraw` function. During the execution of the `EMERGENCY_WALLET.receive` function, it can call functions that do not have the `nonReentrant` modifier, for example, the `changeState` function. ```solidity function _safeTransfer(address to, uint256 amount) private { if (to == address(0) || to == address(this)) revert InvalidRecipientError(); if (address(this).balance < amount) revert InsufficientBalanceError(); (bool success, ) = payable(to).call{value: amount}(""); if (!success) { emit TransferFailed(to, amount); revert TransferFailedError(); } } ```

LowEvent Not IndexedFixed

If an event is not indexed in a smart contract, it means that the event's parameters are not tagged with the `indexed` keyword. This has implications for how the event data can be searched and filtered when looking through blockchain logs. Without indexing, the event will still emit the data as part of the transaction log, but users won't be able to query for these events using the parameters. They'll have to retrieve the entire set of logs and manually sift through them to find events with the specific data. This can be less efficient and more time-consuming, especially on a blockchain with a high volume of transactions and events.

LowMissing Emit EventsFixed

There should always be events emitted in the sensitive functions that are controlled by centralization roles.

LowNo Cooldown When Admin Revokes a ValidatorFixed

If a validator is revoked by the admin, there is no enforced waiting period at all; the user can claim the pending stake immediately. By contrast, if the validator initiates their own exit, they must wait for the full `WITHDRAW_COOLDOWN` before claiming. It is inconsistent and can be unfair from the validator’s perspective. If the admin can instantly revoke you (maybe maliciously or by mistake) and you can immediately withdraw, it might sound good for you. On the other side, if an honest validator wants to leave, they must wait for the cooldown.

LowNo Expiration Check for SignatureFixed

The signature has no timeout detection or expiration check. Attackers can hoard signatures of some operations rather than execute them, which may threaten the system in the future.

LowNo Expiration Check in `executeAction`Fixed

The `TimedAction` lacks timeout detection or expiration checks. Attackers can hold `TimedAction` for certain operations instead of executing them immediately once the `action.executeAfter` condition is met, which could pose a risk to the system in the future. ```solidity function executeAction( string memory actionId ) external nonReentrant onlyRootValidator { ... if (!success) { // Extract revert reason from return data if (returnData.length > 0) { // Bubble up the revert reason assembly { let returndata_size := mload(returnData) revert(add(32, returnData), returndata_size) } } else { revert ActionExecutionFailed(); } } action.executed = true; emit ActionExecuted(actionId); } ``` Furthermore, if the `executeAction` initially fails due to a revert, it can be re-executed later without any expiration checks. The `timeoutBlock` is used solely for `queueAction` and has no impact on `executeAction`.

Show 5 more
LowPotential Hash Collision Caused by Dynamic VariabFixed

Using `abi.encodePacked()` with multiple variable-length arguments can lead to hash collisions. This function packs all elements in order, regardless of whether they're part of an array, allowing for the possibility of moving elements between arrays without changing the overall order, thus returning the same encoding. ```solidity abi.encodePacked( "\x19Ethereum Signed Message:\n32", // Prefix for Ethereum signed messages keccak256( abi.encodePacked( actionId, actionType ) ) ) ); ``` For instance, if you have: - actionId = "123" and actionType = "ARV". - actionId = "123A" and actionType = "RV". These two example will yield the same message hash for "123ARV".

LowRedundant Code ComponentsFixed

The linked statements do not affect the functionality of the codebase and appear to be either leftovers from test code or older functionality.

LowUnderscore Prefix For Non-External VariablesFixed

The current contract doesn't follow the naming convention specified by [Solidity DOC](https://docs.soliditylang.org/en/v0.8.20/style-guide.html#underscore-prefix-for-non-external-functions-and-variables): The state variable `variable` is used as `private` or `internal` and is not exposed publicly. It should have `an underscore prefix` like `_varaible`. Leading underscores allow you to immediately recognize the intent of such functions, but more importantly, if you change a function from non-external to external (including public) and rename it accordingly, this forces you to review every call site while renaming. This can be an important manual check against unintended external functions and a common source of security vulnerabilities (avoid find-replace-all tooling for this change).

LowUnsafe Cast from `msg.value` to `stake`Fixed

The contract constructor accepts `_requiredStake` as a uint256 but restricts it only to the range `> 0 && <= type(uint128).max`. ```solidity if (_requiredStake == 0 || _requiredStake > type(uint128).max) revert InvalidStakeError(); ``` The `ValidatorInfo` struct defines the stake field as a `uint96`. ```solidity struct ValidatorInfo { uint96 stake; uint32 registeredTime; uint32 activatedTime; uint16 index; ValidatorType validatorType; } ``` If `_requiredStake` (and consequently `msg.value` sent during registerValidator) is allowed to go beyond `type(uint96).max`, the assignment `uint96(msg.value)` in `registerValidator` will truncate the value. That means any overflow beyond 96 bits is discarded, leading to an incorrect (reduced) recorded stake amount. ```solidity _validatorInfo[msg.sender]= ValidatorInfo({ stake: uint96(msg.value), registeredTime:uint32(block.timestamp), activatedTime:uint32(0), index:uint16(0), validatorType: ValidatorType.WAITING }); ```

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 (1)

  • smart_contract/contracts/ValidatorSmartContract.sol

Part of BEBE’s security record

BEBE
Audits3 audits by 1 firmLast audit Mar 2025, by CertiK.
Critical findingsNo open critical findingsAcross the 3 reports, 1 critical finding was reported. It is marked fixed by the firm that reported it.
Open BEBE’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