CertiK reviewed Sakai Vault (SakaiDAO - Audit) on 15 Jul 2024: 21 findings, 2 critical; 10 marked fixed, 5 acknowledged, 6 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 | 4 | 0 |
| Medium | 0 | 1 | 5 |
| Low | 6 | 0 | 3 |
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.
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.
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]`.
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.
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`.
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.
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); } ```
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.
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.
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); ```
| Address | Chain | Source code |
|---|---|---|
| 0xfa8b…68ce0xfa8b0197219b6cc8f3336039d50bc03220a368ce | Chain: BNB Chain | Source code: Verified source |
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