MultiTokenPresale

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "MultiTokenPresale.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "./Authorizer.sol";

/**
 * @title MultiTokenPresale
 * @notice Advanced presale contract with KYC voucher authorization system
 * @dev Implements strict beneficiary validation through cryptographic vouchers:
 * - UnityPresaleV2.sol: Enforces beneficiary == msg.sender (no delegated purchases)
 * - MultiTokenPresale.sol: Allows delegated purchases ONLY through authorized vouchers
 * - This ensures consistent validation across the presale system
 * 
 */
contract MultiTokenPresale is Ownable, ReentrancyGuard, Pausable {
    using SafeERC20 for IERC20;
    
    // GRO-02: Hardcoded owner/treasury address (hardware wallet)
    // Security rationale: 1 hardware wallet > 2-of-3 multisig with hot wallets
    // - Hardware wallet keys never exposed to internet (physical 2FA)
    // - Multisig with 2 hot wallets = increased attack surface
    // - If 2 multisig keys lost = funds lost forever
    address public constant OWNER_ADDRESS = 0xd81d23f2e37248F8fda5e7BF0a6c047AE234F0A2;
    
    // Token price structure
    struct TokenPrice {
        uint256 priceUSD;        // Price in USD (8 decimals)
        bool isActive;           // Whether this token is accepted
        uint8 decimals;          // Token decimals
    }
    
    // Presale token details
    IERC20 public presaleToken;
    uint256 public immutable presaleRate;  // Tokens per USD (18 decimals)
    uint256 public immutable maxTokensToMint;
    uint256 public totalTokensMinted;
    
    // Authorizer integration for voucher-based purchases
    Authorizer public authorizer;
    bool public voucherSystemEnabled = false; // Disabled by default for compatibility
    
    // Treasury / beneficiary wallet
    address public treasury;
    address public pendingTreasury;
    
    // Dev treasury - incentives for developers who contributed to this project (receives 4% fee)
    address public immutable devTreasury;
    
    // GRO-06 Fix: Fixed gas buffer (not tx.gasprice dependent)
    uint256 public gasBuffer = 0.0005 ether; // Default 0.0005 ETH buffer
    
    // Price management
    mapping(address => TokenPrice) public tokenPrices;
    
    // User tracking
    mapping(address => mapping(address => uint256)) public purchasedAmounts; // user => token => amount
    mapping(address => uint256) public totalPurchased; // Total tokens purchased by user
    mapping(address => uint256) public totalUsdPurchased; // User's cumulative USD spent (8 decimals)
    mapping(address => bool) public hasClaimed;
    
    // GRO-19: In-contract replay protection (defense-in-depth)
    mapping(bytes32 => bool) private usedVoucherHashes; // Track consumed voucher hashes independently
    
    // Presale timing controls (for manual startPresale)
    uint256 public presaleStartTime;
    uint256 public presaleEndTime;
    bool public presaleEnded;
    
    // Escrow presale timing controls (for autoStartIEscrowPresale)
    uint256 public escrowPresaleStartTime;
    uint256 public escrowPresaleEndTime;
    bool public escrowPresaleEnded;
    
    // Scheduled launch and two rounds
    uint256 public constant PRESALE_LAUNCH_DATE = 1762819200; // Nov 11, 2025 00:00 UTC
    uint256 public constant MAX_PRESALE_DURATION = 34 days;
    uint256 public constant ROUND1_DURATION = 23 days;
    uint256 public constant ROUND2_DURATION = 11 days;
    
    // Main presale round management
    uint256 public currentRound = 0; // 0 = not started, 1 = round 1, 2 = round 2
    uint256 public round1EndTime;
    uint256 public round1TokensSold;
    uint256 public round2TokensSold;
    
    // Escrow presale round management
    uint256 public escrowCurrentRound = 0; // 0 = not started, 1 = round 1, 2 = round 2
    uint256 public escrowRound1EndTime;
    uint256 public escrowRound1TokensSold;
    uint256 public escrowRound2TokensSold;
    
    // Constants
    address public constant NATIVE_ADDRESS = address(0); // ETH on Ethereum
    uint256 public constant USD_DECIMALS = 8;
    
    // Ethereum Mainnet Token Addresses
    address public constant WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address public constant WBNB_ADDRESS = 0x418D75f65a02b3D53B2418FB8E1fe493759c7605;
    address public constant LINK_ADDRESS = 0x514910771AF9Ca656af840dff83E8264EcF986CA;
    address public constant WBTC_ADDRESS = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
    address public constant USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    address public constant USDT_ADDRESS = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
    
    // Events
    event TokenPurchase(
        address indexed purchaser,
        address indexed beneficiary,
        address indexed paymentToken,
        uint256 paymentAmount,
        uint256 tokenAmount
    );
    
    event TokensClaimed(address indexed user, uint256 amount);
    event PriceUpdated(address indexed token, uint256 newPrice);
    event TokenStatusUpdated(address indexed token, bool isActive);
    event PresaleStarted(uint256 startTime, uint256 endTime);
    event PresaleEnded(uint256 endTime);
    event PresaleEndedEarly(string reason, uint256 endTime);
    event RoundAdvanced(uint256 fromRound, uint256 toRound, uint256 timestamp);
    event EmergencyEnd(uint256 timestamp);
    event AutoStartTriggered(uint256 timestamp);
    event GasBufferUpdated(uint256 oldBuffer, uint256 newBuffer);
    event MaxPurchasePerUserUpdated(uint256 oldMax, uint256 newMax);
    event AuthorizerUpdated(address indexed oldAuthorizer, address indexed newAuthorizer);
    event VoucherSystemToggled(bool enabled);
    event VoucherPurchase(
        address indexed purchaser,
        address indexed beneficiary,
        address indexed paymentToken,
        uint256 paymentAmount,
        uint256 tokenAmount,
        bytes32 voucherHash
    );
    event VoucherHashConsumed(bytes32 indexed voucherHash, address indexed buyer);
    event TreasuryUpdateRequested(address indexed newTreasury);
    event TreasuryUpdated(address indexed previousTreasury, address indexed newTreasury);
    
    constructor(
        address _presaleToken,
        uint256 _presaleRate, // 0.0015 dollar per token => 666.666... tokens per USD with 18 decimals: ~666666666666666667000
        uint256 _maxTokensToMint, // 5 billion tokens to presale
        address _devTreasury // Dev treasury address for 4% fee (immutable)
    ) Ownable(OWNER_ADDRESS) {
        require(_presaleToken != address(0), "Invalid presale token");
        require(_presaleRate > 0, "Invalid presale rate");
        require(_maxTokensToMint > 0, "Invalid max tokens");
        require(_devTreasury != address(0), "Invalid dev treasury");
        
        presaleToken = IERC20(_presaleToken);
        presaleRate = _presaleRate;
        maxTokensToMint = _maxTokensToMint;
        
        // Initialize default token prices and limits
        _initializeDefaultTokens();
        
        // GRO-02: Treasury is same as owner (hardcoded hardware wallet)
        treasury = OWNER_ADDRESS;
        
        // Dev treasury for 4% fee (set in constructor, immutable)
        devTreasury = _devTreasury;
    }
    
    // ============ MODIFIERS ============
    // GRO-02: All sensitive functions restricted to owner (hardware wallet)
    modifier onlyGovernance() {
        require(msg.sender == owner(), "Only owner");
        _;
    }

    /// @notice Propose a new treasury address that will custody withdrawn funds.
    function proposeTreasury(address newTreasury) external onlyGovernance {
        require(newTreasury != address(0), "Invalid treasury");

        pendingTreasury = newTreasury;
        emit TreasuryUpdateRequested(newTreasury);
    }

    /// @notice Accept the treasury role.
    function acceptTreasury() external {
        require(msg.sender == pendingTreasury, "Caller not pending treasury");

        address previousTreasury = treasury;
        treasury = pendingTreasury;
        pendingTreasury = address(0);

        emit TreasuryUpdated(previousTreasury, treasury);
    }
    
    // Initialize default token settings for UnityFinance presale
    function _initializeDefaultTokens() internal {
        // ETH (Native) - $4200, 18 decimals, per-token cap disabled (use global cap)
        tokenPrices[NATIVE_ADDRESS] = TokenPrice({
            priceUSD: 4200 * 1e8,  // $4200
            isActive: true,
            decimals: 18
        });
        
        // WETH - $4200, 18 decimals, per-token cap disabled (use global cap)
        tokenPrices[WETH_ADDRESS] = TokenPrice({
            priceUSD: 4200 * 1e8,  // $4200
            isActive: true,
            decimals: 18
        });

        // WBNB - $1000, 18 decimals, per-token cap disabled (use global cap)
        tokenPrices[WBNB_ADDRESS] = TokenPrice({
            priceUSD: 1000 * 1e8,   // $1000
            isActive: true,
            decimals: 18
        });
        
        
        // LINK - $20, 18 decimals, per-token cap disabled (use global cap)
        tokenPrices[LINK_ADDRESS] = TokenPrice({
            priceUSD: 20 * 1e8,    // $20
            isActive: true,
            decimals: 18
        });        
        // WBTC - $45000, 8 decimals, per-token cap disabled (use global cap)
        tokenPrices[WBTC_ADDRESS] = TokenPrice({
            priceUSD: 45000 * 1e8, // $45000
            isActive: true,
            decimals: 8
        });
        
        // USDC - $1, 6 decimals, per-token cap disabled (use global cap)
        tokenPrices[USDC_ADDRESS] = TokenPrice({
            priceUSD: 1 * 1e8,     // $1
            isActive: true,
            decimals: 6
        });
        
        // USDT - $1, 6 decimals, per-token cap disabled (use global cap)
        tokenPrices[USDT_ADDRESS] = TokenPrice({
            priceUSD: 1 * 1e8,     // $1
            isActive: true,
            decimals: 6
        });
        
    }
    
    // ============ PRICE MANAGEMENT ============
    
    function setTokenPrice(
        address token,
        uint256 priceUSD,
        uint8 decimals,
        bool isActive
    ) external onlyGovernance {
        require(priceUSD > 0, "Invalid price");
        require(decimals <= 18, "Invalid decimals");
        
        tokenPrices[token] = TokenPrice({
            priceUSD: priceUSD,
            isActive: isActive,
            decimals: decimals
        });
        
        emit PriceUpdated(token, priceUSD);
        emit TokenStatusUpdated(token, isActive);
    }
    
    /// @notice Set multiple token prices atomically (can be done anytime by owner)
    function setTokenPrices(
        address[] calldata tokens,
        uint256[] calldata pricesUSD,
        uint8[] calldata decimalsArray,
        bool[] calldata activeArray
    ) external onlyGovernance {
        require(tokens.length == pricesUSD.length, "Array length mismatch");
        require(tokens.length == decimalsArray.length, "Array length mismatch");
        require(tokens.length == activeArray.length, "Array length mismatch");
        require(tokens.length > 0, "Empty arrays");
        
        for (uint256 i = 0; i < tokens.length; i++) {
            require(pricesUSD[i] > 0, "Invalid price");
            require(decimalsArray[i] <= 18, "Invalid decimals");
            
            tokenPrices[tokens[i]] = TokenPrice({
                priceUSD: pricesUSD[i],
                isActive: activeArray[i],
                decimals: decimalsArray[i]
            });
            
            emit PriceUpdated(tokens[i], pricesUSD[i]);
            emit TokenStatusUpdated(tokens[i], activeArray[i]);
        }
    }
    
    // Presale timing controls
    function startPresale(uint256 _duration) external onlyGovernance {
        require(presaleStartTime == 0, "Presale already started");
        require(!presaleEnded, "Presale already ended - cannot restart");
        require(_duration == MAX_PRESALE_DURATION, "Duration must match schedule");
        require(
            presaleToken.balanceOf(address(this)) >= maxTokensToMint,
            "Insufficient presale tokens in contract"
        );

        presaleStartTime = block.timestamp;
        round1EndTime = block.timestamp + ROUND1_DURATION;
        presaleEndTime = block.timestamp + _duration;
        currentRound = 1;

        emit PresaleStarted(presaleStartTime, presaleEndTime);
        _handleRoundTransition(0, 1);
    }
    
    // Auto-start presale on November 11, 2025 - Anyone can trigger
    function autoStartIEscrowPresale() external {
        require(escrowPresaleStartTime == 0, "Escrow presale already started");
        require(!escrowPresaleEnded, "Escrow presale already ended - cannot restart");
        require(block.timestamp >= PRESALE_LAUNCH_DATE, "Too early - presale starts Nov 11, 2025");
        
        // Verify contract has enough presale tokens (5B $ESCROW)
        uint256 contractBalance = presaleToken.balanceOf(address(this));
        require(contractBalance >= maxTokensToMint, "Insufficient presale tokens in contract");
        
        // Start Escrow Presale Round 1
        escrowPresaleStartTime = block.timestamp;
        escrowRound1EndTime = block.timestamp + ROUND1_DURATION;
        escrowPresaleEndTime = block.timestamp + MAX_PRESALE_DURATION;
        escrowCurrentRound = 1;
        
        emit PresaleStarted(escrowPresaleStartTime, escrowPresaleEndTime);
        emit AutoStartTriggered(block.timestamp);
        emit RoundAdvanced(0, 1, block.timestamp);
    }
    
    function endPresale() external onlyGovernance {
        require(presaleStartTime > 0, "Presale not started");
        require(!presaleEnded, "Presale already ended");
        if(block.timestamp < presaleEndTime) revert("Presale not ended yet");
        presaleEnded = true;
        presaleEndTime = block.timestamp;
        emit PresaleEnded(presaleEndTime);
    }
    
    function extendPresale(uint256 _additionalDuration) external onlyGovernance {
        require(presaleStartTime > 0, "Presale not started");
        require(!presaleEnded, "Presale already ended");
        require(_additionalDuration <= 7 days, "Cannot extend more than 7 days");
        uint256 newEnd = presaleEndTime + _additionalDuration;
        require(
            newEnd <= presaleStartTime + MAX_PRESALE_DURATION,
            "Cannot extend beyond max duration"
        );
        presaleEndTime = newEnd;
    }
    
    // Emergency end presale immediately
    function emergencyEndPresale() external onlyGovernance {
        require(presaleStartTime > 0, "Presale not started");
        require(!presaleEnded, "Presale already ended");
        
        presaleEnded = true;
        presaleEndTime = block.timestamp;
        
        emit EmergencyEnd(block.timestamp);
        emit PresaleEnded(presaleEndTime);
    }
    
    // End escrow presale
    function endEscrowPresale() external onlyGovernance {
        require(escrowPresaleStartTime > 0, "Escrow presale not started");
        require(!escrowPresaleEnded, "Escrow presale already ended");
        if(block.timestamp < escrowPresaleEndTime) revert("Escrow presale not ended yet");
        escrowPresaleEnded = true;
        escrowPresaleEndTime = block.timestamp;
        emit PresaleEnded(escrowPresaleEndTime);
    }
    
    // Extend escrow presale
    function extendEscrowPresale(uint256 _additionalDuration) external onlyGovernance {
        require(escrowPresaleStartTime > 0, "Escrow presale not started");
        require(!escrowPresaleEnded, "Escrow presale already ended");
        require(_additionalDuration <= 7 days, "Cannot extend more than 7 days");
        uint256 newEnd = escrowPresaleEndTime + _additionalDuration;
        require(
            newEnd <= escrowPresaleStartTime + MAX_PRESALE_DURATION,
            "Cannot extend beyond max duration"
        );
        escrowPresaleEndTime = newEnd;
    }
    
    // Emergency end escrow presale immediately
    function emergencyEndEscrowPresale() external onlyGovernance {
        require(escrowPresaleStartTime > 0, "Escrow presale not started");
        require(!escrowPresaleEnded, "Escrow presale already ended");
        
        escrowPresaleEnded = true;
        escrowPresaleEndTime = block.timestamp;
        
        emit EmergencyEnd(block.timestamp);
        emit PresaleEnded(escrowPresaleEndTime);
    }
    
    // Manually advance from Round 1 to Round 2 with required price updates
    function moveToRound2(
        address[] calldata tokens,
        uint256[] calldata pricesUSD,
        uint8[] calldata decimalsArray,
        bool[] calldata activeArray
    ) external onlyGovernance {
        require(currentRound == 1, "Not in round 1");
        require(!presaleEnded, "Presale already ended");
        require(tokens.length == pricesUSD.length, "Array length mismatch");
        require(tokens.length == decimalsArray.length, "Array length mismatch");
        require(tokens.length == activeArray.length, "Array length mismatch");
        require(tokens.length > 0, "Must provide round 2 prices");
        
        // Set new prices for round 2
        for (uint256 i = 0; i < tokens.length; i++) {
            require(pricesUSD[i] > 0, "Invalid price");
            require(decimalsArray[i] <= 18, "Invalid decimals");
            
            TokenPrice memory oldPrice = tokenPrices[tokens[i]];
            require(oldPrice.priceUSD != pricesUSD[i], "Round 2 price must differ from round 1");
            
            tokenPrices[tokens[i]] = TokenPrice({
                priceUSD: pricesUSD[i],
                isActive: activeArray[i],
                decimals: decimalsArray[i]
            });
            
            emit PriceUpdated(tokens[i], pricesUSD[i]);
            emit TokenStatusUpdated(tokens[i], activeArray[i]);
        }
        
        // Advance to round 2
        currentRound = 2;
        round1EndTime = block.timestamp;
        
        emit RoundAdvanced(1, 2, block.timestamp);
    }
    
    // Manually advance escrow presale from Round 1 to Round 2 with required price updates
    function moveEscrowToRound2(
        address[] calldata tokens,
        uint256[] calldata pricesUSD,
        uint8[] calldata decimalsArray,
        bool[] calldata activeArray
    ) external onlyGovernance {
        require(escrowCurrentRound == 1, "Escrow presale not in round 1");
        require(!escrowPresaleEnded, "Escrow presale already ended");
        require(tokens.length == pricesUSD.length, "Array length mismatch");
        require(tokens.length == decimalsArray.length, "Array length mismatch");
        require(tokens.length == activeArray.length, "Array length mismatch");
        require(tokens.length > 0, "Must provide round 2 prices");
        
        // Set new prices for round 2
        for (uint256 i = 0; i < tokens.length; i++) {
            require(pricesUSD[i] > 0, "Invalid price");
            require(decimalsArray[i] <= 18, "Invalid decimals");
            
            TokenPrice memory oldPrice = tokenPrices[tokens[i]];
            require(oldPrice.priceUSD != pricesUSD[i], "Round 2 price must differ from round 1");
            
            tokenPrices[tokens[i]] = TokenPrice({
                priceUSD: pricesUSD[i],
                isActive: activeArray[i],
                decimals: decimalsArray[i]
            });
            
            emit PriceUpdated(tokens[i], pricesUSD[i]);
            emit TokenStatusUpdated(tokens[i], activeArray[i]);
        }
        
        // Advance escrow presale to round 2
        escrowCurrentRound = 2;
        escrowRound1EndTime = block.timestamp;
        
        emit RoundAdvanced(1, 2, block.timestamp);
    }
    
    /// @notice Emergency function to update prices during round transitions (use with extreme caution)
    /// @dev Only to be used if auto-advancement occurred without price updates
    function emergencyUpdatePrices(
        address[] calldata tokens,
        uint256[] calldata pricesUSD,
        uint8[] calldata decimalsArray,
        bool[] calldata activeArray
    ) external onlyGovernance {
        require(tokens.length == pricesUSD.length, "Array length mismatch");
        require(tokens.length == decimalsArray.length, "Array length mismatch");
        require(tokens.length == activeArray.length, "Array length mismatch");
        require(tokens.length > 0, "Empty arrays");
        // Only allow during active presale for emergency situations
        require(presaleStartTime > 0 && !presaleEnded, "Presale not active");
        
        for (uint256 i = 0; i < tokens.length; i++) {
            require(pricesUSD[i] > 0, "Invalid price");
            require(decimalsArray[i] <= 18, "Invalid decimals");
            
            tokenPrices[tokens[i]] = TokenPrice({
                priceUSD: pricesUSD[i],
                isActive: activeArray[i],
                decimals: decimalsArray[i]
            });
            
            emit PriceUpdated(tokens[i], pricesUSD[i]);
            emit TokenStatusUpdated(tokens[i], activeArray[i]);
        }
    }
    
    // ============ VOUCHER-ONLY PURCHASE FUNCTIONS ============
    // NOTE: All purchases MUST use vouchers (KYC verified off-chain)
    // No direct purchase functions to prevent non-KYC purchases
    
    // ============ VOUCHER-BASED PURCHASE FUNCTIONS ============
    
    /// @notice Purchase with native currency using voucher authorization (KYC-AUTHORIZED DELEGATED PURCHASES)
    /// @dev BENEFICIARY VALIDATION POLICY: Allows delegated purchases ONLY through authorized vouchers
    /// - voucher.buyer must equal msg.sender (only voucher holder can use it)
    /// - voucher.beneficiary must equal beneficiary parameter (specified in voucher)
    /// - This enables KYC-verified delegated purchases while preventing unauthorized ones
    /// @param beneficiary Address that will receive the tokens (must match voucher.beneficiary)
    /// @param voucher Purchase voucher containing authorization details
    /// @param signature EIP-712 signature of the voucher
    function buyWithNativeVoucher(
        address beneficiary,
        Authorizer.Voucher calldata voucher,
        bytes calldata signature
    ) external payable nonReentrant whenNotPaused {
        require(voucherSystemEnabled, "Voucher system not enabled");
        require(address(authorizer) != address(0), "Authorizer not set");
        require(beneficiary != address(0), "Invalid beneficiary");
        require(msg.value > 0, "No native currency sent");
        // Check if any presale is active
        uint8 activeMode = _getActivePresaleMode();
        require(activeMode == 1 || activeMode == 2, "No presale active");
        require(activeMode != 3, "Cannot run both presales simultaneously");
        require(voucher.buyer == msg.sender, "Only buyer can use voucher");
        require(voucher.beneficiary == beneficiary, "Beneficiary mismatch");
        require(voucher.paymentToken == NATIVE_ADDRESS, "Invalid payment token");
        
        TokenPrice memory nativePrice = tokenPrices[NATIVE_ADDRESS];
        require(nativePrice.isActive, "Native currency not accepted");
        
        // Apply configured gas buffer (if any) to keep allocation independent from tx.gasprice
        uint256 paymentAmount = _applyGasBuffer(msg.value);
        
        // Calculate USD amount for authorization (8 decimals) - user gets tokens for full amount
        uint256 usdAmount = (paymentAmount * nativePrice.priceUSD) / (10 ** nativePrice.decimals);
        require(usdAmount > 0, "Payment amount too small");
        
        // GRO-19: In-contract replay protection (defense-in-depth)
        bytes32 voucherHash = _computeVoucherHash(voucher);
        require(!usedVoucherHashes[voucherHash], "Voucher already used in this contract");
        
        // Authorize purchase with voucher (external Authorizer)
        bool authorized = authorizer.authorize(voucher, signature, NATIVE_ADDRESS, usdAmount);
        require(authorized, "Voucher authorization failed");
        
        // Mark voucher as used in this contract
        usedVoucherHashes[voucherHash] = true;
        emit VoucherHashConsumed(voucherHash, voucher.buyer);
        
        uint256 tokenAmount = _calculateTokenAmountForVoucher(NATIVE_ADDRESS, paymentAmount, beneficiary, usdAmount);
        require(tokenAmount > 0, "Token amount too small");
        
        // Calculate and transfer 4% fee to dev treasury (using 400/10000 for better precision)
        uint256 devFee = (paymentAmount * 400) / 10000;
        payable(devTreasury).transfer(devFee);
        _processVoucherPurchase(beneficiary, NATIVE_ADDRESS, paymentAmount, tokenAmount, voucher);
    }
    
    /// @notice Purchase with ERC20 tokens using voucher authorization (KYC-AUTHORIZED DELEGATED PURCHASES)
    /// @dev BENEFICIARY VALIDATION POLICY: Allows delegated purchases ONLY through authorized vouchers
    /// - voucher.buyer must equal msg.sender (only voucher holder can use it)
    /// - voucher.beneficiary must equal beneficiary parameter (specified in voucher)
    /// - This enables KYC-verified delegated purchases while preventing unauthorized ones
    /// @param token Payment token address
    /// @param amount Payment token amount
    /// @param beneficiary Address that will receive the tokens (must match voucher.beneficiary)
    /// @param voucher Purchase voucher containing authorization details
    /// @param signature EIP-712 signature of the voucher
    function buyWithTokenVoucher(
        address token,
        uint256 amount,
        address beneficiary,
        Authorizer.Voucher calldata voucher,
        bytes calldata signature
    ) external nonReentrant whenNotPaused {
        require(voucherSystemEnabled, "Voucher system not enabled");
        require(address(authorizer) != address(0), "Authorizer not set");
        require(beneficiary != address(0), "Invalid beneficiary");
        require(amount > 0, "Invalid amount");
        require(token != NATIVE_ADDRESS, "Use buyWithNativeVoucher for native currency");
        // Check if any presale is active
        uint8 activeMode = _getActivePresaleMode();
        require(activeMode == 1 || activeMode == 2, "No presale active");
        require(activeMode != 3, "Cannot run both presales simultaneously");
        require(voucher.buyer == msg.sender, "Only buyer can use voucher");
        require(voucher.beneficiary == beneficiary, "Beneficiary mismatch");
        require(voucher.paymentToken == token, "Invalid payment token");
        
        TokenPrice memory tokenPrice = tokenPrices[token];
        require(tokenPrice.isActive, "Token not accepted");
        
        // Transfer and calculate actual received amount
        uint256 actualAmount = _transferAndCalculateActualAmount(token, amount);
        
        // Calculate USD amount based on actual received amount (8 decimals) - user gets tokens for full amount
        uint256 usdAmount = (actualAmount * tokenPrice.priceUSD) / (10 ** tokenPrice.decimals);
        require(usdAmount > 0, "Payment amount too small");
        
        // GRO-19: In-contract replay protection (defense-in-depth)
        bytes32 voucherHash = _computeVoucherHash(voucher);
        require(!usedVoucherHashes[voucherHash], "Voucher already used in this contract");
        
        // Authorize purchase with voucher (external Authorizer)
        require(authorizer.authorize(voucher, signature, token, usdAmount), "Voucher authorization failed");
        
        // Mark voucher as used in this contract
        usedVoucherHashes[voucherHash] = true;
        emit VoucherHashConsumed(voucherHash, voucher.buyer);
        
        // Calculate token amount based on actual received amount
        uint256 tokenAmount = _calculateTokenAmountForVoucher(token, actualAmount, beneficiary, usdAmount);
        require(tokenAmount > 0, "Token amount too small");
        
        // Calculate and transfer 4% fee to dev treasury (using 400/10000 for better precision)
        uint256 devFee = (actualAmount * 400) / 10000;
        IERC20(token).safeTransfer(devTreasury, devFee);
        _processVoucherPurchase(beneficiary, token, actualAmount, tokenAmount, voucher);
    }
    
    // ============ INTERNAL FUNCTIONS ============
    
    /// @notice Transfer tokens and calculate actual received amount (handles fee-on-transfer tokens)
    /// @param token Token address to transfer
    /// @param amount Amount to transfer
    /// @return actualAmount Actual amount received after transfer
    function _transferAndCalculateActualAmount(address token, uint256 amount) internal returns (uint256 actualAmount) {
        // Record balance before transfer to detect fee-on-transfer tokens
        uint256 balanceBefore = IERC20(token).balanceOf(address(this));
        
        // Transfer tokens (SafeERC20 handles USDT compatibility)
        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
        
        // Calculate actual amount received (handles deflationary/fee-on-transfer tokens)
        uint256 balanceAfter = IERC20(token).balanceOf(address(this));
        actualAmount = balanceAfter - balanceBefore;
        require(actualAmount > 0, "No tokens received");
        
        // GRO-09: Reject deflationary tokens
        require(actualAmount == amount, "Deflationary token not supported");
    }
    
    function _ensurePresaleActive() internal view {
        bool mainPresaleActive = presaleStartTime > 0 &&
            block.timestamp >= presaleStartTime &&
            block.timestamp <= presaleEndTime &&
            !presaleEnded;
            
        bool escrowPresaleActive = escrowPresaleStartTime > 0 &&
            block.timestamp >= escrowPresaleStartTime &&
            block.timestamp <= escrowPresaleEndTime &&
            !escrowPresaleEnded;
        
        require(mainPresaleActive || escrowPresaleActive, "No presale active");
    }
    
    /// @notice Get active presale mode: 0 = none, 1 = main, 2 = escrow, 3 = both (error case)
    function _getActivePresaleMode() internal view returns (uint8) {
        bool mainPresaleActive = presaleStartTime > 0 &&
            block.timestamp >= presaleStartTime &&
            block.timestamp <= presaleEndTime &&
            !presaleEnded;
            
        bool escrowPresaleActive = escrowPresaleStartTime > 0 &&
            block.timestamp >= escrowPresaleStartTime &&
            block.timestamp <= escrowPresaleEndTime &&
            !escrowPresaleEnded;
        
        if (mainPresaleActive && escrowPresaleActive) {
            return 3; // Error case - both active
        } else if (mainPresaleActive) {
            return 1; // Main presale active
        } else if (escrowPresaleActive) {
            return 2; // Escrow presale active
        } else {
            return 0; // No presale active
        }
    }
    
    /// @notice Calculate token amount for voucher purchases (USD amount already calculated in 8 decimals)
    /// @dev USD tracking is now handled in _processVoucherPurchase to avoid double counting
    function _calculateTokenAmountForVoucher(address /* paymentToken */, uint256 /* paymentAmount */, address /* beneficiary */, uint256 usdAmount) internal view returns (uint256) {
        require(usdAmount > 0, "USD amount too small");
        
        // Calculate presale tokens: usdAmount (8 dec) * presaleRate (18 dec) / 1e8 = tokens (18 dec)
        uint256 tokenAmount = (usdAmount * presaleRate) / 1e8;
        require(tokenAmount > 0, "Token amount too small");
        
        return tokenAmount;
    }
    
    function _processPurchase(
        address beneficiary,
        address paymentToken,
        uint256 paymentAmount,
        uint256 tokenAmount
    ) internal {
        _ensurePresaleActive();
        
        // Check if we can mint enough tokens
        require(totalTokensMinted + tokenAmount <= maxTokensToMint, "Not enough tokens left");
        
        // Update tracking
        purchasedAmounts[beneficiary][paymentToken] += paymentAmount;
        totalPurchased[beneficiary] += tokenAmount;
        totalTokensMinted += tokenAmount;
        
        // Track tokens sold per round based on active presale mode
        uint8 activeMode = _getActivePresaleMode();
        if (activeMode == 1) {
            // Main presale
            if (currentRound == 1) {
                round1TokensSold += tokenAmount;
            } else if (currentRound == 2) {
                round2TokensSold += tokenAmount;
            }
        } else if (activeMode == 2) {
            // Escrow presale
            if (escrowCurrentRound == 1) {
                escrowRound1TokensSold += tokenAmount;
            } else if (escrowCurrentRound == 2) {
                escrowRound2TokensSold += tokenAmount;
            }
        }
        
        emit TokenPurchase(msg.sender, beneficiary, paymentToken, paymentAmount, tokenAmount);
        
        // Check auto-end conditions
        _checkAutoEndConditions();
    }
    
    /// @notice Process voucher-based purchase
    function _processVoucherPurchase(
        address beneficiary,
        address paymentToken,
        uint256 paymentAmount,
        uint256 tokenAmount,
        Authorizer.Voucher calldata voucher
    ) internal {
        _ensurePresaleActive();
        
        // Check if we can mint enough tokens
        require(totalTokensMinted + tokenAmount <= maxTokensToMint, "Not enough tokens left");
        
        // Calculate and track USD spent for analytics
        // Note: paymentAmount for native payments is already adjusted for gas buffer
        TokenPrice memory price = tokenPrices[paymentToken];
        uint256 usdAmount = (paymentAmount * price.priceUSD) / (10 ** price.decimals);
        totalUsdPurchased[beneficiary] += usdAmount;
        
        // Update tracking
        purchasedAmounts[beneficiary][paymentToken] += paymentAmount;
        totalPurchased[beneficiary] += tokenAmount;
        totalTokensMinted += tokenAmount;
        
        // Track tokens sold per round based on active presale mode
        uint8 activeMode = _getActivePresaleMode();
        if (activeMode == 1) {
            // Main presale
            if (currentRound == 1) {
                round1TokensSold += tokenAmount;
            } else if (currentRound == 2) {
                round2TokensSold += tokenAmount;
            }
        } else if (activeMode == 2) {
            // Escrow presale
            if (escrowCurrentRound == 1) {
                escrowRound1TokensSold += tokenAmount;
            } else if (escrowCurrentRound == 2) {
                escrowRound2TokensSold += tokenAmount;
            }
        }
        
        // Generate voucher hash for event
        bytes32 voucherHash = keccak256(abi.encode(
            voucher.buyer,
            voucher.beneficiary,
            voucher.paymentToken,
            voucher.usdLimit,
            voucher.nonce,
            voucher.deadline,
            voucher.presale
        ));
        
        emit VoucherPurchase(msg.sender, beneficiary, paymentToken, paymentAmount, tokenAmount, voucherHash);
        emit TokenPurchase(msg.sender, beneficiary, paymentToken, paymentAmount, tokenAmount);
        
        // Check auto-end conditions
        _checkAutoEndConditions();
    }
    
    // Check if presale should auto-end
    function _checkAutoEndConditions() internal {
        uint8 activeMode = _getActivePresaleMode();
        
        // End if all tokens sold
        if (totalTokensMinted >= maxTokensToMint) {
            if (activeMode == 1) {
                // End main presale
                presaleEnded = true;
                presaleEndTime = block.timestamp;
            } else if (activeMode == 2) {
                // End escrow presale
                escrowPresaleEnded = true;
                escrowPresaleEndTime = block.timestamp;
            }
            emit PresaleEndedEarly("All tokens sold", block.timestamp);
            emit PresaleEnded(block.timestamp);
            return;
        }
        
        // Check duration limits based on active presale
        if (activeMode == 1) {
            // Main presale: End if 34 days passed
            if (block.timestamp >= presaleStartTime + MAX_PRESALE_DURATION) {
                presaleEnded = true;
                presaleEndTime = block.timestamp;
                emit PresaleEndedEarly("Maximum duration reached", block.timestamp);
                emit PresaleEnded(block.timestamp);
                return;
            }
        } else if (activeMode == 2) {
            // Escrow presale: End if 34 days passed
            if (block.timestamp >= escrowPresaleStartTime + MAX_PRESALE_DURATION) {
                escrowPresaleEnded = true;
                escrowPresaleEndTime = block.timestamp;
                emit PresaleEndedEarly("Maximum duration reached", block.timestamp);
                emit PresaleEnded(block.timestamp);
                return;
            }
        }
        
        // Note: Auto-advancement to Round 2 disabled to prevent price inconsistencies
        // Use moveToRound2() or moveEscrowToRound2() functions instead to ensure proper price updates
    }
    
    // ============ CLAIM FUNCTIONS ============
    
    function claimTokens() external nonReentrant whenNotPaused {
        require(totalPurchased[msg.sender] > 0, "No tokens to claim");
        require(!hasClaimed[msg.sender], "Already claimed");
        require(presaleEnded || escrowPresaleEnded, "No presale ended yet");
        
        uint256 claimAmount = totalPurchased[msg.sender];
        hasClaimed[msg.sender] = true;
        
        presaleToken.safeTransfer(msg.sender, claimAmount);
        
        emit TokensClaimed(msg.sender, claimAmount);
    }
    
    // ============ ADMIN FUNCTIONS ============
    
    function withdrawNative() external onlyGovernance {
        uint256 balance = address(this).balance;
        require(balance > 0, "No native currency to withdraw");
        payable(treasury).transfer(balance);
    }
    
    function withdrawToken(address token) external onlyGovernance {
        require(token != address(presaleToken), "Cannot withdraw presale tokens directly");

        uint256 balance = IERC20(token).balanceOf(address(this));
        require(balance > 0, "No tokens to withdraw");
        IERC20(token).safeTransfer(treasury, balance);
    }
    
    /// @notice Burn unsold presale tokens after presale ends
    /// @dev Only owner can call this to burn remaining tokens in presale contract
    function burnUnsoldTokens() external onlyGovernance nonReentrant whenNotPaused {
        require(presaleEnded || escrowPresaleEnded, "Presale must be ended first");
        
        uint256 unsoldAmount = presaleToken.balanceOf(address(this));
        require(unsoldAmount > 0, "No tokens to burn");
        
        // Call burn function on EscrowToken (ERC20Burnable)
        // This will burn tokens from this contract's balance
        (bool success, ) = address(presaleToken).call(
            abi.encodeWithSignature("burn(uint256)", unsoldAmount)
        );
        require(success, "Burn failed");
    }
    
    function pause() external onlyGovernance {
        _pause();
    }
    
    function unpause() external onlyGovernance {
        _unpause();
    }
    
    // ============ VIEW FUNCTIONS ============
    
    function getTokenPrice(address token) external view returns (TokenPrice memory) {
        return tokenPrices[token];
    }
    
    function getUserPurchases(address user) external view returns (
        uint256 nativeAmount,
        uint256 totalTokens,
        bool claimed
    ) {
        nativeAmount = purchasedAmounts[user][NATIVE_ADDRESS];
        totalTokens = totalPurchased[user];
        claimed = hasClaimed[user];
    }
    
    function calculateTokenAmount(address paymentToken, uint256 paymentAmount, address /* beneficiary */) external view returns (uint256) {
        TokenPrice memory price = tokenPrices[paymentToken];
        require(price.isActive, "Token not accepted");
        
        // Convert payment amount to USD value
        uint256 usdValue = (paymentAmount * price.priceUSD) / (10 ** price.decimals * 10 ** USD_DECIMALS);
        require(usdValue > 0, "Payment amount too small");
        
        // View function for external queries - no limits enforced here
        // Per-user and total token supply limits enforced in actual purchase functions
        // Calculate presale tokens
        uint256 tokenAmount = (usdValue * presaleRate);
        require(tokenAmount > 0, "Token amount too small");
        
        return tokenAmount;
    }
    
    function getRemainingTokens() external view returns (uint256) {
        return maxTokensToMint - totalTokensMinted;
    }
    
    // Presale status functions
    function getPresaleStatus() external view returns (
        bool started,
        bool ended,
        uint256 startTime,
        uint256 endTime,
        uint256 currentTime
    ) {
        started = presaleStartTime > 0;
        ended = presaleEnded;
        startTime = presaleStartTime;
        endTime = presaleEndTime;
        currentTime = block.timestamp;
    }
    
    function isPresaleActive() external view returns (bool) {
        bool mainPresaleActive = presaleStartTime > 0 && 
               block.timestamp >= presaleStartTime && 
               block.timestamp <= presaleEndTime && 
               !presaleEnded;
               
        bool escrowPresaleActive = escrowPresaleStartTime > 0 && 
               block.timestamp >= escrowPresaleStartTime && 
               block.timestamp <= escrowPresaleEndTime && 
               !escrowPresaleEnded;
               
        return mainPresaleActive || escrowPresaleActive;
    }
    
    function canClaim() external view returns (bool) {
        // Can claim if either presale has explicitly ended OR if time has expired
        bool mainPresaleTimeExpired = presaleStartTime > 0 && block.timestamp > presaleEndTime;
        bool escrowPresaleTimeExpired = escrowPresaleStartTime > 0 && block.timestamp > escrowPresaleEndTime;
        
        return presaleEnded || escrowPresaleEnded || mainPresaleTimeExpired || escrowPresaleTimeExpired;
    }
    
    // Get escrow presale status
    function getEscrowPresaleStatus() external view returns (
        bool started,
        bool ended,
        uint256 startTime,
        uint256 endTime,
        uint256 currentTime
    ) {
        started = escrowPresaleStartTime > 0;
        ended = escrowPresaleEnded;
        startTime = escrowPresaleStartTime;
        endTime = escrowPresaleEndTime;
        currentTime = block.timestamp;
    }
    
    // Get comprehensive escrow presale status
    function getIEscrowPresaleStatus() external view returns (
        uint256 currentRoundNumber,
        uint256 roundTimeRemaining,
        uint256 totalTimeRemaining,
        uint256 tokensRemainingTotal,
        uint256 round1Sold,
        uint256 round2Sold,
        bool canPurchase,
        string memory statusMessage
    ) {
        currentRoundNumber = escrowCurrentRound;
        round1Sold = escrowRound1TokensSold;
        round2Sold = escrowRound2TokensSold;
        tokensRemainingTotal = maxTokensToMint - totalTokensMinted;
        
        if (escrowPresaleEnded) {
            canPurchase = false;
            statusMessage = "Escrow presale ended";
            roundTimeRemaining = 0;
            totalTimeRemaining = 0;
        } else if (escrowCurrentRound == 0) {
            canPurchase = false;
            statusMessage = "Escrow presale starts Nov 11, 2025";
            roundTimeRemaining = block.timestamp >= PRESALE_LAUNCH_DATE ? 0 : PRESALE_LAUNCH_DATE - block.timestamp;
            totalTimeRemaining = roundTimeRemaining;
        } else if (escrowCurrentRound == 1) {
            canPurchase = true;
            statusMessage = "Escrow Round 1 Active";
            roundTimeRemaining = block.timestamp >= escrowRound1EndTime ? 0 : escrowRound1EndTime - block.timestamp;
            totalTimeRemaining = block.timestamp >= escrowPresaleEndTime ? 0 : escrowPresaleEndTime - block.timestamp;
        } else if (escrowCurrentRound == 2) {
            canPurchase = true;
            statusMessage = "Escrow Round 2 Active";
            roundTimeRemaining = block.timestamp >= escrowPresaleEndTime ? 0 : escrowPresaleEndTime - block.timestamp;
            totalTimeRemaining = roundTimeRemaining;
        }
        
        return (currentRoundNumber, roundTimeRemaining, totalTimeRemaining, tokensRemainingTotal, round1Sold, round2Sold, canPurchase, statusMessage);
    }
    
    // Get round allocation details
    function getRoundAllocation() external view returns (
        uint256 round1Sold,
        uint256 round2Sold,
        uint256 round1Remaining,
        uint256 round2Remaining,
        uint256 totalRemaining
    ) {
        round1Sold = round1TokensSold;
        round2Sold = round2TokensSold;
        totalRemaining = maxTokensToMint - totalTokensMinted;
        
        // For display purposes - no hard limits per round in iEscrow spec
        round1Remaining = totalRemaining;
        round2Remaining = totalRemaining;
        
        return (round1Sold, round2Sold, round1Remaining, round2Remaining, totalRemaining);
    }
    
    // Validate contract setup before launch
    function validateIEscrowSetup() external view returns (
        bool hasCorrectTokens,
        bool startDateConfigured,
        bool limitsConfigured,
        bool tokensDeposited,
        string memory issues
    ) {
        hasCorrectTokens = true; // All 7 tokens configured in constructor
        startDateConfigured = PRESALE_LAUNCH_DATE == 1762819200; // Nov 11, 2025
        limitsConfigured = maxTokensToMint == 5000000000 * 1e18; // 5B tokens
        
        uint256 contractBalance = presaleToken.balanceOf(address(this));
        tokensDeposited = contractBalance >= maxTokensToMint;
        
        if (!tokensDeposited) {
            issues = "Insufficient ESCROW tokens in contract";
        } else if (!startDateConfigured) {
            issues = "Incorrect start date";
        } else if (!limitsConfigured) {
            issues = "Incorrect token limits";
        } else {
            issues = "Setup validated - ready for launch";
        }
        
        return (hasCorrectTokens, startDateConfigured, limitsConfigured, tokensDeposited, issues);
    }
    
    // Anyone can call to trigger auto-end checks
    function checkAutoEndConditions() external {
        uint8 activeMode = _getActivePresaleMode();
        require(activeMode == 1 || activeMode == 2, "No presale active");
        _checkAutoEndConditions();
    }
    
    // Helper functions for USD value calculations - GRO-13 Refactoring
    function _convertToUsd(address paymentToken, uint256 amount) internal view returns (uint256 usdValue) {
        TokenPrice memory price = tokenPrices[paymentToken];
        require(price.isActive, "Token not accepted");
        
        // Convert to USD value in 8 decimal format
        // price.priceUSD is already in 8 decimals (e.g., $4200 = 420000000000)
        usdValue = (amount * price.priceUSD) / (10 ** price.decimals);
    }
    
    function _handleRoundTransition(uint256 fromRound, uint256 toRound) internal {
        require(fromRound < toRound, "Invalid round transition");
        require(!presaleEnded, "Presale already ended");
        
        currentRound = toRound;
        
        // Update round end time if transitioning from round 1
        if (fromRound == 1) {
            round1EndTime = block.timestamp;
        }
        
        emit RoundAdvanced(fromRound, toRound, block.timestamp);
    }
    
    function _getUSDValue(address token, uint256 amount) internal view returns (uint256) {
        return _convertToUsd(token, amount);
    }
    
    function _getUserTotalUSDValue(address user) internal view returns (uint256) {
        return totalUsdPurchased[user];
    }
    
    function getUserTotalUSDValue(address user) external view returns (uint256) {
        return totalUsdPurchased[user];
    }
    
    // Get all supported tokens information
    function getSupportedTokens() external view returns (
        address[] memory tokens,
        string[] memory symbols,
        uint256[] memory prices,
        bool[] memory active
    ) {
        tokens = new address[](7);
        symbols = new string[](7);
        prices = new uint256[](7);
        active = new bool[](7);
        
        tokens[0] = NATIVE_ADDRESS;
        symbols[0] = "ETH";
        prices[0] = tokenPrices[NATIVE_ADDRESS].priceUSD;
        active[0] = tokenPrices[NATIVE_ADDRESS].isActive;
        
        tokens[1] = WETH_ADDRESS;
        symbols[1] = "WETH";
        prices[1] = tokenPrices[WETH_ADDRESS].priceUSD;
        active[1] = tokenPrices[WETH_ADDRESS].isActive;
        
        tokens[2] = WBNB_ADDRESS;
        symbols[2] = "WBNB";
        prices[2] = tokenPrices[WBNB_ADDRESS].priceUSD;
        active[2] = tokenPrices[WBNB_ADDRESS].isActive;
        
        tokens[3] = LINK_ADDRESS;
        symbols[3] = "LINK";
        prices[3] = tokenPrices[LINK_ADDRESS].priceUSD;
        active[3] = tokenPrices[LINK_ADDRESS].isActive;
        
        tokens[4] = WBTC_ADDRESS;
        symbols[4] = "WBTC";
        prices[4] = tokenPrices[WBTC_ADDRESS].priceUSD;
        active[4] = tokenPrices[WBTC_ADDRESS].isActive;
        
        tokens[5] = USDC_ADDRESS;
        symbols[5] = "USDC";
        prices[5] = tokenPrices[USDC_ADDRESS].priceUSD;
        active[5] = tokenPrices[USDC_ADDRESS].isActive;
        
        tokens[6] = USDT_ADDRESS;
        symbols[6] = "USDT";
        prices[6] = tokenPrices[USDT_ADDRESS].priceUSD;
        active[6] = tokenPrices[USDT_ADDRESS].isActive;
    }
    
    // Get user's purchases for all tokens
    function getUserAllPurchases(address user) external view returns (
        uint256[] memory amounts,
        uint256[] memory usdValues
    ) {
        amounts = new uint256[](7);
        usdValues = new uint256[](7);
        
        address[] memory tokens = new address[](7);
        tokens[0] = NATIVE_ADDRESS;
        tokens[1] = WETH_ADDRESS;
        tokens[2] = WBNB_ADDRESS;
        tokens[3] = LINK_ADDRESS;
        tokens[4] = WBTC_ADDRESS;
        tokens[5] = USDC_ADDRESS;
        tokens[6] = USDT_ADDRESS;
        
        for (uint256 i = 0; i < tokens.length; i++) {
            amounts[i] = purchasedAmounts[user][tokens[i]];
            if (amounts[i] > 0) {
                usdValues[i] = _convertToUsd(tokens[i], amounts[i]);
            }
        }
    }
    
    // Apply an owner-configurable buffer so allocations don't depend on tx.gasprice
    function _applyGasBuffer(uint256 amount) internal view returns (uint256) {
        uint256 buffer = gasBuffer;
        if (buffer == 0) {
            return amount;
        }
        require(amount > buffer, "Insufficient payment after gas buffer");
        return amount - buffer;
    }
    
    function setGasBuffer(uint256 _gasBuffer) external onlyGovernance {
        uint256 oldBuffer = gasBuffer;
        gasBuffer = _gasBuffer;
        emit GasBufferUpdated(oldBuffer, _gasBuffer);
    }
    
    /// @notice Compute voucher hash for in-contract replay protection (GRO-19)
    /// @param voucher The voucher to hash
    /// @return Hash of the voucher
    function _computeVoucherHash(Authorizer.Voucher calldata voucher) internal pure returns (bytes32) {
        return keccak256(abi.encode(
            voucher.buyer,
            voucher.beneficiary,
            voucher.paymentToken,
            voucher.usdLimit,
            voucher.nonce,
            voucher.deadline,
            voucher.presale
        ));
    }
    
    /// @notice Check if a voucher hash has been used in this contract (GRO-19)
    /// @param voucherHash Hash of the voucher
    /// @return True if already used
    function isVoucherUsed(bytes32 voucherHash) external view returns (bool) {
        return usedVoucherHashes[voucherHash];
    }
    
    /// @notice Get active presale mode for external consumption
    function getActivePresaleMode() external view returns (uint8) {
        return _getActivePresaleMode();
    }
    
    /// @notice Get comprehensive status of both presale modes
    function getBothPresalesStatus() external view returns (
        bool mainStarted,
        bool mainEnded,
        bool escrowStarted,
        bool escrowEnded,
        uint8 activeMode,
        string memory statusMessage
    ) {
        mainStarted = presaleStartTime > 0;
        mainEnded = presaleEnded;
        escrowStarted = escrowPresaleStartTime > 0;
        escrowEnded = escrowPresaleEnded;
        activeMode = _getActivePresaleMode();
        
        if (activeMode == 0) {
            statusMessage = "No presale active";
        } else if (activeMode == 1) {
            statusMessage = "Main presale active";
        } else if (activeMode == 2) {
            statusMessage = "Escrow presale active";
        } else if (activeMode == 3) {
            statusMessage = "ERROR: Both presales active";
        }
    }
    
    /// @notice Get escrow presale round allocation details
    function getEscrowRoundAllocation() external view returns (
        uint256 round1Sold,
        uint256 round2Sold,
        uint256 round1Remaining,
        uint256 round2Remaining,
        uint256 totalRemaining
    ) {
        round1Sold = escrowRound1TokensSold;
        round2Sold = escrowRound2TokensSold;
        totalRemaining = maxTokensToMint - totalTokensMinted;
        
        // For display purposes - no hard limits per round in iEscrow spec
        round1Remaining = totalRemaining;
        round2Remaining = totalRemaining;
        
        return (round1Sold, round2Sold, round1Remaining, round2Remaining, totalRemaining);
    }
    
    
    // ============ AUTHORIZER MANAGEMENT FUNCTIONS ============
    
    /// @notice Update the Authorizer contract address
    /// @param _authorizer New Authorizer contract address
    function updateAuthorizer(address _authorizer) external onlyGovernance {
        require(_authorizer != address(0), "Invalid authorizer");
        require(_authorizer.code.length > 0, "Authorizer must be contract");
        address oldAuthorizer = address(authorizer);
        authorizer = Authorizer(_authorizer);
        emit AuthorizerUpdated(oldAuthorizer, _authorizer);
    }
    
    /// @notice Toggle voucher system on/off
    /// @param _enabled Whether voucher system is enabled
    function setVoucherSystemEnabled(bool _enabled) external onlyGovernance {
        voucherSystemEnabled = _enabled;
        emit VoucherSystemToggled(_enabled);
    }
    
    /// @notice Get Authorizer contract address and system status
    /// @return authorizerAddress Address of the Authorizer contract
    /// @return enabled Whether voucher system is enabled
    function getAuthorizerInfo() external view returns (address authorizerAddress, bool enabled) {
        authorizerAddress = address(authorizer);
        enabled = voucherSystemEnabled;
    }
    
    /// @notice Validate a voucher without consuming it (view function)
    /// @param voucher The purchase voucher to validate
    /// @param signature EIP-712 signature of the voucher
    /// @param paymentToken Token being used for payment
    /// @param usdAmount USD amount being purchased (8 decimals)
    /// @return valid True if voucher is valid
    /// @return reason Reason for invalidity (empty if valid)
    function validateVoucher(
        Authorizer.Voucher calldata voucher,
        bytes calldata signature,
        address paymentToken,
        uint256 usdAmount
    ) external view returns (bool valid, string memory reason) {
        if (!voucherSystemEnabled) {
            return (false, "Voucher system not enabled");
        }
        if (address(authorizer) == address(0)) {
            return (false, "Authorizer not set");
        }
        return authorizer.validateVoucher(voucher, signature, paymentToken, usdAmount);
    }
    
}
"
    },
    "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)

pragma solidity >=0.4.16;

/**
 * @dev Interface of the ERC-20 standard as defined in the ERC.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the value of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the value of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 value) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the
     * allowance mechanism. `value` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}
"
    },
    "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)

pragma solidity ^0.8.20;

import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";

/**
 * @title SafeERC20
 * @dev Wrappers around ERC-20 operations that throw on failure (when the token
 * contract returns false). Tokens that return no value (and instead revert or
 * throw on failure) are also supported, non-reverting calls are assumed to be
 * successful.
 * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
 * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
 */
library SafeERC20 {
    /**
     * @dev An operation with an ERC-20 token failed.
     */
    error SafeERC20FailedOperation(address token);

    /**
     * @dev Indicates a failed `decreaseAllowance` request.
     */
    error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);

    /**
     * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     */
    function safeTransfer(IERC20 token, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
    }

    /**
     * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
     * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
     */
    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
    }

    /**
     * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.
     */
    function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
        return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value)));
    }

    /**
     * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.
     */
    function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {
        return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value)));
    }

    /**
     * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     *
     * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
     * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
     * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
     * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
     */
    function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
        uint256 oldAllowance = token.allowance(address(this), spender);
        forceApprove(token, spender, oldAllowance + value);
    }

    /**
     * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
     * value, non-reverting calls are assumed to be successful.
     *
     * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
     * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
     * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
     * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
     */
    function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
        unchecked {
            uint256 currentAllowance = token.allowance(address(this), spende

Tags:
ERC20, ERC165, Multisig, Burnable, Pausable, Voting, Upgradeable, Multi-Signature, Factory|addr:0x1bccc48cedeb4240b10cefa4cde7b35995c48c40|verified:true|block:23744896|tx:0xedcd7d45c83c814bc0348bd56150629d8810332683305fa048c37b05ad75bd92|first_check:1762514041

Submitted on: 2025-11-07 12:14:02

Comments

Log in to comment.

No comments yet.