CertiK reviewed NetMind (NetMind XYZ) on 13 Jan 2025: 23 findings, 0 critical; 12 marked fixed, 11 acknowledged.
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 | 4 | 0 |
| Medium | 0 | 2 | 6 |
| Low | 0 | 5 | 6 |
When the remaining token amount in the pair drops below `gradThreshold`, the internal function `_addInitialLiquidity` is triggered to create a Uniswap V2 pair and add liquidity. However, due to the following condition: **contracts/AgentToken.sol** ```solidity=516 if (to_ == uniswapV2Pair && from_ != address(this) && fundedDate == 0) { revert InitialLiquidityNotYetAdded(); } ``` Since `from` is the `Bonding` contract rather than `address(this)` (token contract), the condition fails and the transaction reverts, preventing liquidity addition. **contracts/fun/Bonding.sol** ```solidity=305 function _addInitialLiquidity(IAgentToken token_, IERC20 _assetToken) internal returns(address){ address uniswapV2Pair_ = IUniswapV2Factory(uniswapRouter.factory()).createPair( address(token_), address(_assetToken) ); token_.addLiquidityPool(uniswapV2Pair_); token_.approve(address(uniswapRouter), type(uint256).max); _assetToken.approve(address(uniswapRouter), type(uint256).max); // Add the liquidity: (, , uint256 lpTokens) = uniswapRouter .addLiquidity( address(token_), address(_assetToken), token_.balanceOf(address(this)), _assetToken.balanceOf(address(this)), 0, 0, address(this), block.timestamp ); token_.setTokenSta(); IStakeVault stakeVault = IStakeVault(Clones.clone(stakeVaultImpl)); tokenStake[address(token_)] = address(stakeVault); IERC20(uniswapV2Pair_).approve(address(stakeVault), lpTokens); stakeVault.initialize(uniswapV2Pair_, lpTokens); return uniswapV2Pair_; } ```
The `addInitialLiquidity()` function, which is called by the `EXECUTOR_ROLE`, is responsible for adding liquidity to a pair. At line 93, tokenA is transferred to the pair, but tokenB is not. Then, at line 95, the function `mint` is invoked to initialize the amounts of both tokens. The omission of the tokenB transfer can lead to an imbalance in the liquidity provided, affecting the functionality of the pair initialization process. ```solidity=80 function addInitialLiquidity( address token_, uint256 amountToken_, uint256 amountAsset_ ) public onlyRole(EXECUTOR_ROLE) returns (uint256, uint256) { require(token_ != address(0), "Zero addresses are not allowed."); address pairAddress = factory.getPair(token_, assetToken); IFPair pair = IFPair(pairAddress); IERC20 token = IERC20(token_); token.safeTransferFrom(msg.sender, pairAddress, amountToken_); pair.mint(amountToken_, amountAsset_); return (amountToken_, amountAsset_); } ```
The `sellTax`, `buyTax` variables in the contract have no set limits, allowing fees to potentially reach 100%. If this occurs, users would receive no tokens from the contract, resulting in a complete loss of their investment.
The `approval()` function is invoked by the `Router` contract to approve token transfers from a pair to the user. However, the token utilizes the standard OpenZeppelin (OZ) library, which does not include a `forceApprove` function, causing compilation to fail. ```solidity=5 import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; ``` ```solidity=89 function approval( address _user, address _token, uint256 amount ) public onlyRouter returns (bool) { require(_user != address(0), "Zero addresses are not allowed."); require(_token != address(0), "Zero addresses are not allowed."); IERC20 token = IERC20(_token); token.forceApprove(_user, amount); return true; } ```
The `uniswapV2Pair` variable, representing the liquidity pool (LP) address for the token, is not initialized before being used in the AgentToken contract. This may lead to incorrect behavior when the contract interacts with Uniswap or other liquidity functions. ```solidity=137 function isLiquidityPool(address queryAddress_) public view returns (bool) { /** @dev We check the uniswapV2Pair address first as this is an immutable variable and therefore does not need * to be fetched from storage, saving gas if this address IS the uniswapV2Pool. We also add this address * to the enumerated set for ease of reference (for example it is returned in the getter), and it does * not add gas to any other calls, that still complete in 0(1) time. */ return (queryAddress_ == uniswapV2Pair || _liquidityPools.contains(queryAddress_)); } ``` ```solidity=516 if (to_ == uniswapV2Pair && from_ != address(this) && fundedDate == 0) { revert InitialLiquidityNotYetAdded(); } ```
The `buy()` function can be called by anyone to purchase tokens. When the token amount in the pair is less than the `gradThreshold`, it triggers an internal call to `_openTradingOnUniswap` to withdraw the remaining tokens in the pair. The function then proceeds to call `_addInitialLiquidity`, which creates a new Uniswap V2 pair and adds tokens for liquidity. However, if someone calls the createPair function and creates the Uniswap V2 pair ahead of time, the subsequent call to `_addInitialLiquidity` fails, causing the `buy` function to revert. ```solidity=305 function _addInitialLiquidity(IAgentToken token_, IERC20 _assetToken) internal returns(address){ address uniswapV2Pair_ = IUniswapV2Factory(uniswapRouter.factory()).createPair( address(token_), address(_assetToken) ); token_.addLiquidityPool(uniswapV2Pair_); token_.approve(address(uniswapRouter), type(uint256).max); _assetToken.approve(address(uniswapRouter), type(uint256).max); // Add the liquidity: (, , uint256 lpTokens) = uniswapRouter .addLiquidity( address(token_), address(_assetToken), token_.balanceOf(address(this)), _assetToken.balanceOf(address(this)), 0, 0, address(this), block.timestamp ); token_.setTokenSta(); IStakeVault stakeVault = IStakeVault(Clones.clone(stakeVaultImpl)); tokenStake[address(token_)] = address(stakeVault); IERC20(uniswapV2Pair_).approve(address(stakeVault), lpTokens); stakeVault.initialize(uniswapV2Pair_, lpTokens); return uniswapV2Pair_; } ```
The `_registerAgentEID()` function can be called by any user to register an `agentEID`. If the `agentEID` parameter is passed as an empty value, the condition at line 74 will not execute, allowing the registration process to proceed without proper validation of the `agentEID`. ```solidity=72 function _registerAgentEID(string memory agentEID) internal { bytes32 hash = keccak256(abi.encodePacked(agentEID)); if (hash != keccak256(abi.encodePacked(""))){ require(!_agentEIDs[hash], "Agent EID already registered"); _agentEIDs[hash] = true; } } ```
Performing integer division before multiplication truncates the low bits, losing the precision of calculation. ```solidity=608 uint256 swapThresholdInTokens = (_totalSupply * swapThresholdBasisPoints) / BP_DENOM; ``` ```solidity=618 swapBalance > swapThresholdInTokens * MAX_SWAP_THRESHOLD_MULTIPLE ``` ```solidity=608 uint256 swapThresholdInTokens = (_totalSupply * swapThresholdBasisPoints) / BP_DENOM; ``` ```solidity=621 swapBalance = swapThresholdInTokens * MAX_SWAP_THRESHOLD_MULTIPLE; ``` ```solidity=223 uint256 liquidity = (((k * 10000 ether) / supply) * 1 ether) / 10000; ```
In general, if we want to use Token-B to evaluate Token-A, the price of Token-A represents the amount of Token-B per Token-A. This can be calculated using the formula: $\frac{Token-B\_Amount}{Token-A\_Amount}$. From the implementation of the `createPair()` function, we can see that within a pair, `reserve0` represents the reserve amount of the agent token, while `reserve1` represents the reserve amount of the asset token. Therefore, the value returned by the `priceBLast()` function, which indicates the number of agent tokens per asset token, is the price of the asset token, not the agent token. As a result, in the following code block, the `price` field of `Data`, which contains attributes of the agent token, is incorrectly assigned the price of the asset token rather than the agent token. *contracts/fun/Bonding.sol* ```solidity=197 @> address _pair = factory.createPair(address(token), assetToken); .... Data memory _data = Data({ token: address(token), name: string.concat(_name, " by NetMind AI"), ticker: _ticker, supply: supply, marketCap: liquidity, @> price: IFPair(_pair).priceBLast() }); ``` ```solidity=131 function priceBLast() public view returns (uint256) { @> return _pool.reserve0 / _pool.reserve1; } ```
Upon initialization, the `swapThresholdBasisPoints` variable is of `uint256`. However, in the `setSwapThresholdBasisPoints` function, `swapThresholdBasisPoints_` is assigned a `uint16`, which may be too small. This discrepancy can cause the `setSwapThresholdBasisPoints` to malfunction, leading to potential data loss or incorrect processing, compromising the system's reliability and accuracy.
Addresses are not validated before assignment or external calls, potentially allowing the use of zero addresses and leading to unexpected behavior or vulnerabilities. For example, transferring tokens to a zero address can result in a permanent loss of those tokens.
In `Bonding.sol`, the `fee` state variable is initialized in the `initialize()` function and updated in the `setFee()` function; however, the calculation formulas for its value differ between the two functions. *contracts/fun/Bonding.sol* ```solidity=83 function initialize( address factory_, address router_, address feeTo_, uint256 fee_, .... ) external initializer { __Ownable_init(msg.sender); __ReentrancyGuard_init(); factory = FFactory(factory_); router = FRouter(router_); feeTo = feeTo_; @> fee = (fee_ * 1 ether) / 1000; .... } ``` ```solidity=133 function setFee(uint256 newFee, address newFeeTo) public onlyOwner { @> fee = newFee; feeTo = newFeeTo; } ```
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (10)
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