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:
-
TokenGatecontract (tokengated.sol) for rule-based access control -
TestTokencontract (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
- ERC-20 via
-
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)
- Deploy
TokenGate. - Owner adds one or more rules using
addRule(...). - User calls
checkAccess(userAddress)(or hits gated methods directly). - Contract loops rules and checks token balance.
- Access passes if
balance >= minBalancefor 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
- Deploy an ERC-20 or ERC-721 token (or use mock token).
- Deploy
TokenGateTemplate. - Add rule:
- ERC-20: minimum
100tokens, or - ERC-721: minimum
1NFT
- ERC-20: minimum
- In frontend/API, call
checkAccess(walletAddress)before showing gated UI. - Enforce access on-chain via gated methods (never rely only on frontend checks).
Production Notes
Before production, consider:
- transfer ownership pattern (
Ownablefrom 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