SakaiDAO - Audit

CertiK1 address on BNB Chain

CertiK reviewed Sakai Vault (SakaiDAO - Audit) on 15 Jul 2024: 21 findings, 2 critical; 10 marked fixed, 5 acknowledged, 6 open.

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

Report hosted on skynet.certik.com.

Trustblock does not review code.

Findings21

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

SeverityOpenAcknowledgedFixed
Critical002
High040
Medium015
Low603
CriticalDuplicate CalculationFixed

According to the logic of the function `_calculateReward()`, new reward will be accumulated to the `pendingCalculateReward`. Once new epoch is triggered, the pending reward will be distributed to each share. ```solidity function _calculateReward(uint256 rewardAmount) internal { /** this function for calculate reward and count dividendsPerShare */ pendingCalculateReward += rewardAmount; if(totalShares > 0 && (lastResetEpochAt + resetEpochEvery <= block.timestamp)){ uint256 amount = pendingCalculateReward; totalDividends += amount; dividendsPerShare += (dividendsPerShareAccuracyFactor * amount / totalShares); currentEpoch++; lastResetEpochAt = block.timestamp; pendingCalculateReward = 0; } else { pendingCalculateReward += rewardAmount; } } ``` However, the pending reward is added twice if new epoch is not triggered.

CriticalInsufficient Rewards due to Tax DeductionFixed

According to the logic of the function `_claimReward()`, a reward won't be sent to the claimer if there isn't enough rewards in the contract. However, a certain amount of tax had been deducted and distributed to the staking pool and vault proportionally. ```solidity function _claimReward(address account) internal { /** this function for claim reward */ require(account != address(0), "SakaiDAO: account is the zero address"); require(shares[account].amount > 0, "SakaiDAO: account is not stake"); uint256 originalAmount = dividendOf(account); uint256 amount = _deductTaxClaimReward(originalAmount); if(IERC20(tokenAddress).balanceOf(address(this)) >= amount){ IERC20(tokenAddress).transfer(account, amount); _setClaimed(account, originalAmount); } } ``` If the actual reward transferred to the contract is less than the accumulated reward obtained by the referrer, then when the referrer claims the reward, the token amount in the contract will become very low. The above-mentioned scenario will occur, resulting in the incorrect transfer of tokens from the contract to the staking pool and vault, causing losses for the users. For example, when claiming the reward, if the condition `IERC20(tokenAddress).balanceOf(address(this)) >= amount` is not met, the user will not get the reward, but a portion of the reward has been distributed to the staking pool and vault. Therefore reducing the rewards for the user. In an extreme case, users may repeatedly claim the reward but not receive any rewards. Instead, the reward tokens will all be distributed to the staking pool and vault.

MediumIncorrect Key OrderFixed

According to the code logic of the function `_stake()`, referrer rewards will be calculated to different layer referrer proportionally. State variable `referrerTotalAmountByAccount` is used to record how many rewards are earned by referrer through inviting staker. ```solidity if(referrerLayer3 != address(0)){ uint256 amountLayer3 = amount * percentDistributionForReferrerLayer3 / percentDenominator; rewardsFromReferrer[referrerLayer3] += amountLayer3; referrerPendingAmount[referrerLayer3] += amountLayer3; referrerTotalAmountByAccount[referrerLayer3][account] += amountLayer3; totalAccumulateReferrerAmount += amountLayer3; totalPendingReferrerAmount += amountLayer3; } ``` However, the mapping key order is incorrect when calculating rewards for layer3 referrer. The right mapping key should be the referred person and the referrer which is `[account][referrerLayer3]`.

MediumIncorrect Ratio of Referrer RewardFixed

The `require()` check in the function `updatePercentDistributionForReferrer()` requires the total proportion of `_layer1`, `_layer2` and `_layer3` should equal to `percentDenominator`. According to the calculation logic for referral rewards in the function `_stake()`, this means the referrer rewards equal to the users' staked amount which seems to be very high. ```solidity function updatePercentDistributionForReferrer( uint256 _layer1, uint256 _layer2, uint256 _layer3 ) external onlyOwner { require( _layer1 + _layer2 + _layer3 == percentDenominator, "SakaiDAO: Maximum Referrer should 100%" ); percentDistributionForReferrerLayer1 = _layer1; percentDistributionForReferrerLayer2 = _layer2; percentDistributionForReferrerLayer3 = _layer3; emit UpdatePercentDistributionReferrer(_layer1, _layer2, _layer3); } ``` Besides, the initial sum of these three percentage is 6% which is inconsistent with the `require` check.

MediumIncorrect Reward AddedFixed

