CertiK reviewed TokenFi (TokenFi Token Launcher) on 6 Feb 2024: 48 findings, 0 critical; 33 marked fixed, 2 acknowledged, 13 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 | 2 | 0 |
| Medium | 2 | 0 | 11 |
| Low | 11 | 0 | 22 |
The `TokenMintPaymentModule` and `TokenMintPricingModule` contracts are designed to use charge fee for creating new TokenFiERC721. However, the attacker has chance to use their own malicious `TokenMintPaymentModule` and `TokenMintPricingModule` contracts to be free for creating TokenFiERC721. Within `createErc721()` function and TokenFiERC721 constructor, there is no validation for `_input.tokenMintPricingModule` and `_input.tokenMintPaymentModule` before deploying TokenFiERC721 contract, at L53. ```solidity=29 function createErc721(CreateErc721Input memory input) external payable override { // First let's calculate the price ITokenLauncherPricingModule.AddOns memory addOns = ITokenLauncherPricingModule.AddOns({ erc20TransferFee: false, erc20BurnFee: false, erc20Reflection: false, erc20Buyback: false }); ITokenLauncherPricingModule.PriceResult memory price = ITokenLauncherPricingModule(pricingModule).getPrice( msg.sender, addOns, ITokenLauncherPricingModule.TokenType.ERC721 ); // Now let's process the payment ITokenLauncherPaymentModule.ProcessPaymentInput memory paymentInput = ITokenLauncherPaymentModule.ProcessPaymentInput( msg.sender, input.referrer, price.usdPrice, ITokenLauncherPaymentModule.PaymentMethod(uint256(input.paymentMethod)) ); ITokenLauncherPaymentModule.ProcessPaymentOutput memory paymentOutput = ITokenLauncherPaymentModule(paymentModule).processPayment{ value: msg.value }( paymentInput ); // Create the new ERC20 Token TokenFiERC721 token = new TokenFiERC721(input); bytes32 paymentAdminRole = ITokenMintPaymentModule(token.paymentModule()).PAYMENT_ADMIN_ROLE(); ITokenMintPaymentModule(input.tokenMintPaymentModule).grantRole(paymentAdminRole, address(token)); ``` ```solidity=36 constructor(ITokenLauncherERC721.CreateErc721Input memory _input) ERC721(_input.name, _input.symbol) { _baseTokenURI = _input.tokenURI; maxSupply = _input.maxSupply; collectionLogo = _input.collectionLogo; isPublicMintEnabled = _input.isPublicMintEnabled; isAdminMintEnabled = _input.isAdminMintEnabled; pricingModule = _input.tokenMintPricingModule; paymentModule = _input.tokenMintPaymentModule; ``` So, any malicious contracts can be assigned to the two state variables, `pricingModule` and `paymentModule`, and may charge zero fee for launching TokenFiERC721 contract. A similar issue also exists in the process of launching TokenFiERC1155. --- According to the code logic of `createV2LiquidityPool()` function, the router address used to create liquidity pool is passed in by users. However, the attacker user has chance to pass in a malicious router contract address because the absence of validation for `input.liquidityPoolDetails.routerAddress`. **TokenLauncherLiquidityPoolFactoryV1.sol** ```solidity=28 function createV2LiquidityPool(CreateV2Input memory input) external payable override onlyRole(DEFAULT_ADMIN_ROLE) returns (CreateV2Output memory) { IUniswapV2Router02 router = IUniswapV2Router02(input.liquidityPoolDetails.routerAddress); IERC20 sourceToken = IERC20(input.liquidityPoolDetails.sourceToken); uint256 sourceInitialBalance = sourceToken.balanceOf(address(this)); sourceToken.safeTransferFrom(input.owner, address(this), input.liquidityPoolDetails.amountSourceToken); uint256 sourceEarnedAmount = sourceToken.balanceOf(address(this)) - sourceInitialBalance; sourceToken.safeApprove(input.liquidityPoolDetails.routerAddress, sourceEarnedAmount); uint256 liquidity = _addLiquidity(input, sourceEarnedAmount); // Lock Liquidity if applicable address pairAddress = IUniswapV2Factory(router.factory()).getPair(input.liquidityPoolDetails.sourceToken, input.liquidityPoolDetails.pairedToken); if (input.lockLPDetails.lockLPTokenPercentage > 0) { _lockLiquidity(input, pairAddress); } return CreateV2Output({ liquidityPoolToken: pairAddress, liquidity: liquidity }); } ```
Any user has the ability to utilize the `createV2LiquidityPool` function within the `TokenLauncherERC20` contract for creating a liquidity pool. However, it is crucial to note that this process mandates the TokenFi ERC20 allowance to be equal to or greater than the specified `input.liquidityPoolDetails.amountSourceToken`. The flaw in this code logic lies in its susceptibility to exploitation, as it allows potential attackers to generate profits by leveraging the assets of the token owner. **TokenLauncherERC20.sol** ```solidity=91 function createV2LiquidityPool(ITokenLauncherLiquidityPoolFactory.CreateV2Input memory input) external payable { // Prepare TokenFiERC20 token = TokenFiERC20(input.liquidityPoolDetails.sourceToken); uint256 balance = token.allowance(input.owner, liquidityPoolFactory); require(balance >= input.liquidityPoolDetails.amountSourceToken, "TokenLauncherERC20: Not enough allowance"); // Create LP ITokenLauncherLiquidityPoolFactory lpFactory = ITokenLauncherLiquidityPoolFactory(liquidityPoolFactory); ITokenLauncherLiquidityPoolFactory.CreateV2Output memory lpOutput = lpFactory.createV2LiquidityPool{ value: msg.value }(input); .... ``` **TokenLauncherLiquidityPoolFactoryV1.sol** ```solidity=30 function createV2LiquidityPool(CreateV2Input memory input) external payable override onlyRole(DEFAULT_ADMIN_ROLE) returns (CreateV2Output memory) { IUniswapV2Router02 router = IUniswapV2Router02(input.liquidityPoolDetails.routerAddress); IERC20 sourceToken = IERC20(input.liquidityPoolDetails.sourceToken); uint256 sourceInitialBalance = sourceToken.balanceOf(address(this)); sourceToken.safeTransferFrom(input.owner, address(this), input.liquidityPoolDetails.amountSourceToken); uint256 sourceEarnedAmount = sourceToken.balanceOf(address(this)) - sourceInitialBalance; sourceToken.safeApprove(input.liquidityPoolDetails.routerAddress, sourceEarnedAmount); uint256 liquidity = _addLiquidity(input, sourceEarnedAmount); // Lock Liquidity if applicable address pairAddress = IUniswapV2Factory(router.factory()).getPair(input.liquidityPoolDetails.sourceToken, input.liquidityPoolDetails.pairedToken); if (input.lockLPDetails.lockLPTokenPercentage > 0) { _lockLiquidity(input, pairAddress); } return CreateV2Output({ liquidityPoolToken: pairAddress, liquidity: liquidity }); } ``` ```solidity=47 function _addLiquidity(CreateV2Input memory input, uint256 sourceEarnedAmount) private returns (uint256 liquidity) { address to = address(this); if (input.lockLPDetails.lockLPTokenPercentage == 0) { to = input.treasury; } IUniswapV2Router02 router = IUniswapV2Router02(input.liquidityPoolDetails.routerAddress); address weth = router.WETH(); IERC20 pairedToken = IERC20(input.liquidityPoolDetails.pairedToken); if (input.liquidityPoolDetails.pairedToken == weth) { (, , liquidity) = router.addLiquidityETH{ value: msg.value }(input.liquidityPoolDetails.sourceToken, sourceEarnedAmount, 0, 0, to, block.timestamp); } else { .... ```
**TokenLauncherERC20.sol** ```solidity=49 // Now let's process the payment ITokenLauncherPaymentModule.ProcessPaymentInput memory paymentInput = ITokenLauncherPaymentModule.ProcessPaymentInput( msg.sender, input.referrer, price.usdPrice, ITokenLauncherPaymentModule.PaymentMethod(uint256(input.paymentMethod)) ); ``` There is no validation for the `input.referrer` when calling `createErc20()` in `TokenLauncherERC20.sol`. Anyone can launch TokenFiERC20 without paying referrer fee by setting the `input.referrer` to be zero address, even though that user is invited by others. **TokenLauncherPaymentModuleV1.sol** ```solidity=81 uint256 referrerUsdShare = 0; if (input.referrer != address(0)) { referrerUsdShare = (input.usdPrice * referrerBasisPoints) / 10000; usdToken.safeTransfer(input.referrer, referrerUsdShare); } ``` A similar issue also exists in `TokenLauncherERC1155` and `TokenLauncherERC721` contract.
The absence of validation for `msg.sender` within `constructor()` function in `TokenFiERC20` indicates that anyone can deploy a new `TokenFiERC20` contract with any inputs. However, the new `TokenFiERC20` contract isn't listed on launcher store.
The `balanceOf()` function call at line 130 retrieves the quantity of the pair token, which is not ETH. Therefore, the presence of the `Wei` unit, which is specifically associated with ETH measurements, in the local variable `remainingWeiBalance` indicating a non-ETH pair token amount is inappropriate. ```solidity=130 uint256 remainingWeiBalance = _pairToken.balanceOf(address(this)); if (remainingWeiBalance > 0) { _pairToken.safeTransfer(treasury, remainingWeiBalance); } ```
Coin sniping is a trading strategy focused on purchasing a newly launched cryptocurrency token. Traders sell it for a profit when the hype spreads and the price pumps. Within the `TokenFiERC20` contract, there is no code logic for protecting sniper. Reference: https://medium.com/@ProdigySniperBot/snipe-to-success-the-best-sniper-bot-to-trade-shiba-inu-shib-and-shibarium-tokens-4668822bd5b1
The cited address input is missing a check that it is not `address(0)`.
The `updateTokenOwner()` function is designed to update tokenfi ERC20 owner. However, the tokenfi ERC20 owner can be set to the zero address because of the absence of zero address validation of the argument `newOwner`. **TokenLauncherStore.sol** ```solidity=54 address owner = tokenOwner[tokenAddress]; require(msg.sender == owner, "TokenLauncherStore: Not owner"); require(owner != newOwner, "TokenLauncherStore: Same owner"); ``` ```solidity=65 _tokensByOwner[tokenType][newOwner].push(tokenAddress); tokenOwner[tokenAddress] = newOwner; ```
The state variables, `isPublicMintEnabled` and `isAdminMintEnabled`, are intended to regulate NFT minting authority. However, a flaw in the current code allows both state variables to be set to `true` simultaneously, and their values are never updated. This situation leads to two issues: 1. Any account can mint NFTs, even if the NFT owner specifically desires only the Admin to have minting authority. This results in a lack of control over the minting process. 2. The functions `adminMint()` and `adminMintBatch()` become meaningless when the NFT owner prefers any account to have minting privileges. ```solidity=36 isPublicMintEnabled = _input.isPublicMintEnabled; isAdminMintEnabled = _input.isAdminMintEnabled; ``` ```solidity=65 function adminMint(address _to) external adminMintAvailable whenNotPaused supplyAvailable(1) onlyRole(DEFAULT_ADMIN_ROLE) { _mintBatch(_to, 1); } function adminMintBatch(address _to, uint256 _amount) external adminMintAvailable whenNotPaused supplyAvailable(_amount) onlyRole(DEFAULT_ADMIN_ROLE) { _mintBatch(_to, _amount); } ``` ```solidity=79 function mint(address _to) external payable publicMintAvailable whenNotPaused supplyAvailable(1) { _processPayment(1); _mintBatch(_to, 1); } function mintBatch(address _to, uint256 _amount) external payable publicMintAvailable whenNotPaused supplyAvailable(_amount) { _processPayment(_amount); _mintBatch(_to, _amount); } ``` A similar issue also exists in `TokenFiERC1155.sol`.
In the context of the reflection token's code logic, the condition `_tOwned[holder] > 0` indicates that the holder is exempted from reflection. The existing flaw in the code lies in the fact that, despite the value of `_tOwned[_input.treasury]` being assigned as `_input.initialSupply` and being greater than zero, the `treasury` account is not properly excluded from reflection. ```solidity=75 if (_input.fees.reflection.percentage > 0) { require(_input.initialSupply > 0, "TokenFiERC20.constructor: initialSupply must be greater than 0"); totalReflection.r = (MAX - (MAX % _input.initialSupply)); _rOwned[_input.treasury] = totalReflection.r; totalReflection.t = _input.initialSupply; _tOwned[_input.treasury] = _input.initialSupply; emit Transfer(address(0), _input.treasury, _input.initialSupply); } else { _mint(_input.treasury, _input.initialSupply); } ```
In general, the `_setURI()` function, designed to map the token metadata URI to its corresponding token ID, should be invoked once upon the creation of a new type token. Furthermore, token assets associated with the same token ID should utilize the identical metadata URI. ```solidity=42 for (uint256 i = 0; i < _input.initialTokens.length; i++) { require(_input.initialTokens[i].maxSupply > 0, "TokenFiERC1155: maxSupply must be greater than 0"); maxSupply[_input.initialTokens[i].tokenId] = _input.initialTokens[i].maxSupply; decimals[_input.initialTokens[i].tokenId] = _input.initialTokens[i].decimals; _setURI(_input.initialTokens[i].tokenId, _input.initialTokens[i].uri); existingTokenIds.push(_input.initialTokens[i].tokenId); } ``` ```solidity=70 function createToken(ITokenLauncherERC1155.CreateErc1155TokenInput memory input) external onlyRole(DEFAULT_ADMIN_ROLE) { require(!exists(input.tokenId), "TokenFiERC1155: token already exists"); require(input.maxSupply > 0, "TokenFiERC1155: maxSupply must be greater than 0"); maxSupply[input.tokenId] = input.maxSupply; decimals[input.tokenId] = input.decimals; _setURI(input.tokenId, input.uri); existingTokenIds.push(input.tokenId); } ``` The `DEFAULT_ADMIN_ROLE` in the `TokenFiERC1155` contract enables the updating of the metadata URI for a specific token during each minting process through the `adminMint()` or `mintWithSignature()` function. ```solidity=59 function adminMint( address _to, uint256 _tokenId, uint256 _amount, string memory _uri ) external whenNotPaused tokenExists(_tokenId) supplyAvailable(_tokenId) onlyRole(DEFAULT_ADMIN_ROLE) { require(isAdminMintEnabled, "TokenFiERC1155: admin mint disabled"); _setURI(_tokenId, _uri); _mint(_to, _tokenId, _amount, ""); } ``` ```solidity=98 function mintWithSignature( address _to, uint256 _tokenId, uint256 _amount, string memory _uri, string memory signature, uint256 nonce ) external whenNotPaused tokenExists(_tokenId) supplyAvailable(_tokenId) checkSignature(_uri, nonce, signature) { if (bytes(_uri).length > 0) { _setURI(_tokenId, _uri); } _mint(_to, _tokenId, _amount, ""); } ```
The linked functions are equivalent to the compiler-generated getter functions for the respective variables.
The linked variables 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. A big advantage of immutable variables is that reading them is significantly cheaper than reading from regular state variables since they will not be stored in storage.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (13)
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