Token-Gated Smart Contract in Solidity

Building a Token-Gated Smart Contract in Solidity

Token gating is one of the cleanest patterns for Web3 products: allow access only to wallets that hold a required token or NFT.

In this post, we will break down a minimal token-gating system using:

  • TokenGate contract (tokengated.sol) for rule-based access control
  • TestToken contract (toke.sol) as a simple token mock

Why Token Gating?

Token gating helps you:

  • lock premium features to holders
  • gate chat, community, or app actions
  • combine multiple rules (fungible token OR NFT ownership)

It is great for MVPs because you can verify ownership on-chain with a simple balanceOf() check.


Contract Overview

Your TokenGate contract defines:

  • Owner-controlled rules
    • add a rule with token type, token address, and minimum balance
    • enable/disable rules later
  • Dual token support
    • ERC-20 via IERC20.balanceOf
    • ERC-721 via IERC721.balanceOf
  • OR-based access logic
    • if any active rule passes, access is granted
  • Gated action example
    • postMessage() can only be called by eligible wallets

Your TestToken contract is a tiny helper that assigns initial balance to deployer for local testing.


Current Logic (Short Walkthrough)

  1. Deploy TokenGate.
  2. Owner adds one or more rules using addRule(...).
  3. User calls checkAccess(userAddress) (or hits gated methods directly).
  4. Contract loops rules and checks token balance.
  5. Access passes if balance >= minBalance for any active rule.

Code Template: Reusable Token Gate

Use this as a clean starting template for new projects.

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

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
}

interface IERC721 {
    function balanceOf(address owner) external view returns (uint256);
}

contract TokenGateTemplate {
    address public owner;

    enum GateType {
        ERC20,
        ERC721
    }

    struct Rule {
        GateType gateType;
        address token;
        uint256 minBalance;
        bool active;
    }

    Rule[] public rules;

    event RuleAdded(uint256 indexed id, GateType gateType, address indexed token, uint256 minBalance);
    event RuleToggled(uint256 indexed id, bool active);
    event GatedAction(address indexed user, string actionTag);

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function addRule(
        GateType _type,
        address _token,
        uint256 _minBalance
    ) external onlyOwner {
        require(_token != address(0), "Invalid token");
        require(_minBalance > 0, "Invalid balance");

        rules.push(
            Rule({
                gateType: _type,
                token: _token,
                minBalance: _minBalance,
                active: true
            })
        );

        emit RuleAdded(rules.length - 1, _type, _token, _minBalance);
    }

    function toggleRule(uint256 _id, bool _active) external onlyOwner {
        require(_id < rules.length, "Invalid rule");
        rules[_id].active = _active;
        emit RuleToggled(_id, _active);
    }

    function checkAccess(address user) public view returns (bool) {
        for (uint256 i = 0; i < rules.length; i++) {
            if (!rules[i].active) continue;
            if (_balanceOf(rules[i], user) >= rules[i].minBalance) {
                return true;
            }
        }
        return false;
    }

    function gatedAction(string calldata actionTag) external {
        require(checkAccess(msg.sender), "No access");
        emit GatedAction(msg.sender, actionTag);
    }

    function ruleCount() external view returns (uint256) {
        return rules.length;
    }

    function _balanceOf(Rule memory rule, address user) internal view returns (uint256) {
        if (rule.gateType == GateType.ERC20) {
            return IERC20(rule.token).balanceOf(user);
        }
        return IERC721(rule.token).balanceOf(user);
    }
}

Code Template: Minimal Test Token

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

contract TestTokenTemplate {
    mapping(address => uint256) public balanceOf;

    constructor() {
        balanceOf[msg.sender] = 1000;
    }
}

Example Setup Flow

  1. Deploy an ERC-20 or ERC-721 token (or use mock token).
  2. Deploy TokenGateTemplate.
  3. Add rule:
    • ERC-20: minimum 100 tokens, or
    • ERC-721: minimum 1 NFT
  4. In frontend/API, call checkAccess(walletAddress) before showing gated UI.
  5. Enforce access on-chain via gated methods (never rely only on frontend checks).

Production Notes

Before production, consider:

  • transfer ownership pattern (Ownable from OpenZeppelin)
  • pausability/emergency controls
  • clearer rule metadata (name/description)
  • AND logic or grouped logic (not only OR logic)
  • gas optimization if rule count becomes large
  • upgradeability only if your product truly needs it

Resources;

GitHub: https://github.com/Vikash-8090-Yadav/tokenGated
Video: https://youtu.be/ezIjbAgZxqA?si=lxcNsJWJ5Evml1-m