Created
July 28, 2025 09:50
-
-
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=
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; | |
| // 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