Help for my ERC-20 token

Hi guys, I create this token, honestly i couldn't tested properly locally because of the router i'm setting as i couldn't create a mock router locally using Foundry. However, i was able to deploy the contract on BSC Testnet and verify the contract.

Here is my challenge after deploying this contract... Using pancakeswap

  1. I can add liquidity successfully.
  2. I can buy/sell when the wallet is add to excluded wallets
  3. I can buy/sell if i toggle tax fee off

But

  1. I can't buy/sell if i toggle tax on
  2. I can't buy/sell if the wallet is not included in isExcludedFromFee mapping
    Debug:
    When i check the event logs on bsc scan, i noticed that the _update() is not called at all and no event was logged. So when fee is enabled and i'm transacting from a wallet not added to isExcludedFromFee, my metamask did not pop out for me to confirm transaction and then the transaction timed out.

:1234: Code to reproduce

/*
* Tokenomics
*
*  Name  - CocoToken
*  Symbol - COCO
*  MAX Supply -  2.1 Million
*  Tax 5%  - only applicable for the first 1 year from the time liquidity is provided (0% thereafter)
*  5% tax distribution
    - 1% to liquidity pool
    - 1% to marketing wallet (sent as BNB)
    - 1% to ‘buyback’ wallet (sent as BNB)
    - 2% to RewardHolderNFT Contract (sent as COCO)
*   Other features
    - Anti-dump Max Sell no more than 0.5% of supply (105k) over 72 hours – only applicable for 6 months (0% thereafter)
*/

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

import {SafeMath} from "./library/SafeMath.sol";
import {ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IUniswapV2Factory} from "./interface/IUniswapV2Factory.sol";
import {IUniswapV2Router02} from "./interface/IUniswapV2Router02.sol";
// import {RewardHolderNFT} from "./RewardHolderNFT.sol";

