Skip to content

Instantly share code, notes, and snippets.

@masihtehrani
Created July 30, 2025 11:18
Show Gist options
  • Select an option

  • Save masihtehrani/1aae3f5a3309b76cbe25f81fdc82906b to your computer and use it in GitHub Desktop.

Select an option

Save masihtehrani/1aae3f5a3309b76cbe25f81fdc82906b to your computer and use it in GitHub Desktop.
Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.8.30+commit.73712a01.js&optimize=false&runs=200&gist=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// OpenZeppelin Imports
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
/**
* @title WheatToken
* @author [Your Name/Team]
* @notice A secure ERC20 token with role-based access, pausable functionality, wallet freezing,
* controlled distribution, and gas-less approvals via EIP-2612.
* @dev This contract uses OpenZeppelin's AccessControl for granular, role-based permissions,
* enhancing security over a single-owner model. It also inherits from ERC20Permit
* to enable signature-based approvals.
*/
contract WheatToken is ERC20, AccessControl, Pausable, ReentrancyGuard, ERC20Permit {
using SafeERC20 for IERC20;
// --- Roles ---
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant FREEZER_ROLE = keccak256("FREEZER_ROLE");
bytes32 public constant DISTRIBUTOR_ROLE = keccak256("DISTRIBUTOR_ROLE");
bytes32 public constant RESCUER_ROLE = keccak256("RESCUER_ROLE");
bytes32 public constant CONFIGURATOR_ROLE = keccak256("CONFIGURATOR_ROLE");
// --- State Variables ---
address public immutable burnWallet;
address public immutable batchWallet;
address public immutable airdropWallet;
address public supportWallet;
string private _tokenImageUrl;
string private _websiteUrl;
mapping(address => bool) public frozenWallets;
// --- Constants ---
uint256 public constant DISTRIBUTION_LIMIT = 200;
// --- Events ---
event TokensBurned(address indexed from, uint256 amount);
event WalletFrozen(address indexed wallet);
event WalletUnfrozen(address indexed wallet);
event BatchWalletFunded(uint256 amount);
event AirdropWalletFunded(uint256 amount);
event TokensDistributed(address indexed fromWallet, uint256 totalAmount, uint256 userCount);
event TokenImageUrlUpdated(string newUrl);
event WebsiteUrlUpdated(string newUrl);
event TokensRescued(address indexed token, address indexed to, uint256 amount);
event SupportWalletUpdated(address indexed newWallet);
// --- Modifiers ---
/**
* @dev Modifier to check if an account is not frozen.
* @param account The address to check.
*/
modifier whenNotFrozen(address account) {
require(!frozenWallets[account], "WheatToken: Wallet is frozen");
_;
}
// --- Constructor ---
/**
* @notice Initializes the contract, sets up roles, and mints the total supply.
* @param initialSupply The total supply of tokens, expressed in the smallest unit (e.g., wei).
* @param _burnWallet The permanent address for burning tokens. Cannot be zero.
* @param _batchWallet The system address for batch distributions. Cannot be zero.
* @param _airdropWallet The system address for airdrop campaigns. Cannot be zero.
* @param _supportWallet The initial address for the support wallet. Cannot be zero.
* @param tokenImageUrl_ The initial URL for the token's image.
* @param websiteUrl_ The initial URL for the project's website.
*/
constructor(
uint256 initialSupply,
address _burnWallet,
address _batchWallet,
address _airdropWallet,
address _supportWallet,
string memory tokenImageUrl_,
string memory websiteUrl_
)
ERC20("WheatToken", "GND")
ERC20Permit("WheatToken")
{
require(_burnWallet != address(0), "WheatToken: Burn wallet cannot be zero address");
require(_batchWallet != address(0), "WheatToken: Batch wallet cannot be zero address");
require(_airdropWallet != address(0), "WheatToken: Airdrop wallet cannot be zero address");
require(_supportWallet != address(0), "WheatToken: Support wallet cannot be zero address");
burnWallet = _burnWallet;
batchWallet = _batchWallet;
airdropWallet = _airdropWallet;
supportWallet = _supportWallet;
_tokenImageUrl = tokenImageUrl_;
_websiteUrl = websiteUrl_;
// --- Role Setup ---
address deployer = msg.sender;
_grantRole(DEFAULT_ADMIN_ROLE, deployer);
_grantRole(PAUSER_ROLE, deployer);
_grantRole(FREEZER_ROLE, deployer);
_grantRole(DISTRIBUTOR_ROLE, deployer);
_grantRole(RESCUER_ROLE, deployer);
_grantRole(CONFIGURATOR_ROLE, deployer);
// Mint the entire initial supply to the deployer.
// The deployer is then responsible for funding the distribution wallets.
_mint(deployer, initialSupply);
}
// --- Internal Logic ---
/**
* @dev Internal function to enforce custom transfer rules before any transfer occurs.
* @param from The sender's address.
* @param to The receiver's address.
*/
function _checkTransferRules(address from, address to) internal view {
require(from != to, "WheatToken: Self-transfer is not allowed");
require(!frozenWallets[from], "WheatToken: Sender wallet is frozen");
require(!frozenWallets[to], "WheatToken: Receiver wallet is frozen");
// If the transaction involves the support wallet, bypass further restrictions.
if (from == supportWallet || to == supportWallet) {
return;
}
// Distributors must use dedicated funding functions, not standard transfer.
require(!hasRole(DISTRIBUTOR_ROLE, from), "WheatToken: Distributors must use funding functions");
// Block direct transfers to system wallets to enforce structured funding.
require(to != burnWallet, "WheatToken: Use burn() instead of direct transfer");
require(to != batchWallet, "WheatToken: Use fundBatchWallet() instead of direct transfer");
require(to != airdropWallet, "WheatToken: Use fundAirdropWallet() instead of direct transfer");
}
// --- ERC20 Standard Functions (Overridden) ---
/**
* @notice Returns the number of decimals used to display token amounts.
*/
function decimals() public pure override returns (uint8) {
return 3;
}
/**
* @dev Overrides the standard transfer function to include custom rules.
*/
function transfer(address to, uint256 amount) public virtual override whenNotPaused returns (bool) {
address from = _msgSender();
_checkTransferRules(from, to);
return super.transfer(to, amount);
}
/**
* @dev Overrides the standard transferFrom function to include custom rules.
*/
function transferFrom(address from, address to, uint256 amount) public virtual override whenNotPaused returns (bool) {
_checkTransferRules(from, to);
return super.transferFrom(from, to, amount);
}
// --- Core View Functions ---
/**
* @notice Returns the URL of the token's image.
*/
function tokenImageUrl() public view returns (string memory) {
return _tokenImageUrl;
}
/**
* @notice Returns the URL of the project's official website.
*/
function websiteUrl() public view returns (string memory) {
return _websiteUrl;
}
// --- Core Action Functions ---
/**
* @notice Burns (destroys) a specific amount of the caller's own tokens.
* @param amount The quantity of tokens to burn.
*/
function burn(uint256 amount) public whenNotPaused whenNotFrozen(_msgSender()) {
require(amount > 0, "WheatToken: Burn amount must be greater than zero");
_burn(_msgSender(), amount);
emit TokensBurned(_msgSender(), amount);
}
// --- Role-Protected Functions ---
/**
* @notice Pauses all token transfers.
* @dev Can only be called by an address with the PAUSER_ROLE.
* Scenario: Use in emergencies to halt token activity if a vulnerability is found.
*/
function pause() public onlyRole(PAUSER_ROLE) {
_pause();
}
/**
* @notice Resumes token transfers after a pause.
* @dev Can only be called by an address with the PAUSER_ROLE.
*/
function unpause() public onlyRole(PAUSER_ROLE) {
_unpause();
}
/**
* @notice Updates the URL for the token's image.
* @dev Can only be called by an address with the CONFIGURATOR_ROLE.
* @param newUrl The new URL for the token image.
*/
function setTokenImageUrl(string memory newUrl) public onlyRole(CONFIGURATOR_ROLE) {
_tokenImageUrl = newUrl;
emit TokenImageUrlUpdated(newUrl);
}
/**
* @notice Updates the URL for the project's website.
* @dev Can only be called by an address with the CONFIGURATOR_ROLE.
* @param newUrl The new URL for the project website.
*/
function setWebsiteUrl(string memory newUrl) public onlyRole(CONFIGURATOR_ROLE) {
_websiteUrl = newUrl;
emit WebsiteUrlUpdated(newUrl);
}
/**
* @notice Updates the support wallet address.
* @dev Can only be called by an address with the DEFAULT_ADMIN_ROLE.
* Scenario: Use if the support wallet's key is compromised.
* @param newWallet The address of the new support wallet.
*/
function setSupportWallet(address newWallet) public onlyRole(DEFAULT_ADMIN_ROLE) {
require(newWallet != address(0), "WheatToken: New support wallet cannot be zero address");
supportWallet = newWallet;
emit SupportWalletUpdated(newWallet);
}
/**
* @notice Freezes or unfreezes a wallet, preventing it from sending or receiving tokens.
* @dev Can only be called by an address with the FREEZER_ROLE.
* Scenario: Use to block addresses associated with illicit activities or hacks.
* @param account The address of the wallet to update.
* @param freeze The desired state: `true` to freeze, `false` to unfreeze.
*/
function freezeWallet(address account, bool freeze) public onlyRole(FREEZER_ROLE) {
require(account != address(0), "WheatToken: Cannot freeze the zero address");
frozenWallets[account] = freeze;
if (freeze) {
emit WalletFrozen(account);
} else {
emit WalletUnfrozen(account);
}
}
// --- Custom Distribution Logic ---
/**
* @notice Funds the batch distribution wallet from the caller's balance.
* @dev Can only be called by an address with the DISTRIBUTOR_ROLE. Any previous balance
* in the batch wallet is burned first to prevent accidental mixing of funds.
* @param amount The amount of tokens to deposit.
*/
function fundBatchWallet(uint256 amount) public onlyRole(DISTRIBUTOR_ROLE) nonReentrant whenNotPaused {
address distributor = _msgSender();
require(amount > 0, "WheatToken: Amount must be greater than zero");
require(balanceOf(distributor) >= amount, "WheatToken: Distributor has insufficient funds");
uint256 currentBalance = balanceOf(batchWallet);
if (currentBalance > 0) {
_burn(batchWallet, currentBalance);
}
_transfer(distributor, batchWallet, amount);
emit BatchWalletFunded(amount);
}
/**
* @notice Funds the airdrop wallet from the caller's balance.
* @dev Can only be called by an address with the DISTRIBUTOR_ROLE. Any previous balance
* in the airdrop wallet is burned first.
* @param amount The amount of tokens to deposit.
*/
function fundAirdropWallet(uint256 amount) public onlyRole(DISTRIBUTOR_ROLE) nonReentrant whenNotPaused {
address distributor = _msgSender();
require(amount > 0, "WheatToken: Amount must be greater than zero");
require(balanceOf(distributor) >= amount, "WheatToken: Distributor has insufficient funds");
uint256 currentBalance = balanceOf(airdropWallet);
if (currentBalance > 0) {
_burn(airdropWallet, currentBalance);
}
_transfer(distributor, airdropWallet, amount);
emit AirdropWalletFunded(amount);
}
/**
* @notice Distributes specified token amounts to a list of users from the batch wallet.
* @dev Can only be called by an address with the DISTRIBUTOR_ROLE.
* @param users An array of recipient addresses.
* @param amounts An array of corresponding token amounts to send.
*/
function distributeFromBatch(address[] calldata users, uint256[] calldata amounts) public onlyRole(DISTRIBUTOR_ROLE) nonReentrant whenNotPaused {
distributeFrom(batchWallet, users, amounts);
}
/**
* @notice Distributes specified token amounts to a list of users from the airdrop wallet.
* @dev Can only be called by an address with the DISTRIBUTOR_ROLE.
* @param users An array of recipient addresses.
* @param amounts An array of corresponding token amounts to send.
*/
function distributeFromAirdrop(address[] calldata users, uint256[] calldata amounts) public onlyRole(DISTRIBUTOR_ROLE) nonReentrant whenNotPaused {
distributeFrom(airdropWallet, users, amounts);
}
/**
* @dev Internal core logic for distributing tokens from a source wallet.
* @param fromWallet The system wallet to send tokens from (e.g., batchWallet or airdropWallet).
* @param users An array of recipient addresses.
* @param amounts An array of corresponding token amounts.
*/
function distributeFrom(address fromWallet, address[] calldata users, uint256[] calldata amounts) internal {
uint256 usersLength = users.length;
require(usersLength > 0, "WheatToken: Recipient list cannot be empty");
require(usersLength <= DISTRIBUTION_LIMIT, "WheatToken: Distribution list exceeds limit");
require(usersLength == amounts.length, "WheatToken: users and amounts array length mismatch");
uint256 totalAmount = 0;
for (uint256 i = 0; i < usersLength; ) {
require(amounts[i] < type(uint128).max, "WheatToken: Amount value is too large");
totalAmount += amounts[i];
unchecked { ++i; }
}
require(balanceOf(fromWallet) >= totalAmount, "WheatToken: Insufficient balance in source wallet");
for (uint256 i = 0; i < usersLength; ) {
address recipient = users[i];
require(recipient != address(0), "WheatToken: Invalid recipient (zero address)");
require(!frozenWallets[recipient], "WheatToken: Recipient wallet is frozen");
_transfer(fromWallet, recipient, amounts[i]);
unchecked { ++i; }
}
emit TokensDistributed(fromWallet, totalAmount, usersLength);
}
// --- Rescue Functions ---
/**
* @notice Allows rescuing any other ERC20 token accidentally sent to this contract.
* @dev Can only be called by an address with the RESCUER_ROLE.
* Scenario: A user mistakenly sends a different token (e.g., USDT) to this contract's address.
* This function allows an admin to send those tokens back to the intended owner.
* @param tokenAddress The contract address of the token to be rescued.
* @param to The address to which the rescued tokens will be sent.
* @param amount The amount of tokens to be rescued.
*/
function rescueErc20(address tokenAddress, address to, uint256 amount) public onlyRole(RESCUER_ROLE) {
require(amount > 0, "WheatToken: Amount must be greater than zero");
require(tokenAddress != address(this), "WheatToken: Cannot rescue this contract's own token");
IERC20(tokenAddress).safeTransfer(to, amount);
emit TokensRescued(tokenAddress, to, amount);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment