Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save masihtehrani/293a850acb8f79d62e24f2018799f97c to your computer and use it in GitHub Desktop.

Revisions

  1. masihtehrani created this gist Jul 30, 2025.
    208 changes: 208 additions & 0 deletions contracts...RwaToken.sol
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,208 @@
    // File: RwaToken.sol
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.20;

    // All necessary imports
    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";

    // This struct definition remains the same
    struct RwaTokenParams {
    string name;
    string symbol;
    uint256 initialSupply;
    address burnWallet;
    address batchWallet;
    address airdropWallet;
    address supportWallet;
    string tokenImageUrl;
    string websiteUrl;
    }

    // CORRECTED: Removed ERC20Upgradeable from the inheritance list as it's already included in ERC20PermitUpgradeable
    contract RwaToken is Initializable, AccessControlUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, ERC20PermitUpgradeable {
    using SafeERC20 for IERC20;

    // --- Roles ---
    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;

    // --- 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);

    function initialize(RwaTokenParams calldata params) public initializer {
    __ERC20_init(params.name, params.symbol);
    __ERC20Permit_init(params.name);
    __AccessControl_init();
    __Pausable_init();
    __ReentrancyGuard_init();

    require(params.burnWallet != address(0), "RwaToken: Burn wallet cannot be zero");
    require(params.batchWallet != address(0), "RwaToken: Batch wallet cannot be zero");
    require(params.airdropWallet != address(0), "RwaToken: Airdrop wallet cannot be zero");
    require(params.supportWallet != address(0), "RwaToken: Support wallet cannot be zero");

    burnWallet = params.burnWallet;
    batchWallet = params.batchWallet;
    airdropWallet = params.airdropWallet;
    supportWallet = params.supportWallet;
    _tokenImageUrl = params.tokenImageUrl;
    _websiteUrl = params.websiteUrl;

    address deployer = _msgSender();
    _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(deployer, params.initialSupply);
    }

    modifier whenNotFrozen(address account) {
    require(!frozenWallets[account], "RwaToken: Wallet is frozen");
    _;
    }

    /**
    * @dev Central hook for all transfer-related checks.
    */
    // CORRECTED: Simplified override. The compiler now understands which function to override.
    function _update(address from, address to, uint256 value) internal virtual override {
    require(!paused(), "RwaToken: token transfer while paused");

    if (from == address(0) || to == address(0)) {
    super._update(from, to, value);
    return;
    }

    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");

    if (from == supportWallet || to == supportWallet) {
    super._update(from, to, value);
    return;
    }

    require(!hasRole(DISTRIBUTOR_ROLE, from), "RwaToken: Distributors must use funding functions");
    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);
    }

    function decimals() public pure override returns (uint8) { return 3; }

    function tokenImageUrl() public view returns (string memory) { return _tokenImageUrl; }
    function websiteUrl() public view returns (string memory) { return _websiteUrl; }

    function burn(uint256 amount) public whenNotFrozen(_msgSender()) {
    require(amount > 0, "RwaToken: Burn amount must be > 0");
    _burn(_msgSender(), 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;
    emit TokenImageUrlUpdated(newUrl);
    }

    function setWebsiteUrl(string memory newUrl) public onlyRole(CONFIGURATOR_ROLE) {
    _websiteUrl = newUrl;
    emit WebsiteUrlUpdated(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);
    }

    function fundBatchWallet(uint256 amount) public onlyRole(DISTRIBUTOR_ROLE) nonReentrant {
    require(amount > 0, "RwaToken: Amount must be > 0");
    uint256 currentBalance = balanceOf(batchWallet);
    if (currentBalance > 0) _burn(batchWallet, currentBalance);
    _transfer(_msgSender(), batchWallet, amount);
    emit BatchWalletFunded(amount);
    }

    function fundAirdropWallet(uint256 amount) public onlyRole(DISTRIBUTOR_ROLE) nonReentrant {
    require(amount > 0, "RwaToken: Amount must be > 0");
    uint256 currentBalance = balanceOf(airdropWallet);
    if (currentBalance > 0) _burn(airdropWallet, currentBalance);
    _transfer(_msgSender(), airdropWallet, amount);
    emit AirdropWalletFunded(amount);
    }

    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; ) {
    require(users[i] != address(0), "RwaToken: Invalid recipient");
    _transfer(fromWallet, users[i], amounts[i]);
    unchecked { ++i; }
    }
    emit TokensDistributed(fromWallet, totalAmount, usersLength);
    }

    function rescueErc20(address tokenAddress, address to, uint256 amount) public onlyRole(RESCUER_ROLE) {
    require(amount > 0, "RwaToken: Amount must be > 0");
    require(tokenAddress != address(this), "RwaToken: Cannot rescue self");
    IERC20(tokenAddress).safeTransfer(to, amount);
    emit TokensRescued(tokenAddress, to, amount);
    }
    }