CertiK reviewed Ice Open Network (Ice - Audit 1) on 10 Jan 2024: 17 findings, 0 critical; 15 marked fixed, 2 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 | 2 | 4 |
| Medium | 0 | 0 | 1 |
| Low | 0 | 0 | 10 |
In the `_addLiquidity()` function, the contract sets the minimum amount of the native token and the `ICE` token. However, it is noted that the minimum amount is calculated by the current corresponding balance in the contract, which is possibly inconsistent with the current token price in the pair and causes the failure to add liquidity. In the `_swapTokensForEth()` function, the contract uses this value `(tokenAmount * (100 - _slippage)) / 100` as the `amountOutMin` of `swapExactTokensForETHSupportingFeeOnTransferTokens()`, which is incorrect since the the output ETH amount is determined by the price in the pair.
In the contract `ICEToken`, the role `_owner` has authority to pause/resume the functionality of token transfer. Any compromise to the private key of the `owner` may allow hackers to take advantage of this authority and disable or enable the transfer.
It is noted that the contract calls `_swapBack()` in every sell or normal transfer transaction. In the `_swapBack()` function, the contract will try to add liquidity if `totalTokensToSwap == 0` and `contractBalance != 0`. However, if a malicious holder transfers some tokens to the current token contract when `tokensForLiquidity = 0` and `tokensForOperations = 0`, the normal transfer transaction and sale will fail due to the `INSUFFICIENT_LIQUIDITY_MINTED` error. Although the `totalTokensToSwap` and `address(this).balance` can be changed by buying operations, it still can cause side effects to the project due to the following code. ```solidity=702 tokensForLiquidity = 0; tokensForOperations = 0; ... if (address(this).balance > 0) { (success,) = address(operationsAddress).call{value: address(this).balance}(""); } ```
The contract takes an extra amount of tokens as the fee from the sender when buying, not directly deducting the fee from the transfer amount, and it is also noted that the contract supports more than one pool. However, we found that current implementation is vulnerable to the flashloan attack. For example, the malicious can use a flashloan and combination of calling `skim()` and `sync()` functions to decrease the pool liquidity and make a profit.
The pointed input parameters are not validated. In particular, the following checks should be added: - Check if `pairAddress` already exists in `uniswapV2LiquidityPools` - Check if `routerAddress` is a zero address Moreover, there are no upper boundaries for **setUniswapV2LiquidityPool()** which is used to set the liquidity/operation fee of buy/sell. It is possible to set the total fee rate up to any arbitrary amount, which is unreasonable and may cause the loss of users' assets. - `buyOperationsFee` plus `buyLiquidityFee` should lie in (0, 100) according to the function `_transfer()`. - `sellOperationsFee` plus `sellLiquidityFee` should lie in (0, 100) according to the function `_transfer()`.
The state variable `botsCaught` is used to record caught bots with a default value of zero. However, per the function `_transfer()`, if a bot is caught, the function will raise a `NoBots` error, thus botsCaught won't be updated and will remain zero forever. ```solidity function _transfer(address from, address to, uint256 amount) internal { //... if (bots[from] || bots[to]) { botsCaught++; revert NoBots(); } //... } ```
It is noted that the contract calls `_swapBack()` on every normal transfer and sell transaction, which is too frequent and unnecessary especially when the token amount in the token contract is very small.
The function `transferForeignToken()` allows the owner to transfer any ERC20 token to any address. ```solidity function transferForeignToken(address _token, address _to) external onlyOwner returns (bool _sent) { require(_token != address(0), "_token address cannot be 0"); uint256 _contractBalance = IERC20(_token).balanceOf(address(this)); _sent = IERC20(_token).transfer(_to, _contractBalance); emit TransferForeignToken(_token, _contractBalance); } ``` However, per the function name, only foreign tokens except the' ICE' token can pass in. The owner can pass the ICE token address and transfer all the fee tokens to an arbitrary address.
```solidity for (uint i = 0; i < uniswapV2LiquidityPools.length; i++) { if (uniswapV2LiquidityPoolSlots[uniswapV2LiquidityPools[i]].router == msg.sender) { interactingToRouter = true; } } ``` If `msg.sender` is found as a router, the loop can be finished and popped to save gas.
It is noted that the function `_swapTokensForEth()/_addLiquidity()` doesn't check if the amount variable is above zero before executing the `swapExactTokensForETHSupportingFeeOnTransferTokens()` or `addLiquidityETH()`, which cause the transaction fails. Since these functions are called in the normal transfer/sell/buy transactions, their side effects are amplified in current contract.
A sandwich attack might happen when an attacker observes a transaction swapping tokens or adding liquidity without setting restrictions on slippage or minimum output amount. The attacker can manipulate the exchange rate by frontrunning (before the transaction being attacked) a transaction to purchase one of the assets and make profits by backrunning (after the transaction being attacked) a transaction to sell the asset. The following functions are called without setting restrictions on slippage or minimum output amount, so transactions triggering these functions are vulnerable to sandwich attacks, especially when the input amount is large: - `swapExactTokensForETHSupportingFeeOnTransferTokens()` - `addLiquidityETH()`
It is noted that the contract calls `transferOwnership()` to transfer ownership to `operationsAddress`, which has been set to `msg.sender` in the previous statement. However, the `msg.sender` has been set to the owner of the contract in the `Ownable(msg.sender)`. ```solidity=168 constructor() ERC20("ICE", "ICE") Ownable(msg.sender) { operationsAddress = msg.sender; transferOwnership(operationsAddress); ... } ```
Ignores return value of low-level calls. ```solidity=668 (success,) = address(operationsAddress).call{value: address(this).balance}(""); ``` ```solidity=682 (success,) = address(msg.sender).call{value: address(this).balance}(""); ```
The linked `transfer()` invocation does not check the return value of the function call which should yield a `true` result in case of a proper ERC-20 implementation.
The smart contract does not check or store the return value of an external call in a local or state variable, which may introduce vulnerabilities due to the unhandled outcome. ```solidity=651 IDEXRouter(receiver.router).addLiquidityETH{value: ethAmount}( address(this), tokenAmount, 0, // slippage is unavoidable 0, // slippage is unavoidable address(0xdead), block.timestamp ); ```
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (3)
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