DEV Community

Cover image for How to Build a Production-Ready BEP-20 Token with Anti-Bot Protection and BscScan Auto-Verification
Jordan Zheng
Jordan Zheng

Posted on

How to Build a Production-Ready BEP-20 Token with Anti-Bot Protection and BscScan Auto-Verification

Most BEP-20 token tutorials stop at deployment. They show you how to get a contract on-chain, and that's it.

But a deployed contract is not a launched token. Between deployment and a token that buyers actually trust, there's a pipeline that most tutorials skip: sniper-bot protection from block one, anti-whale mechanics in the transfer logic, BscScan source code verification, and the ABI encoding that most developers get wrong on the first attempt.

This post covers all of it, using real production-grade contracts.

What We're Building

By the end of this tutorial you'll have:

✅ A full BEP-20 token contract with anti-bot and anti-whale (production Solidity)
✅ A TokenFactory that deploys tokens for a flat fee, with a whitelist and MINT-holder discount system
✅ A Hardhat deploy script for BSC mainnet
✅ Auto-verification on BscScan using their API (with the exact constructor encoding)
✅ A GitHub-ready repository structure
Prerequisites: Basic Solidity knowledge, Node.js installed, MetaMask with BNB for gas.

1. The Problem: What Kills Token Launches (Before Buyers Even Arrive)

You've deployed your token. You've added liquidity to PancakeSwap. Your Telegram is live.

And then you watch the chart do this:
Price: ▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁
Block: N........N+1..N+2..N+3
A sharp spike, then a cliff. Your buyers are calling it a rug.

This is not a rug. This is MEV sniping — and it happens on BSC in under 10 seconds.

Here's the exact attack sequence:

Block N: Your AddLiquidity transaction is broadcast to the mempool

Block N: A bot's RPC node sees it before it's confirmed

Block N+1: Bot front-runs with a massive buy at near-zero price
(using high gas to jump the queue)

Block N+2: Real retail buyers start purchasing — price rises

Block N+3: Bot dumps its entire position into those buyers
→ Chart craters. Buyers are underwater.
BSC's 3-second block times make this brutal. There is no time for humans to react.

The fix is not optional. It must be in the contract itself.

2. The BEP-20 Token Contract (Full Production Code)

Here is the complete
BEP20Token.sol
. Every feature flag is decided at constructor time — no upgradeable proxies, no hidden admin functions, everything readable on BscScan.

// 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/access/Ownable.sol";

/**
 * @title BEP20Token
 * @dev BEP-20 token with optional Anti-Bot, Anti-Whale, and Airdrop features.
 * Optimized for BSC 3-second block times and PancakeSwap V3.
 *
 * Anti-Bot: Uses a self-destructing blacklist that expires after 50 blocks
 * (~2.5 minutes on BSC). After expiry, the blacklist logic is permanently
 * disabled even if antiBotEnabled is true — this is intentional to
 * maintain long-term contract trust scores on scanner tools.
 */
