Sabai Ecoverse Audit

CertiKFiles named in the report

CertiK reviewed Sabai Protocol (Sabai Ecoverse Audit) on 21 Jul 2023: 19 findings, 0 critical; 17 marked fixed, 2 acknowledged.

No open critical findings
Open the reportOpen Sabai Protocol’s security record

Report hosted on skynet.certik.com.

Trustblock does not review code.

Findings19

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

SeverityOpenAcknowledgedFixed
Critical000
High022
Medium003
Low0012
HighExecution of `getRestOfDeposits()` can potentiallFixed

In the `getRestOfDeposits()` function, the owner can transfer out `_token.balanceOf(address(this)) - totalActualDepositsAmount` tokens after 30 days of the last deposit's `endTS` date. However, if some users do not call the `getReward()` in time after the `planExpired` date, the owner can run `getRestOfDeposits()` to withdraw all contract funds in excess of user deposits. ```solidity=167 require(_token.transfer(msg.sender, _token.balanceOf(address(this)) - totalActualDepositsAmount)); ``` Since the amount `totalActualDepositsAmount` does not incorporate rewards, the execution of `getRestOfDeposits()` can potentially remove all remaining user rewards. Therefore there might be group of users who will not be able to withdraw both their deposits and rewards.

HighPotential transfer tokens in other addresses withFixed

If `isAntisnipe` is set to true, antisnipe contract can transfer tokens in other addresses without approval, which is a extremely dangerous design.

MediumInconsistent usage of the `depositsCounter`Fixed

In the `createDeposit()` function, when creating a deposit, the contract saves the generated `deposit_id` into the `depositsCounter` `totalDepositsCount+1` position. In the case of the very first deposit, this would mean that the **0** index is never used. ```solidity depositsCounter[totalDepositsCount+1] = deposit_id; ``` When iterating `depositsCounter` to get the deposit info in the `getAmountToCloseactualDiposits()` and `getRestOfDeposits()` functions, the starting index is **0** and the last index of `depositsCounter` will never be reached. ```solidity for(uint8 i = 0 ; i<totalDepositsCount; i++) ```