contract CocoToken is ERC20Capped, Ownable, ReentrancyGuard {
    using SafeMath for uint256;

    // Errors
    error CocoToken__InvalidAddress();
    error CocoToken__TransferFromZeroAddress();
    error CocoToken__AddressCannotBeZeroAddress();
    error CocoToken__AmountMustBeGreaterThanZero();
    error CocoToken__AntiDumpLimitExceeded();

    // State variables
    address private uniswapV2Pair;
    IUniswapV2Router02 private uniswapV2Router;

    // State constants and immutables
    uint256 private constant MAX_SUPPLY = 2100000 * 10 ** 4;
    uint256 private constant ANTI_DUMP_DURATION = 72 hours;
    uint256 private constant ANTI_DUMP_PERIOD = 6 * 30 days;
    uint256 private constant TAX_PERIOD = 365 days;
    uint256 private constant MAX_SELL_PERCENT = 5; // 0.5% (5/1000)
    address payable public immutable nftRewardContract;

    uint256 public taxFee = 500; // 5%
    uint256 public deployTime;
    address payable public marketingWallet;
    address payable public buyBackWallet;

    // State mappings
    mapping(address => bool) private isExcludedFromFee;
    mapping(address => uint256) private soldAmounts;
    mapping(address => uint256) private sellTimestamps;

    // Events
    event DebugEvent(string message);
    event DebugEvent2(string message, bytes data);
    event AccountExcludedFromFee(address account);
    event AccountIncludedInFee(address account);
    event MarketingWalletUpdated(address newMarketingWallet);
    event BuybackWalletUpdated(address newBuybackWallet);
    event FeeStateUpdated(bool enabled);
    event SwapAttempted(uint256 tokenAmount, uint256 minETHExpected, uint256 ethReceived);
    event LiquidityAdded(uint256 tokenAmount, uint256 ethAmount, bool success);
    event AntiDumpChecked(address indexed seller, uint256 sellAmount, bool allowed);
    event FeesDistributed(uint256 marketingETH, uint256 buybackETH, uint256 liquidityETH, uint256 nftTokens);
    event TaxCalculated(
        address indexed sender,
        address indexed recipient,
        uint256 amount,
        uint256 taxAmount,
        bool isSell,
        bool isExcluded
    );

    /**
     * @notice Constructor that initializes the token, mints the max supply, and assigns wallets.
     * @param _marketingWallet The wallet address to receive the marketing fee.
     * @param _buyBackWallet This wallet address to receive buy back and burn CORE.
     */
    constructor(address _marketingWallet, address _buyBackWallet)
        ERC20("CocoToken", "COCO")
        ERC20Capped(MAX_SUPPLY)
        Ownable(msg.sender)
    {
        if (_marketingWallet == address(0) || _buyBackWallet == address(0)) {
            revert CocoToken__InvalidAddress();
        }

        deployTime = block.timestamp;

        marketingWallet = payable(_marketingWallet);
        buyBackWallet = payable(_buyBackWallet);

        // Deploy reward nft contract
        // RewardHolderNFT claimContract = new RewardHolderNFT(address(this));
        // nftRewardContract = payable(claimContract);

        IUniswapV2Router02 _router = IUniswapV2Router02(0xD99D1c33F9fC3444f8101754aBC46c52416550D1);
        uniswapV2Pair = IUniswapV2Factory(_router.factory()).createPair(address(this), _router.WETH());
        uniswapV2Router = _router;

        isExcludedFromFee[msg.sender] = true;
        isExcludedFromFee[address(this)] = true;
        isExcludedFromFee[buyBackWallet] = true;
        isExcludedFromFee[marketingWallet] = true;
        isExcludedFromFee[nftRewardContract] = true;

        // Mint max supply
        _mint(msg.sender, MAX_SUPPLY);
    }

    receive() external payable {}

    /**
     * @param sender The sender's address.
     * @param recipient The recipient's address.
     * @param amount The amount of tokens to transfer.
     * @dev Internal function to handle token transfers.
     * @notice Only buy and sell transactions are subject to fees.
     */
    function _update(address sender, address recipient, uint256 amount) internal override {
        if (amount <= 0) {
            revert CocoToken__AmountMustBeGreaterThanZero();
        }

        uint256 tax = _calculateTax(sender, recipient, amount);
        uint256 transferAmount = amount.sub(tax);

        if (tax > 0) {
            super._update(sender, address(this), tax);

            _distributeFees(tax);
        }

        if (transferAmount > 0) {
            super._update(sender, recipient, transferAmount);
        }
    }

    function _calculateTax(address sender, address recipient, uint256 amount) private returns (uint256) {
        uint256 taxAmount;
        bool isSell = recipient == uniswapV2Pair;
        bool excluded = isExcludedFromFee[sender] || isExcludedFromFee[recipient];

        if (excluded) {
            taxAmount = 0;
            emit TaxCalculated(sender, recipient, amount, taxAmount, isSell, excluded);
            return taxAmount;
        }

        if (block.timestamp <= deployTime + TAX_PERIOD) {
            taxAmount = amount.mul(taxFee).div(10000);
            emit TaxCalculated(sender, recipient, amount, taxAmount, isSell, excluded);
            return taxAmount;
        }

        if (isSell && block.timestamp <= deployTime + ANTI_DUMP_PERIOD) {
            if (!_checkAntiDump(sender, amount)) {
                revert CocoToken__AntiDumpLimitExceeded();
            }
        }

        taxAmount = 0;
        emit TaxCalculated(sender, recipient, amount, taxAmount, isSell, excluded);
        return taxAmount;
    }

    function _checkAntiDump(address seller, uint256 amount) private returns (bool) {
        // Reset if last sell was beyond the anti-dump duration
        if (block.timestamp > sellTimestamps[seller] + ANTI_DUMP_DURATION) {
            soldAmounts[seller] = 0;
            sellTimestamps[seller] = block.timestamp;
        }

        uint256 maxSell = totalSupply().mul(MAX_SELL_PERCENT).div(1000);
        uint256 newSoldAmount = soldAmounts[seller] + amount;

        if (newSoldAmount > maxSell) {
            emit AntiDumpChecked(seller, amount, false);
            return false; // Not allowed
        }

        // Update tracking
        soldAmounts[seller] = newSoldAmount;
        sellTimestamps[seller] = block.timestamp;

        emit AntiDumpChecked(seller, amount, true);
        return true; // Allowed
    }

    function _distributeFees(uint256 taxAmount) private {
        uint256 liquidity = taxAmount.mul(1).div(5); // 1%
        uint256 marketing = taxAmount.mul(1).div(5); // 1%
        uint256 buyback = taxAmount.mul(1).div(5); // 1%
        uint256 nftTokens = taxAmount.mul(2).div(5); // 2%

        // Send tokens to NFT contract
        super._update(address(this), nftRewardContract, nftTokens);

        // Process CORE conversions
        uint256 totalCOREConversion = marketing.add(buyback).add(liquidity.div(2));
        uint256 initialBalance = address(this).balance;

        _swapTokensForEth(totalCOREConversion);

        uint256 newBalance = address(this).balance.sub(initialBalance);

        // Calculate proportional CORE amounts
        uint256 marketingCORE = newBalance.mul(marketing).div(totalCOREConversion);
        uint256 buybackCORE = newBalance.mul(buyback).div(totalCOREConversion);
        uint256 liquidityCORE = newBalance.mul(liquidity.div(2)).div(totalCOREConversion);

        // Send CORE to destinations

        payable(marketingWallet).transfer(marketingCORE);
        payable(buyBackWallet).transfer(buybackCORE);

        // Add liquidity with remaining tokens and CORE
        if (liquidityCORE > 0) {
            _addLiquidity(liquidity.div(2), liquidityCORE);
        }

        emit FeesDistributed(marketingCORE, buybackCORE, liquidityCORE, nftTokens);
    }

    function _addLiquidity(uint256 tokenAmount, uint256 COREAmount) private {
        _approve(address(this), address(uniswapV2Router), tokenAmount);

        try uniswapV2Router.addLiquidityETH{value: COREAmount}(
            address(this), tokenAmount, 0, 0, owner(), block.timestamp
        ) {
            emit LiquidityAdded(tokenAmount, COREAmount, true);
        } catch {
            emit LiquidityAdded(tokenAmount, COREAmount, false);
        }
    }

    function _swapTokensForEth(uint256 tokenAmount) private {
        address[] memory path = new address[](2);
        path[0] = address(this);
        path[1] = uniswapV2Router.WETH();

        _approve(address(this), address(uniswapV2Router), tokenAmount);

        // Adjust for tax fee to calculate expected received tokens
        uint256 adjustedAmount = tokenAmount.mul(10000 - taxFee).div(10000);
        uint256 minETH = uniswapV2Router.getAmountsOut(adjustedAmount, path)[1].mul(99).div(100);

        uint256 ethBefore = address(this).balance;
        uniswapV2Router.swapExactTokensForETHSupportingFeeOnTransferTokens(
            tokenAmount, minETH, path, address(this), block.timestamp
        );

        uint256 ethReceived = address(this).balance - ethBefore;

        emit SwapAttempted(tokenAmount, minETH, ethReceived);
    }

    function contractBalanceSwap() external onlyOwner {
        uint256 contractBalance = balanceOf(address(this));
        _swapTokensForEth(contractBalance);
    }

    // Administrative Functions
    function excludeFromFee(address account) external onlyOwner {
        isExcludedFromFee[account] = true;
        emit AccountExcludedFromFee(account);
    }

    function includeInFee(address account) external onlyOwner {
        isExcludedFromFee[account] = false;
        emit AccountIncludedInFee(account);
    }

    function setBuybackWallet(address _buybackWalletAddr) external onlyOwner {
        if (_buybackWalletAddr == address(0)) revert CocoToken__AddressCannotBeZeroAddress();
        buyBackWallet = payable(_buybackWalletAddr);
        emit BuybackWalletUpdated(_buybackWalletAddr);
    }

    function setMarketingWallet(address _marketingWalletAddr) external onlyOwner {
        if (_marketingWalletAddr == address(0)) revert CocoToken__AddressCannotBeZeroAddress();
        marketingWallet = payable(_marketingWalletAddr);
        emit MarketingWalletUpdated(_marketingWalletAddr);
    }

    function setFeeState(bool enabled) external onlyOwner {
        taxFee = enabled ? 500 : 0;
        emit FeeStateUpdated(enabled);
    }

    function decimals() public view virtual override returns (uint8) {
        return 4;
    }

    function verifyPairCreation() external view returns (bool exists, address pairAddress) {
        address token0 = address(this);
        address token1 = uniswapV2Router.WETH();

        address computedPair = IUniswapV2Factory(uniswapV2Router.factory()).getPair(token0, token1);
        return (computedPair != address(0), computedPair);
    }
}

:laptop: Environment

Contract complied and deployed successfully on Remix

Hi, welcome to the community! :wave:

I think you can fork the network to test, Foundry Fork:

1 Like

Thanks Skype, let me take a look at that.

1 Like

@Skyge Thanks so much. You just saved me after 3 days of debugging. I've been able to create the Fork BSC Testnet environment. I will start testing locally to find the bug.

2 Likes