contract BEP20Token is ERC20, ERC20Burnable, Ownable {
    uint8 private _decimals;

    // --- Feature Flags (immutable after deployment) ---
    bool public antiBotEnabled;
    bool public antiWhaleEnabled;
    bool public airdropEnabled;

    // --- Anti-Bot: Temporary blacklist (self-destructs after 50 blocks) ---
    mapping(address => bool) public isBlacklisted;
    uint256 public antiBotExpiry;

    // --- Anti-Whale: Per-transaction and per-wallet limits ---
    uint256 public maxTransactionAmount;
    uint256 public maxWalletAmount;

    // --- Events ---
    event AddressBlacklisted(address indexed account, bool isBlacklisted);
    event LimitsUpdated(uint256 maxTransaction, uint256 maxWallet);
    event AntiBotToggled(bool enabled);
    event AntiWhaleToggled(bool enabled);

    constructor(
        string memory name_,
        string memory symbol_,
        uint8 decimals_,
        uint256 initialSupply_,
        address tokenOwner_,
        bool _antiBot,
        bool _antiWhale,
        bool _airdrop
    ) ERC20(name_, symbol_) Ownable(tokenOwner_) {
        require(tokenOwner_ != address(0), "Owner cannot be zero address");

        _decimals = decimals_;
        antiBotEnabled  = _antiBot;
        antiWhaleEnabled = _antiWhale;
        airdropEnabled  = _airdrop;

        // Anti-bot window: 50 blocks ≈ 2.5 minutes on BSC
        // After this block, isBlacklisted checks are permanently skipped.
        if (_antiBot) {
            antiBotExpiry = block.number + 50;
        }

        uint256 totalSupply_ = initialSupply_ * (10 ** uint256(decimals_));
        _mint(tokenOwner_, totalSupply_);

        if (_antiWhale) {
            maxTransactionAmount = totalSupply_ / 100;  // 1% max per tx
            maxWalletAmount      = totalSupply_ / 50;   // 2% max per wallet
        }
    }

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

    // =========================================================================
    // Anti-Bot Functions
    // =========================================================================

    /// @notice Blacklist a sniper address during the launch window
    /// @dev Automatically becomes a no-op after antiBotExpiry block
    function setBlacklisted(address account, bool blacklisted) external onlyOwner {
        require(antiBotEnabled, "Anti-Bot not enabled");
        require(block.number < antiBotExpiry, "Anti-Bot protection period expired");
        require(account != owner(), "Cannot blacklist owner");
        isBlacklisted[account] = blacklisted;
        emit AddressBlacklisted(account, blacklisted);
    }

    /// @notice Batch blacklist — useful when multiple sniper wallets hit simultaneously
    function setBlacklistedBatch(
        address[] calldata accounts,
        bool blacklisted
    ) external onlyOwner {
        require(antiBotEnabled, "Anti-Bot not enabled");
        require(block.number < antiBotExpiry, "Anti-Bot protection period expired");
        for (uint256 i = 0; i < accounts.length; i++) {
            if (accounts[i] != owner()) {
                isBlacklisted[accounts[i]] = blacklisted;
                emit AddressBlacklisted(accounts[i], blacklisted);
            }
        }
    }

    /// @notice Manually disable Anti-Bot before expiry (e.g., if no snipers appear)
    function disableAntiBot() external onlyOwner {
        require(antiBotEnabled, "Already disabled");
        antiBotEnabled = false;
        emit AntiBotToggled(false);
    }

    // =========================================================================
    // Anti-Whale Functions
    // =========================================================================

    function setLimits(uint256 _maxTransaction, uint256 _maxWallet) external onlyOwner {
        require(antiWhaleEnabled, "Anti-Whale not enabled");
        require(_maxTransaction > 0 && _maxWallet > 0, "Limits must be > 0");
        maxTransactionAmount = _maxTransaction;
        maxWalletAmount = _maxWallet;
        emit LimitsUpdated(_maxTransaction, _maxWallet);
    }

    function disableAntiWhale() external onlyOwner {
        require(antiWhaleEnabled, "Already disabled");
        antiWhaleEnabled = false;
        maxTransactionAmount = 0;
        maxWalletAmount = 0;
        emit AntiWhaleToggled(false);
    }

    // =========================================================================
    // Core Transfer Override
    // =========================================================================

    /**
     * @dev Enforces Anti-Bot and Anti-Whale rules on every transfer.
     *
     * Anti-Bot check:
     *   - Only active if antiBotEnabled AND current block < antiBotExpiry
     *   - After the 50-block window, this branch is NEVER entered regardless of flag
     *
     * Anti-Whale check:
     *   - Active as long as antiWhaleEnabled is true
     *   - Owner is always exempt (for liquidity operations)
     */
    function _update(address from, address to, uint256 value) internal virtual override {
        // Anti-Bot: self-destructing after antiBotExpiry
        if (antiBotEnabled && block.number < antiBotExpiry) {
            require(!isBlacklisted[from], "Sender blacklisted in launch phase");
            require(!isBlacklisted[to],   "Recipient blacklisted in launch phase");
        }

        // Anti-Whale: max tx + max wallet (owner exempt)
        if (antiWhaleEnabled && from != address(0) && to != address(0)) {
            if (from != owner() && to != owner()) {
                require(value <= maxTransactionAmount, "Exceeds max transaction amount");
                require(
                    balanceOf(to) + value <= maxWalletAmount,
                    "Exceeds max wallet amount"
                );
            }
        }

        super._update(from, to, value);
    }
}