MediumIncorrect amount deducted from `totalActualDeposiFixed

The current implementation deducts the amount `deposits[_depositId].claimed` from amount `totalActualDepositsAmount`. This could result in users not being able to retrieve their deposit and rewards. The variable `totalActualDepositsAmount` is updated by adding on the deposit amount each time a new deposit is created. ```solidity function createDeposit(uint256 count) public payable { // ... Omitted totalDepositsAmount += count; totalDepositsCount ++; totalActualDepositsAmount += count; ``` When deposits and rewards are withdrawn through function `getReward()`, the amount of `deposits[_depositId].claimed`, which is `deposits[_depositId].amount` + rewards, is deducted from the amount `totalActualDepositsAmount` which only keeps track of total deposit amounts. ```solidity function getReward(bytes32 _depositId) public onlyOwner { // ... Omitted // If the deposit is completed if (getCurrentTime() > deposits[_depositId].endTS) { deposits[_depositId].claimed = deposits[_depositId].amount + ((deposits[_depositId].amount / 100) * percentageOfEarnings); } else { // If the deposit is closed ahead of schedule (penalty) deposits[_depositId].claimed = deposits[_depositId].amount - ((deposits[_depositId].amount / 100) * penaltyPercentage); } require(_token.transfer(msg.sender, deposits[_depositId].claimed)); totalActualDepositsAmount -= deposits[_depositId].claimed; ``` The issue with this treatment is that when multiple users withdraw deposits and rewards, there is a chance that the last user(s) to claim rewards will be unable to retrieve the full amount since the value of `totalActualDepositsAmount` since reward withdrawals are not accounted for in the amount `totalActualDepositsAmount`.

MediumIncorrect modifier restricts deposit retrievalsFixed

There is a `onlyOwner` modifier in the public function `getReward()`, which restricts only the owner to call it. However, from its implementation, it checks if the msg.sender matches the `_depositId`, which means it should allow common users to call it.

LowDiscussion for `getAmountToCloseactualDiposits()`Fixed

The `getAmountToCloseactualDiposits()` function is used to calculate how many tokens are required to provide rewards for all active deposits. However, the calculation makes use of the `penaltyPercentage` instead, and it also does not consider completed cases, where `getCurrentTime() > deposits[_depositId].endTS`. The business logic of this function is inconsistent with the `getReward()` function. ```solidity=64 // If the deposit is completed if (getCurrentTime() > deposits[_depositId].endTS) { deposits[_depositId].claimed = deposits[_depositId].amount + ((deposits[_depositId].amount / 100) * percentageOfEarnings); } else { // If the deposit is closed ahead of schedule (penalty) deposits[_depositId].claimed = deposits[_depositId].amount - ((deposits[_depositId].amount / 100) * penaltyPercentage); } ``` The audit team would like to check if this implementation matches the intended design.

LowDiscussion on ensuring contract contains sufficieFixed

The audit team would like to understand how the contract maintains sufficient funds to transfer deposit rewards to beneficiaries and to ensure that transfers of rewards do not fail under normal conditions.

LowDiscussion on reward calculationFixed

The current implementation deals a penalty deducting the amount directly from the user's deposit amount. In most cases, the penalties usually impact the amount of rewards the user receives rather than reducing the user's deposit amount. The audit team would like to confirm whether this matches the intended design.

LowDivide Before MultiplyFixed

Performing integer division before multiplication truncates the low bits, losing the precision of calculation. ### SabaiStaking.sol ```solidity=66 deposits[_depositId].claimed = deposits[_depositId].amount + ((deposits[_depositId].amount / 100) * percentageOfEarnings); ``` --- ```solidity=68 deposits[_depositId].claimed = deposits[_depositId].amount - ((deposits[_depositId].amount / 100) * penaltyPercentage); ``` --- ```solidity=147 totalAmount+= ((deposits[depositsCounter[i]].amount / 100) * penaltyPercentage); ``` --- ```solidity=397 uint256 vestedSlicePeriods = timeFromStart.div(secondsPerSlice); ``` ### Sabai Vesting.sol ```solidity=398 uint256 vestedSeconds = vestedSlicePeriods.mul(secondsPerSlice); ```

LowIncompatibility with Deflationary TokensFixed

When transferring deflationary ERC20 tokens, the input amount may not be equal to the received amount due to the charged transaction fee. For example, if a user sends 100 deflationary tokens (with a 10% transaction fee), only 90 tokens actually arrived to the contract. However, a failure to discount such fees may allow the same user to withdraw 100 tokens from the contract, which causes the contract to lose 10 tokens in such a transaction. Reference: https://thoreum-finance.medium.com/what-exploit-happened-today-for-gocerberus-and-garuda-also-for-lokum-ybear-piggy-caramelswap-3943ee23a39f ### SabaiStaking ```solidity=97 require(_token.transferFrom(_msgSender(), address(this), count)); ``` - Transferring tokens by `count`. ```solidity=103 totalActualDepositsAmount += count; ``` - The `count` appears to be used for bookkeeping purposes without compensating the potential transfer fees. ### SabaiVesting ```solidity=302 _token.safeTransfer(beneficiaryPayable, amount); ``` - Transferring tokens by `amount`. ```solidity=301 vestingSchedulesTotalAmount = vestingSchedulesTotalAmount.sub(amount); ``` - This function call executes the following operation. - In `SafeMath.sub`, - `return a - b;` - The `amount` appears to be used for bookkeeping purposes without compensating the potential transfer fees.

Show 7 more
LowIncorrect check in `createVestingScheduleMany()`Fixed

In the `createVestingScheduleMany()` function, the contract checks if there are sufficient tokens to create new vesting schedules. However, the current logic only compares the withdrawable amount to a single schedule amount, which can potentially lead to the function failing because there may be sufficient funds to create a certain vesting schedule, but not all the vesting schedules contained in array `_schedules`. ```solidity=216 for (uint256 i=0; i<_schedules.length; i++) { require( this.getWithdrawableAmount() >= _schedules[i].amount, "TokenVesting: cannot create vesting schedule because not sufficient tokens" ); require(_schedules[i].duration > 0, "TokenVesting: duration must be > 0"); require(_schedules[i].amount > 0, "TokenVesting: amount must be > 0"); require(_schedules[i].slicePeriodSeconds >= 1, "TokenVesting: slicePeriodSeconds must be >= 1"); } ```

LowIncorrect logic in function `_setIsAntisnipe()`Fixed

From the function name and its parameters, the function `_setIsAntisnipe()` seems to be able to set the `isAntisnipe` flag to `true` or `false`. However, this function actually always sets `isAntisnipe` to `false` without making use of the inputted `value`.

LowLogic for `vestedAmount` calculationFixed

In function `_computeReleasableAmount()` the calculation for `vestedAmount` is as follows: ```solidity=396 uint secondsPerSlice = vestingSchedule.slicePeriodSeconds; uint256 vestedSlicePeriods = timeFromStart.div(secondsPerSlice); uint256 vestedSeconds = vestedSlicePeriods.mul(secondsPerSlice); uint256 vestedAmount = vestingSchedule.amountTotal.mul(vestedSeconds).div(vestingSchedule.duration); ``` The calculation for `vestedAmount` seems to be similar to the original amount for `secondsPerSlice` except that there will be division truncation as a result of the calculation using integers (in the calculation of `vestedSlicePeriods`). There is a risk that if the total `vestingSchedule.duration` is not made of complete `vestedSlicePeriods`, there will be leftover releasable amounts that cannot be claimed. It might be beneficial to include checks in the creation of the vesting schedule to ensure that this issue does not occur. The audit team would like to confirm if this aligns with the intended design.

LowPotential locked native tokensFixed

The `SabaiVesting.fallback()`/`SabaiVesting.receive()`/`SabaiStaking.createDeposit()` functions allow the contract to accept the native tokens, such as ETH, transferred into the contract. However, the corresponding contract seems to have no functions on withdraw native tokens, which can potentially cause the native tokens to be stuck in the contract forever. On a side note, in the case of the `SabaiStaking.createDeposit()` function, the function is marked as `payable` which means that the function can accept native tokens, however there is no logic to handle these native tokens since the function only handles transfers of the specified token contract for valid deposits.

LowUnused EventFixed

Some events are never emitted, which can lead to confusion and code maintainability issues. ```solidity=47 event Released(uint256 amount); ``` - `Released` is declared in `SabaiVesting` but never emitted. --- ```solidity=48 event Revoked(); ``` - `Revoked` is declared in `SabaiVesting` but never emitted.

LowUnused State VariableFixed

Some state variables are not used in the codebase. This can lead to incomplete functionality or potential vulnerabilities if these variables are expected to be utilized. Variable `users` in `SabaiStaking` is never used in `SabaiStaking`. ```solidity=35 mapping(uint256 => address) users; ``` ```solidity=8 contract SabaiStaking is Ownable { ```

LowVariables That Could Be Declared as ImmutableFixed

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.

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

  • Sabai Token/Token/Ownable.sol
  • Sabai Token/Token/AERC20Burnable.sol
  • Sabai Token/Staking/SabaiStaking.sol
  • Sabai Token/Token/IERC20.sol
  • Sabai Token/Token/AERC20.sol
  • Sabai Token/Vesting/contracts/Sabai Vesting.sol
  • Sabai Token/Token/IERC20Metadata.sol
  • Sabai Token/Token/Sabai Token.sol
  • Sabai Token/Token/IAntisnipe.sol
  • Sabai Token/Token/Context.sol

Part of Sabai Protocol’s security record

Sabai Protocol
Audits1 audit by 1 firmLast audit Jul 2023, by CertiK.
Critical findingsNo open critical findingsIn 1 report.
Open Sabai Protocol’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