CertiK reviewed Nut DeFi (Nut DeFi - Audit) on 20 Jan 2025: 33 findings, 0 critical; 18 marked fixed, 3 acknowledged, 12 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 | 0 |
| High | 0 | 3 | 0 |
| Medium | 6 | 0 | 8 |
| Low | 6 | 0 | 10 |
In the contract, functions modified with `onlyWhiteList` can be validated as long as the address is in the fee whitelist or fund address: `nut_live.sol` ```solidity=402 modifier onlyWhiteList() { address msgSender = msg.sender; require(_feeWhiteList[msgSender] || msgSender == fundAddress || msgSender == _owner, "nw"); _; } ``` Therefore, the following functions can be executed as long as the address is in the fee whitelist: - setFundAddress() - setFeeWhiteList() - setSwapPairList() - setLPHoldCondition() - setExcludeLPProvider() - setRewardGas() - startTrade() - setLPRewardTimeDebt() - setRemoveLPFee() - setBuyFee() - setSellFee() the following functions can be executed as long as the address is fund address: - setAutoLPBurnSettings() - setFundAddress() - setFeeWhiteList() - setSwapPairList() - setLPHoldCondition() - setExcludeLPProvider() - setRewardGas() - startTrade() - setLPRewardTimeDebt() - setRemoveLPFee() - setBuyFee() - setSellFee() The following functions can be executed if the address is both the fund address and the fund address is included in the fee whitelist: - updateLPAmount() - setExcludeReward() However, these are core permission functions, and the fee whitelist or fund address should not have such high-level permissions to execute them. Additionally, if the project team renounces ownership, the contract can still be controlled via previously added fee whitelist addresses or the fund address, which poses a certain centralization risk.
In the `NutToken` contract, the system mistakenly categorizes the action of adding liquidity (LP) as a **selling action**, which results in an improper application of transaction fees. Specifically: When a user adds liquidity, the contract charges a **2% fee** on the transferred `NUT` tokens: - **1% is sent to the dead address**. - **1% is sent to the NUT contract address**. According to the contract's intended design, **adding liquidity should not incur any fees**. The action of adding liquidity (transferring tokens to the liquidity pool) is not a "sell" transaction but rather a contribution to the pool, which benefits the ecosystem. This discourages users from participating in liquidity provision, potentially harming the liquidity of the `NUT` token on decentralized exchanges.
When the amount of NUT tokens accumulated in the NUT token contract reaches a certain threshold, a swap will be triggered during the transfer of NUT tokens. However, this swap will update some variables, leading to unintended updates in the state of the pair or NUT token: 1. If USDT is transferred first, then NUT is transferred (which triggers a swap), and then LP tokens for the mainPair are minted (e.g., using `swapRouter`'s `addLiquidity()` to add liquidity), the initially transferred USDT will be synced to the pair's reserve during the swap, resulting in a minting of 0 LP tokens. Due to the pair's LP minting check, the transaction will fail. 2. If `swapRouter`'s `addLiquidity()` is used and a swap is triggered during the transfer of NUT tokens, the `transfer` of NUT will set the previously transferred `_lastMaybeLPAddress` to 0, and since `mint()` for the pair hasn't been called yet, the LP token amount will be 0. **The user adding liquidity will not be added as an LP provider, causing a legitimate liquidity provider to not receive rewards.**
When a user successfully adds liquidity for the first time, the reward timestamp `_lastLPRewardTimes` will change from 0 to the current time of adding liquidity, and the user will be added to the LP provider list: `nut_live.sol` ```solidity=540 if (lastMaybeAddLPAddress != address(0)) { _lastMaybeAddLPAddress = address(0); uint256 lpBalance = IERC20(mainPair).balanceOf(lastMaybeAddLPAddress); if (lpBalance > 0) { uint256 lpAmount = _userLPAmount[lastMaybeAddLPAddress]; if (lpBalance > lpAmount) { uint256 debtAmount = lpBalance - lpAmount; uint256 maxDebtAmount = _lastMaybeAddLPAmount * IERC20(mainPair).totalSupply().div(_balances[mainPair]); if (debtAmount > maxDebtAmount) { excludeLpProvider[lastMaybeAddLPAddress] = true; } else { _addLpProvider(lastMaybeAddLPAddress); _userLPAmount[lastMaybeAddLPAddress] = lpBalance; uint256 blockTime = block.timestamp; if (0 == _lastLPRewardTimes[lastMaybeAddLPAddress]) { _lastLPRewardTimes[lastMaybeAddLPAddress] = blockTime; } } } } } ``` However, when a user subsequently removes all their liquidity, there is no mechanism to remove the user from the LP provider list, and the reward calculation time does not stop : `nut_live.sol` ```solidity=623 if (isRemoveLP) { if (!_feeWhiteList[to]) { takeFee = true; uint256 liquidity = (amount * ISwapPair(_mainPair).totalSupply() + 1).div(balanceOf(_mainPair) - 1); if (from != address(_swapRouter)) { liquidity = (amount * ISwapPair(_mainPair).totalSupply() + 1).div(balanceOf(_mainPair) - amount - 1); } require(_userLPAmount[to] >= liquidity, ">uLP"); _userLPAmount[to] -= liquidity; } } ``` This will result in the user's reward time continuing to accrue even when they do not hold any LP tokens. As long as they hold a certain amount of LP tokens before the reward cycle ends, they can claim the rewards for that period (5 days). However, since the user did not hold tokens during that time, the reward time should not be calculated.
After transferring tokens to the pair, the following logic can be used to mark the `from` address as `_lastMaybeAddLPAddress` and the amount as `_lastMaybeAddLPAmount`: `nut_live.sol` ```solidity=639 if (from != address(this)) { if (to == mainPair) { _lastMaybeAddLPAddress = from; _lastMaybeAddLPAmount = amount; } ``` However, by transferring a certain amount of LP tokens to the `from` address before the next transfer, this sell token transaction can be misidentified as an add liquidity transaction, and the `from` address will be added to the `lpProviders` array to be eligible for rewards, even though the user did not actually add liquidity: `nut_live.sol` ```solidity=540 if (lastMaybeAddLPAddress != address(0)) { _lastMaybeAddLPAddress = address(0); uint256 lpBalance = IERC20(mainPair).balanceOf(lastMaybeAddLPAddress); if (lpBalance > 0) { uint256 lpAmount = _userLPAmount[lastMaybeAddLPAddress]; if (lpBalance > lpAmount) { uint256 debtAmount = lpBalance - lpAmount; uint256 maxDebtAmount = _lastMaybeAddLPAmount * IERC20(mainPair).totalSupply().div(_balances[mainPair]); if (debtAmount > maxDebtAmount) { excludeLpProvider[lastMaybeAddLPAddress] = true; } else { _addLpProvider(lastMaybeAddLPAddress); _userLPAmount[lastMaybeAddLPAddress] = lpBalance; uint256 blockTime = block.timestamp; if (0 == _lastLPRewardTimes[lastMaybeAddLPAddress]) { _lastLPRewardTimes[lastMaybeAddLPAddress] = blockTime; } } } } ``` This will introduce a security risk to the contract.
First, the time for calculating staking reward after the user adds liquidity is not the time of addition, but rather the next transfer: `nut_live.sol` ```solidity=540 if (lastMaybeAddLPAddress != address(0)) { _lastMaybeAddLPAddress = address(0); uint256 lpBalance = IERC20(mainPair).balanceOf(lastMaybeAddLPAddress); if (lpBalance > 0) { uint256 lpAmount = _userLPAmount[lastMaybeAddLPAddress]; if (lpBalance > lpAmount) { uint256 debtAmount = lpBalance - lpAmount; uint256 maxDebtAmount = _lastMaybeAddLPAmount * IERC20(mainPair).totalSupply().div(_balances[mainPair]); if (debtAmount > maxDebtAmount) { excludeLpProvider[lastMaybeAddLPAddress] = true; } else { _addLpProvider(lastMaybeAddLPAddress); _userLPAmount[lastMaybeAddLPAddress] = lpBalance; uint256 blockTime = block.timestamp; if (0 == _lastLPRewardTimes[lastMaybeAddLPAddress]) { _lastLPRewardTimes[lastMaybeAddLPAddress] = blockTime; } } } } } ``` At the time when the user is supposed to receive rewards, not all the rewards accumulated during this period are distributed to the user and inviter; only the rewards for a 5-day period are distributed: `nut_live.sol` ```solidity=870 if (userUSDTContribution >= lpHoldCondition && block.timestamp > _lastLPRewardTimes[shareHolder] + lpRewardTimeDebt) { // Calculate 0.8% daily reward in USDT, then convert to NUT using pool price uint256 dailyUsdtReward = (userUSDTContribution.mul(DAILY_REWARD_RATE)).div(1000); uint256 rewardAmountInNUT = (dailyUsdtReward.mul(rNut)).div(rUsdt); rewardAmountInNUT *= 2; // Calculate total reward for the 5-day interval rewardAmount = rewardAmountInNUT.mul(5); if (rewardAmount > 0) { _lastLPRewardTimes[shareHolder] = block.timestamp; processInviterRewards(shareHolder, rewardAmount); _rewardTransfer(shareHolder, rewardAmount); } } ``` As a result, the rewards for the excess time will not be included in the calculation. Additionally, an attacker can increase the length of the LP provider array by mint small amounts of LP with multiple addresses in the main pair. This would delay the user's turn to receive the staking rewards, resulting in further staking reward losses.
The call to `_transfer()` of `AbsToken` is overcomplicated, involving dozens of logic, and some actions are performed on different levels.
During a flash loan in the pair, when repaying NUT tokens, the `autoBurnLiquidityPairTokens` function may automatically burn tokens in the pair. In this case, since both the `swap` and `sync` functions of the pair have reentrancy locks, the transaction will revert when executing `mainPair.sync()`, causing the transaction to fail.
There should always be events emitted in the sensitive functions that are controlled by centralization roles.
If the value of `gasleft()` outside the `while` loop is less than the passed parameter `gas`, the `while` loop may throw an `out-of-gas` exception. ``` solidity=126 while (gasUsed < gas && iterations < shareholderCount) { if (currentIndex >= shareholderCount) { currentIndex = 0; } if (shouldDistribute(shareholders[currentIndex])) { distributeDividend(shareholders[currentIndex]); } gasUsed = gasUsed.add(gasLeft.sub(gasleft())); gasLeft = gasleft(); currentIndex++; iterations++; } ```
A sandwich attack can occur when an attacker notices a transaction involving token swaps or liquidity additions that lacks slippage restrictions or minimum output amount settings. The attacker can exploit this by frontrunning the transaction to purchase one of the assets, thereby manipulating the exchange rate, and then backrunning the transaction to sell the asset for a profit. The referenced functions are invoked without setting slippage restrictions or minimum output amounts, making transactions that trigger these functions particularly vulnerable to sandwich attacks, especially when large input amounts are involved.
The contract is serving as the underlying entity to interact with one or more third party protocols. The scope of the audit treats third party entities as black boxes and assume their functional correctness. However, in the real world, third parties can be compromised and this may lead to lost or stolen assets. In addition, upgrades of third parties can possibly create severe impacts, such as increasing fees of third parties, migrating to new LP pools, etc. ```solidity=391 ISwapRouter public immutable _swapRouter; ``` - The contract `AbsToken` interacts with third party contract with `ISwapRouter` interface via `_swapRouter`. --- ```solidity=392 address public immutable _mainPair; ``` - The contract `AbsToken` interacts with third party contract with `ISwapPair` interface via `_mainPair`.
| Address | Chain | Source code |
|---|---|---|
| 0x29fd…978d0x29fdf61b4ab5a4af38c745d4e1ef282525d4978d | 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