Enter fullscreen mode Exit fullscreen mode

*Why the "self-destructing" anti-bot design?
*

After block antiBotExpiry, the anti-bot check in _update can never execute — not because it's been disabled, but because block.number < antiBotExpiry is permanently false. This matters for scanner tools like Honeypot.is and DexScreener, which flag contracts that retain permanent owner blacklist power. Tying the protection window to a block number makes the risk surface visible and time-limited.

3. The TokenFactory Contract

Deploying tokens manually is fine for one token. If you're building a platform, you need a factory.

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

import "./BEP20Token.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
 * @title TokenFactory
 * @dev Deploys BEP-20 tokens for a flat fee. Includes:
 *   - Flat pricing model (all features included, no upsells)
 *   - Whitelist for free/discounted launches
 *   - MINT token holder discount system
 *   - Emergency pause (Pausable)
 *   - Reentrancy protection on createToken
 *   - Automatic refund of excess BNB sent
 */
contract TokenFactory is Ownable, ReentrancyGuard, Pausable {
    uint256 public baseFee;       // Flat fee in wei (e.g., 0.15 BNB initially)
    address public mintToken;     // Utility token for holder discounts
    uint256 public discountThreshold;
    uint256 public discountPercentage;

    mapping(address => bool) public isWhitelisted;

    uint256 public totalTokensCreated;
    uint256 public totalFeesCollected;

    event TokenCreated(
        address indexed tokenAddress,
        address indexed creator,
        string name,
        string symbol,
        uint256 totalSupply,
        uint256 feePaid
    );
    event WhitelistUpdated(address indexed user, bool status);
    event FeesWithdrawn(address indexed to, uint256 amount);

    constructor(uint256 _baseFee, address _mintToken) Ownable(msg.sender) {
        baseFee   = _baseFee;
        mintToken = _mintToken;
    }

    // -------------------------------------------------------------------------
    // Fee Calculation
    // -------------------------------------------------------------------------

    /**
     * @dev Returns fee for `user`. Zero if whitelisted.
     *      Applies MINT-holder discount if configured.
     *      Feature flags kept for ABI compatibility but don't affect price.
     */
    function calculateFee(
        bool, /* antiBot   — included in flat fee */
        bool, /* antiWhale — included in flat fee */
        bool, /* airdrop   — included in flat fee */
        address user
    ) public view returns (uint256) {
        if (isWhitelisted[user]) return 0;

        uint256 fee = baseFee;

        if (mintToken != address(0) && discountPercentage > 0) {
            // try/catch avoids revert if MINT token has a bug
            try IERC20(mintToken).balanceOf(user) returns (uint256 balance) {
                if (balance >= discountThreshold) {
                    fee = (fee * (100 - discountPercentage)) / 100;
                }
            } catch {}
        }

        return fee;
    }

    // -------------------------------------------------------------------------
    // Token Creation
    // -------------------------------------------------------------------------

    /**
     * @dev Deploy a new BEP20Token.
     *      msg.sender becomes the token owner.
     *      Excess BNB is refunded automatically.
     */
    function createToken(
        string memory name,
        string memory symbol,
        uint8 decimals,
        uint256 initialSupply,
        bool antiBot,
        bool antiWhale,
        bool airdrop
    ) external payable nonReentrant whenNotPaused returns (address tokenAddress) {
        // --- Validation ---
        require(bytes(name).length > 0 && bytes(name).length <= 50,    "Invalid name length");
        require(bytes(symbol).length > 0 && bytes(symbol).length <= 10, "Invalid symbol length");
        require(decimals >= 6 && decimals <= 18, "Decimals must be 6-18");
        require(initialSupply > 0, "Supply must be > 0");

        uint256 requiredFee = calculateFee(antiBot, antiWhale, airdrop, msg.sender);
        require(msg.value >= requiredFee, "Insufficient fee");

        // --- Deploy ---
        BEP20Token token = new BEP20Token(
            name,
            symbol,
            decimals,
            initialSupply,
            msg.sender,  // creator is the token owner
            antiBot,
            antiWhale,
            airdrop
        );

        tokenAddress = address(token);
        totalTokensCreated++;
        totalFeesCollected += requiredFee;

        emit TokenCreated(tokenAddress, msg.sender, name, symbol,
            initialSupply * (10 ** uint256(decimals)), requiredFee);

        // --- Refund excess ---
        if (msg.value > requiredFee) {
            (bool ok, ) = payable(msg.sender).call{value: msg.value - requiredFee}("");
            require(ok, "Refund failed");
        }
    }

    // -------------------------------------------------------------------------
    // Admin Functions
    // -------------------------------------------------------------------------

    function setBaseFee(uint256 _baseFee) external onlyOwner {
        baseFee = _baseFee;
    }

    function setWhitelist(address user, bool status) external onlyOwner {
        isWhitelisted[user] = status;
        emit WhitelistUpdated(user, status);
    }

    function setDiscountConfig(
        address _mintToken,
        uint256 _threshold,
        uint256 _percentage
    ) external onlyOwner {
        require(_percentage <= 100, "Invalid percentage");
        mintToken          = _mintToken;
        discountThreshold  = _threshold;
        discountPercentage = _percentage;
    }

    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "Nothing to withdraw");
        (bool ok, ) = payable(owner()).call{value: balance}("");
        require(ok, "Withdraw failed");
        emit FeesWithdrawn(owner(), balance);
    }

    function pause()   external onlyOwner { _pause(); }
    function unpause() external onlyOwner { _unpause(); }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

