Created
July 19, 2025 12:19
-
-
Save masihtehrani/6388b015250607af4284e37b1fbda8fa 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; | |
| import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | |
| import "@openzeppelin/contracts/access/Ownable2Step.sol"; | |
| import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; | |
| import "@openzeppelin/contracts/security/Pausable.sol"; | |
| import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | |
| /** | |
| * @title WheatToken | |
| * @author Your Name/Team | |
| * @notice A pausable, securitized ERC20 token with a fixed supply, controlled distribution workflow, and wallet freezing capabilities. | |
| * @dev This contract enhances standard ERC20 functionality with several security patterns: | |
| * - Ownership transfer is a two-step process to prevent errors (`Ownable2Step`). | |
| * - All token movements can be halted in an emergency (`Pausable`). | |
| * - The owner is restricted from using standard `transfer`, enforcing a transparent distribution process. | |
| * - All token amounts in function parameters and events are handled in their smallest unit (e.g., like wei). | |
| */ | |
| contract WheatToken is ERC20, Ownable2Step, ReentrancyGuard, Pausable { | |
| using SafeERC20 for IERC20; | |
| // --- State Variables --- | |
| /// @notice The designated address for burning tokens. It's immutable for security. | |
| address public immutable burnWallet; | |
| /// @notice A system wallet used for funding planned, smaller distribution batches. | |
| address public immutable batchWallet; | |
| /// @notice A system wallet used for funding large-scale airdrop campaigns. | |
| address public immutable airdropWallet; | |
| /// @dev Private storage for the token's image URL, accessible via a public getter. | |
| string private _tokenImageUrl; | |
| /// @dev Private storage for the project's website URL, accessible via a public getter. | |
| string private _websiteUrl; | |
| /// @notice A mapping to track frozen (blacklisted) wallets. `true` means frozen. | |
| mapping(address => bool) public frozenWallets; | |
| // --- Constants --- | |
| /// @notice To prevent out-of-gas errors, this limits the number of recipients in a single distribution call. | |
| uint256 public constant DISTRIBUTION_LIMIT = 200; | |
| // --- Events --- | |
| /// @notice Emitted when tokens are permanently destroyed from an account. | |
| event TokensBurned(address indexed from, uint256 amount); | |
| /// @notice Emitted when an account is frozen by the owner. | |
| event WalletFrozen(address indexed wallet); | |
| /// @notice Emitted when an account is unfrozen by the owner. | |
| event WalletUnfrozen(address indexed wallet); | |
| /// @notice Emitted when the batch wallet is successfully funded by the owner. | |
| event BatchWalletFunded(uint256 amount); | |
| /// @notice Emitted when the airdrop wallet is successfully funded by the owner. | |
| event AirdropWalletFunded(uint256 amount); | |
| /// @notice Emitted after a successful distribution from a system wallet. | |
| event TokensDistributed(address indexed fromWallet, uint256 totalAmount, uint256 userCount); | |
| /// @notice Emitted when the token image URL is updated. | |
| event TokenImageUrlUpdated(string newUrl); | |
| /// @notice Emitted when the project website URL is updated. | |
| event WebsiteUrlUpdated(string newUrl); | |
| /// @notice Emitted when other ERC20 tokens are rescued from this contract. | |
| event TokensRescued(address indexed token, address indexed to, uint256 amount); | |
| // --- Modifiers --- | |
| /** | |
| * @dev Ensures that direct transfers to critical system wallets are blocked. | |
| * This forces the use of dedicated funding functions, enhancing clarity and security. | |
| */ | |
| modifier checkSystemWallets(address to) { | |
| require(to != burnWallet, "WheatToken: Cannot transfer to burn wallet"); | |
| require(to != batchWallet, "WheatToken: Use dedicated function to fund batch wallet"); | |
| require(to != airdropWallet, "WheatToken: Use dedicated function to fund airdrop wallet"); | |
| require(to != owner(), "WheatToken: Cannot transfer directly to owner"); | |
| _; | |
| } | |
| /** | |
| * @dev Enforces the core security policy that the contract owner cannot use standard `transfer` or | |
| * `transferFrom`. This forces all owner-initiated token movements through the transparent | |
| * fund-and-distribute workflow. | |
| */ | |
| modifier ownerCannotTransfer(address from) { | |
| require(from != owner(), "WheatToken: Owner cannot be the source of standard transfers"); | |
| _; | |
| } | |
| /** | |
| * @dev Checks if an account is frozen. Reverts the transaction if the account is frozen. | |
| */ | |
| modifier whenNotFrozen(address account) { | |
| require(!frozenWallets[account], "WheatToken: Wallet is frozen"); | |
| _; | |
| } | |
| // --- Constructor --- | |
| /** | |
| * @notice Initializes the contract, sets immutable wallet addresses, and mints the total supply. | |
| * @param initialSupply The total supply of tokens, expressed in the smallest unit (e.g., if decimals is 3, 1 token = 1000 units). | |
| * @param _burnWallet The address to be used as the permanent burn wallet. | |
| * @param _batchWallet The address for the batch distribution system wallet. | |
| * @param _airdropWallet The address for the airdrop distribution system wallet. | |
| * @param tokenImageUrl_ The initial URL of the token's image. | |
| * @param websiteUrl_ The initial URL of the project's website. | |
| */ | |
| constructor( | |
| uint256 initialSupply, | |
| address _burnWallet, | |
| address _batchWallet, | |
| address _airdropWallet, | |
| string memory tokenImageUrl_, | |
| string memory websiteUrl_ | |
| ) ERC20("WheatToken", "GND") Ownable(msg.sender) { | |
| 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"); | |
| burnWallet = _burnWallet; | |
| batchWallet = _batchWallet; | |
| airdropWallet = _airdropWallet; | |
| _tokenImageUrl = tokenImageUrl_; | |
| _websiteUrl = websiteUrl_; | |
| _mint(owner(), initialSupply); | |
| } | |
| // --- ERC20 Functions & Overrides --- | |
| /** | |
| * @notice Returns 3, meaning the token's whole unit can be divided into 1000 smaller units. | |
| */ | |
| function decimals() public pure override returns (uint8) { | |
| return 3; | |
| } | |
| /** | |
| * @notice Overrides the standard ERC20 transfer with added security checks: pausable, wallet freeze, | |
| * owner transfer restriction, and system wallet protection. | |
| */ | |
| function transfer(address to, uint256 amount) | |
| public | |
| virtual | |
| override | |
| whenNotPaused | |
| whenNotFrozen(_msgSender()) | |
| whenNotFrozen(to) | |
| ownerCannotTransfer(_msgSender()) | |
| checkSystemWallets(to) | |
| returns (bool) | |
| { | |
| return super.transfer(to, amount); | |
| } | |
| /** | |
| * @notice Overrides the standard ERC20 transferFrom with added security checks. | |
| */ | |
| function transferFrom(address from, address to, uint256 amount) | |
| public | |
| virtual | |
| override | |
| whenNotPaused | |
| whenNotFrozen(from) | |
| whenNotFrozen(to) | |
| ownerCannotTransfer(from) | |
| checkSystemWallets(to) | |
| returns (bool) | |
| { | |
| return super.transferFrom(from, to, amount); | |
| } | |
| // --- Core 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; | |
| } | |
| /** | |
| * @notice Allows any user to burn their own tokens, permanently removing them from the total supply. | |
| * @param amount The amount of tokens (in the smallest unit) 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); | |
| } | |
| /// @notice Pauses all token transfers and distributions. Can only be called by the owner. | |
| function pause() public onlyOwner { | |
| _pause(); | |
| } | |
| /// @notice Unpauses the contract, resuming all token transfers and distributions. Can only be called by the owner. | |
| function unpause() public onlyOwner { | |
| _unpause(); | |
| } | |
| /// @notice Updates the token image URL. Can only be called by the owner. | |
| function setTokenImageUrl(string memory newUrl) public onlyOwner { | |
| _tokenImageUrl = newUrl; | |
| emit TokenImageUrlUpdated(newUrl); | |
| } | |
| /// @notice Updates the project website URL. Can only be called by the owner. | |
| function setWebsiteUrl(string memory newUrl) public onlyOwner { | |
| _websiteUrl = newUrl; | |
| emit WebsiteUrlUpdated(newUrl); | |
| } | |
| /** | |
| * @notice Freezes or unfreezes a wallet address. Frozen wallets cannot send or receive tokens. | |
| * @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 onlyOwner { | |
| require(account != address(0), "WheatToken: Cannot freeze the zero address"); | |
| require(account != owner(), "WheatToken: Owner wallet cannot be frozen"); | |
| frozenWallets[account] = freeze; | |
| if (freeze) { | |
| emit WalletFrozen(account); | |
| } else { | |
| emit WalletUnfrozen(account); | |
| } | |
| } | |
| // --- Custom Distribution Logic --- | |
| /** | |
| * @notice Funds the batch wallet from the owner's account. Any previous balance is burned first. | |
| * @param amount The raw amount in the smallest unit to fund. | |
| */ | |
| function fundBatchWallet(uint256 amount) public onlyOwner nonReentrant whenNotPaused { | |
| require(amount > 0, "WheatToken: Amount must be greater than zero"); | |
| require(balanceOf(owner()) >= amount, "WheatToken: Owner has insufficient funds"); | |
| uint256 currentBalance = balanceOf(batchWallet); | |
| if (currentBalance > 0) { | |
| _burn(batchWallet, currentBalance); | |
| } | |
| _transfer(owner(), batchWallet, amount); | |
| emit BatchWalletFunded(amount); | |
| } | |
| /** | |
| * @notice Funds the airdrop wallet from the owner's account. Any previous balance is burned first. | |
| * @param amount The raw amount in the smallest unit to fund. | |
| */ | |
| function fundAirdropWallet(uint256 amount) public onlyOwner nonReentrant whenNotPaused { | |
| require(amount > 0, "WheatToken: Amount must be greater than zero"); | |
| require(balanceOf(owner()) >= amount, "WheatToken: Owner has insufficient funds"); | |
| uint256 currentBalance = balanceOf(airdropWallet); | |
| if (currentBalance > 0) { | |
| _burn(airdropWallet, currentBalance); | |
| } | |
| _transfer(owner(), airdropWallet, amount); | |
| emit AirdropWalletFunded(amount); | |
| } | |
| /** | |
| * @notice Initiates a token distribution from the batch wallet to multiple users. | |
| * @param users A list of recipient addresses. | |
| * @param amounts A list of token amounts (in the smallest unit) for each user. | |
| */ | |
| function distributeFromBatch( | |
| address[] calldata users, | |
| uint256[] calldata amounts | |
| ) public onlyOwner nonReentrant whenNotPaused { | |
| distributeFrom(batchWallet, users, amounts); | |
| } | |
| /** | |
| * @notice Initiates a token distribution from the airdrop wallet to multiple users. | |
| * @param users A list of recipient addresses. | |
| * @param amounts A list of token amounts (in the smallest unit) for each user. | |
| */ | |
| function distributeFromAirdrop( | |
| address[] calldata users, | |
| uint256[] calldata amounts | |
| ) public onlyOwner nonReentrant whenNotPaused { | |
| distributeFrom(airdropWallet, users, amounts); | |
| } | |
| /** | |
| * @dev Internal core logic for distributing tokens to avoid code duplication. | |
| * @param fromWallet The system wallet (batch or airdrop) to send tokens from. | |
| * @param users The list of recipient addresses. | |
| * @param amounts The list of token amounts to send. | |
| */ | |
| 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; ) { | |
| 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 the owner to rescue any other ERC20 tokens accidentally sent to this contract. | |
| * @dev This is a crucial safety feature to prevent loss of funds. | |
| * @param tokenAddress The address of the ERC20 token to rescue. | |
| * @param to The address to send the rescued tokens to. | |
| * @param amount The amount of tokens to rescue. | |
| */ | |
| function rescueErc20(address tokenAddress, address to, uint256 amount) public onlyOwner { | |
| 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