How to add Fees to ERC20 Tokens

We want now to add a fee system into our newly created ERC20

In the world of economy, fee systems play a crucial role.
In DeFi is the same, but here at least we make the rules and all is transparent.
Have you ever wondered about the fees you pay when you use your VISA card at a store?
Well, in the world of Ethereum Virtual Machine (EVM) you can create a fee system directly within your smart contract.
This guide will walk you through the process, so let's get started!
(Prepare some coffee as this is gonna be long)


My way-to-go for brainstorming & trying some logic is using Remix.

When approaching a new system I find it helpful to start with brainstorming and logic experimentation.
In this phase I recommend focusing on the main core logic of your idea before diving into the actual contract code.
Break down the logic into easy functions.

Here are some key concepts to keep in mind as you work through the process:

  • Tokens are not in your wallet, never has been.
  • Your balance is a number in a mapping inside the token contract.
  • Your account never receive anything besides ETH
  • External applications display your balance based on the current blockchain state.
  • Blockchain explorers rely on events emitted by smart contracts, though this data may not always be completely accurate or truthful.

Are you confused?
sorry for that.
Some discoveries has been quite strange even for me... but that's nothing compared to what I've discovered months later...

Anyway, we're on Remix now.
Let me give you a little bonus here.
Let's create a new contract called Fees.sol and let's prepare a basic contract called "TokenWithFees" inheriting ERC20

  • ERC20 decimals() by default (if you don't override it) returns 18. Leave it there for now and create another contract inside the same file called "Fees". image

This is a basic implementation of a token.
(notice: Anyone can modify anyone's balances)
Nobody can trade that as it's not an ERC20 token.
image

To be an ERC20-compatible token you MUST respect all the IERC20 interface.
EVERY function must have same parameters & same return parameters except if you're called USDT and you're kinda special...

Focus on the fees now.

What are the fees?
How we can apply those?
What we need to do?

We need to subtract numbers from the amount in the transfer.
Where?

function _update

How? By overriding it and applying the fee logic.

So let's write what we need to reach the fee logic from scratch.

Why from scratch? because you keep your mind trained to be at the ground level. Doing the boring thing over and over again will make you faster and precise. So fast that you will question why it took you so long to do things. Like a muscle is something you need to train.

function _update(address sender, address recipient, uint256 amount) internal override {
        uint tradeType = 0; // by default, it's a transfer
        uint fee;
    // checks
        require(amount != 0, "Amount must be greater then 0");
    // getting order type
        if (sender == pair) { // buy
            tradeType = 1;
            fee = amount * fees.buy / 100;
        } else if (recipient == pair) { // sell
            tradeType = 2;
            fee = amount * fees.sell / 100;
        } else {
            fee = amount * fees.transfer / 100;
        }
    // apply fee if necessary
        if (fee > 0) {
            amount -= fee;
            super._update(sender, feeReceiver, fee);
            emit FeeCollected(sender, feeReceiver, fee);
        }
    // transfer tokens
        super._update(sender, recipient, amount);
    }

Let's add some ""trading"" logic
We have 3 choices now

  1. Test it on Remix
  2. Test it on HardHat
  3. Test it on Foundry

Let's stick to Remix as it's a really nice IDE to test things fast with the UI. image What we did here:

  1. added a mapping address user --> uint balance to store balances (Like ERC20)
  2. uint to track fees, 5%
  3. uint to track token prices, it's 100:1ETH (won't lose the peg btw)
  4. constructor to add the address who will receive fees & 100 tokens to us because we're so cool
  5. Two events to track buy/sell off-chain (emit = "dear external scripts, at this block.number, this happened: [EVENT]")
  6. Three functions to handle the trades
    1. buyTokens() is payable, we need to send ETH to the contract (if the function is payable you don't need receive() external payable {})
      1. we add the ETH * tokensPerETH to the user balance
        if you wonder why / 10 ** 18 it's because i don't like decimals during brainstorming BUT BE CAREFUL! it may tricks you! also never do division BEFORE multiplication, it can lead to precision error.
        In brainstorm session all is permitted to a certain degree!!!
      2. we emit the buy event
    2. sellTokens() with one parameter, the amount of tokens to sell for ETH
      1. Calculate the amount of ETH we will receive (without decimals)
      2. Remove the amount of tokens from user's balance
      3. Send the ETH to the seller (with the decimals otherwise he will be so angry)
      4. Require the calls to be completed without problems
      5. Emit the sell event

Let's try it out!
(btw, do you need a gif recorder?)

Ok so far so good but we still have no fees.
Let's continue.
image

I've created a new function applyFee, who returns the final amount of tokens and the fee subtracted.
I've also added an event when we collect fees.
Finally i've edited both buy&sell functions to apply fees.

Now... let's try again and watch carefully.

what i didn't use earlier?

Fees are not working!
Where???
decimals! of course! 😡

This is one of the maaany "strange" errors you'll get.
I've ran into this error while writing this guide but today those kind of problems takes me few seconds to detect & resolve.
When I've started i lost hours and hours on stupid errors like this.
Now let's fix that!

We're back on track!
Have you noticed that we take the fees on the ETH-side instead of the token-side on the sell?
That's up to you and to your fee design.
Some systems take the fee on the token and keep it on the contract or forward it to an external account, some others take it on ETH, some others both.
That's a design choice, we don't need to go further in that brainstorm/training session.

Great! now we made a fee system from a blank contract.

Now let's add it to the ERC20! (finally?????)
Say hello to remix and close it.
(no, we don't copy the code, we do the ERC20 implementation from scratch... again.)
Let's go back to our lovely HardHat workspace.
image from now on, i'll remove all the natSpec/useless comments to focus on the code.
If you're uncertain, please read the old article [here]

Let's add the storage, events and edit the constructor.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

contract TokenWithFees is ERC20, Ownable {
    address public feeReceiver;
    address public pair;
    Fees public fees;

    struct Fees {
        uint buy;
        uint sell;
        uint transfer;
        uint burn;
    }

    event FeesSet(uint buy, uint sell, uint transfer, uint burn);
    event FeeReceiverSet(address feeReceiver);
    event PairSet(address pair);
    event FeeCollected(address from, address to, uint amount);

    constructor(string memory tokenName, string memory tokenSymbol, uint tokensToMint, address _feeReceiver) ERC20(tokenName, tokenSymbol) Ownable(msg.sender) {
        _mint(msg.sender, tokensToMint * 10 ** decimals());
        feeReceiver = _feeReceiver;
    }

    function setFees(uint buy, uint sell, uint transfer_, uint burn) external onlyOwner {
        require(buy + sell + transfer_ + burn <= 20, "Fees must be equal or less then 20%");
        fees = Fees(buy, sell, transfer_, burn);
        emit FeesSet(buy, sell, transfer_, burn);
    }

    function setFeeReceiver(address _feeReceiver) external onlyOwner {
        feeReceiver = _feeReceiver;
        emit FeeReceiverSet(feeReceiver);
    }

    function setPair(address pair_) external onlyOwner {
        pair = pair_;
        emit PairSet(pair);
    }

    function _update(address sender, address recipient, uint256 amount) internal override {
        uint tradeType = 0; // by default, it's a transfer
        uint fee;
    // checks
        require(amount != 0, "Amount must be greater then 0");
    // getting order type
        if (sender == pair) { // buy
            tradeType = 1;
            fee = amount * fees.buy / 100;
        } else if (recipient == pair) { // sell
            tradeType = 2;
            fee = amount * fees.sell / 100;
        } else {
            fee = amount * fees.transfer / 100;
        }
    // apply fee if necessary
        if (fee > 0) {
            amount -= fee;
            super._update(sender, feeReceiver, fee);
            emit FeeCollected(sender, feeReceiver, fee);
        }
    // transfer tokens
        super._update(sender, recipient, amount);
    }
}

Starting to get big uh!!!
Remember, each line of code can open up to an infinite number of exploits.

Let's recap what we did here

  1. added storage for fees, pair and feeReceiver
  2. added events
  3. edited constructor for feeReceiver
  4. created functions to setFee and feeReceiver
  5. override of _update

Now let's run our tests again image Damn krako wtf, this book is full of errors!

Yeah i know, that's how i learned what you've reading.
I'm showing you all the errors so you can identify and fix them faster.
That's an underrated soft-skill that will come handy sometimes, i can guarantee.

Now go edit your tests and run them again! you know what to do :P image (if you don't, constructoooor)

Wonderful! now we need to configure the fees!
Let's edit our test file. image I've added this under the "add Liquidity" it block.

Now let's run the tests. image So we got one error here and we got SO CLOSE to another.

We add liquidity BEFORE applying the fees, notice that as that's a design.
If we set the fees on constructor, our addLiquidity will get taxed!

With systems like uniswap v2 you CANNOT take fees from the ETH side.
Means you CANNOT take the fees directly from the ETH used to buy the token.

why?
because Uniswap EXPECT that amount in the pair.
You need to create a swap system, but that's the tutorial for the fees.

Let's go down to this.
This is kinda advance so feel free to skip this part.

npx hardhat test --trace

image

Don't be scared, hardhat-tracer is one of your BEST friends out there.

So the revert is happening inside the pair contract. image Why is this happening?

Because we're removing the amount of tokens in the transaction between the router AND the pair.
This create an unbalance between the tokens:ETH in the pool, Tokens * ETH = k
If k is not a constant anymore the transaction will revert. We need to call swapExactTokensForETHSupportingFeeOnTransferTokens instead. image Run the tests again image oof... we're back on track.

Do you know how much time you would spend on remix for doing that at hand??? Hours!
(But don't forget remix has scripting too! so it can be powerful anyway!)

Seems all is good now! let's write some more tests.
We need to expect the precise amounts, precise precise precise!! image This will check if the buyer is taxed 5% and the balance of the feeReceiver will increase by 5%.

Let's go deeper and add expects into the emits too. image Let's add that check on the sell-side too! image

We successfully added a fee system into our ERC20!

It's pretty standard, but if you've done it from scratch, now you will remember it better.

This template has been made based on a copy of /erc20 template.
If you wanna do practice, re-write this system and then compare the files to my workspace for this guide.
Once it's 1:1, do it again without looking the guide.
Once you're done, iterate and make your own.