TokenFi security record

BNB Chain, Ethereumtokenfi.comXGitHub

As of 3 Nov 2026, TokenFi has 2 audit reports by 1 security firm on Trustblock; the latest is by CertiK, dated 6 Feb 2024. Those reports list no open critical findings.

Critical findings
No open critical findings
Across 2 reports.
Open the latest report
Your label, live
Get the label for your site

Record last updated . It holds audits, findings and contracts up to Nov 2026, and incidents up to Oct 2026. Trustblock does not review code.

Audits2

Newest first

AuditSecurity firmDateScopeCritical findingsReport
TokenFi Token LauncherSecurity firm: CertiKDate: Scope: Files named in the reportNo open critical findingsOpen · skynet.certik.com
TokenFiSecurity firm: CertiKDate: Scope: Files named in the reportNo open critical findingsOpen · skynet.certik.com

Findings58

Statuses are the security firm’s own, from each report.

SeverityOpenAcknowledgedFixed
Critical000
High320
Medium3011
Low15024
MediumAnyone Can Modify State Variable `liquidityPoolToTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The `registerLiquidityPool()` function is intended for recording tokenfi ERC20 liquidity pool information. However, the recorded information for the liquidity pool can be altered by any address since no access restrictions are applied to the `registerLiquidityPool()` function. **TokenLauncherStore.sol** ```solidity=145 function registerLiquidityPool(RegisterLiquidityPoolInput memory input) external override { liquidityPoolTokensByToken[input.tokenAddress].push(input.liquidityPoolToken); emit LiquidityPoolCreated(currentBlockLiquidityPoolCreated, input.liquidityPoolToken, input.tokenAddress); currentBlockLiquidityPoolCreated = block.number; } ``` Consequently, querying the corresponding liquidity pool for tokenfi ERC20 may yield inaccurate values in such instances. ```solidity=112 function getLiquidityPoolTokensByToken(address token) external view returns (address[] memory) { return liquidityPoolTokensByToken[token]; } ```

Open the report
MediumIncorrect Pass-In Value of Argument `amountOutMinTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The `_swapFlokiByWeth()` function is designed to use Floki to buy WETH. However, the argument `amountOutMin` value is incorrect when calling `router.swapExactTokensForTokensSupportingFeeOnTransferTokens()` function to buy WETH. **IUniswapV2Router02.sol** ```solidity=24 function swapExactTokensForTokensSupportingFeeOnTransferTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external; ``` According to the `swapExactTokensForTokensSupportingFeeOnTransferTokens` function signature, the second argument `amountOutMin` denotes the minimum amount of exchanged out token assets, in this codebase, WETH. **TokenLauncherPaymentModuleV1.sol** ```solidity=149 // Calculate the required FLOKi for the expected USD amount address[] memory path = new address[](2); path[0] = router.WETH(); path[1] = address(usdToken); uint256[] memory amounts = router.getAmountsIn(usdExpected, path); path[1] = path[0]; path[0] = address(flokiToken); amounts = router.getAmountsIn(amounts[0], path); requiredFloki = (amounts[0] / 995) * 1000; // Account for FLOKI tax + buyback ``` After executing the statements from L149-L157, the items in `amounts` variable is `[Floki_in_amount, WETH_out_amount]`, so amounts[0] is the amount of Floki, not WETH. **TokenLauncherPaymentModuleV1.sol** ```solidity=165 router.swapExactTokensForTokensSupportingFeeOnTransferTokens(requiredFloki, amounts[0], path, address(this), block.timestamp); ``` When executing the statement at L165, it is incorrect that the second argument is passed in Floki amount.

Open the report
MediumMismatch Between NFT Id and URITokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

According to the code logic of `_mintBatch()` function, each new minted NFT token has its own URI. However, the URIs aren't bound with the correct NFTs. **TokenFiERC721.sol** ```solidity=52 function _mintBatch( address _to, uint256 _amount, string[] memory _uris ) private { require(_uris.length == _amount, "TokenFiERC721: uris length does not match amount"); uint256 tokenId = totalSupply(); for (uint256 i = 0; i < _amount; i++) { _safeMint(_to, tokenId + i); if (bytes(_uris[i]).length > 0) { _setTokenURI(tokenId, _uris[i]); } } } ``` The local variable `tokenId` denotes the last NFT identifier and its value is unchanged in the `_mintBatch()` function scope. When executing the statement at L62, all the uris are bound with the last NFT.

Open the report
MediumMissing Liquidity Pool Configuration When TokenFiTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