According to the code logic of the function `_stake()`, referrer rewards will be calculated to different layer referrer proportionally. ```solidity if(referrerLayer2 != address(0)){ uint256 amountLayer2 = amount * percentDistributionForReferrerLayer2 / percentDenominator; rewardsFromReferrer[referrerLayer2] += amountLayer1; referrerPendingAmount[referrerLayer2] += amountLayer2; referrerTotalAmountByAccount[account][referrerLayer2] += amountLayer2; totalAccumulateReferrerAmount += amountLayer2; totalPendingReferrerAmount += amountLayer2; } ``` However, the correct new added reward should be `amountLayer2` but not `amountLayer1` when calculating reward for `referrerLayer2`.

MediumPotential Risk Of Losing RewardsFixed

In the **sakaiDAO** contract, when a user stakes or unstakes, the contract updates the user's shares through the `_setShares()` function and calculates the user's accumulated dividends with the latest shares. This part is considered as already claimed rewards. ```solidity function _setShare(address account, uint256 amount) internal { ... totalShares = totalShares - shares[account].amount + amount; shares[account].amount = amount; shares[account].totalExcluded = _getCumulativeDividend(shares[account].amount); ... } function _getCumulativeDividend(uint256 share) internal view returns (uint256) { /** this function for calculate cumulative dividend */ return share * dividendsPerShare / dividendsPerShareAccuracyFactor; } ``` However, before the user performs the operation, `dividendsPerShare` might change. If the user doesn't call the `claimReward()` function to receive rewards before staking or unstaking, the user will lose these part of rewards.

MediumSending Incorrect Amount When Claiming ReffererFixed

According to the design, we believe that `percentTaxClaimReferrer` represents the fee charged when the referrer claims rewards. This fee will be distributed among all stakers, and the remaining rewards, after deducting the fee, will be transferred to the referrer. However, the contract transfers the fee to the referrer, and the remaining rewards are distributed to all stakers. ```solidity function _claimRewardReferrer(address account) internal { ... uint256 amountAfterTax = amount * percentTaxClaimReferrer / percentDenominator; uint256 amountForReferrer = amount - amountAfterTax; IERC20(tokenAddress).transfer(account, amountAfterTax); _calculateReward(amountForReferrer); emit ReceiveRewardReferrer(account, amountAfterTax, currentEpoch); } ```

LowLack of Sanity ChecksFixed

In the contract **SakaiDAO**, the `_percentTaxClaimReferrer` lacks of sanity checks. It is better to add a reasonable range check for `_percentTaxClaimReferrer` to avoid potential risks caused by incorrect input.

LowPrecision Loss When Distributing Tax RewardFixed

The function `_distributeTaxClaimReward()` is used to distribute tax to staking pool and vault proportionally. However, there may be a precision loss issue if calculate related values by multiplying each ratio. ```solidity function _distributeTaxClaimReward(uint256 taxAmount) internal { /** * 1. Distribute tax to staking pool * 2. Distribute tax to vault capital */ uint256 totalTax = percentTaxForStakingPool + percentTaxForVaultCapital; uint256 amountForStakingPool = taxAmount * percentTaxForStakingPool / totalTax; IERC20(tokenAddress).transfer(addressStakingPool, amountForStakingPool); uint256 amountForVaultCapital = taxAmount * percentTaxForVaultCapital / totalTax; IERC20(tokenAddress).transfer(addressVaultCapital, amountForVaultCapital); } ``` It means there is a small amount of tokens will be locked in the current contract.

LowUnchecked ERC-20 `transfer()`/`transferFrom()` CaFixed

The return values of the `transfer()` and `transferFrom()` calls in the smart contract are not checked. Some ERC-20 tokens' transfer functions return no values, while others return a bool value, they should be handled with care. If a function returns `false` instead of reverting upon failure, an unchecked failed transfer could be mistakenly considered successful in the contract. ```solidity=550 IERC20(tokenAddress).transfer(account, amount); ``` --- ```solidity=564 IERC20(tokenAddress).transfer(account, amountAfterTax); ``` --- ```solidity=603 IERC20(tokenAddress).transfer(addressStakingPool, amountForStakingPool); ``` --- ```solidity=606 IERC20(tokenAddress).transfer(addressVaultCapital, amountForVaultCapital); ``` --- ```solidity=699 IERC20(tokenAddress).transferFrom(_msgSender(), address(this), amount); ``` --- ```solidity=759 IERC20(tokenAddress).transfer(_msgSender(), amountAfterTax); ``` --- ```solidity=795 IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this))); ``` --- ```solidity=860 IERC20(tokenAddress).transferFrom(_msgSender(), address(this), rewardAmount); ```

Contracts in scope1

AddressChainSource code
0xfa8b0197219b6cc8f3336039d50bc03220a368ceChain: BNB ChainSource code: Verified source

Part of Sakai Vault’s security record

Sakai Vault
Audits4 audits by 2 firmsLast audit Jul 2024, by CertiK.
Critical findingsNo open critical findingsAcross the 4 reports, 2 critical findings were reported. Each is marked fixed by the firm that reported it.
Open Sakai Vault’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