Created
August 4, 2025 11:28
-
-
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=
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-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