**TokenLauncherERC20.sol** ```solidity=99 // if the source token is a tokenfi token, we need to add the new LP to the exchange pool if (ITokenLauncherStore(tokenLauncherStore).tokenOwner(input.liquidityPoolDetails.sourceToken) != address(0)) { if (!token.isExchangePool(lpOutput.liquidityPoolToken)) { token.addExchangePool(lpOutput.liquidityPoolToken); } if (token.isReflectionToken() && !token.isExcludedFromReflectionRewards(lpOutput.liquidityPoolToken)) { token.excludeAccount(lpOutput.liquidityPoolToken); } updateBuyBackDetails(input.buybackDetails, address(token)); } ``` The newly created LP token, designed for the TokenFiERC20 contract, is excluded from reflection only if the owner of the TokenFiERC20 contract has a non-zero address. Furthermore, when creating the TokenFiERC20 contract or updating its owner, the provided owner address can be set to zero. **TokenFiERC20.sol** ```solidity=90 if (_input.owner != address(0)) { _grantRole(DEFAULT_ADMIN_ROLE, _input.owner); } ``` **TokenLauncherStore.sol** ```solidity=45 function addToken(CreateTokenInput memory input) external override onlyRole(DEFAULT_ADMIN_ROLE) { tokensByOwnerByType[input.tokenType][input.owner].push(input.tokenAddress); tokenOwner[input.tokenAddress] = input.owner; ``` ```solidity=62 function updateTokenOwner(TokenType tokenType, address tokenAddress, address newOwner) external override { address owner = tokenOwner[tokenAddress]; require(msg.sender == owner, "TokenLauncherStore: Not owner"); require(owner != newOwner, "TokenLauncherStore: Same owner"); ``` In this scenario, if the owner of the TokenFiERC20 contract is set to the zero address by default, the liquidity pool remains unconfigured within the TokenFiERC20 contract. Consequently, the `Pair Address Reflection Exploit` issue persists (refer to FFF-01 Finding).

Open the report
MediumMissing Ownership TransferTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

**TokenFiERC20.sol** ```solidity=85 tokenLauncher = msg.sender; _grantRole(DEFAULT_ADMIN_ROLE, _input.owner); _grantRole(FEE_MANAGER_ROLE, msg.sender); ``` When deploying TokenFiERC20 contract, the given token owner is granted to be `DEFAULT_ADMIN_ROLE` of that new deployed TokenFiERC20. If the owner is updated, its `DEFAULT_ADMIN_ROLE` ownership should be transferred to the new owner at the same time. The current code logic issue is that after calling `updateTokenOwner()` function, the original owner still has rights to exclude/include accounts, mint token, etc. **TokenLauncherStore.sol** ```solidity=49 function updateTokenOwner( uint8 tokenType, address tokenAddress, address newOwner ) external override { address owner = tokenOwner[tokenAddress]; require(msg.sender == owner, "TokenLauncherStore: Not owner"); require(owner != newOwner, "TokenLauncherStore: Same owner"); address[] storage tokens = _tokensByOwner[tokenType][owner]; for (uint256 i = 0; i < _tokensByOwner[tokenType][owner].length; i++) { if (_tokensByOwner[tokenType][owner][i] == tokenAddress) { _tokensByOwner[tokenType][owner][i] = _tokensByOwner[tokenType][owner][tokens.length - 1]; _tokensByOwner[tokenType][owner].pop(); break; } } _tokensByOwner[tokenType][newOwner].push(tokenAddress); tokenOwner[tokenAddress] = newOwner; emit TokenOwnerUpdated(currentBlockTokenOwnerUpdated, tokenType, owner, newOwner); currentBlockTokenOwnerUpdated = block.number; } ``` There is no code logic for transferring `DEFAULT_ADMIN_ROLE` ownership from the original to the new owner, however, the `tokenAddress` owner can be successfully updated. A similar issue also exists in the TokenFiERC721 and TokenFiERC1155 management.