4. GitHub Repository Structure

Here is the recommended layout to publish this on GitHub cleanly:

bep20-token-factory/
├── contracts/
│ ├── BEP20Token.sol ← Token contract
│ └── TokenFactory.sol ← Factory contract
├── scripts/
│ ├── deploy.ts ← Hardhat deploy script
│ └── verify.ts ← BscScan verification script
├── test/
│ └── BEP20Token.test.ts ← Unit tests
├── hardhat.config.ts
├── package.json
└── README.md
hardhat.config.ts

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@nomicfoundation/hardhat-verify";
import * as dotenv from "dotenv";
dotenv.config();

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.24",
    settings: {
      optimizer: { enabled: true, runs: 200 },
    },
  },
  networks: {
    bscTestnet: {
      url: "https://data-seed-prebsc-1-s1.binance.org:8545/",
      chainId: 97,
      accounts: [process.env.PRIVATE_KEY!],
    },
    bsc: {
      url: "https://bsc-dataseed1.binance.org/",
      chainId: 56,
      accounts: [process.env.PRIVATE_KEY!],
    },
  },
  etherscan: {
    apiKey: {
      bsc:        process.env.BSCSCAN_API_KEY!,
      bscTestnet: process.env.BSCSCAN_API_KEY!,
    },
  },
};

export default config;

Enter fullscreen mode Exit fullscreen mode

scripts/deploy.ts

import { ethers } from "hardhat";

