[Modifying Smart Contract Based On ERC-20 Token for Fee Management]

Introduction

Greetings, everyone! Today I want to speak about interesting case in my practice of developing smart contracts. During the development of a contract based on the ERC-20 standard for tokens on the Ethereum blockchain, one of the interesting aspects was the possibility of charging fees from exchange pools. Client was deeply concerned about an ability to create a lot of swap pools that would be out of his control, using his token as a step in a sequence of transactions. To meet the client's needs, it was decided to modify the contract to include mechanisms for fee management.

The problem

The mechanism for managing fees is a common issue when developing smart contracts. At first glance, the task seems trivial – simply add a pool registration system and a mapping of fees or a general fee level for all registered pools.

However, what if one pool uses another in a transaction chain? Wouldn't we end up in an awkward situation, charging fees multiple times or consuming too much gas to determine the status of transaction participants?

The problem of optimizing gas flow is especially crucial for Ethereum blockchain.

The solution

As part of the updates, functionality was introduced that allows the contract owner to specify addresses to be recognized as exchange pools. This enables the contract creator to control which transactions are subject to fees based on their originating addresses.

function setFeeOn(address feeAddress, bool enabled) external onlyOwner {
    if (feeAddress == address(0)) {
        revert ZeroAddress();
    }
    feeOn[feeAddress] = enabled;
    emit SetFeeAddress(feeAddress, enabled);
}

setFee method was introduced, allowing the setting of a universal fee percentage for all exchange pools. This method gives the contract owner an efficient and cost-effective (in terms of gas) way to regulate the fee levels within the system.

function setFee(uint32 fee_) external onlyOwner {
    emit SetFee(tradeFee, fee_);
    tradeFee = fee_;
}

And of course here comes an overriding update for _update method:

function _update(
    address from,
    address to,
    uint256 value
) internal override onlyInitiated {
    uint256 calculatedFee;
    if (feeOn[to] != feeOn[from]) {
        calculatedFee = (tradeFee * value) / 1000000;
        ERC20._update(from, owner(), calculatedFee);
    }
    ERC20._update(from, to, value - calculatedFee);
}

All the magic of defining entities of participants of any transfer is hidden in the next simple condition:

feeOn[to] != feeOn[from]

Down the text we would name this condition as fee condition.

In the other words, the fee would not be payed when two users interacts between themselves or when two registered pools interact between themselves.

Examples

Presenting the main entities of our equation:

  • our token (token А)
  • some registered swap pool with some other token
  • another token paired with ours in some swap pool
  • users
  • unregistered swap pools that are using our pool as a step in a sequence of transactions.

We will try some though experiments with transactions sequences and listed entities

Example 1

User with token A swaps it for another token through the registered pool. User pays fee at once. It's trivial and simple behaviour.

Example 1

Example 2

User with token B want to swap it on token C, but there are no direct pools between B and C. So, another pool uses ours as a step in transactions sequence.

How swaps look like:

Token B => Token A => Token C

So, user goes through swap pool which is swapping token B on A and uses ours pool to swap token A on C, then gives user token C. There are at least two pools in this sequense, let's call them pool 1 and pool 2. Anyway, one of this pools must be ours (if only we are not talking about the case when we lost control of the token).

Let's look at this sequence in details taking into account our knowledge about how new _update() method works.

Case A. Both pools are registered

If both pools are registered pools, the fee condition would be true when at the first step when we go to the pool, but all other transactions would have no fee. You can see that step-by-step on the image below.

Both Pools Are Registered
Case B. Unregistered pool

So, one of the pools had not been registered, but there are still two _update operations on token A! This means, that by ours fee contidion the fee would be taken at least twice.

Unregistered Pool

Conclusion

Gas flow for our token linear transfer is equal ~141 000 gas. I could not find a token with the same fee system to compare, but I found one similar token which gas flow in the relevant situation was equal ~155 000 gas.

According to this, our solution did well.

Calculation of fee cost was performed on etherscan taking into account a requirement that transaction must be linear (not a part of huge transaction sequence).