Open the report
MediumMissing Update `_totalReflection.r` in Function `TokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The purpose of the `_mintReflection()` function is to mint RFI tokens. However, the minting operations performed via `_mintReflection()` may result in a decreased reflection rate. Consequently, assets held by users (excluding those who are excluded) become more valuable. This means that users receive rewards not only through reflect token transfer operations but also via reflect token minting operations. Furthermore, the recipient of the minted tokens gets rewards immediately after minting. **TokenFiERC20.sol** ```solidity=294 function _getRate() private view returns (uint256) { (uint256 rSupply, uint256 tSupply) = _getCurrentSupply(); return rSupply / tSupply; } function _getCurrentSupply() private view returns (uint256, uint256) { uint256 rSupply = _totalReflection.r; uint256 tSupply = _totalReflection.t; for (uint256 i = 0; i < _excluded.length; i++) { if (_rOwned[_excluded[i]] > rSupply || _tOwned[_excluded[i]] > tSupply) return (_totalReflection.r, _totalReflection.t); rSupply = rSupply - _rOwned[_excluded[i]]; tSupply = tSupply - _tOwned[_excluded[i]]; } if (rSupply < _totalReflection.r / _totalReflection.t) return (_totalReflection.r, _totalReflection.t); return (rSupply, tSupply); } ``` Let's assume that no holders are excluded, then $Rate = \frac{\_totalReflection.r}{\_totalReflection.t}$ . **TokenFiERC20.sol** ```solidity=392 function _mintReflection(address account, uint256 amount) private { _totalReflection.t = _totalReflection.t + amount; if (isExcluded(account)) { _tOwned[account] = _tOwned[account] + amount; } else { _rOwned[account] = _rOwned[account] + amount * _getRate(); } emit Transfer(address(0), account, amount); } ``` When the `_mintReflection()` function is executed along with the argument `account` that is not excluded, the $Rate = \frac{\_totalReflection.r}{\_totalReflection.t}$ becomes smaller because `_totalReflection.t` is increased and `_totalReflection.r` is unchanged. **TokenFiERC20.sol** ```solidity=152 function tokenFromReflection(uint256 rAmount) public view returns (uint256) { require(rAmount <= _totalReflection.r, "Amount must be less than total reflections"); uint256 currentRate = _getRate(); return rAmount / currentRate; } ``` After minting a reflect token, if you call the `tokenFromReflection()` function with the same `rAmount` value before minting, you will receive a larger balance value.

Open the report
MediumNo Value Assigned to Variable `flokiBurned`TokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

**TokenLauncherPaymentModuleV1.sol** ```solidity=76 flokiBurned = _processFlokiPayment(input.user, initialUsdBalance, usdExpected); ... return ProcessPaymentOutput(usdBurnShare, flokiBurned, referrerUsdShare, treasuryUsdShare); ``` **TokenLauncherPaymentModuleV1.sol** ```solidity=166 // Burn remaining FLOKI flokiToken.safeTransfer(burnAddress, flokiToken.balanceOf(address(this)) - initialFlokiBalance); ``` **TokenLauncherStore.sol** ```solidity=44 if (input.flokiBurned > 0) { _notifyFlokiBurned(input.tokenAddress, input.owner, input.usdBurned, input.flokiBurned); } ``` The variable `flokiBurned` represents the amount of Floki burned during the execution of the `_processFlokiPayment()` function. This value is intended to be returned to the caller, the `TokenLauncherERC20` contract, and ultimately included in a Floki burn notification within the `TokenLauncherStore` contract. However, it's worth noting that the variable `flokiBurned` is consistently assigned a value of zero at line 76. After the `_processFlokiPayment()` function finishes, the variable `flokiBurned` returned at line 123 is not given a value. **TokenLauncherPaymentModuleV1.sol** ```solidity=119 function _processFlokiPayment( address user, uint256 initialUsdBalance, uint256 usdAmount ) private returns (uint256 flokiBurned) { uint256 usdBasisPoints = 10000 - burnBasisPoints; // we don't need the entire usdPrice swapped (some FLOKI will be burned) uint256 usdExpected = (usdAmount * usdBasisPoints) / 10000; (uint256 requiredFloki, uint256 initialFlokiBalance, uint256 receivedFloki, uint256 initialWethBalance, uint256 flokiToBurn) = _swapFlokiByWeth( user, usdBasisPoints, usdExpected ); _swapWethByUsd(user, usdExpected, initialUsdBalance, usdBasisPoints, initialWethBalance); } ```

Open the report
MediumPotential Pair Address Reflection ExploitTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

According to the launch code logic of the `TokenFiERC20` contract, it is essential to note that, by default, the liquidity pair address is not excluded from reflection rewards, making it to receive rewards during reflection transfers. In this case, a hacker can exploit the protocol using the reflection mechanism. **TokenFiERC20.sol** ```solidity=152 function tokenFromReflection(uint256 rAmount) public view returns (uint256) { require(rAmount <= _totalReflection.r, "Amount must be less than total reflections"); uint256 currentRate = _getRate(); return rAmount / currentRate; } ``` The RFI token balance that include rewards is calculated by $\frac{rAmount}{currentRate}$. When decreasing rate, the reflection balance will be increased. **TokenFiERC20.sol** ```solidity=132 function reflect(uint256 tAmount) external onlyReflection { address sender = _msgSender(); require(!isExcluded(sender), "Excluded addresses cannot call this function"); (uint256 rAmount, , , , ) = _getValues(tAmount, false); _rOwned[sender] = _rOwned[sender] - rAmount; _totalReflection.r = _totalReflection.r - rAmount; _totalReflection.tFee = _totalReflection.tFee + tAmount; } ``` And the aforementioned `currentRate` is determined by the $\frac{\_totalReflection.r }{\_totalReflection.t}$. If the `reflect()` function is executed with significant input, it can significantly decrease `_totalReflection.r`, thereby decreasing the rate. Reflection balance will be significantly increased. When the liquidity pair receives reflection rewards, it's crucial to recognize that the RFI token reserve in the pair is not immediately updated. This delay persists until the occurrence of the next swap, mint, burn, or sync operation (as outlined in the [UniswapV2Pair.sol](https://github.com/Uniswap/v2-core/blob/ee547b17853e71ed4e0101ccfd52e70d5acded58/contracts/UniswapV2Pair.sol#L73C99-L73C99) code). During this interim period, the RFI token balance exceeds its reserve in the liquidity pair. The attacker can uses the above exceeded RFI token assets to swap out significant assets.

