Created
July 30, 2025 11:18
-
-
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=
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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