async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deploying with:", deployer.address);

  const balance = await ethers.provider.getBalance(deployer.address);
  console.log("Balance:", ethers.formatEther(balance), "BNB");

  // Deploy TokenFactory
  // Args: baseFee (0.15 BNB), mintToken (address(0) = disabled initially)
  const baseFee   = ethers.parseEther("0.15");
  const mintToken = "0x0000000000000000000000000000000000000000";

  const Factory = await ethers.getContractFactory("TokenFactory");
  const factory = await Factory.deploy(baseFee, mintToken);
  await factory.waitForDeployment();

  const factoryAddress = await factory.getAddress();
  console.log("✅ TokenFactory deployed:", factoryAddress);
  console.log("   BscScan:", `https://bscscan.com/address/${factoryAddress}`);

  // Verify on BscScan (Hardhat plugin method)
  console.log("\n⏳ Verifying on BscScan...");
  try {
    await run("verify:verify", {
      address: factoryAddress,
      constructorArguments: [baseFee, mintToken],
    });
    console.log("✅ Verified!");
  } catch (e: any) {
    if (e.message.includes("Already Verified")) {
      console.log("Already verified.");
    } else {
      console.error("Verification failed:", e.message);
    }
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

Enter fullscreen mode Exit fullscreen mode

Run it:

npx hardhat run scripts/deploy.ts --network bsc
Enter fullscreen mode Exit fullscreen mode

5. BscScan Verification — The Manual Pipeline

The Hardhat plugin covers most cases. But understanding the raw API is essential when:

You're running verification from a backend service after deployment
The plugin fails silently (wrong compiler version, ABI mismatch)
You need to retroactively verify contracts already on-chain
The API Call

import axios from "axios";
import { encodeAbiParameters, parseAbiParameters } from "viem";

const BSC_API = "https://api.bscscan.com/api";

interface VerificationRequest {
  contractaddress: string;
  sourceCode: string;             // Flattened Solidity source
  codeformat: "solidity-single-file" | "solidity-standard-json-input";
  contractname: string;
  compilerversion: string;        // Must EXACTLY match what was used to compile
  optimizationUsed: "0" | "1";
  runs: string;
  constructorArguements?: string; // ⚠️ Note the typo — BscScan's API spells it wrong
}

async function verifyOnBscScan(req: VerificationRequest) {
  const params = new URLSearchParams();
  params.append("apikey",            process.env.BSCSCAN_API_KEY!);
  params.append("module",            "contract");
  params.append("action",            "verifysourcecode");
  params.append("contractaddress",   req.contractaddress);
  params.append("sourceCode",        req.sourceCode);
  params.append("codeformat",        req.codeformat);
  params.append("contractname",      req.contractname);
  params.append("compilerversion",   req.compilerversion);
  params.append("optimizationUsed",  req.optimizationUsed);
  params.append("runs",              req.runs);

  if (req.constructorArguements) {
    // The key IS misspelled — "Arguements" not "Arguments"
    // Using the correct spelling will cause silent failure
    params.append("constructorArguements", req.constructorArguements);
  }

  const response = await axios.post(BSC_API, params, {
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
  });

  if (response.data.status === "1") {
    return { status: "submitted", guid: response.data.result };
  }

  if (response.data.result?.includes("Already Verified")) {
    return { status: "already_verified" };
  }

  throw new Error(response.data.result || response.data.message);
}


Enter fullscreen mode Exit fullscreen mode

The most common failure point: constructorArguements is intentionally misspelled in BscScan's API. If you spell it correctly (constructorArguments), the parameter is silently ignored and verification always fails for contracts with constructor args. This is a known quirk, undocumented, and bites everyone the first time.

Encoding Constructor Arguments
Your contract's constructor args must be ABI-encoded — not just concatenated. For BEP20Token:

import { encodeAbiParameters, parseAbiParameters } from "viem";

// Constructor signature:
// (string name_, string symbol_, uint8 decimals_, uint256 initialSupply_,
//  address tokenOwner_, bool _antiBot, bool _antiWhale, bool _airdrop)

const constructorArgs = encodeAbiParameters(
  parseAbiParameters(
    "string, string, uint8, uint256, address, bool, bool, bool"
  ),
  [
    "MyToken",           // name_
    "MTK",               // symbol_
    18,                  // decimals_  (uint8)
    BigInt("1000000000000000000000000000"), // initialSupply (1B × 10^18)
    "0xYourWalletAddress",
    true,                // antiBot
    true,                // antiWhale
    false                // airdrop
  ]
);

// Strip the 0x prefix — BscScan expects raw hex, no prefix
const argsForBscScan = constructorArgs.replace("0x", "");

Enter fullscreen mode Exit fullscreen mode

Polling Verification Status
BscScan queues verifications asynchronously. Save the GUID and poll:

async function pollVerificationStatus(guid: string): Promise<string> {
  const MAX_ATTEMPTS = 10;
  const DELAY_MS     = 10_000; // 10 seconds between polls

  for (let i = 0; i < MAX_ATTEMPTS; i++) {
    await new Promise(r => setTimeout(r, DELAY_MS));

    const res = await axios.get(BSC_API, {
      params: {
        module: "contract",
        action: "checkverifystatus",
        guid,
        apikey: process.env.BSCSCAN_API_KEY,
      },
    });

    const result: string = res.data.result;
    console.log(`[${i + 1}/${MAX_ATTEMPTS}] Status: ${result}`);

    if (result === "Pass - Verified") return "verified";
    if (result.startsWith("Fail"))    return result;
    // "Pending in queue" or "In progress" → keep polling
  }

  return "timeout";
}

Enter fullscreen mode Exit fullscreen mode

6. Flattening Your Solidity (Required for Single-File Verification)

BscScan's solidity-single-file mode needs all imports inlined. Hardhat has a built-in flattener:

npx hardhat flatten contracts/BEP20Token.sol > BEP20Token.flat.sol

Watch out for duplicate SPDX headers. Hardhat's flattener concatenates multiple // SPDX-License-Identifier lines, which causes a compiler warning. Remove all but the first one from the flattened output.

For production, do this programmatically:
`import fs from "fs/promises";

let source = await fs.readFile("BEP20Token.flat.sol", "utf8");

// Remove duplicate SPDX lines (keep only the first)
let spdxFound = false;
source = source
.split("\n")
.filter(line => {
if (line.startsWith("// SPDX-License-Identifier")) {
if (spdxFound) return false;
spdxFound = true;
}
return true;
})
.join("\n");
`

7. Deploying a Token via the Factory (Frontend/Script)

Once your factory is live, deploying tokens through it looks like this:

import { ethers } from "hardhat"; // or viem/wagmi from the frontend

const FACTORY_ABI = [
  "function createToken(string name, string symbol, uint8 decimals, uint256 initialSupply, bool antiBot, bool antiWhale, bool airdrop) external payable returns (address)",
  "function calculateFee(bool, bool, bool, address user) external view returns (uint256)",
  "event TokenCreated(address indexed tokenAddress, address indexed creator, string name, string symbol, uint256 totalSupply, uint256 feePaid)"
];

const FACTORY_ADDRESS = "0xCCb8F444B2e3a0dFD1F5f91AAED75c114F6B8514"; // BSC Mainnet

async function launchToken(signer: ethers.Signer) {
  const factory = new ethers.Contract(FACTORY_ADDRESS, FACTORY_ABI, signer);

  // Get the required fee for this user
  const fee = await factory.calculateFee(true, true, false, await signer.getAddress());
  console.log("Required fee:", ethers.formatEther(fee), "BNB");

  // Deploy the token
  const tx = await factory.createToken(
    "PepeArmy",         // name
    "PEPEARMY",         // symbol
    18,                 // decimals
    ethers.parseUnits("1000000000", 0), // 1 billion (before contract multiplies by 10^18)
    true,               // antiBot  ← sniper protection ON
    true,               // antiWhale ← max wallet/tx ON
    false,              // airdrop
    { value: fee }
  );

  const receipt = await tx.wait();
  console.log("Block:", receipt.blockNumber);

  // Parse the TokenCreated event to get the deployed address
  const iface  = new ethers.Interface(FACTORY_ABI);
  const log    = receipt.logs.find(l => {
    try { iface.parseLog(l); return true; } catch { return false; }
  });
  const event  = iface.parseLog(log!);
  const tokenAddress = event!.args.tokenAddress;

  console.log("✅ Token deployed at:", tokenAddress);
  console.log("   BscScan:", `https://bscscan.com/token/${tokenAddress}`);

  return tokenAddress;
}

Enter fullscreen mode Exit fullscreen mode

8. After Launch: Managing the Anti-Bot Window

You have 50 blocks (~2.5 minutes) to use the anti-bot tools. Here's the management script:

const TOKEN_ABI = [
  "function setBlacklisted(address account, bool blacklisted) external",
  "function setBlacklistedBatch(address[] calldata accounts, bool blacklisted) external",
  "function disableAntiBot() external",
  "function antiBotExpiry() view returns (uint256)",
  "function isBlacklisted(address) view returns (bool)",
];

async function manageAntiBotWindow(tokenAddress: string, signer: ethers.Signer) {
  const token = new ethers.Contract(tokenAddress, TOKEN_ABI, signer);

  const expiry      = await token.antiBotExpiry();
  const currentBlock = await ethers.provider.getBlockNumber();
  const blocksLeft   = Number(expiry) - currentBlock;

  console.log(`Anti-bot window: ${blocksLeft} blocks remaining (~${blocksLeft * 3} seconds)`);

  if (blocksLeft <= 0) {
    console.log("Anti-bot window has expired — blacklisting is now permanently disabled.");
    return;
  }

  // --- Blacklist known sniper wallets from your monitoring ---
  const sniperWallets = [
    "0xSniper1...",
    "0xSniper2...",
  ];

  if (sniperWallets.length > 0) {
    const tx = await token.setBlacklistedBatch(sniperWallets, true);
    await tx.wait();
    console.log(`Blacklisted ${sniperWallets.length} wallets.`);
  }

  // --- Or just disable early if no snipers appear ---
  // const tx = await token.disableAntiBot();
  // await tx.wait();
  // console.log("Anti-bot disabled early.");
}

Enter fullscreen mode Exit fullscreen mode

9. Full Verification Checklist Before Going Live

# 1. Compile and check for warnings
npx hardhat compile

# 2. Run tests
npx hardhat test

# 3. Deploy to testnet first
npx hardhat run scripts/deploy.ts --network bscTestnet

# 4. Verify on testnet BscScan
npx hardhat verify --network bscTestnet <FACTORY_ADDRESS> "150000000000000000" "0x0000000000000000000000000000000000000000"

# 5. Deploy to mainnet
npx hardhat run scripts/deploy.ts --network bsc

# 6. Verify on mainnet BscScan
npx hardhat verify --network bsc <FACTORY_ADDRESS> "150000000000000000" "0x0000000000000000000000000000000000000000"

Enter fullscreen mode Exit fullscreen mode

*Post-deployment checklist:
*

✅ Factory deployed and verified on BscScan (green checkmark)
✅ Test token creation on mainnet via the factory (confirm gas + fee)
✅ Confirm anti-bot 50-block window activates on test token
✅ Confirm anti-whale limits are set correctly (1% tx, 2% wallet)
✅ Add PancakeSwap V3 liquidity immediately after token creation
✅ Lock liquidity (Mudra Lock or similar — required for buyer trust)
✅ Confirm token shows verified on DexScreener and Honeypot.is

Summary
What you now have:
Component Status
BEP20Token.sol
Complete — anti-bot (self-destructing 50-block window) + anti-whale
TokenFactory.sol
Complete — flat fee, whitelist, MINT discount, reentrancy guard
Hardhat config BSC mainnet + testnet, etherscan plugin
Deploy script Auto-verifies via Hardhat plugin
Manual BscScan API Full verification pipeline with constructor encoding
ABI encoding viem encodeAbiParameters with correct type signature
Anti-bot management Post-launch blacklist and batch blacklist script

If you want this entire pipeline as a managed service on BSC — deploy, auto-verify, anti-bot, anti-whale, PancakeSwap V3 — without configuring Hardhat, BscScan API keys, or ABI encoding: MagicMint is the production version of exactly this stack. Flat $100 fee, verified on day one, your LP in your wallet.

Questions about Hardhat, BscScan verification, or the anti-bot implementation? Drop them in the comments.

Top comments (0)