Open the report
MediumToken Type isn't Determined After TokenFiERC20 DeTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

```solidity=129 function updateFees(ITokenLauncherERC20.Fees memory _fees) external onlyRole(DEFAULT_ADMIN_ROLE) { uint256 maxFee = _fees.transferFee.percentage + _fees.burn.percentage + _fees.reflection.percentage + _fees.buyback.percentage; require(maxFee <= MULTIPLIER_BASIS, "TokenFiERC20: fees sum must be less than 100%"); fees = _fees; } ``` The `DEFAULT_ADMIN_ROLE` role account can update `fees.reflection.percentage` value. In this case, 1. After updating `fees.reflection.percentage` from zero to non-zero value, normal ERC20 token contract becomes reflection token contract. In this case, even though Alice holds normal token assets, the `balanceOf(Alice)` function call may return zero because `_rOwned[Alice]=0`. 3. After updating `fees.reflection.percentage` from non-zero to zero value, reflection token contract becomes normal ERC20 token contract. In this case, even though Alice holds reflection token assets, the `balanceOf(Alice)` function call may return zero because `_balances[Alice]=0`.

Open the report
MediumTotalSupply May Exceed MaxSupplyTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The modifier `supplyAvailable()` is designed to restrict the token amount in circulation. The current code logic issue is that the modifier `supplyAvailable()` cannot avoid overissuance. **TokenFiERC721.sol** ```solidity=193 modifier supplyAvailable() { require(totalSupply() < maxSupply, "TokenFiERC721: maxSupply reached"); _; } ``` When calling `adminMintBatch()` function to mint token, if `totalSupply() + amount` is greater than `maxSupply`, the `adminMintBatch()` still can be successfully executed because the modifier `supplyAvailable()` doesn't take the new minted token amount into account. Finally, the totalSupply exceeds the maxSupply. **TokenFiERC721.sol** ```solidity=73 function adminMintBatch( address _to, string[] memory _uris, uint256 _amount ) external adminMintAvailable whenNotPaused supplyAvailable onlyRole(DEFAULT_ADMIN_ROLE) { _mintBatch(_to, _amount, _uris); } ``` A similar issue also exists in `TokenFiERC1155` contract.

Open the report
Show 10 more
MediumVariable `_rOwned[account]` Not Updated in FunctiTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

**TokenFiERC20.sol** ```solidity=167 function includeAccount(address account) external onlyReflection onlyRole(DEFAULT_ADMIN_ROLE) { require(isExcluded(account), "Account is already included"); for (uint256 i = 0; i < _excluded.length; i++) { if (_excluded[i] == account) { _excluded[i] = _excluded[_excluded.length - 1]; _tOwned[account] = 0; _isExcluded[account] = false; _excluded.pop(); break; } } } ``` Variable `_rOwned[account]` is not updated in the function `includeAccount()`, which will make the accounts included siphon off the tokens out of the balances of all token holders. The Rate was higher at the moment of the `excludeAccount(account)` call, so the `_rOwned[account] / _tOwned[account]` ratio is bigger than expected for accounts included in the reward.

Open the report
Low`existingTokenIds` May Contain Duplicated Token ITokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

There are two functions, `constructor()` and `createToken()`, can be called to create new type token. The current code logic issue is that the state variable `existingTokenIds` may contain two same token id. ```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); } ``` After new `TokenFiERC1155` contract is deployed, multiple tokens may be created through executing statements at L42-L47. ```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); } ``` Because the `exists()` function called at L71 is based on token totalSupply, the already existed token id can pass the check at L71 if that token doesn't been minted before calling `createToken()` function. In this case, the already existed token id is again inserted into `existingTokenIds` and `existingTokenIds` is array type. Finally, `existingTokenIds` contains two same token id. **ERC1155Supply.sol** ```solidity=29 function exists(uint256 id) public view virtual returns (bool) { return ERC1155Supply.totalSupply(id) > 0; } ```

Open the report
LowAllows Users Specify URI in Mint Param Without AcTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The problem arises from the fact that public users can supply their own URI for the NFT. The URI is a string that represents the location of metadata associated with the NFT, which typically includes information about the artwork. However, in this case, the URI is user-defined, meaning that users can provide any value they want, potentially leading to misrepresentation or fraudulent behavior.

Open the report
LowAnyone can Create TokenFiERC20 Liquidity Pool ConTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

There is no validation to check that whether the `msg.sender` is the owner of source TokenFiERC20 token, so anyone can create liquidity pool for any source TokenFiERC20 token. ```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); if (!token.isExchangePool(lpOutput.liquidityPoolToken)) { token.addExchangePool(lpOutput.liquidityPoolToken); } // Log into store ITokenLauncherStore.RegisterLiquidityPoolInput memory lpInput = ITokenLauncherStore.RegisterLiquidityPoolInput( address(token), lpOutput.liquidityPoolToken ); ITokenLauncherStore(tokenLauncherStore).registerLiquidityPool(lpInput); } ```

