Skip to content

Instantly share code, notes, and snippets.

@masihtehrani
Created July 19, 2025 12:19
Show Gist options
  • Select an option

  • Save masihtehrani/6388b015250607af4284e37b1fbda8fa to your computer and use it in GitHub Desktop.

Select an option

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=
// 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