Skip to content

Instantly share code, notes, and snippets.

@masihtehrani
Created July 28, 2025 09:50
Show Gist options
  • Select an option

  • Save masihtehrani/91201e9cfef263a7ca91dba4c8b94db4 to your computer and use it in GitHub Desktop.

Select an option

Save masihtehrani/91201e9cfef263a7ca91dba4c8b94db4 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;
// Importing standard libraries from OpenZeppelin
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol"; // Using AccessControl instead of Ownable for granular permissions
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/**
* @title WheatToken
* @notice This contract is a secure ERC20 token with role-based access control, pausable functionality, wallet freezing, and a controlled distribution workflow.
* @dev This version uses AccessControl for more granular permissions instead of a single Owner, enhancing security and decentralization.
*/
contract WheatToken is ERC20, AccessControl, Pausable, ReentrancyGuard {
using SafeERC20 for IERC20;
// --- Roles ---
// The DEFAULT_ADMIN_ROLE grants the ability to manage all other roles.
// It's critical to keep the address with this role extremely secure.
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; // Now a changeable state variable for security reasons
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 ---
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.
* @param _burnWallet The permanent address for burning tokens.
* @param _batchWallet The system address for batch distributions.
* @param _airdropWallet The system address for airdrop campaigns.
* @param _supportWallet The initial address for the support wallet.
* @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") {
require(_burnWallet != address(0), "Burn wallet address cannot be zero");
require(_batchWallet != address(0), "Batch wallet address cannot be zero");
require(_airdropWallet != address(0), "Airdrop wallet address cannot be zero");
require(_supportWallet != address(0), "Support wallet address cannot be zero");
burnWallet = _burnWallet;
batchWallet = _batchWallet;
airdropWallet = _airdropWallet;
supportWallet = _supportWallet;
_tokenImageUrl = tokenImageUrl_;
_websiteUrl = websiteUrl_;
// --- Role Setup ---
// The deployer of the contract gets the admin role by default.
// The admin can then grant/revoke roles to other addresses for decentralization.
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 initial supply to the deployer, who can then fund the distribution wallets.
_mint(deployer, initialSupply);
}
// --- Internal Logic ---
/**
* @dev Central control function for all custom token transfer rules.
* @notice Scenarios:
* 1. Universal Rule: Frozen wallets can never send or receive tokens.
* 2. Self-Transfer Rule: Transferring tokens to the same address is disallowed.
* 3. Support Wallet Exception: If a transaction involves the support wallet, it bypasses standard system wallet restrictions.
* 4. Distributor Rule: Addresses with the DISTRIBUTOR_ROLE are blocked from using standard transfer.
* 5. Standard Rule: For all other transfers, direct deposits to system wallets are blocked.
* @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;
}
// --- Standard Rules for all other transfers ---
// ADDED: Rule to prevent distributors from using standard transfer.
// This forces them to use the transparent funding functions.
require(!hasRole(DISTRIBUTOR_ROLE, from), "WheatToken: Distributors must use funding functions");
// Rule: Transfers to system wallets are restricted.
require(to != burnWallet, "WheatToken: Cannot transfer to burn wallet directly; use burn()");
require(to != batchWallet, "WheatToken: Use dedicated function to fund batch wallet");
require(to != airdropWallet, "WheatToken: Use dedicated function to fund airdrop wallet");
}
// --- Standard ERC20 Functions (Overridden) ---
function decimals() public pure override returns (uint8) {
return 3;
}
function transfer(address to, uint256 amount) public virtual override whenNotPaused returns (bool) {
address from = _msgSender();
_checkTransferRules(from, to);
return super.transfer(to, amount);
}
function transferFrom(address from, address to, uint256 amount) public virtual override whenNotPaused returns (bool) {
_checkTransferRules(from, to);
return super.transferFrom(from, to, amount);
}
// --- Core Functions ---
function tokenImageUrl() public view returns (string memory) {
return _tokenImageUrl;
}
function websiteUrl() public view returns (string memory) {
return _websiteUrl;
}
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 For whom: Only addresses with the PAUSER_ROLE.
* Scenario: Used in emergencies to halt all token activity if a vulnerability is discovered.
*/
function pause() public onlyRole(PAUSER_ROLE) {
_pause();
}
/**
* @notice Unpauses the contract, resuming all token transfers.
* @dev For whom: Only addresses with the PAUSER_ROLE.
*/
function unpause() public onlyRole(PAUSER_ROLE) {
_unpause();
}
/**
* @notice Updates the URL of the token's image.
* @dev For whom: Only addresses 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 of the project's website.
* @dev For whom: Only addresses 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 For whom: Only addresses with the DEFAULT_ADMIN_ROLE.
* Scenario: Used if the private key for the current support wallet is compromised, allowing the admin to assign a new, secure wallet.
* @param newWallet The address of the new support wallet.
*/
function setSupportWallet(address newWallet) public onlyRole(DEFAULT_ADMIN_ROLE) {
require(newWallet != address(0), "WheatToken: Invalid address");
supportWallet = newWallet;
emit SupportWalletUpdated(newWallet);
}
/**
* @notice Freezes or unfreezes a wallet, preventing it from sending or receiving tokens.
* @dev For whom: Only addresses with the FREEZER_ROLE.
* Scenario: Used 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. Any previous balance is burned first.
* @dev For whom: Only addresses with the DISTRIBUTOR_ROLE.
* Scenario: A distributor funds this wallet from the main treasury before starting a distribution campaign.
* @param amount The amount to deposit into the wallet.
*/
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);
}
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);
}
function distributeFromBatch(
address[] calldata users,
uint256[] calldata amounts
) public onlyRole(DISTRIBUTOR_ROLE) nonReentrant whenNotPaused {
distributeFrom(batchWallet, users, amounts);
}
function distributeFromAirdrop(
address[] calldata users,
uint256[] calldata amounts
) public onlyRole(DISTRIBUTOR_ROLE) nonReentrant whenNotPaused {
distributeFrom(airdropWallet, users, 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: Arrays length mismatch");
uint256 totalAmount = 0;
for (uint256 i = 0; i < usersLength; ) {
// Added sanity check for amount size to prevent potential issues.
require(amounts[i] < type(uint128).max, "WheatToken: Amount 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(!frozenWallets[recipient], "WheatToken: Recipient wallet is frozen");
require(recipient != address(0), "WheatToken: Invalid recipient (zero address)");
require(recipient != burnWallet, "WheatToken: Invalid recipient (burn wallet)");
require(recipient != batchWallet, "WheatToken: Invalid recipient (batch wallet)");
require(recipient != airdropWallet, "WheatToken: Invalid recipient (airdrop wallet)");
_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 For whom: Only addresses with the RESCUER_ROLE.
* Scenario: A user mistakenly sends USDT to this contract address. The rescuer can call this function to send the USDT back to the user.
* @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 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), "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