Open the report
LowGas OptimizationTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The `_getAddOnsPrice()` function is designed to append addons price when token type is ERC20. It would reduce gas fee cost if executing the statement at L64 only when `tokenType` argument value is `ERC20`. ```solidity=64 price += _getAddOnsPrice(addOns); ``` ```solidity=117 function _getAddOnsPrice(AddOns memory addOns) private view returns (uint256) { uint256 price = 0; if (addOns.erc20BurnFee) { price += ERC20_BURN_FEE_ADDON_PRICE; } if (addOns.erc20Buyback) { price += ERC20_BUYBACK_FEE_ADDON_PRICE; } if (addOns.erc20Reflection) { price += ERC20_REFLECTION_FEE_ADDON_PRICE; } if (addOns.erc20TransferFee) { price += ERC20_TRANSFER_FEE_ADDON_PRICE; } return price; } ``` --- When the argument `_amount` equals zero, executing payment and mint codes is meaningless. Reverting the transaction when `_amount` argument value is zero would reduce gas cost. **TokenFiERC1155.sol** ```solidity=79 function mint( address _to, uint256 _tokenId, uint256 _amount ) external payable whenNotPaused tokenExists(_tokenId) supplyAvailable(_tokenId) { require(isPublicMintEnabled, "TokenFiERC1155: public mint disabled"); ITokenMintPricingModule.PriceResult memory price = ITokenMintPricingModule(pricingModule).getPrice( msg.sender, ITokenMintPricingModule.TokenType.ERC1155 ); ITokenMintPaymentModule.ProcessPaymentInput memory paymentInput = ITokenMintPaymentModule.ProcessPaymentInput( msg.sender, price.nativePrice * _amount, ITokenMintPaymentModule.PaymentMethod.NATIVE ); ITokenMintPaymentModule(paymentModule).processPayment{ value: msg.value }(paymentInput); _mint(_to, _tokenId, _amount, ""); } ``` The similar issue also exists in `mintBatch()` function defined in `TokenFiERC721.sol`.

Open the report
LowImproper Approval MechanismTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

Sometimes, when approving a non-zero amount directly to an address, it's necessary to check the current allowance beforehand to determine if it needs to be cleared. Otherwise, 'safeApprove' will revert. This vulnerability will impact consequent listings and cause them to fatally fail for a token that has been used in the past and is part of the non-compliant EIP-20 tokens, with USDT being the prime and most popular example. The code in USDT that causes this complication is as follows: ```solidity require(!((_value != 0) && (allowed[msg.sender][_spender] != 0))); ```

Open the report
LowInaccurate Approve AmountTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The cited codes is designed to handle the case that `pairedToken` is deflation token ( charging transfer tax ) when adding liquidity. However, the approved amount for `router` is inaccurate. ```solidity=56 uint256 initialBalance = pairedToken.balanceOf(address(this)); pairedToken.safeTransferFrom(input.owner, address(this), input.liquidityPoolDetails.amountPairedToken); uint256 earnedAmount = pairedToken.balanceOf(address(this)) - initialBalance; pairedToken.safeApprove(input.liquidityPoolDetails.routerAddress, input.liquidityPoolDetails.amountPairedToken); ``` The local variable `earnedAmount` denotes the `TokenLauncherLiquidityPoolFactoryV1` contract actually received amount for adding liquidity. When executing L60-L69 statements, the approved amount `input.liquidityPoolDetails.amountPairedToken`for `router` may be greater than the actually needed amount `earnedAmount` when adding liquidty.

Open the report
LowInconsistency Between Code and CommentTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The comment at L52 says to create ERC20 token, however, the statement at L53 is creating ERC721 token. **TokenLauncherERC721.sol** ```solidity=52 // Create the new ERC20 Token TokenFiERC721 token = new TokenFiERC721(input); ``` The similar issue also exists in `TokenLauncherERC1155.sol`.

Open the report
LowMissing `msg.value` ValidationTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The `_addLiquidity()` function is designed for creating TokenFiERC20 liquidity pool that is paired with ETH or ERC20 token. The current code logic issue is that the ETH payment may be inconsistent with the actually required amount. **TokenLauncherLiquidityPoolFactoryV1.sol** ```solidity=45 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 { uint256 initialBalance = pairedToken.balanceOf(address(this)); pairedToken.safeTransferFrom(input.owner, address(this), input.liquidityPoolDetails.amountPairedToken); uint256 earnedAmount = pairedToken.balanceOf(address(this)) - initialBalance; pairedToken.safeApprove(input.liquidityPoolDetails.routerAddress, input.liquidityPoolDetails.amountPairedToken); (, , liquidity) = router.addLiquidity( input.liquidityPoolDetails.sourceToken, input.liquidityPoolDetails.pairedToken, sourceEarnedAmount, earnedAmount, 0, 0, to, block.timestamp ); } } ``` The argument `input.liquidityPoolDetails.amountPairedToken` denotes the required amount of the paired token. When paired token is ERC20, the amount transferred from user equals `input.liquidityPoolDetails.amountPairedToken` (refer to code at L57). However, when paired token is ETH, the absence of comparing `msg.value` with `input.liquidityPoolDetails.amountPairedToken` will cause ETH payment amount is less than the actually required amount, `input.liquidityPoolDetails.amountPairedToken`.

