Skip to content

Instantly share code, notes, and snippets.

@masihtehrani
Created August 4, 2025 11:28
Show Gist options
  • Select an option

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

Select an option

Save masihtehrani/fef1dab79044d5f082acd30043cec188 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=true&runs=200&gist=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
/**
* @title RwaTokenParams Struct
* @dev Defines the set of parameters required to initialize a new RwaToken.
* This struct is passed to the factory to create a new token proxy.
*/
struct RwaTokenParams {
string name; // The full name of the token (e.g., "Gandom Token").
string symbol; // The symbol or ticker of the token (e.g., "GNDM").
uint8 decimals; // The number of decimal places for the token (e.g., 18).
uint256 initialSupply; // The amount of tokens to mint upon creation and send to the initial owner.
address burnWallet; // A designated wallet for burning tokens.
address batchWallet; // A wallet used for funding "push" distributions.
address airdropWallet; // A wallet used for funding "push" airdrops (distinct from the Merkle airdrop).
address supportWallet; // A wallet that can bypass certain transfer restrictions for emergencies.
string tokenImageUrl; // A URL pointing to the token's logo.
string websiteUrl; // A URL pointing to the project's official website.
address initialOwner; // The address that will receive all administrative roles and the initial supply.
}
/**
* @title RwaToken
* @author Your Name
* @notice This is the final, secure, and upgradeable implementation for RWA tokens.
* It includes robust access control, security features, and a gas-efficient Merkle Airdrop system.
*/
contract RwaToken is Initializable, ERC20PermitUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, AccessControlUpgradeable {
using SafeERC20 for IERC20;
// --- Roles ---
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
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 burnWallet;
address public batchWallet;
address public airdropWallet;
address public supportWallet;
string private _tokenImageUrl;
string private _websiteUrl;
mapping(address => bool) public frozenWallets;
uint8 private _customDecimals;
// --- Merkle Airdrop State ---
bytes32 public merkleRoot;
mapping(address => bool) public hasClaimedAirdrop;
// --- Constants ---
uint256 public constant DISTRIBUTION_LIMIT = 50;
// --- Events ---
event MerkleRootSet(bytes32 indexed newRoot, uint256 indexed timestamp);
event AirdropClaimed(address indexed user, uint256 amount);
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);
/**
* @notice Initializes the contract, setting all parameters and granting roles to the specified initial owner.
* @param params An RwaTokenParams struct containing all the initial settings for the token.
* @dev Function Owner: This is an `initializer` called by the BeaconProxy upon creation.
* @dev Scenario: The factory calls this function once when a new token is created. It explicitly uses `params.initialOwner`
* to grant all admin roles, ensuring the correct address receives permissions, regardless of the call context.
*/
function initialize(RwaTokenParams calldata params) public initializer {
__ERC20_init(params.name, params.symbol);
__ERC20Permit_init(params.name);
__AccessControl_init();
__Pausable_init();
__ReentrancyGuard_init();
__ERC165_init();
_customDecimals = params.decimals;
require(params.initialOwner != address(0), "RwaToken: Initial owner cannot be zero");
burnWallet = params.burnWallet;
batchWallet = params.batchWallet;
airdropWallet = params.airdropWallet;
supportWallet = params.supportWallet;
_tokenImageUrl = params.tokenImageUrl;
_websiteUrl = params.websiteUrl;
address initialOwner = params.initialOwner;
_grantRole(DEFAULT_ADMIN_ROLE, initialOwner);
_grantRole(MINTER_ROLE, initialOwner);
_grantRole(PAUSER_ROLE, initialOwner);
_grantRole(FREEZER_ROLE, initialOwner);
_grantRole(DISTRIBUTOR_ROLE, initialOwner);
_grantRole(RESCUER_ROLE, initialOwner);
_grantRole(CONFIGURATOR_ROLE, initialOwner);
if (params.initialSupply > 0) {
_mint(initialOwner, params.initialSupply);
}
}
/**
* @dev Modifier to check if an account is frozen.
*/
modifier whenNotFrozen(address account) {
require(!frozenWallets[account], "RwaToken: Wallet is frozen");
_;
}
/**
* @dev Central hook for all token transfers, applying custom rules.
* Overrides the standard `_update` function to inject custom logic.
* This version is refactored for clarity and security, removing the need for a separate state flag.
*/
function _update(address from, address to, uint256 value) internal virtual override {
require(!paused(), "RwaToken: token transfer while paused");
// Minting (from == 0) and burning (to == 0) bypass all custom rules.
if (from == address(0) || to == address(0)) {
super._update(from, to, value);
return;
}
// A special case to allow funding operations: a DISTRIBUTOR sending TO a special wallet.
bool isFundingOperation = hasRole(DISTRIBUTOR_ROLE, from) && (to == airdropWallet || to == batchWallet);
if (isFundingOperation) {
super._update(from, to, value);
return;
}
// Standard checks for all other regular transfers.
require(from != to, "RwaToken: Self-transfer is not allowed");
require(!frozenWallets[from], "RwaToken: Sender wallet is frozen");
require(!frozenWallets[to], "RwaToken: Receiver wallet is frozen");
// The support wallet is exempt from certain rules for emergency operations.
if (from == supportWallet || to == supportWallet) {
super._update(from, to, value);
return;
}
// For regular transfers, prevent direct sends to special wallets to enforce proper function usage.
require(to != burnWallet, "RwaToken: Use burn() instead of direct transfer");
require(to != batchWallet, "RwaToken: Use fundBatchWallet() instead of direct transfer");
require(to != airdropWallet, "RwaToken: Use fundAirdropWallet() instead of direct transfer");
super._update(from, to, value);
}
/**
* @notice See {IERC165-supportsInterface}.
* @dev Declares support for ERC165 and AccessControl interfaces.
* This makes the contract more compatible with other dApps and wallets (including AA).
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControlUpgradeable) returns (bool) {
return interfaceId == type(IAccessControlUpgradeable).interfaceId || super.supportsInterface(interfaceId);
}
// --- Merkle Airdrop Functions ---
/**
* @notice Sets the Merkle root for a new airdrop campaign.
* @dev The total supply for the airdrop must be minted TO THIS CONTRACT's address beforehand.
* @param _newRoot The new Merkle root generated off-chain.
* @dev Function Owner: An address with `DISTRIBUTOR_ROLE`.
* @dev Scenario: Admin generates a Merkle tree from the list of airdrop recipients and calls this function
* once to set the root hash, officially starting the airdrop campaign.
*/
function setMerkleRoot(bytes32 _newRoot) public onlyRole(DISTRIBUTOR_ROLE) {
merkleRoot = _newRoot;
emit MerkleRootSet(_newRoot, block.timestamp);
}
/**
* @notice Allows a user to claim their airdrop allocation by providing a valid proof.
* @param amount The amount of tokens the user is eligible for.
* @param merkleProof The proof generated off-chain for the user.
* @dev Function Owner: Any user.
* @dev Scenario: A user visits the project's website, connects their wallet. The frontend finds the user's
* allocation amount and their specific proof, then calls this function. The contract verifies the proof
* and transfers the tokens if valid.
*/
function claimAirdrop(uint256 amount, bytes32[] calldata merkleProof) public nonReentrant {
require(merkleRoot != bytes32(0), "Airdrop: Not active");
require(!hasClaimedAirdrop[msg.sender], "Airdrop: Already claimed");
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, amount));
require(MerkleProof.verify(merkleProof, merkleRoot, leaf), "Airdrop: Invalid proof");
hasClaimedAirdrop[msg.sender] = true;
require(balanceOf(address(this)) >= amount, "Airdrop: Insufficient funds in contract");
_transfer(address(this), msg.sender, amount);
emit AirdropClaimed(msg.sender, amount);
}
// --- View Functions ---
function decimals() public view virtual override returns (uint8) { return _customDecimals; }
function tokenImageUrl() public view returns (string memory) { return _tokenImageUrl; }
function websiteUrl() public view returns (string memory) { return _websiteUrl; }
// --- Public User Functions ---
/**
* @notice Allows any user to burn their own tokens.
* @param amount The quantity of tokens to burn.
* @dev Function Owner: Any token holder.
* @dev Scenario: A user wants to permanently remove their tokens from circulation.
* @dev Example: A user calls `burn(100 * 10**18)` to destroy 100 of their tokens.
*/
function burn(uint256 amount) public whenNotFrozen(_msgSender()) {
require(amount > 0, "RwaToken: Burn amount must be > 0");
_burn(_msgSender(), amount);
emit TokensBurned(_msgSender(), amount);
}
// --- Administrative Functions ---
/**
* @notice Mints new tokens and assigns them to a specified address.
* @param to The address to receive the new tokens.
* @param amount The quantity of new tokens to create.
* @dev Function Owner: An address with `MINTER_ROLE`.
* @dev Scenario: The project team needs to increase the total supply of the token.
* @dev Example: Admin calls `mint("0xRecipientAddress", 10000 * 10**18)` to create 10,000 new tokens.
*/
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { _mint(to, amount); }
function pause() public onlyRole(PAUSER_ROLE) { _pause(); }
function unpause() public onlyRole(PAUSER_ROLE) { _unpause(); }
function setTokenImageUrl(string memory newUrl) public onlyRole(CONFIGURATOR_ROLE) { _tokenImageUrl = newUrl; }
function setWebsiteUrl(string memory newUrl) public onlyRole(CONFIGURATOR_ROLE) { _websiteUrl = newUrl; }
function setSupportWallet(address newWallet) public onlyRole(DEFAULT_ADMIN_ROLE) {
require(newWallet != address(0), "RwaToken: Invalid address");
supportWallet = newWallet;
emit SupportWalletUpdated(newWallet);
}
function freezeWallet(address account, bool freeze) public onlyRole(FREEZER_ROLE) {
require(account != address(0), "RwaToken: Cannot freeze zero address");
frozenWallets[account] = freeze;
if (freeze) emit WalletFrozen(account);
else emit WalletUnfrozen(account);
}
// --- "Push" Distribution Functions ---
/**
* @notice Funds the batch wallet from the caller's balance. Any existing balance is burned first.
* @param amount The amount of tokens to send to the batch wallet.
* @dev Function Owner: An address with `DISTRIBUTOR_ROLE`.
*/
function fundBatchWallet(uint256 amount) public onlyRole(DISTRIBUTOR_ROLE) nonReentrant {
uint256 currentBalance = balanceOf(batchWallet);
if (currentBalance > 0) {
_burn(batchWallet, currentBalance);
emit TokensBurned(batchWallet, currentBalance);
}
_transfer(_msgSender(), batchWallet, amount);
emit BatchWalletFunded(amount);
}
/**
* @notice Funds the airdrop wallet from the caller's balance. Any existing balance is burned first.
* @dev This is for traditional "push" airdrops, separate from the Merkle airdrop.
* @param amount The amount of tokens to send to the airdrop wallet.
* @dev Function Owner: An address with `DISTRIBUTOR_ROLE`.
*/
function fundAirdropWallet(uint256 amount) public onlyRole(DISTRIBUTOR_ROLE) nonReentrant {
uint256 currentBalance = balanceOf(airdropWallet);
if (currentBalance > 0) {
_burn(airdropWallet, currentBalance);
emit TokensBurned(airdropWallet, currentBalance);
}
_transfer(_msgSender(), airdropWallet, amount);
emit AirdropWalletFunded(amount);
}
/**
* @notice Distributes tokens from the batch wallet to multiple users.
* @param users An array of recipient addresses.
* @param amounts An array of token amounts corresponding to each recipient.
* @dev Function Owner: An address with `DISTRIBUTOR_ROLE`.
* @dev Example: `distributeFromBatch(["0xUserA", "0xUserB"], [100e18, 500e18])`.
*/
function distributeFromBatch(address[] calldata users, uint256[] calldata amounts) public onlyRole(DISTRIBUTOR_ROLE) nonReentrant {
distributeFrom(batchWallet, users, amounts);
}
function distributeFromAirdrop(address[] calldata users, uint256[] calldata amounts) public onlyRole(DISTRIBUTOR_ROLE) nonReentrant {
distributeFrom(airdropWallet, users, amounts);
}
function distributeFrom(address fromWallet, address[] calldata users, uint256[] calldata amounts) internal {
uint256 usersLength = users.length;
require(usersLength > 0 && usersLength <= DISTRIBUTION_LIMIT && usersLength == amounts.length, "RwaToken: Invalid distribution arrays");
uint256 totalAmount = 0;
for (uint256 i = 0; i < usersLength; ) {
totalAmount += amounts[i];
unchecked { ++i; }
}
require(balanceOf(fromWallet) >= totalAmount, "RwaToken: Insufficient funds in source wallet");
for (uint256 i = 0; i < usersLength; ) {
_transfer(fromWallet, users[i], amounts[i]);
unchecked { ++i; }
}
}
/**
* @notice Rescues other ERC20 tokens that were accidentally sent to this contract's address.
* @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.
* @dev Function Owner: An address with `RESCUER_ROLE`.
* @dev Scenario: A user mistakenly sends USDC to this contract address. The admin can recover the USDC.
*/
function rescueErc20(address tokenAddress, address to, uint256 amount) public onlyRole(RESCUER_ROLE) {
require(tokenAddress != address(this), "RwaToken: Cannot rescue self");
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