As of 3 Nov 2026, Sakai Vault has 4 audit reports by 2 security firms on Trustblock; the latest is by CertiK, dated 15 Jul 2024. Those reports list no open critical findings. It also holds 8 deployed contracts on 1 chain.
Record last updated . It holds audits, findings and contracts up to Nov 2026, and incidents up to Oct 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| SakaiDAO - Audit | Security firm: CertiK | Date: | Scope: 1 address on BNB Chain | 2 critical findings, fixed | Open · skynet.certik.com |
| Sakaivault - Audit | Security firm: CertiK | Date: | Scope: 4 addresses on BNB Chain | No open critical findings | Open · skynet.certik.com |
| SakaiDAO | Security firm: SolidProof | Date: | Scope: 1 address on BNB Chain | No open critical findings | Open · IPFS |
| Sakai Vault | Security firm: SolidProof | Date: | Scope: 2 addresses on BNB Chain | No open critical findings | Open · IPFS |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 2 |
| High | 0 | 6 | 0 |
| Medium | 1 | 3 | 6 |
| Low | 17 | 0 | 6 |
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.
Open the reportAccording 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.
Open the reportAccording 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]`.
Open the reportThe `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.
Open the reportAccording 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`.
Open the reportIn 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.
Open the reportAccording 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); } ```
Open the reportThe `withdraw()` function can be called by anyone to retrieve the tokens they deposit into the contract, but there is no check if the variable `_stakers[msg.sender].balance` is greater than 0. If someone whose balance is equal to 0 calls this method multiple times, then `totalAddressStaked` will keep decreasing and other users will not be able to retrieve tokens, and the tokens will be locked into the contract.
Open the reportIn 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.
Open the reportThe 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.
Open the reportThe 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); ```
Open the reportFlash loans are a way to borrow large amounts of money for a certain fee. The requirement is that the loans need to be returned within the same transaction in a block. If not, the transaction will be reverted. An attacker can use the borrowed money as the initial funds for an exploit to enlarge the profit or manipulate the token price in decentralized exchanges. The `getAmountsOut()` function relies on price calculations that are based on-chain, meaning they are susceptible to flash-loan attacks by manipulating the price of given pairs to the attacker's benefit.
Open the reportA reentrancy attack can occur when the contract creates a function that makes an external call to another untrusted contract before resolving any effects. If the attacker can control the untrusted contract, they can make a recursive call back to the original function, repeating interactions that would have otherwise not run after the external call resolved the effects. ### External call(s) ```solidity=1658 ERC20token.safeTransfer(msg.sender, balance); ``` ### State variables written after the call(s) ```solidity=1663 delete _stakers[msg.sender]; ```
Open the reportThe variable `minimumStakeEqualtoUSDT`, `interestRate`, and `interestRateDenominator` 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. An advantage of immutable variables is that reading them is significantly cheaper than reading from regular state variables since they are not in storage. ```solidity=1358 uint256 public minimumStakeEqualtoUSDT; uint256 public interestRate; uint256 public interestRateDenominator; ```
Open the report| Address | Chain | Source code |
|---|---|---|
| 0x6729…16090x6729324119a210cf493fa7455e581df9245d1609 | Chain: BNB Chain | Source code: Verified source |
| 0xb8de…1abd0xb8de9c3ede83cd4bba7982273e6c1c0852201abd | Chain: BNB Chain | Source code: Verified source |
| 0xc20a…a42a0xc20a079c7962d9fc92173cda349e80d484dfa42a | Chain: BNB Chain | Source code: Verified source |
| 0x43b3…c4af0x43b35e89d15b91162dea1c51133c4c93bdd1c4af | Chain: BNB Chain | Source code: Verified source |
| 0xfa8b…68ce0xfa8b0197219b6cc8f3336039d50bc03220a368ce | Chain: BNB Chain | Source code: Verified source |
| 0x8f2e…cfd70x8f2e2bad6020d8bb5bf947199cae8eb86d24cfd7 | Chain: BNB Chain | Source code: Verified source |
| 0x65d4…33f40x65d4cb29e0edaefb18246b22513483c21a8033f4 | Chain: BNB Chain | Source code: Verified source |
| 0xebb3…da7d0xebb30b32e74ad446b00b6333c372c5a1ccabda7d | Chain: BNB Chain | Source code: Verified source |
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="sakai-vault"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
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