Open the report
LowMissing Input ValidationTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

**TokenFiERC20.sol** When deploying new TokenFi ERC20 contract, the absence of validation codes, that ensure `_input.initialSupply` isn't greater than `_input.maxSupply`, causes misleading information displayed on the frontend and the `mint()` function cannot be accessed. ```solidity=410 function mint(address to, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) { require(totalSupply() + amount <= maxSupply, "TokenFiERC20: max supply exceeded"); if (fees.reflection.percentage > 0) { _mintReflection(to, amount); } else { super._mint(to, amount); } } ``` **TokenLauncherPaymentModuleV1.sol** There is no validation for ensuring that the given `amount` isn't greater than the `tokenAddress` amount held by `TokenLauncherPaymentModuleV1` contract. ```solidity=110 function adminWithdraw(address tokenAddress, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) { if (tokenAddress == address(0)) { // We specifically ignore this return value. payable(treasury).call{ value: amount }(""); } else { IERC20(tokenAddress).safeTransfer(treasury, amount); } } ``` The similar issue also exists in `TokenMintPaymentModuleV1.sol`.

Open the report
Show 10 more
LowPotential Assets Lock in `TokenLauncherLiquidityPTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

**TokenLauncherERC20.sol** ```solidity=104 uint256 allowedAmount = token.allowance(input.owner, liquidityPoolFactory); require(allowedAmount >= 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 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); ``` ```solidity=51 require(input.liquidityPoolDetails.amountPairedToken == msg.value, "TokenLauncherLiquidityPoolFactoryV1: INVALID_ETH_AMOUNT"); ``` ```solidity=61 uint256 initialBalance = pairedToken.balanceOf(address(this)); pairedToken.safeTransferFrom(input.owner, address(this), input.liquidityPoolDetails.amountPairedToken); uint256 earnedAmount = pairedToken.balanceOf(address(this)) - initialBalance; pairedToken.safeApprove(input.liquidityPoolDetails.routerAddress, earnedAmount); ``` When creating a V2 liquidity pool and adding liquidity to it, the required source and paired token assets are transferred from users to the `TokenLauncherLiquidityPoolFactoryV1` contract. Their amounts are determined by dynamic argument values, specifically `input.liquidityPoolDetails.amountSourceToken` and `input.liquidityPoolDetails.amountPairedToken`. It's worth noting that these argument values may surpass the actual amount needed by UniswapV2 when adding liquidity. Consequently, the surplus assets remain within the `TokenLauncherLiquidityPoolFactoryV1` contract. In this scenario, the excess assets become locked and accumulated in the `TokenLauncherLiquidityPoolFactoryV1` contract due to the absence of an asset withdrawal function within this contract.

Open the report
LowPotential Duplicated Discount NFT AddressesTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

There is no code to check if the given NFT address is already exists within `discountNFTs`. In this case, the `discountNFTs` may contain duplicated NFT addresses. ```solidity=53 function addDiscountNFTs(address[] memory newDiscountNFTs) external onlyRole(DEFAULT_ADMIN_ROLE) { for (uint256 i = 0; i < newDiscountNFTs.length; i++) { require(newDiscountNFTs[i] != address(0), "TokenLauncherPricingModuleV1::addDiscountNFTs::ZERO: Discount NFT cannot be zero address."); require( IERC721(newDiscountNFTs[i]).supportsInterface(type(IERC721).interfaceId), "TokenLauncherPricingModuleV1::addDiscountNFTs::INVALID: Discount NFT must implement ERC721." ); discountNFTs.push(IERC721(newDiscountNFTs[i])); } } ```

Open the report
LowPotential Sandwich AttacksTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

A sandwich attack may 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 (executing before the target) a transaction to purchase one of the assets and make profits by backrunning (executing after the target) 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()` - `swapExactTokensForTokensSupportingFeeOnTransferTokens()` - `addLiquidity()`

Open the report
LowRedundant StatementsTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The following codes are designed to call correct transfer function based on whether or not `sender/recipient` is excluded. There are only four scenarios and the last `else` branch at L367 would never be executed. **TokenFiERC20.sol** ```solidity=359 if (isExcluded(sender) && !isExcluded(recipient)) { _transferFromExcluded(sender, recipient, amount, isSwap); } else if (!isExcluded(sender) && isExcluded(recipient)) { _transferToExcluded(sender, recipient, amount, isSwap); } else if (!isExcluded(sender) && !isExcluded(recipient)) { _transferStandard(sender, recipient, amount, isSwap); } else if (isExcluded(sender) && isExcluded(recipient)) { _transferBothExcluded(sender, recipient, amount, isSwap); } else { _transferStandard(sender, recipient, amount, isSwap); } ```

