CertiK reviewed BEBE on 20 Aug 2024: 29 findings, 1 critical; 22 marked fixed, 7 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 | 1 |
| High | 0 | 0 | 2 |
| Medium | 0 | 0 | 5 |
| Low | 7 | 0 | 14 |
The functions `addIncomePool()` and `confirmAddIncomePool()` do not have any access restriction and can be called by anyone. This can allow a attacker to call these with an `incomeDate` that would be used by the `BEBEMiningManager`. By calling these before the `BEBEMiningManager` it will cause all calls to `mine()` in the `BEBEMiningManager` to revert once the `_nextMiningDate` reaches the `incomeDate` used by the attacker. If this is done after the `BEBEMiningManager` has mined some amount of tokens, then `_nextMiningDate` will no longer be able to be updated. This will make it impossible for any further funds to be mined by the `BEBEMiningManager` and require a new contract to be deployed. The need to deploy a new contract for users to interact with would cause damage to the reputation of the protocol and prevent users from having access to funds at the expected time.
### GameStakerManager In the contract `GameStakerManager` the role `_owner` has authority over the function `setStakeEnabled()`. Any compromise to the `_owner` account may allow the hacker to take advantage of this authority and set the `_stakeEnabled` to true to enable staking when it is not intended to be enabled or set it to false to cause a denial of service. In addition they can transfer ownership to an account they have the sole control over or renounce the ownership so that it cannot be recovered. --- ### BEBEMiningManager In the contract `BEBEMiningManager` the role `_owner` has authority over the following functions: - `setMiningEnabled()` - `setStartMiningDate()` Any compromise to the `_owner` account may allow the hacker to take advantage of this authority and do the following: - Set the `_startMiningDate` to any date provided `mine()` has not successfully been called yet. An attacker can use this to cause a denial of service and require the contract to be redeployed or can make it so that mining is started sooner than expected so that they receive greater rewards. - Enable or disable mining. An attacker can use this to cause a denial of service and require the contract to be redeployed or can make it so that mining is started sooner than expected so that they receive greater rewards. In addition they can transfer ownership to an account they have the sole control over or renounce the ownership so that it cannot be recovered. --- ### BEBEStakeManager In the contract `BEBEStakeManager` the role `_owner` has authority over the following functions: - `setMiningManager()` - `setStakeEnabled()` - `importStake()` Any compromise to the `_owner` account may allow the hacker to take advantage of this authority and do the following: - Call `setStakeEnabled()` to enable staking when it is not intended to be enabled or to disable it to cause a denial of service. - Call `importStake()` to stake themselves for a large amount in order to capture all rewards for themselves or call it on accounts that already have stakes with a smaller staked amount in order to make it so they receive less income than they should be entitled to. In addition, `importStake()` may be called with unexpected inputs causing a misallocation of income. More details on this can be found in the finding **`importStake()` Can Cause Misallocation Of income** - Call `setMiningManager()` to set the mining manager to a malicious contract to take advantage of the privileges outlined below. In addition they can transfer ownership to an account they have the sole control over or renounce the ownership so that it cannot be recovered. --- In the contract `BEBEStakeManager` the role `_miningManager` has authority over the following functions: - `updateIncomePool()` Any compromise to the `_miningManager` account may allow the hacker to take advantage of this authority and either perform a denial of service by not calling the update income pool function or to update the income pool without transferring funds so that some users will be unable to claim their allocated tokens. In the worst case scenario an attacker can call this function with a large amount so that they can claim all tokens held in the contract for themselves.
If the token `_bebe` utilizes hooks or makes an external call during transfer that an attacker can use for reentrancy, then an attacker can utilize it to continually call `releaseStake()` to steal all `_bebe` tokens held by the contract. It should be noted that in other contracts calls to `_bebe` may also be used for reentrancy if they make an external call that can be used by an attacker.
The function `computeTotalPower()` iterates through all stakes to determine the amount that was staked prior to the input `incomeDay`. However, if there have been no stakes for the day during a mining, then `mine()` will call `computeTotalPower()`. It is possible for the number of stakers to be so large that this will cause the call to `mine()` to exceed the block gas limit. There is no method to unstake, so that this will prevent any further tokens from being mined and distributed as income. Note, that the removal of the minimum stake increases the likelihood of an attacker utilizing this to cause a denial of service. In particular, an attacker could stake 1 token with different accounts in order to increase the amount of stakes so that it is high enough that calls to `mine()` for a day where there are no stakes would exceed the block gas limit. As only 1 token needs to be staked, this decreases the cost of the attack significantly. However, this requires thousands of stakes, so the cost to the attacker will be high making this attack less likely. In addition, considering the large amount of stakes required it is unlikely to be reached naturally so we give this a *Medium* severity.
The comment above the function `getBonusMultiple()` states that the expected output should range between 0 and 12. However, to be a valid duration, the duration must be less than or equal to 360. Thus the output of this function is less than or equal to `360/30/10 = 1`, so that it will only return 0 or 1. This can result in users not receiving the bonus that they should be entitled to. However, as the use of the `bonusMultiple` is not clear within the scope of this audit we cannot be sure of this and we give this a *Medium* severity.
The function `isValidStakeDuration()` is used to determine if a users input stake duration should be allowed. As the comment above the function states, the intended supported durations should be "1 for 1 day, and multiples of 30 for monthly duration until 12 months". However, the logic will also return true for a duration of zero. This is because the function uses the logic: ```solidity return (duration == 1 || (duration <= 360 && duration % 30 == 0)); ``` And `0 <= 360` and `0 % 30 == 0`. Considering that both a duration of 1 and a duration of 0 would give the same bonus multiple, it is possible that this may allow a user who staked and released their stake in the same transaction to receive the same amount of rewards as a user who staked their tokens for 1 day. However, as the use of the `bonusMultiple` is not clear within the scope of this audit we cannot be sure of this and we give this a *Medium* severity.
The function `mine()` transfers tokens to the `BEBEStakeManager` manager when it is called. This amount is then stored in the `_incomePools[incomeDay].poolAmount` to distribute proportionally to users who have staked in the contract. However, if there are no stakes in the contract for that day, then this amount will not be allocated to any users and will be locked in the contract.
The function `claimIncome()` iterates through a for loop of all mining dates from the stored `nextClaimDate` of the stake to the `_lastPoolProcessDate`. However, `mine()` will be able to be called over 200,000 times before it distributes its 9.5 trillion tokens. In the extreme scenario a user may never claim their tokens until after all tokens are mined, in which case the for-loop will iterate over 200,000 times which would cause the gas cost of the transaction to exceed the block gas limit and revert. In general, if a user goes for too long without calling `claimIncome()`, the for-loop may require iterating enough times that it will exceed the block gas-limit. In these scenarios the user will be unable to claim their income.
The `_startMiningDate` in the `BEBEMiningManager` contract is designed to be indicate when the first call to `mine()` can be made. It is set to any value in the `constructor()` and can be set to any value via `setStartMiningDate()` as long as `mine()` has not yet been successfully called. The `_startMiningDate` in the `BEBEStakeManager` contract is used to determine the first date that `mine()` should be called so that it does not check that the previous day has had its income set. It is set to any value in the `constructor()` and can be set to any value via `setStartMiningDate()`. These values are intended to match. For example if the `_startMiningDate` in the `BEBEStakeManager` is a timestamp of a day prior to the `BEBEStakeManager` of the `BEBEMiningManager`, then calls to `mine()` will revert as the check that the previous days are populated will not pass.
The reward mechanism involving `totalPower` and storing the income for each `incomeDate` is complex and creates many fringe cases that must be handled carefully and can be hard to implement. Many findings in this report are related to this reward mechanism, so it may be beneficial to explore alternative mechanisms that are easier to implement and maintain.
From Solidity `v0.8.4`, there are more gas-efficient ways to explain to users why an operation failed than through strings. Using custom errors can significantly reduce the size of the deployed bytecode and reduce the gas cost when calls revert.
Can you clarify if the BEBE token takes a fee on transfer. In particular, some functions use the input amount during the transfer for bookkeeping. However the function `confirmAddIncomePool()` checks that the input amount is transferred.
The function `mine()` is intended to be able to be called until all 9.5 trillion tokens are mined. Each iteration of `mine()` is intended to distribute the remaining amount of tokens to be mined divided by 5700, with a certain percentage given to the `_stakingManager` and the rest to the `_gameIncomePoolAddress`. However, at some point there will be a remaining amount of tokens that is less than `5700` so the calculated amount to mine would be zero, this is handled in the code as follows: ```solidity dailyMintAmount = _remainingTokens / DIVISOR; //handle the last mint if (dailyMintAmount == 0) { dailyMintAmount = _remainingTokens; } ``` Note that prior to this stage the `dailyMintAmount` that is calculated will be 1 token. As the `stakeShare` is calculated as a percentage of this, the `stakeShare = 0` and `gameIncomePoolShare = 1`. As the `stakeShare` is zero, it will not execute the following code: ```solidity if (stakeShare > 0) { //we notify the staking manager that we will add to the incomepool _stakingManager.addIncomePool(stakeShare, _nextMiningDate); _bebe.operatorMint(address(_stakingManager), stakeShare); //we confirm the adding to the pool and this only takes effect if the amount to be added has already been transferred _stakingManager.confirmAddIncomePool(_nextMiningDate); } ``` In particular, it will not call `addIncomePool()` and `confirmAddIncomePool()` so that the `_incomePools` mapping in the `BEBEStakeManager` contract will not be set to a nonzero-value for that mining day. Then when the final `mine()` is called it will calculate a nonzero `stakeShare` so that it will call `addIncomePool()`, which will revert as the previous day was not populated ```solidity if (incomeDay > getDate(_startMiningDate)) { require( _incomePools[incomeDay - SECONDS_PER_DAY].poolAmount > 0, "PREVIOUS_DAY_NOT_YET_POPULATED" ); } ``` This happens far into the future and leaves only 5699 tokens that are unable to be mined, so we give this a *minor* severity.
The comment above the contract `BEBEMiningManager` states the following: ```solidity * 80% of 40% to Staking contract * 20% of 40% to Staking active income contract * 60% to Gaming contract ``` However, the logic of the contract sends the `_stakingContractShare` percentage to the `_stakingManager` contact and the remaining amount to the `_gameIncomePoolAddress`. So that it does not send funds to 3 separate contracts, only 2. In addition, the amount sent to the `_stakingManager` contract starts at 30 percent and then decreases by 2% each time `mine()` is called `SHARE_ADJ_MINE_COUNT_INTERVAL = 30` times until it reaches the `MIN_STAKING_CONTRACT_SHARE` whose current value is 6%. So that the comment does not reflect that the percentages change depending on the number of times `mine()` has been called.
MAXIMUM_STAKE is used for precision, which can cause confusion. The formula for income is ```solidity uint256 income = (((instance.amount * MAXIMUM_STAKE) / _incomePools[day].totalPower) * _incomePools[day].poolAmount) / MAXIMUM_STAKE; ``` Simplifying this equation we would get `instance.amount * _incomePools[day].poolAmount / _incomePools[day].totalPower` In particular, it gives a percentage of the income for that day (_incomePools[day].poolAmount). Where the percentage is given by the users staked amount (instance.amount) vs. the total amount that was staked for that day (_incomePools[day].totalPower). Thus MAXIMUM_STAKE is used in order to scale the percentage of the users stake vs. the total stake as it will be a decimal value.
Specific imports are recommended to be used to avoid unpredictable pollution of the namespace. For example: import {A, B, C} from "X";
The contract `BEBEStakeManager` has a method to `stake()` and `claimIncome()`, however, there is no method to withdraw a users stake. This means that an address is only allowed to stake once and that the staked amount cannot be recovered and will be locked in the contract. In particular, the function `claimIncome()` only allocates tokens that have been sent to the contract via the `addIncomePool()` and `confirmAddIncomePool()` methods, so that any tokens staked or sent directly to the contract will be locked in the contract.
In the function `mine()`, the `mineCount` will always be greater than zero. This is because the code is only reached if `block.timestamp >= _nextMiningDate`, so that the calculated `mineCount` will be greater than or equal to 1. In addition, `mineCount` calculation is unnecessary as the logic always mines 1 day.
`uint256` is by default initialized to `0` and `bool` is by default initialized to `false`, so that it is not necessary to explicitly initialize the lines cited above.
The variables in the lines sited above are not used with the contracts they are defined in.
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. A big 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 view functions `getAllIncomePools()` and `getIncomePoolDates()` may run out of gas if the length of `_poolDates` grows too large. The view function `getMiningRecords()` may run out of gas if the length of `_miningRecords` grows too large.
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