Uncertainty regarding the implementation of the liquidity fee

Hello OpenZeppelin Community,

I work on an ERC20 token and integrate into it my own functionality for transaction fees. I have a specific question regarding the implementation of liquidity fees in my _transfer function.

In my contract I charge a 2% transaction fee for marketing and another 2% for liquidity. Here is a simplified part of my code to give an idea:

// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";

contract CustomToken is ERC20, ERC20Burnable, ReentrancyGuard, Ownable {
    // Public variables for contract configuration
    address public marketingWallet;
    address public uniswapV2RouterAddress;
    IUniswapV2Router02 public uniswapV2Router;
    address public uniswapV2Pair;

    // Constants and state variables
    uint256 public constant TOTAL_SUPPLY = 450000000000000 * 10**18;
    uint256 public minEthBeforeLiquidity = 0.05 ether;
    uint256 public lastLiquidityAddTime;

    // Constructor to initialize the contract with specified parameters
    constructor(
        address _marketingWallet,
        address _uniswapV2RouterAddress,
        address initialOwner
    ) ERC20("Testing", "GRC") {
        marketingWallet = _marketingWallet;
        uniswapV2RouterAddress = _uniswapV2RouterAddress;

        // Initialize Uniswap V2 Router and Pair
        uniswapV2Router = IUniswapV2Router02(uniswapV2RouterAddress);
        uniswapV2Pair = IUniswapV2Factory(uniswapV2Router.factory()).createPair(address(this), uniswapV2Router.WETH());

        // Mint the total supply to the initial owner
        _mint(initialOwner, TOTAL_SUPPLY);
    }

    // Overridden _transfer function to implement custom transfer logic with fees
    function _transfer(address from, address to, uint256 amount) internal virtual override {
        // Calculate marketing and liquidity fees
        uint256 marketingFee = (amount * 2) / 100;
        uint256 liquidityFee = (amount * 2) / 100;
        uint256 fees = marketingFee + liquidityFee;

        // Calculate the amount after deducting fees
        uint256 sendAmount = amount - fees;

        // Perform the transfer with the deducted amount
        super._transfer(from, to, sendAmount);

        // Transfer marketing fee to the marketing wallet
        if (marketingFee > 0) {
            super._transfer(from, marketingWallet, marketingFee);
        }

        // Check conditions for adding liquidity and add if conditions are met
        uint256 minTimeInterval = 24 hours; // Example: 24 hours
        if (liquidityFee > 0 && from != uniswapV2Pair && address(this).balance >= minEthBeforeLiquidity && (block.timestamp - lastLiquidityAddTime) >= minTimeInterval) {
            addLiquidity(liquidityFee, address(this).balance);
            lastLiquidityAddTime = block.timestamp;
        }
    }

    // Internal function to swap tokens for ETH
    function swapTokensForEth(uint256 tokenAmount) private {
        // Setup the path for token swap
        address[] memory path = new address[](2);
        path[0] = address(this);
        path[1] = uniswapV2Router.WETH();

        // Approve token amount for the router and perform the swap
        _approve(address(this), address(uniswapV2Router), tokenAmount);
        uniswapV2Router.swapExactTokensForETHSupportingFeeOnTransferTokens(
            tokenAmount,
            0, // Minimum amount of ETH to accept
            path,
            address(this),
            block.timestamp
        );
    }

    // Internal function to add liquidity to Uniswap
    function addLiquidity(uint256 tokenAmount, uint256 ethAmount) private {
        // Approve token amount for the router
        _approve(address(this), address(uniswapV2Router), tokenAmount);

        // Add liquidity to Uniswap pool
        uniswapV2Router.addLiquidityETH{value: ethAmount}(
            address(this),
            tokenAmount,
            0, // Minimum amount of tokens to add
            0, // Minimum amount of ETH to add
            owner(), // Recipient of the liquidity tokens
            block.timestamp

I have checked the transactions on the Goerli testnet and I am not sure where these fees are stored.

Here is a link to the relevant transaction on Goerli:

(https://goerli.etherscan.io/tx/0xe7c56de792f4fd88c6e53d47c98ad7e8e79cb82ab6f1002d632c88fde2fd4efe)

I would be very grateful for any suggestions, examples or advice on how to efficiently and securely process these types of fees within a smart contract.

Thank you in advance for your help and advice!

1 Like

Your contract function transfers them to marketingWallet, whose address is known to you, because it is one of the input values which you have passed upon the deployment (construction) of your contract.

1 Like

Good morning, I didn't even realize it, but now that I think about it, I have set up in the contract that the addliquidity function should be called when the contract reaches 0.05 ETH, but here comes the problem, at least that's what I think, since the contract doesn't have any ETH, it can't be called. So, I changed the logic:

function _transfer(address from, address to, uint256 amount) internal virtual override {
    // Calculate the marketing and liquidity fees as 2% of the transaction amount
    uint256 marketingFee = (amount * 2) / 100;
    uint256 liquidityFee = (amount * 2) / 100;

    // Sum up the total fees
    uint256 fees = marketingFee + liquidityFee;

    // Calculate the amount to be transferred after deducting fees
    uint256 sendAmount = amount - fees;

    // Execute the transfer with the reduced amount
    super._transfer(from, to, sendAmount);

    // Transfer the marketing fee to the marketing wallet, if applicable
    if (marketingFee > 0) {
        super._transfer(from, marketingWallet, marketingFee);
    }

    // Set a threshold for accumulated tokens (e.g., 1000 tokens)
    uint256 accumulatedTokenThreshold = 1000 * 10**18;

    // If liquidity fee is charged, the sender is not the Uniswap pair, and the token balance
    // meets the threshold, then swap accumulated tokens for ETH
    if (liquidityFee > 0 && from != uniswapV2Pair && balanceOf(address(this)) >= accumulatedTokenThreshold) {
        swapAccumulatedTokensForEth(balanceOf(address(this)));
    }

    // Set a minimum time interval for adding liquidity (e.g., 24 hours)
    uint256 minTimeInterval = 24 hours;

    // If the liquidity fee is greater than zero, the sender is not the Uniswap pair,
    // the contract's ETH balance meets the minimum threshold, and the required time
    // has passed since the last liquidity addition, then add liquidity
    if (liquidityFee > 0 && from != uniswapV2Pair && address(this).balance >= minEthBeforeLiquidity && (block.timestamp - lastLiquidityAddTime) >= minTimeInterval) {
        addLiquidity(liquidityFee, address(this).balance);
        lastLiquidityAddTime = block.timestamp; // Update the time of the last liquidity addition
    }
}

Maybe it's better now, but I'd still like to hear advice from someone more experienced

What has that got to do with your original question about the fees?

Hi, I apologize since you already wrote where the tax for liquidity goes, but the problem also occurs with the addLiquidity function because of:

and another problem would be how the addLiquidity function determines which tokens should be exchanged for ETH when everything would be in the marketing wallet.

What problem? You haven't described any problem in the original question, you just asked where the tax goes. If there's a problem, then you need to describe it. If there are two separate issues, then you need to elaborate on each one of them separately, perhaps even in two separate posts. Otherwise, it is rather difficult to conduct an efficient process of helping you out.

The problem is that when I execute a transaction, the 2% liquidity tax either doesn't execute, or it does execute but gets stored in the marketing wallet as mentioned above. Another issue is that it doesn't get stored in the contract, which then prevents the liquidity addition logic from executing. I don't know how to define the logic for adding liquidity. :cold_sweat:

Well, your entire question remains unclear.
It gets stored in the marketing wallet, because that's exactly what you wrote in your code:

super._transfer(from, marketingWallet, marketingFee);

If that's a problem, then why did you write your code this way, and why not just simply change it to transfer the tax to where you actually want it to go?

Is it possible to change it so that the tokens from the liquidity tax are accumulated on the contract? Then I need to modify this function: uint256 public minEthBeforeLiquidity = 0.05 ether; but how to set it up to be gas-efficient?

Yes:

super._transfer(from, address(this), liquidityFee);

I'm going to test it for now, thank you. I'll let you know later. For now, take care. :pray:

Two options:

  1. uint256 public constant minEthBeforeLiquidity = 0.05 ether;
  2. uint256 public immutable minEthBeforeLiquidity = 0.05 ether;
1 Like

Hello Barakman, I did it and changed it as you said. I deployed the contract and verified it, and now it is accumulating tokens from the given liquidity. https://goerli.etherscan.io/address/0xabaa10e340ce9005165d96f7b3e744043a7a2f1b

 function _transfer(address from, address to, uint256 amount) internal virtual override {
        uint256 marketingFee = (amount * 2) / 100;
        uint256 liquidityFee = (amount * 2) / 100;
        uint256 fees = marketingFee + liquidityFee;
        uint256 sendAmount = amount - fees;

        super._transfer(from, to, sendAmount);

        if (marketingFee > 0) {
            super._transfer(from, marketingWallet, marketingFee);
        }

        if (liquidityFee > 0) {
            super._transfer(from, address(this), liquidityFee);
        }

However, I am encountering an issue when initiating a transaction to add liquidity. Specifically, I am facing an error 'TransferHelper: TRANSFER_FROM_FAILED', which is caused by the transaction failing when trying to transfer tokens to the Uniswap Router. I tried using both a script and the Uniswap website.

reason: 'execution reverted: TransferHelper: TRANSFER_FROM_FAILED',
  code: 'UNPREDICTABLE_GAS_LIMIT',
  method: 'estimateGas',
  transaction: {
    from: '0x4C3Fd440deaA3Ea0127750932253cB7897C0f177',
    to: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D',
    value: BigNumber { value: "1000000000000000000" },
    data: '0xf305d719000000000000000000000000abaa10e340ce9005165d96f7b3e744043a7a2f1b0000000000000000000000000000000000000f87542f5e5479abb53eb00000000000000000000000000000000000000000000f87542f5e5479abb53eb00000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000004c3fd440deaa3ea0127750932253cb7897c0f177000000000000000000000000000000000000000000000000000000006592d131',
    accessList: null
  },
  error: ProviderError: execution reverted: TransferHelper: TRANSFER_FROM_FAILED
      at HttpProvider.request (C:\Users\GamingPC\Desktop\token\node_modules\hardhat\src\internal\core\providers\http.ts:88:21)
      at processTicksAndRejections (node:internal/process/task_queues:95:5)
      at EthersProviderWrapper.send (C:\Users\GamingPC\Desktop\token\node_modules\@nomiclabs\hardhat-ethers\src\internal\ethers-provider-wrapper.ts:13:20)
}
PS C:\Users\GamingPC\Desktop\token>

Have you approved the Uniswap Router contract to transfer tokens from your account?

yes, but after I click on create pool and supply it doesn't work

Well, you're posting fragments of information and describing fragments of the problem, so it's a little difficult to address your question.

Your description needs to include the exact sequence of transactions that you are executing, each contract function which you are calling, the input values that you are passing to each one of these functions, and if possible then also the relevant transaction hashes.

And all - within the same single comment (i.e., don't make your readers have to scan this entire thread and gather up those pieces of information).

Here, you have only provided the code of one INTERNAL function, which is obviously NOT the function which you are trying to execute.

So in short, it is rather difficult to address your question when posted the way it is.

I can take a wild guess and tell you that since you have overridden the ERC20 _transfer function and made it NOT transfer the entire requested amount, the Uniswap Router contract function has detected that and reverted the transaction.

And why wouldn't it? This DEX is expected to perform a fair trade, where the original input amount MUST be transferred in full. Otherwise, it is putting itself or its users (or both) at risk of getting "ripped off" sort to speak.

In other words - DEXs are not very fond of tax tokens, which is what your token appears to be.

Thank you for your detailed explanation and valuable advice. I understand the need to provide complete and specific information. I apologize for the misunderstanding and I am grateful for your patience.Here is a more detailed description of my problem:
Change in Variable Definition:

  • Before: uint256 public minEthBeforeLiquidity = 0.05 ether;
  • After: uint256 public constant minEthBeforeLiquidity = 0.05 ether;

I made this change to set minEthBeforeLiquidity as a constant value, meaning that it will not change during the lifetime of the contract. I had no problem creating liquidity before

Adding the _Transfer Function:

  • I added the line: super._transfer(from, address(this), liquidityFee);
  • This line was added to transfer a certain percentage of transactions (defined as liquidityFee) to the contract's address. This step is part of the process of adding liquidity.

I made these changes to improve the liquidity mechanism in my token contract. I noticed that after these changes, there is a problem interacting with the Uniswap Router contract, specifically with the error "TransferHelper: TRANSFER_FROM_FAILED".

Token Burning: I burned 30% of the total number of tokens, which is important in terms of the overall supply.

Liquidity Creation: When creating liquidity, I used all the tokens in the wallet. The approval (approve) went through without any problems. Attaching txn:

Problem with Pool Creation: When I proceeded to create the pool and supply liquidity ("create pool & supply"), a problem occurred. I decided to examine the Uniswap website, and here is a list of errors:

MetaMask - RPC Error: execution reverted: TransferHelper: TRANSFER_FROM_FAILED Object
(anonymné) @ VM57:1
instrument.js:1 Error: cannot estimate gas; transaction may fail or may require manual gas limit [ See: https://links.ethers.org/v5-errors-UNPREDICTABLE_GAS_LIMIT ] (reason="execution reverted: TransferHelper: TRANSFER_FROM_FAILED", method="estimateGas", transaction={"from":"0x4C3Fd440deaA3Ea0127750932253cB7897C0f177","to":"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D","value":{"type":"BigNumber","hex":"0x0de0b6b3a7640000"},"data":"0xf305d719000000000000000000000000abaa10e340ce9005165d96f7b3e744043a7a2f1b0000000000000000000000000000000000000f87d56e97cd723fbec2f00000000000000000000000000000000000000000000f87d56e97cd723fbec2f00000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000004c3fd440deaa3ea0127750932253cb7897c0f1770000000000000000000000000000000000000000000000000000000065931edc","accessList":null}, error={"code":-32603,"message":"execution reverted: TransferHelper: TRANSFER_FROM_FAILED","data":{"originalError":{"code":3,"data":"0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000245472616e7366657248656c7065723a205452414e534645525f46524f4d5f4641494c454400000000000000000000000000000000000000000000000000000000","message":"execution reverted: TransferHelper: TRANSFER_FROM_FAILED"}}}, code=UNPREDICTABLE_GAS_LIMIT, version=providers/5.7.2)
    at d.makeError (index.js:1:14)
    at d.throwError (index.js:1:14)
    at b (json-rpc-provider.js:1:14)
    at me.<anonymous> (json-rpc-provider.js:1:14)
    at Generator.throw (<anonymous>)
    at s (json-rpc-provider.js:1:14)
(anonymné) @ instrument.js:1

"I will be grateful for any suggestions or guidance on how to solve this problem and improve the compatibility of my contract with Uniswap.

Again - your description is a total mess.

Here are a few points, for example:

  • You are not stating the problem in a coherent manner, from start to end.
  • You are describing changes that have apparently taken place during your work, and that are apparently relative to previous comments here, which means that the reader needs to scroll up and gather a bunch of pieces of information scattered throughout this thread.
  • You are incorporating details which seem completely unrelated to the problem, for example, changing some variable in your contract from non-constant to constant (and that's before you even describe what the actual problem is, when that detail is obviously not related to ANY problem whatsoever).

Your ongoing process is not important, and describing it here just adds a ton of noise, which means that no one is going to bother to even read through your question.

My suggestion to you (one last time):

  • Specify which contract function you are calling
  • Specify what input values you are passing to that function
  • Specify the source code of that function
  • Specify any other relevant source code (related state variables, etc)
  • Specify the transaction details (gas, eth value, etc)
  • Specify the exact error-message
  • Specify any previous steps that you've taken prior to executing the failed transaction (e.g., executing function approve), including all the relevant details (e.g., input values passed to function approve)

Lastly, you seem to have completely ignored my previous note about the Uniswap Router function reverting due to the tax-mechanism implemented in your token contract:

Hi barakman

I am facing the following issue while interacting with Uniswap through their web interface:

Activity on Uniswap: I am trying to add liquidity for my own token and ETH on Uniswap V2 using their web interface. The goal is to deposit these assets into a liquidity pool.

Approval: I have performed the approval of my token for the Uniswap Router with the following details:

  • Token Owner: 0x4c3fd440deaa3ea0127750932253cb7897c0f177
  • Spender Address (Uniswap Router): 0x7a250d5630b4cf539739df2c5dacb4c659f2488d
  • Amount of Approved Tokens: 314998000000000 RTR

Transaction Details:

  • Gas Limit: 29238 units
  • Gas Used: 29238 units
  • Base Fee: 0.000000009 GWEI
  • Priority Fee: 1.5 GWEI
  • Total Gas Fee: 0.000044 GoerliETH

Error Message: VM281:1 MetaMask - RPC Error: execution reverted: TransferHelper: TRANSFER_FROM_FAILED

  1. Object

  2. code: -32603

This issue occurred while using standard functions of the Uniswap web interface, such as addLiquidity or addLiquidityETH, which internally call Uniswap's smart contracts. I do not have access to the source code of these internal functions, as I was calling these functions directly through the Uniswap web interface.

I would be very grateful for any suggestions or guidance that could help me solve this problem and ensure the successful addition of liquidity for my token on Uniswap. :cold_sweat:

Of course you do - since you have the Uniswap Router contract address at hand, you can easily view the entire source code of this contract on etherscan.

But instead of looking into those functions, you may as well just search where the error-message TRANSFER_FROM_FAILED appears in that code.

And if you do, then you'll see that it only appears in one place:

function safeTransferFrom(address token, address from, address to, uint value) internal {
    // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
    (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
    require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FROM_FAILED');
}

Which means that whatever function on the Uniswap Router contract you called, this function has ended up reverting when it attempted to call function transferFrom on the designated token contract, which is... YOUR token contract.

So what you need to do is to investigate the related code on YOUR contract.

Function transferFrom is implemented in the ERC20 contract which your contract inherits from.

It calls function _transfer, which is also implemented in the ERC20 contract which your contract inherits from, but since you have overridden that function in your contract, function transferFrom ends up calling function _transfer on YOUR contract.

You haven't provided the exact code of that function, so I'm having to rely on several different versions of it which you have posted throughout this thread.

And this is where things go... a little weird...

Inside function _transfer, you seem to be calling function addLiquidity in YOUR contract.

Again, you haven't provided the code of that function, but by name, I am inclined to guess that this function calls function addLiquidity on the Uniswap Router contract...?

If so, then what you get here is a kind of a recursive loop, starting as you add liquidity via the Uniswap web interface:

  • Which calls function addLiquidity on the Uniswap Router contract
  • Which calls function transferFrom on your contract
  • Which calls function _transfer on your contract
  • Which calls function addLiquidity on your contract
  • Which calls function addLiquidity on the Uniswap Router contract