Open the report
LowRedundant WETH in The Swap PathTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

According to the code logic of `buyBack()` , we know that `WETH` acts as intermediate token if `pairToken` isn't WETH. **BuyBackHandler.sol** ```solidity=117 address[] memory path = new address[](3); path[0] = info.tokenAddress; path[1] = router.WETH(); path[2] = pairToken; info.initialBalance = _pairToken.balanceOf(address(this)); IERC20(info.tokenAddress).approve(buybackRouter, info.tokensForSwap); router.swapExactTokensForTokensSupportingFeeOnTransferTokens(info.tokensForSwap, 0, path, address(this), block.timestamp); ``` According to the code logic of `_addLiquidity()` function in `TokenLauncherLiquidityPoolFactoryV1.sol`, we know that users can choose non-WETH pair token to create TokenFi erc20 liquidity pool. **TokenLauncherLiquidityPoolFactoryV1.sol** ```solidity=53 if (input.liquidityPoolDetails.pairedToken == weth) { (, , liquidity) = router.addLiquidityETH{ value: msg.value }(input.liquidityPoolDetails.sourceToken, sourceEarnedAmount, 0, 0, to, block.timestamp); } else { uint256 initialBalance = pairedToken.balanceOf(address(this)); pairedToken.safeTransferFrom(input.owner, address(this), input.liquidityPoolDetails.amountPairedToken); uint256 earnedAmount = pairedToken.balanceOf(address(this)) - initialBalance; pairedToken.safeApprove(input.liquidityPoolDetails.routerAddress, input.liquidityPoolDetails.amountPairedToken); (, , liquidity) = router.addLiquidity( input.liquidityPoolDetails.sourceToken, input.liquidityPoolDetails.pairedToken, sourceEarnedAmount, earnedAmount, 0, 0, to, block.timestamp ); } ```

Open the report
LowSolidity 0.8.20 Opcode `PUSH0` May not ExistTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The compiler for Solidity 0.8.20 switches the default target EVM version to [Shanghai](https://blog.soliditylang.org/2023/05/10/solidity-0.8.20-release-announcement/#important-note), which includes the new `PUSH0` op code. This op code may not yet be implemented on all L2s, so deployment on these chains will fail. To work around this issue, use an earlier [EVM](https://docs.soliditylang.org/en/v0.8.20/using-the-compiler.html?ref=zaryabs.com#setting-the-evm-version-to-target) [version](https://book.getfoundry.sh/reference/config/solidity-compiler#evm_version)

Open the report
LowState Variables Are Incompletely UpdatedTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

Within `updateTokenOwner()` function, the argument `tokenType` is used to query token list belonged to `msg.sender`, the token owner. However, there is no validation for `tokenType`. The current code logic can cause inconsistency between state variables values. **TokenLauncherStore.sol** ```solidity=49 function updateTokenOwner( uint8 tokenType, address tokenAddress, address newOwner ) external override { address owner = tokenOwner[tokenAddress]; require(msg.sender == owner, "TokenLauncherStore: Not owner"); require(owner != newOwner, "TokenLauncherStore: Same owner"); address[] storage tokens = _tokensByOwner[tokenType][owner]; for (uint256 i = 0; i < _tokensByOwner[tokenType][owner].length; i++) { if (_tokensByOwner[tokenType][owner][i] == tokenAddress) { _tokensByOwner[tokenType][owner][i] = _tokensByOwner[tokenType][owner][tokens.length - 1]; _tokensByOwner[tokenType][owner].pop(); break; } } _tokensByOwner[tokenType][newOwner].push(tokenAddress); tokenOwner[tokenAddress] = newOwner; emit TokenOwnerUpdated(currentBlockTokenOwnerUpdated, tokenType, owner, newOwner); currentBlockTokenOwnerUpdated = block.number; } ``` When calling `updateTokenOwner()` function, the caller can pass in arbitrary `tokenType` argument value. This may let the local variable `tokens` to be empty array, the for loop at L58-64 is skipped and `tokenOwner[tokenAddress] = newOwner` is successfully executed. In this case, the `tokenAddress` owner is updated, however, it still stays in the original owner token list.

Open the report
LowThe Added Liquidity Pool Cannot be CheckedTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The purpose of the `isExchangePool()` function is to verify whether the specified liquidity pool has been created. However, the current implementation of the function is incomplete, as it contains an empty function body, resulting in it always returning a value. Consequently, users may receive inaccurate results when attempting to confirm the addition of a liquidity pool and the check code at L105 becomes meaningless. ```solidity=133 function isExchangePool(address pool) external view returns (bool) {} ``` ```solidity=105 if (!token.isExchangePool(lpOutput.liquidityPoolToken)) { token.addExchangePool(lpOutput.liquidityPoolToken); } ```

Open the report
LowUnnecessary `renounceRole()` Function CallTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

```solidity=53 TokenFiERC721 token = new TokenFiERC721(input); bytes32 paymentAdminRole = ITokenMintPaymentModule(token.paymentModule()).PAYMENT_ADMIN_ROLE(); ITokenMintPaymentModule(input.tokenMintPaymentModule).grantRole(paymentAdminRole, address(token)); // Log new token into store ITokenLauncherStore.CreateTokenInput memory storeInput = ITokenLauncherStore.CreateTokenInput( address(token), input.owner, input.referrer, price.usdPrice, paymentOutput.feeCollected, paymentOutput.referrerSharePaid, paymentOutput.usdBurned, paymentOutput.flokiBurned, uint8(ITokenLauncherPricingModule.TokenType.ERC721) ); ITokenLauncherStore(tokenLauncherStore).addToken(storeInput); token.renounceRole(token.DEFAULT_ADMIN_ROLE(), address(this)); ``` It is unnecessary to call `renounceRole()` function in `TokenFiERC721` contract, at L70, because the `DEFAULT_ADMIN_ROLE` never granted to `TokenLauncherERC721` contract. The default `DEFAULT_ADMIN_ROLE` of TokenFi ERC721 token is its owner address. ```solidity=39 _grantRole(DEFAULT_ADMIN_ROLE, _input.owner); _grantRole(PAUSER_ROLE, _input.owner); _grantRole(SIGNER_ROLE, _input.owner); ``` The `TokenLauncherERC1155` and `TokenLauncherERC20` contracts have the similar issue.

Open the report
LowUnused Argument VariableTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The argument `nonce` isn't used both in `mintWithSignature` and `checkSignature()` functions. **TokenFiERC721.sol** ```solidity=110 function mintWithSignature( address _to, string memory _uri, string memory signature, uint256 nonce ) external whenNotPaused supplyAvailable checkSignature(_uri, nonce, signature) { string[] memory _uris = new string[](1); _uris[0] = _uri; _mintBatch(_to, 1, _uris); } ``` ```solidity=179 modifier checkSignature( string memory message, uint256 nonce, string memory signature ) { bytes32 messageHash = keccak256(abi.encodePacked(message, msg.sender)); bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", nonces[msg.sender], messageHash)); address recoveredAddress = _recoverSigner(ethSignedMessageHash, signature); require(hasRole(SIGNER_ROLE, recoveredAddress), "TokenFiERC721: invalid signature"); nonces[msg.sender] += 1; _; } ```

Open the report
Show 5 more
LowUnused Local VariablesTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The variable `receivedFloki` is both not assigned values in `_swapFlokiByWeth()` function and not used in `_processFlokiPayment()` function.

Open the report
LowUnused State VariableTokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The cited state variables are not used in the codebase. This can lead to incomplete functionality or potential vulnerabilities if these variables are expected to be utilized.

Open the report
LowUseless State Variable `primaryPool`TokenFi Token Launcher · CertiK · 6 Feb 2024Fixed

The local variable `pairAddress` at L66 is determined by the TokenFi token contract which is the caller of `buyBack()` function. Its value cannot be predicted before calling the `buyBack()` function. So the state variable `primaryPool`, whose value is assigned when deploying `BuyBackHandler` contract, may be inconsistent with the actually needed TokenFi token contract. ```solidity=66 address pairAddress = primaryPool; // get pair address from router if (pairAddress == address(0)) { pairAddress = IUniswapV2Factory(router.factory()).getPair(info.tokenAddress, router.WETH()); } ```

Open the report
LowMissing Zero Address ValidationTokenFi · CertiK · 31 Oct 2023Fixed

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. ```solidity=75 taxHandler = ITaxHandler(taxHandlerAddress); treasuryHandler = ITreasuryHandler(treasuryHandlerAddress); ``` - `taxHandler`,`treasuryHandler` are not zero-checked before being used. ```solidity=60 treasury = payable(treasuryAddress); ``` - `treasuryAddress` is not zero-checked before being used.

Open the report
LowUntrusted Router Address RiskTokenFi · CertiK · 31 Oct 2023Fixed

The contract allows the router address to be set arbitrarily during contract deployment. This introduces a potential vulnerability where a malicious router can be set during the contract deployment process. A malicious router could potentially behave in unexpected ways, leading to loss of funds or other vulnerabilities being exploited.

Open the report

Contracts

No deployed address is recorded for these audits.

The reports name a repository or files. Open a report to see its scope.

The repositories and files the reports name

Show this on your site

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.

Content

Facts

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.

Fixed as
Language

Style

Layout
Theme
Accent
Corners

Format

Size
Date
Embed as
HTML
<head>
<script src="https://widget.trustblock.run/project-widget.min.js" defer></script>
</head>
<body>
<div
data-tb-project-widget
data-tb-project-slug="tokenfi"
></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