XEBB Token v2 Smart Contract Source Code

← Back to Site

XEBBToken.sol ERC-20 Token

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

/**
 * @title XEBBToken
 * @notice XEBB Token v2 — ERC-20 on Base with EIP-2612 permits, burnable, UUPS upgradeable.
 *         Fixed supply of 100,000,000 XEBB minted at initialization.
 *         Includes ERC20Votes for on-chain governance support.
 * @dev Uses UUPS proxy pattern. The implementation contract is initialized through the proxy.
 */
contract XEBBToken is
    Initializable,
    ERC20Upgradeable,
    ERC20BurnableUpgradeable,
    ERC20PermitUpgradeable,
    ERC20VotesUpgradeable,
    OwnableUpgradeable,
    UUPSUpgradeable
{
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    /**
     * @notice Initializes the token contract (called by proxy).
     * @param initialOwner The address that will own the contract (should be a multi-sig).
     */
    function initialize(address initialOwner) external initializer {
        require(initialOwner != address(0), "XEBB: zero owner");

        __ERC20_init("XEBB Token", "XEBB");
        __ERC20Burnable_init();
        __ERC20Permit_init("XEBB Token");
        __ERC20Votes_init();
        __Ownable_init(initialOwner);

        // Mint fixed supply of 100,000,000 XEBB to the owner
        uint256 TOTAL_SUPPLY = 100_000_000 * 10 ** decimals();
        _mint(initialOwner, TOTAL_SUPPLY);
    }

    /**
     * @notice Returns the number of decimals (18, standard for ERC-20).
     */
    function decimals() public pure override returns (uint8) {
        return 18;
    }

    /**
     * @notice Returns the total supply cap. 100M XEBB, all minted at initialization.
     */
    function MAX_SUPPLY() external pure returns (uint256) {
        return 100_000_000 * 10 ** 18;
    }

    // Required overrides for ERC20Votes + ERC20Permit
    function _update(address from, address to, uint256 value)
        internal
        override(ERC20Upgradeable, ERC20VotesUpgradeable)
    {
        super._update(from, to, value);
    }

    function nonces(address owner)
        public
        view
        override(ERC20PermitUpgradeable, NoncesUpgradeable)
        returns (uint256)
    {
        return super.nonces(owner);
    }

    /// @inheritdoc UUPSUpgradeable
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

XEBBStaking.sol Staking Platform

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

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

/**
 * @title XEBBStaking
 * @notice 4-tier staking contract for XEBB Token with escalating APY and airdrop multipliers.
 *
 * Tiers:
 *   0 = Flexible  — 8% APY,  no lock,     1.0x airdrop weight
 *   1 = 30-day    — 15% APY, 30-day lock, 1.5x airdrop weight
 *   2 = 90-day    — 25% APY, 90-day lock, 2.0x airdrop weight  (Popular)
 *   3 = 365-day   — 35% APY, 365-day lock, 3.0x airdrop weight
 *
 * Early unstaking from a locked tier incurs a 10% penalty redirected to the airdrop pool.
 */
contract XEBBStaking is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    // ============ Structs ============

    struct Stake {
        uint256 amount;
        uint8 tier;
        uint64 stakedAt;
        uint64 lockEnd;
        uint128 rewardDebt;
    }

    struct Tier {
        uint256 apyBps;       // Annual percentage yield in basis points (800 = 8%)
        uint256 lockDays;    // Lock-up period in days (0 = flexible)
        uint256 airdropMult; // Airdrop weight multiplier in basis points (10000 = 1.0x)
    }

    // ============ State ============

    IERC20 public immutable xebbToken;

    Tier[4] public tiers;

    /// @dev Total staked amount per tier.
    uint256[4] public totalStakedPerTier;

    /// @dev Total rewards pool available for distribution.
    uint256 public rewardsPool;

    /// @dev Accumulated reward per staked token (scaled by 1e18).
    uint256 public accRewardPerShare;

    /// @dev Last block the reward pool was updated.
    uint256 public lastRewardBlock;

    /// @dev Airdrop pool accumulation (from early-unstake penalties).
    uint256 public airdropPool;

    /// @dev Mapping from staker address to their stake.
    mapping(address => Stake) public stakes;

    /// @dev Whether an address is an authorized airdrop checker.
    mapping(address => bool) public authorizedCallers;

    // ============ Events ============

    event Staked(address indexed user, uint8 tier, uint256 amount);
    event Unstaked(address indexed user, uint8 tier, uint256 amount, uint256 penalty);
    event RewardPaid(address indexed user, uint256 reward);
    event RewardsDeposited(uint256 amount);
    event AirdropPoolIncreased(uint256 amount);
    event EmergencyUnstake(address indexed user, uint256 amount);

    // ============ Constructor ============

    constructor(address _xebbToken) Ownable(msg.sender) {
        xebbToken = IERC20(_xebbToken);

        // Initialize tiers
        tiers[0] = Tier({apyBps: 800, lockDays: 0, airdropMult: 10000});    // Flexible: 8%
        tiers[1] = Tier({apyBps: 1500, lockDays: 30, airdropMult: 15000});  // 30-day: 15%
        tiers[2] = Tier({apyBps: 2500, lockDays: 90, airdropMult: 20000});  // 90-day: 25%
        tiers[3] = Tier({apyBps: 3500, lockDays: 365, airdropMult: 30000}); // 365-day: 35%
    }

    // ============ Modifiers ============

    modifier onlyAuthorized() {
        require(msg.sender == owner() || authorizedCallers[msg.sender], "XEBBStaking: not authorized");
        _;
    }

    // ============ External Functions ============

    /**
     * @notice Stakes XEBB tokens into a specified tier.
     * @param tierId The staking tier (0-3).
     * @param amount The amount of XEBB to stake.
     */
    function stake(uint8 tierId, uint256 amount) external nonReentrant {
        require(tierId < 4, "XEBBStaking: invalid tier");
        require(amount > 0, "XEBBStaking: zero amount");

        // Claim pending rewards before updating
        _updatePool();

        Stake storage userStake = stakes[msg.sender];

        if (userStake.amount > 0) {
            // Must unstake previous position first
            require(
                userStake.tier == tierId,
                "XEBBStaking: unstake previous tier first"
            );
            _claimReward(msg.sender);
        }

        // Transfer tokens in
        xebbToken.safeTransferFrom(msg.sender, address(this), amount);

        // Update stake
        userStake.amount += amount;
        userStake.tier = tierId;
        userStake.stakedAt = uint64(block.timestamp);

        if (tiers[tierId].lockDays > 0) {
            userStake.lockEnd = uint64(block.timestamp + (tiers[tierId].lockDays * 1 days));
        } else {
            userStake.lockEnd = 0;
        }

        userStake.rewardDebt = uint128((userStake.amount * accRewardPerShare) / 1e18);

        totalStakedPerTier[tierId] += amount;

        emit Staked(msg.sender, tierId, amount);
    }

    /**
     * @notice Unstakes XEBB tokens. Locked tiers incur 10% penalty if before lockEnd.
     * @param amount The amount to unstake.
     */
    function unstake(uint256 amount) external nonReentrant {
        Stake storage userStake = stakes[msg.sender];
        require(amount > 0, "XEBBStaking: zero amount");
        require(userStake.amount >= amount, "XEBBStaking: insufficient stake");

        _updatePool();
        _claimReward(msg.sender);

        uint8 tierId = userStake.tier;
        uint256 penalty = 0;

        // Check lock period
        if (userStake.lockEnd > 0 && block.timestamp < userStake.lockEnd) {
            // Early unstake penalty: 10% to airdrop pool
            penalty = (amount * 1000) / 10000; // 10%
            airdropPool += penalty;
            emit AirdropPoolIncreased(penalty);
        }

        uint256 transferAmount = amount - penalty;

        userStake.amount -= amount;
        totalStakedPerTier[tierId] -= amount;
        userStake.rewardDebt = uint128((userStake.amount * accRewardPerShare) / 1e18);

        xebbToken.safeTransfer(msg.sender, transferAmount);

        emit Unstaked(msg.sender, tierId, amount, penalty);
    }

    /**
     * @notice Claims pending staking rewards.
     */
    function claimReward() external nonReentrant {
        _updatePool();
        _claimReward(msg.sender);
    }

    /**
     * @notice Deposits XEBB rewards into the rewards pool (owner only).
     * @param amount The amount of XEBB to deposit as rewards.
     */
    function depositRewards(uint256 amount) external onlyOwner {
        require(amount > 0, "XEBBStaking: zero amount");
        _updatePool();
        xebbToken.safeTransferFrom(msg.sender, address(this), amount);
        rewardsPool += amount;
        emit RewardsDeposited(amount);
    }

    /**
     * @notice Returns the pending reward for a staker.
     * @param user The staker address.
     */
    function pendingReward(address user) external view returns (uint256) {
        Stake storage userStake = stakes[user];
        if (userStake.amount == 0) return 0;

        uint256 currentAccReward = accRewardPerShare;
        if (block.number > lastRewardBlock && rewardsPool > 0) {
            uint256 blocksPassed = block.number - lastRewardBlock;
            uint256 totalStaked = getTotalStaked();
            if (totalStaked > 0) {
                // Simplified reward calculation
                uint256 tierApy = tiers[userStake.tier].apyBps;
                // Reward per block = (staked * APY) / (blocksPerYear)
                uint256 blocksPerYear = 2_592_000; // ~2s blocks per year
                uint256 rewardPerBlock = (totalStaked * tierApy) / (10000 * blocksPerYear);
                uint256 newReward = (rewardPerBlock * blocksPassed);
                currentAccReward = accRewardPerShare + (newReward * 1e18) / totalStaked;
            }
        }
        return (userStake.amount * currentAccReward) / 1e18 - userStake.rewardDebt;
    }

    /**
     * @notice Returns the airdrop weight for a user (staked amount * tier multiplier).
     * @param user The staker address.
     */
    function getAirdropWeight(address user) external view returns (uint256) {
        Stake storage userStake = stakes[user];
        if (userStake.amount == 0) return 0;
        return (userStake.amount * tiers[userStake.tier].airdropMult) / 10000;
    }

    /**
     * @notice Returns the tier info for a given tier ID.
     */
    function getTier(uint8 tierId) external view returns (Tier memory) {
        require(tierId < 4, "XEBBStaking: invalid tier");
        return tiers[tierId];
    }

    /**
     * @notice Returns total staked across all tiers.
     */
    function getTotalStaked() public view returns (uint256) {
        uint256 total = 0;
        for (uint8 i = 0; i < 4; i++) {
            total += totalStakedPerTier[i];
        }
        return total;
    }

    /**
     * @notice Sets authorized caller status (owner only).
     */
    function setAuthorized(address caller, bool status) external onlyOwner {
        authorizedCallers[caller] = status;
    }

    // ============ Internal Functions ============

    function _updatePool() internal {
        if (block.number <= lastRewardBlock) return;

        uint256 totalStaked = getTotalStaked();
        if (totalStaked == 0 || rewardsPool == 0) {
            lastRewardBlock = block.number;
            return;
        }

        uint256 blocksPassed = block.number - lastRewardBlock;
        uint256 blocksPerYear = 2_592_000;

        // Average APY across all stakers (simplified)
        uint256 avgApy = _weightedAvgApy();
        uint256 rewardPerBlock = (totalStaked * avgApy) / (10000 * blocksPerYear);
        uint256 newReward = rewardPerBlock * blocksPassed;

        if (newReward > rewardsPool) {
            newReward = rewardsPool;
        }

        accRewardPerShare += (newReward * 1e18) / totalStaked;
        rewardsPool -= newReward;
        lastRewardBlock = block.number;
    }

    function _claimReward(address user) internal {
        Stake storage userStake = stakes[user];
        if (userStake.amount == 0) return;

        uint256 pending = (userStake.amount * accRewardPerShare) / 1e18 - userStake.rewardDebt;
        if (pending > 0 && rewardsPool + pending <= address(this).balance) {
            // Transfer rewards from this contract's balance
            uint256 contractBalance = xebbToken.balanceOf(address(this));
            uint256 transferable = pending;
            // Don't transfer staked amounts — only rewards
            uint256 totalStaked = getTotalStaked();
            uint256 availableForRewards = contractBalance > totalStaked
                ? contractBalance - totalStaked
                : 0;
            if (transferable > availableForRewards) {
                transferable = availableForRewards;
            }
            if (transferable > 0) {
                xebbToken.safeTransfer(user, transferable);
                emit RewardPaid(user, transferable);
            }
        }
        userStake.rewardDebt = uint128((userStake.amount * accRewardPerShare) / 1e18);
    }

    function _weightedAvgApy() internal view returns (uint256) {
        uint256 totalStaked = getTotalStaked();
        if (totalStaked == 0) return 0;

        uint256 weightedSum = 0;
        for (uint8 i = 0; i < 4; i++) {
            weightedSum += totalStakedPerTier[i] * tiers[i].apyBps;
        }
        return weightedSum / totalStaked;
    }
}

XEBBArtNFT.sol NFT Art Engine

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

/**
 * @title XEBBArtNFT
 * @notice ERC-721 contract for XEBB monthly art airdrops.
 *         Each token features on-chain generated SVG optical illusion art.
 *
 * Rarity tiers:
 *   0 = Common    (60%) — 2-color geometric op-art (café wall, vibrating lines)
 *   1 = Rare      (30%) — 3-color depth illusions (moiré, perspective tunnels)
 *   2 = Legendary (10%) — Full-color impossible objects + animated vortex
 *
 * Metadata is generated dynamically in tokenURI() — no on-chain SVG storage,
 * keeping mint gas low even for complex artwork.
 */
contract XEBBArtNFT is ERC721Burnable, Ownable {
    using Strings for uint256;

    enum Rarity { Common, Rare, Legendary }
    enum IllusionType {
        CafeWall,           // 0 - Common
        RadiatingLines,     // 1 - Common
        ConcentricRings,    // 2 - Rare
        MoireGrid,          // 3 - Rare
        SpiralVortex,       // 4 - Rare
        PenroseTriangle,   // 5 - Legendary
        PerspectiveTunnel,  // 6 - Legendary
        ScintillatingGrid,  // 7 - Legendary
        // Animated illusions (SMIL <animate> / <animateTransform>)
        SlidingCafeWall,   // 8 - Common (animated)
        RotatingSpiral,     // 9 - Rare (animated)
        PulsatingRings,     // 10 - Legendary (animated)
        RotatingImpossible  // 11 - Legendary (animated)
    }

    struct ArtPiece {
        uint8 rarity;
        uint8 palette;
        uint8 illusion;   // IllusionType
        uint16 complexity; // Shape count / detail level
        uint64 mintedAt;
        uint32 seed;
    }

    // ============ State ============

    mapping(uint256 => ArtPiece) public artPieces;
    mapping(address => bool) public authorizedMinters;

    uint256 private _nextTokenId;

    /// @notice 12 curated palettes, 4 colors each — optical-illusion optimized
    string[4][12] private palettes;

    uint256 public constant MAX_SUPPLY = 10000;

    // ============ Events ============

    event ArtMinted(address indexed to, uint256 tokenId, uint8 rarity, uint8 palette, uint8 illusion, uint16 complexity);
    event MinterSet(address indexed minter, bool status);

    // ============ Constructor ============

    constructor() ERC721("XEBB Art", "XEBBART") Ownable(msg.sender) {
        // High-contrast palettes: 3 bright colors + pure black bg for maximum illusion
        palettes[0]  = ["#e8833a", "#a855f7", "#3b82f6", "#000000"]; // Amber/Purple/Blue
        palettes[1]  = ["#f59e0b", "#8b5cf6", "#06b6d4", "#000000"]; // Amber/Violet/Cyan
        palettes[2]  = ["#ef4444", "#ec4899", "#8b5cf6", "#000000"]; // Red/Pink/Violet
        palettes[3]  = ["#10b981", "#06b6d4", "#3b82f6", "#000000"]; // Emerald/Cyan/Blue
        palettes[4]  = ["#f97316", "#facc15", "#84cc16", "#000000"]; // Orange/Yellow/Lime
        palettes[5]  = ["#6366f1", "#8b5cf6", "#ec4899", "#000000"]; // Indigo/Violet/Pink
        palettes[6]  = ["#ffffff", "#cccccc", "#e8833a", "#000000"]; // Mono+Amber (max contrast)
        palettes[7]  = ["#00ffff", "#ff00ff", "#ffff00", "#000000"]; // CMY on Black
        palettes[8]  = ["#ff6b6b", "#4ecdc4", "#ffe66d", "#000000"]; // Coral/Teal/Sand
        palettes[9]  = ["#e94560", "#f5a623", "#bd6cff", "#000000"]; // Crimson/Amber/Purple
        palettes[10] = ["#ffffff", "#999999", "#e8833a", "#000000"]; // Mono Grid+Amber
        palettes[11] = ["#c04dff", "#00f5d4", "#fee440", "#000000"]; // Neon Purple/Mint/Yellow
    }

    // ============ Modifiers ============

    modifier onlyMinter() {
        require(msg.sender == owner() || authorizedMinters[msg.sender], "XEBBArtNFT: not authorized minter");
        _;
    }

    // ============ External Functions ============

    function mint(address to, uint8 rarity, uint256 seed) external onlyMinter returns (uint256) {
        require(_nextTokenId < MAX_SUPPLY, "XEBBArtNFT: max supply reached");
        require(rarity <= 2, "XEBBArtNFT: invalid rarity");

        uint256 tokenId = _nextTokenId++;
        _storeArt(tokenId, rarity, uint32(seed));

        _safeMint(to, tokenId);
        emit ArtMinted(to, tokenId, rarity, artPieces[tokenId].palette, artPieces[tokenId].illusion, artPieces[tokenId].complexity);
        return tokenId;
    }

    function batchMint(
        address[] calldata recipients,
        uint8[] calldata rarities,
        uint256 seed
    ) external onlyMinter returns (uint256[] memory tokenIds) {
        require(recipients.length == rarities.length, "XEBBArtNFT: length mismatch");
        require(_nextTokenId + recipients.length <= MAX_SUPPLY, "XEBBArtNFT: exceeds max supply");

        tokenIds = new uint256[](recipients.length);
        for (uint256 i = 0; i < recipients.length; i++) {
            require(rarities[i] <= 2, "XEBBArtNFT: invalid rarity");
            uint256 tokenId = _nextTokenId++;
            _storeArt(tokenId, rarities[i], uint32(seed + i * 7919));

            _safeMint(recipients[i], tokenId);
            emit ArtMinted(recipients[i], tokenId, rarities[i], artPieces[tokenId].palette, artPieces[tokenId].illusion, artPieces[tokenId].complexity);
            tokenIds[i] = tokenId;
        }
    }

    function setMinter(address minter, bool status) external onlyOwner {
        authorizedMinters[minter] = status;
        emit MinterSet(minter, status);
    }

    function getArtPiece(uint256 tokenId) external view returns (ArtPiece memory) {
        require(_ownerOf(tokenId) != address(0), "XEBBArtNFT: token does not exist");
        return artPieces[tokenId];
    }

    function rarityName(uint8 rarity) public pure returns (string memory) {
        if (rarity == 0) return "Common";
        if (rarity == 1) return "Rare";
        return "Legendary";
    }

    function illusionName(uint8 illusion) public pure returns (string memory) {
        if (illusion == 0) return "Cafe Wall";
        if (illusion == 1) return "Radiating Lines";
        if (illusion == 2) return "Concentric Rings";
        if (illusion == 3) return "Moire Grid";
        if (illusion == 4) return "Spiral Vortex";
        if (illusion == 5) return "Penrose Triangle";
        if (illusion == 6) return "Perspective Tunnel";
        if (illusion == 7) return "Scintillating Grid";
        if (illusion == 8) return "Sliding Cafe Wall";
        if (illusion == 9) return "Rotating Spiral";
        if (illusion == 10) return "Pulsating Rings";
        return "Rotating Impossible";
    }

    function totalSupply() external view returns (uint256) {
        return _nextTokenId;
    }

    // ============ Dynamic Metadata ============

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_ownerOf(tokenId) != address(0), "XEBBArtNFT: token does not exist");
        ArtPiece memory art = artPieces[tokenId];
        return _buildMetadata(tokenId, art);
    }

    // ============ Internal: Art Storage ============

    function _storeArt(uint256 tokenId, uint8 rarity, uint32 seed) internal {
        uint8 paletteIdx = uint8((seed >> 4) % 12);
        uint8 illusionIdx;
        uint16 complexity;

        if (rarity == 0) {
            // Common: 4-illusion pool (2 static + 2 animated)
            uint8[4] memory commonPool = [0, 1, 8, 9]; // CafeWall, RadiatingLines, SlidingCafeWall, RotatingSpiral
            illusionIdx = commonPool[seed % 4];
            complexity = 8 + uint16(seed % 6); // 8-13
        } else if (rarity == 1) {
            // Rare: 4-illusion pool (3 static + 1 animated)
            uint8[4] memory rarePool = [2, 3, 4, 9]; // ConcentricRings, MoireGrid, SpiralVortex, RotatingSpiral
            illusionIdx = rarePool[seed % 4];
            complexity = 14 + uint16(seed % 8); // 14-21
        } else {
            // Legendary: 5-illusion pool (3 static + 2 animated)
            uint8[5] memory legendaryPool = [5, 6, 7, 10, 11]; // Penrose, Perspective, Scintillating, PulsatingRings, RotatingImpossible
            illusionIdx = legendaryPool[seed % 5];
            complexity = 20 + uint16(seed % 10); // 20-29
        }

        artPieces[tokenId] = ArtPiece({
            rarity: rarity,
            palette: paletteIdx,
            illusion: illusionIdx,
            complexity: complexity,
            mintedAt: uint64(block.timestamp),
            seed: seed
        });
    }

    // ============ Internal: Metadata Builder ============

    function _buildMetadata(uint256 tokenId, ArtPiece memory art) internal view returns (string memory) {
        string[4] memory colors = palettes[art.palette];
        string memory svg = _generateSVG(tokenId, art, colors);
        string memory rName = rarityName(art.rarity);
        string memory iName = illusionName(art.illusion);

        string memory json = string(abi.encodePacked(
            '{"name":"XEBB Art #', tokenId.toString(), '",',
            '"description":"XEBB Token v2 Monthly Art Airdrop - ', iName, ' optical illusion",',
            '"image":"data:image/svg+xml;base64,', Base64.encode(bytes(svg)), '",',
            '"attributes":[',
                '{"trait_type":"Rarity","value":"', rName, '"},',
                '{"trait_type":"Illusion","value":"', iName, '"},',
                '{"trait_type":"Palette","value":', uint256(art.palette).toString(), '},',
                '{"trait_type":"Complexity","value":', uint256(art.complexity).toString(), '},',
                '{"trait_type":"Seed","value":', uint256(art.seed).toString(), '},',
                '{"trait_type":"Token","value":"XEBBART"}',
            ']}'
        ));

        return string(abi.encodePacked("data:application/json;base64,", Base64.encode(bytes(json))));
    }

    // ============ Internal: SVG Generation Engine ============

    function _generateSVG(
        uint256 tokenId,
        ArtPiece memory art,
        string[4] memory colors
    ) internal view returns (string memory) {
        string memory bg = colors[3]; // dark background from palette
        string memory c0 = colors[0];
        string memory c1 = colors[1];
        string memory c2 = colors[2];

        string memory defs = _buildDefs(art.rarity, c0, c1, c2, bg);
        string memory illusion = _buildIllusion(art, c0, c1, c2, bg);
        string memory frame = _buildFrame(art.rarity, c0, c1);

        return string(abi.encodePacked(
            '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" shape-rendering="geometricPrecision">',
            defs,
            '<rect width="500" height="500" fill="', bg, '"/>',
            '<rect width="500" height="500" fill="url(#bgGrad)" opacity="0.6"/>',
            illusion,
            frame,
            '<text x="250" y="475" text-anchor="middle" font-family="monospace" font-size="11" fill="', c0, '" opacity="0.5">XEBB #', tokenId.toString(), ' - ', illusionName(art.illusion), '</text>',
            '</svg>'
        ));
    }

    // ============ SVG Defs: gradients, filters ============

    function _buildDefs(
        uint8 rarity,
        string memory c0,
        string memory c1,
        string memory c2,
        string memory bg
    ) internal pure returns (string memory) {
        string memory base = string(abi.encodePacked(
            '<defs>',
            '<radialGradient id="bgGrad" cx="50%" cy="50%" r="60%">',
            '<stop offset="0%" stop-color="', c0, '" stop-opacity="0.15"/>',
            '<stop offset="60%" stop-color="', c1, '" stop-opacity="0.05"/>',
            '<stop offset="100%" stop-color="', bg, '" stop-opacity="0"/>',
            '</radialGradient>',
            '<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">',
            '<feGaussianBlur stdDeviation="3" result="blur"/>',
            '<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>',
            '</filter>'
        ));

        if (rarity >= 1) {
            base = string(abi.encodePacked(base,
                '<filter id="softBlur" x="-20%" y="-20%" width="140%" height="140%">',
                '<feGaussianBlur stdDeviation="1.5"/>',
                '</filter>'
            ));
        }

        if (rarity >= 2) {
            base = string(abi.encodePacked(base,
                '<filter id="turbulence" x="0%" y="0%" width="100%" height="100%">',
                '<feTurbulence type="fractalNoise" baseFrequency="0.02" numOctaves="3" seed="42" result="noise"/>',
                '<feColorMatrix in="noise" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"/>',
                '<feComposite in2="SourceGraphic" operator="in"/>',
                '</filter>',
                '<filter id="vortex" x="-50%" y="-50%" width="200%" height="200%">',
                '<feGaussianBlur stdDeviation="2" result="b1"/>',
                '<feMerge><feMergeNode in="b1"/><feMergeNode in="SourceGraphic"/></feMerge>',
                '</filter>',
                '<linearGradient id="legendGrad" x1="0%" y1="0%" x2="100%" y2="100%">',
                '<stop offset="0%" stop-color="', c0, '"/>',
                '<stop offset="33%" stop-color="', c1, '"/>',
                '<stop offset="66%" stop-color="', c2, '"/>',
                '<stop offset="100%" stop-color="', c0, '"/>',
                '</linearGradient>'
            ));
        }

        return string(abi.encodePacked(base, '</defs>'));
    }

    // ============ SVG Illusion Router ============

    function _buildIllusion(
        ArtPiece memory art,
        string memory c0,
        string memory c1,
        string memory c2,
        string memory bg
    ) internal pure returns (string memory) {
        if (art.illusion == uint8(IllusionType.CafeWall)) return _cafeWall(art.complexity, c0, c1, bg);
        if (art.illusion == uint8(IllusionType.RadiatingLines)) return _radiatingLines(art.complexity, c0, c1, c2, bg);
        if (art.illusion == uint8(IllusionType.ConcentricRings)) return _concentricRings(art.complexity, c0, c1, c2, bg);
        if (art.illusion == uint8(IllusionType.MoireGrid)) return _moireGrid(art.complexity, c0, c1, c2, bg);
        if (art.illusion == uint8(IllusionType.SpiralVortex)) return _spiralVortex(art.complexity, c0, c1, c2, bg);
        if (art.illusion == uint8(IllusionType.PenroseTriangle)) return _penroseTriangle(c0, c1, c2, bg);
        if (art.illusion == uint8(IllusionType.PerspectiveTunnel)) return _perspectiveTunnel(art.complexity, c0, c1, c2, bg);
        if (art.illusion == uint8(IllusionType.ScintillatingGrid)) return _scintillatingGrid(art.complexity, c0, c1, bg);
        if (art.illusion == uint8(IllusionType.SlidingCafeWall)) return _slidingCafeWall(art.complexity, c0, c1, bg);
        if (art.illusion == uint8(IllusionType.RotatingSpiral)) return _rotatingSpiral(art.complexity, c0, c1, c2, bg);
        if (art.illusion == uint8(IllusionType.PulsatingRings)) return _pulsatingRings(art.complexity, c0, c1, c2, bg);
        return _rotatingImpossible(c0, c1, c2, bg);
    }

    // ============ Illusion 1: Café Wall ============

    function _cafeWall(
        uint16 complexity,
        string memory c0,
        string memory c1,
        string memory bg
    ) internal pure returns (string memory) {
        uint256 rows = complexity + 4; // 12-17 rows
        uint256 cellW = 500 / (complexity + 6); // cell width
        uint256 cellH = 500 / rows;

        string memory shapes = "";
        for (uint256 r = 0; r < rows; r++) {
            uint256 yOffset = r * cellH;
            bool offsetRow = (r % 2 != 0);

            // Mortar line (thin gray line between rows)
            shapes = string(abi.encodePacked(shapes,
                '<rect x="0" y="', yOffset.toString(), '" width="500" height="1" fill="#888888" opacity="0.4"/>'
            ));

            for (uint256 c = 0; c < 500 / cellW + 2; c++) {
                // Use signed math to handle negative offsets on offset rows
                int256 xRaw = int256(c * cellW);
                if (offsetRow) xRaw -= int256(cellW / 2);
                uint256 x = xRaw >= 0 ? uint256(xRaw) : 0;

                string memory fill = ((r + c) % 2 == 0) ? c0 : c1;
                shapes = string(abi.encodePacked(shapes,
                    '<rect x="', x.toString(), '" y="', yOffset.toString(),
                    '" width="', cellW.toString(), '" height="', cellH.toString(),
                    '" fill="', fill, '"/>'
                ));
            }
        }

        // Add a subtle vignette overlay to enhance the illusion
        return string(abi.encodePacked(
            '<g>', shapes, '</g>',
            '<rect width="500" height="500" fill="url(#bgGrad)" opacity="0.3"/>'
        ));
    }

    // ============ Illusion 2: Radiating Lines ============

    function _radiatingLines(
        uint16 complexity,
        string memory c0,
        string memory c1,
        string memory c2,
        string memory bg
    ) internal pure returns (string memory) {
        uint256 numLines = complexity * 4; // 32-52 lines
        string memory shapes = "";

        for (uint256 i = 0; i < numLines; i++) {
            uint256 angle = (i * 360) / numLines;
            (int256 dx, int256 dy) = _polar(240, angle);
            uint256 x2 = _addInt(250, dx);
            uint256 y2 = _addInt(250, dy);

            string memory color;
            if (i % 3 == 0) color = c0;
            else if (i % 3 == 1) color = c1;
            else color = c2;

            uint256 width = 3 + (i % 3); // 3-5 px wide
            shapes = string(abi.encodePacked(shapes,
                '<line x1="250" y1="250" x2="', x2.toString(), '" y2="', y2.toString(),
                '" stroke="', color, '" stroke-width="', width.toString(), '" opacity="0.9"/>'
            ));
        }

        // Central glow target
        shapes = string(abi.encodePacked(shapes,
            '<circle cx="250" cy="250" r="20" fill="', c0, '" opacity="0.3"/>',
            '<circle cx="250" cy="250" r="12" fill="', c0, '"/>',
            '<circle cx="250" cy="250" r="6" fill="', bg, '"/>',
            '<circle cx="250" cy="250" r="3" fill="', c1, '"/>'
        ));

        return string(abi.encodePacked('<g>', shapes, '</g>'));
    }

    // ============ Illusion 3: Concentric Rings ============

    function _concentricRings(
        uint16 complexity,
        string memory c0,
        string memory c1,
        string memory c2,
        string memory bg
    ) internal pure returns (string memory) {
        uint256 numRings = complexity + 8; // 22-29 rings
        string memory shapes = "";

        for (uint256 i = 0; i < numRings; i++) {
            uint256 radius = 8 + (i * 230) / numRings;
            string memory color;
            if (i % 3 == 0) color = c0;
            else if (i % 3 == 1) color = c1;
            else color = c2;

            uint256 sw = (i % 2 == 0) ? 4 : 2; // thicker strokes
            shapes = string(abi.encodePacked(shapes,
                '<circle cx="250" cy="250" r="', radius.toString(),
                '" fill="none" stroke="', color, '" stroke-width="', sw.toString(), '"/>'
            ));
        }

        // Offset rings (second center) creating moiré interference
        for (uint256 i = 0; i < numRings / 2; i++) {
            uint256 radius = 10 + (i * 230) / (numRings / 2);
            shapes = string(abi.encodePacked(shapes,
                '<circle cx="260" cy="245" r="', radius.toString(),
                '" fill="none" stroke="', c2, '" stroke-width="2" opacity="0.6"/>'
            ));
        }

        // Central accent
        shapes = string(abi.encodePacked(shapes,
            '<circle cx="250" cy="250" r="6" fill="', c0, '"/>'
        ));

        return string(abi.encodePacked('<g>', shapes, '</g>'));
    }

    // ============ Illusion 4: Moiré Grid ============

    function _moireGrid(
        uint16 complexity,
        string memory c0,
        string memory c1,
        string memory c2,
        string memory bg
    ) internal pure returns (string memory) {
        uint256 spacing = 500 / (complexity + 4); // grid spacing
        string memory shapes = "";

        // Grid 1: bright lines
        for (uint256 i = 0; i <= 500 / spacing; i++) {
            uint256 pos = i * spacing;
            shapes = string(abi.encodePacked(shapes,
                '<line x1="', pos.toString(), '" y1="0" x2="', pos.toString(), '" y2="500" stroke="', c0, '" stroke-width="3"/>',
                '<line x1="0" y1="', pos.toString(), '" x2="500" y2="', pos.toString(), '" stroke="', c0, '" stroke-width="3"/>'
            ));
        }

        // Grid 2: offset lines in different color creating moiré
        for (uint256 i = 0; i <= 500 / spacing + 2; i++) {
            uint256 pos = i * spacing;
            int256 offset = int256(int8(uint8((i * 3) % 7))) - 3;
            uint256 adjPos = _addInt(pos, offset);
            shapes = string(abi.encodePacked(shapes,
                '<line x1="', adjPos.toString(), '" y1="0" x2="', adjPos.toString(), '" y2="500" stroke="', c1, '" stroke-width="2" opacity="0.7"/>',
                '<line x1="0" y1="', adjPos.toString(), '" x2="500" y2="', adjPos.toString(), '" stroke="', c1, '" stroke-width="2" opacity="0.7"/>'
            ));
        }

        // Central glowing diamond accent
        shapes = string(abi.encodePacked(shapes,
            '<g transform="translate(250 250) rotate(45)">',
            '<rect x="-50" y="-50" width="100" height="100" fill="none" stroke="', c2, '" stroke-width="4"/>',
            '<rect x="-35" y="-35" width="70" height="70" fill="', c2, '" opacity="0.3"/>',
            '<rect x="-20" y="-20" width="40" height="40" fill="', c0, '"/>',
            '</g>'
        ));

        return string(abi.encodePacked('<g>', shapes, '</g>'));
    }

    // ============ Illusion 5: Spiral Vortex ============

    function _spiralVortex(
        uint16 complexity,
        string memory c0,
        string memory c1,
        string memory c2,
        string memory bg
    ) internal pure returns (string memory) {
        uint256 numArms = 4 + (complexity % 4); // 4-7 spiral arms
        uint256 steps = complexity * 6; // points per arm
        string memory shapes = "";

        for (uint256 arm = 0; arm < numArms; arm++) {
            string memory color;
            if (arm % 3 == 0) color = c0;
            else if (arm % 3 == 1) color = c1;
            else color = c2;

            string memory path = "M";
            for (uint256 s = 0; s < steps; s++) {
                uint256 theta = (arm * 360 * 100) / numArms + (s * 360 * 100) / (steps * 2);
                uint256 r = 5 + (s * 240) / steps;
                (int256 dx, int256 dy) = _polar(r, theta / 100);
                uint256 px = _addInt(250, dx);
                uint256 py = _addInt(250, dy);

                if (s == 0) {
                    path = string(abi.encodePacked(path, ' ', px.toString(), ' ', py.toString()));
                } else {
                    path = string(abi.encodePacked(path, ' L', px.toString(), ' ', py.toString()));
                }
            }

            uint256 width = 4 - (arm % 2); // 3-4 px
            shapes = string(abi.encodePacked(shapes,
                '<path d="', path, '" fill="none" stroke="', color,
                '" stroke-width="', width.toString(), '" stroke-linecap="round"/>'
            ));
        }

        // Central vortex eye
        shapes = string(abi.encodePacked(shapes,
            '<circle cx="250" cy="250" r="15" fill="', c0, '" opacity="0.4"/>',
            '<circle cx="250" cy="250" r="8" fill="', c0, '"/>',
            '<circle cx="250" cy="250" r="4" fill="', bg, '"/>'
        ));

        return string(abi.encodePacked('<g>', shapes, '</g>'));
    }

    // ============ Illusion 6: Penrose Triangle (Impossible Object) ============

    function _penroseTriangle(
        string memory c0,
        string memory c1,
        string memory c2,
        string memory bg
    ) internal pure returns (string memory) {
        // Classic Penrose impossible triangle — three beams that appear connected
        // but use perspective tricks to create an impossible 3D object
        return string(abi.encodePacked(
            '<g transform="translate(250 260)">',
            // Outer triangle outline
            '<polygon points="-140,-80 140,-80 0,160" fill="none" stroke="', c2, '" stroke-width="1" opacity="0.3"/>',

            // === BEAM A (left-bottom beam) ===
            // Outer face
            '<polygon points="-140,-80 -50,-80 -30,-40 -120,-40" fill="', c0, '"/>',
            // Top face (lighter)
            '<polygon points="-140,-80 -50,-80 -70,-50 -160,-50" fill="', c0, '" opacity="0.5"/>',
            // Inner connecting face
            '<polygon points="-120,-40 -30,-40 -10,0 -100,0" fill="', c1, '"/>',
            // Top of inner
            '<polygon points="-120,-40 -30,-40 -50,-10 -140,-10" fill="', c1, '" opacity="0.5"/>',
            // Bottom continuation
            '<polygon points="-100,0 -10,0 -30,40 -80,40" fill="', c0, '" opacity="0.8"/>',
            '<polygon points="-100,0 -10,0 10,-30 -120,-30" fill="', c0, '" opacity="0.4"/>',
            // Final corner
            '<polygon points="-80,40 10,40 -10,80 -60,80" fill="', c1, '" opacity="0.7"/>',
            '<polygon points="-80,40 10,40 -20,10 -100,10" fill="', c1, '" opacity="0.3"/>',

            // === BEAM B (right-bottom beam) ===
            '<polygon points="140,-80 50,-80 30,-40 120,-40" fill="', c1, '"/>',
            '<polygon points="140,-80 50,-80 70,-50 160,-50" fill="', c1, '" opacity="0.5"/>',
            '<polygon points="120,-40 30,-40 10,0 100,0" fill="', c0, '"/>',
            '<polygon points="120,-40 30,-40 50,-10 140,-10" fill="', c0, '" opacity="0.5"/>',
            '<polygon points="100,0 10,0 -10,40 80,40" fill="', c1, '" opacity="0.8"/>',
            '<polygon points="100,0 10,0 -10,-30 110,-30" fill="', c1, '" opacity="0.4"/>',
            '<polygon points="80,40 -10,40 10,80 60,80" fill="', c0, '" opacity="0.7"/>',
            '<polygon points="80,40 -10,40 0,10 100,10" fill="', c0, '" opacity="0.3"/>',

            // === BEAM C (top beam, connecting both sides) ===
            '<polygon points="-50,-80 50,-80 30,-40 -30,-40" fill="', c2, '"/>',
            '<polygon points="-50,-80 50,-80 70,-110 -70,-110" fill="', c2, '" opacity="0.5"/>',
            // Top face connecting
            '<polygon points="-70,-50 70,-50 50,-20 -50,-20" fill="', c2, '" opacity="0.7"/>',

            // === IMPOSSIBLE CONNECTIONS ===
            // The key trick: beam A appears to go behind beam C at left,
            // beam B appears to go behind beam C at right,
            // but beam A and B appear to connect at the bottom
            // Bottom connecting piece (the impossibility)
            '<polygon points="-60,80 60,80 40,120 -40,120" fill="', c0, '" opacity="0.6"/>',
            '<polygon points="-60,80 60,80 80,50 -80,50" fill="', c1, '" opacity="0.4"/>',
            // Front face of bottom connection
            '<polygon points="-40,120 40,120 20,150 -20,150" fill="', c2, '" opacity="0.8"/>',

            // Glow outlines on key edges
            '<polygon points="-140,-80 -50,-80 -30,-40 -120,-40" fill="none" stroke="', c0, '" stroke-width="1"/>',
            '<polygon points="140,-80 50,-80 30,-40 120,-40" fill="none" stroke="', c1, '" stroke-width="1"/>',
            '<polygon points="-50,-80 50,-80 30,-40 -30,-40" fill="none" stroke="', c2, '" stroke-width="1"/>',
            '</g>',
            // Outer rotating ring
            '<circle cx="250" cy="250" r="200" fill="none" stroke="', c2, '" stroke-width="0.5" opacity="0.2" stroke-dasharray="6 12">',
            '<animateTransform attributeName="transform" type="rotate" from="0 250 250" to="360 250 250" dur="60s" repeatCount="indefinite"/>',
            '</circle>'
        ));
    }

    // ============ Illusion 7: Perspective Tunnel ============

    function _perspectiveTunnel(
        uint16 complexity,
        string memory c0,
        string memory c1,
        string memory c2,
        string memory bg
    ) internal pure returns (string memory) {
        uint256 numFrames = complexity + 6;
        string memory shapes = "";

        for (uint256 i = 0; i < numFrames; i++) {
            uint256 scale = ((numFrames - i) * 220) / numFrames;
            uint256 cx = 250;
            uint256 cy = 250;
            uint256 halfSize = scale / 2;

            int256 ox = int256(int8(uint8((i * 37) % 9))) - 4;
            int256 oy = int256(int8(uint8((i * 53) % 9))) - 4;
            uint256 fx = _addInt(cx, ox);
            uint256 fy = _addInt(cy, oy);

            string memory color;
            if (i % 3 == 0) color = c0;
            else if (i % 3 == 1) color = c1;
            else color = c2;

            uint256 sw = 3 + (i % 3); // 3-5 px
            shapes = string(abi.encodePacked(shapes,
                '<rect x="', _subInt(fx, int256(halfSize)).toString(), '" y="', _subInt(fy, int256(halfSize)).toString(),
                '" width="', scale.toString(), '" height="', scale.toString(),
                '" fill="none" stroke="', color, '" stroke-width="', sw.toString(), '" rx="4"/>'
            ));
        }

        // Vanishing point
        shapes = string(abi.encodePacked(shapes,
            '<circle cx="250" cy="250" r="10" fill="', c0, '"/>',
            '<circle cx="250" cy="250" r="4" fill="', bg, '"/>'
        ));

        return string(abi.encodePacked('<g>', shapes, '</g>'));
    }

    // ============ Illusion 8: Scintillating Grid ============

    function _scintillatingGrid(
        uint16 complexity,
        string memory c0,
        string memory c1,
        string memory bg
    ) internal pure returns (string memory) {
        // Classic Hermann grid with scintillating dots
        // Requires high contrast: bright dots on dark grid
        uint256 grid = complexity / 2 + 5;
        uint256 cellSize = 400 / grid;
        uint256 offsetX = (500 - cellSize * grid) / 2;
        uint256 offsetY = offsetX;

        string memory shapes = "";

        // Grid lines (bright for contrast)
        for (uint256 i = 0; i <= grid; i++) {
            uint256 pos = offsetX + i * cellSize;
            shapes = string(abi.encodePacked(shapes,
                '<line x1="', pos.toString(), '" y1="', offsetY.toString(),
                '" x2="', pos.toString(), '" y2="', (offsetY + grid * cellSize).toString(),
                '" stroke="', c1, '" stroke-width="2"/>',
                '<line x1="', offsetX.toString(), '" y1="', pos.toString(),
                '" x2="', (offsetX + grid * cellSize).toString(), '" y2="', pos.toString(),
                '" stroke="', c1, '" stroke-width="2"/>'
            ));
        }

        // Bright dots at intersections (these create the scintillating effect)
        uint256 dotR = cellSize / 5;
        for (uint256 r = 0; r <= grid; r++) {
            for (uint256 c = 0; c <= grid; c++) {
                uint256 cx = offsetX + c * cellSize;
                uint256 cy = offsetY + r * cellSize;
                shapes = string(abi.encodePacked(shapes,
                    '<circle cx="', cx.toString(), '" cy="', cy.toString(),
                    '" r="', dotR.toString(), '" fill="', c0, '"/>'
                ));
            }
        }

        // Animated scintillation (pulsing center dot)
        shapes = string(abi.encodePacked(shapes,
            '<circle cx="', (offsetX + (grid / 2) * cellSize).toString(),
            '" cy="', (offsetY + (grid / 2) * cellSize).toString(),
            '" r="', (dotR + 3).toString(), '" fill="', c0, '">',
            '<animate attributeName="opacity" values="1;0.3;1" dur="3s" repeatCount="indefinite"/>',
            '</circle>'
        ));

        return string(abi.encodePacked('<g>', shapes, '</g>'));
    }

    // ============ Rarity Frame ============

    function _buildFrame(
        uint8 rarity,
        string memory c0,
        string memory c1
    ) internal pure returns (string memory) {
        if (rarity == 0) {
            // Common: simple thin border
            return string(abi.encodePacked(
                '<rect x="2" y="2" width="496" height="496" fill="none" stroke="', c0, '" stroke-width="1" opacity="0.3"/>'
            ));
        } else if (rarity == 1) {
            // Rare: double border with corner accents
            return string(abi.encodePacked(
                '<rect x="2" y="2" width="496" height="496" fill="none" stroke="', c0, '" stroke-width="2" opacity="0.5"/>',
                '<rect x="8" y="8" width="484" height="484" fill="none" stroke="', c1, '" stroke-width="1" opacity="0.4"/>',
                '<rect x="0" y="0" width="20" height="3" fill="', c0, '"/>',
                '<rect x="0" y="0" width="3" height="20" fill="', c0, '"/>',
                '<rect x="480" y="0" width="20" height="3" fill="', c0, '"/>',
                '<rect x="497" y="0" width="3" height="20" fill="', c0, '"/>',
                '<rect x="0" y="497" width="20" height="3" fill="', c0, '"/>',
                '<rect x="0" y="480" width="3" height="20" fill="', c0, '"/>',
                '<rect x="480" y="497" width="20" height="3" fill="', c0, '"/>',
                '<rect x="497" y="480" width="3" height="20" fill="', c0, '"/>'
            ));
        } else {
            // Legendary: ornate frame with gradient + corner gems
            return string(abi.encodePacked(
                '<rect x="2" y="2" width="496" height="496" fill="none" stroke="url(#legendGrad)" stroke-width="3" opacity="0.7"/>',
                '<rect x="10" y="10" width="480" height="480" fill="none" stroke="', c1, '" stroke-width="1" opacity="0.5"/>',
                // Corner gems
                '<circle cx="12" cy="12" r="6" fill="', c0, '" filter="url(#glow)"/>',
                '<circle cx="488" cy="12" r="6" fill="', c1, '" filter="url(#glow)"/>',
                '<circle cx="12" cy="488" r="6" fill="', c1, '" filter="url(#glow)"/>',
                '<circle cx="488" cy="488" r="6" fill="', c0, '" filter="url(#glow)"/>',
                // Edge accents
                '<line x1="250" y1="2" x2="250" y2="10" stroke="', c0, '" stroke-width="2" opacity="0.6"/>',
                '<line x1="250" y1="490" x2="250" y2="498" stroke="', c0, '" stroke-width="2" opacity="0.6"/>',
                '<line x1="2" y1="250" x2="10" y2="250" stroke="', c0, '" stroke-width="2" opacity="0.6"/>',
                '<line x1="490" y1="250" x2="498" y2="250" stroke="', c0, '" stroke-width="2" opacity="0.6"/>'
            ));
        }
    }

    // ============ Math Helpers ============

    /// @notice Returns (dx, dy) for a given radius and angle in degrees
    function _polar(uint256 radius, uint256 angle) internal pure returns (int256 dx, int256 dy) {
        // Normalize angle to 0-359
        angle = angle % 360;
        // Use a 256-entry lookup table approach via quadratic approximation
        // cos: 1 at 0, 0 at 90, -1 at 180, 0 at 270
        // sin: 0 at 0, 1 at 90, 0 at 180, -1 at 270
        int256 cosVal;
        int256 sinVal;

        if (angle <= 90) {
            // cos: 1 -> 0, sin: 0 -> 1
            cosVal = 1000 - int256(angle * 1000 / 90);
            sinVal = int256(angle * 1000 / 90);
        } else if (angle <= 180) {
            // cos: 0 -> -1, sin: 1 -> 0
            cosVal = -int256((angle - 90) * 1000 / 90);
            sinVal = 1000 - int256((angle - 90) * 1000 / 90);
        } else if (angle <= 270) {
            // cos: -1 -> 0, sin: 0 -> -1
            cosVal = -1000 + int256((angle - 180) * 1000 / 90);
            sinVal = -int256((angle - 180) * 1000 / 90);
        } else {
            // cos: 0 -> 1, sin: -1 -> 0
            cosVal = int256((angle - 270) * 1000 / 90);
            sinVal = -1000 + int256((angle - 270) * 1000 / 90);
        }

        dx = (int256(radius) * cosVal) / 1000;
        dy = (int256(radius) * sinVal) / 1000;
    }

    // ============ Animated Illusion 9: Sliding Café Wall ============

    function _slidingCafeWall(
        uint16 complexity,
        string memory c0,
        string memory c1,
        string memory bg
    ) internal pure returns (string memory) {
        uint256 rows = complexity / 2 + 4;
        uint256 rowH = 400 / rows;
        uint256 cols = 7;
        uint256 cellW = 400 / cols;
        uint256 offsetX = 50;
        uint256 offsetY = 50;

        string memory shapes = "";

        for (uint256 r = 0; r < rows; r++) {
            uint256 yPos = offsetY + r * rowH;
            uint256 shift = (r % 2 == 0) ? 0 : cellW / 2;
            // Alternate colors per row
            string memory rowC0 = (r % 2 == 0) ? c0 : c1;
            string memory rowC1 = (r % 2 == 0) ? c1 : c0;

            // Animate each row sliding back and forth at different speeds
            uint256 dur = 4 + (r % 3); // 4-6 second cycles
            string memory animateX = string(abi.encodePacked(
                '<animateTransform attributeName="transform" type="translate" ',
                'values="0 0;', uint256(cellW / 2).toString(), ' 0;0 0" ',
                'dur="', dur.toString(), 's" repeatCount="indefinite"/>'
            ));

            string memory rowShapes = "";
            for (uint256 c = 0; c < cols + 1; c++) {
                uint256 xPos = offsetX + c * cellW - shift;
                string memory color = (c % 2 == 0) ? rowC0 : rowC1;
                rowShapes = string(abi.encodePacked(rowShapes,
                    '<rect x="', xPos.toString(), '" y="', yPos.toString(),
                    '" width="', cellW.toString(), '" height="', rowH.toString(),
                    '" fill="', color, '"/>'
                ));
            }
            // Wrap row in a <g> with sliding animation
            shapes = string(abi.encodePacked(shapes,
                '<g>', rowShapes, animateX, '</g>'
            ));
        }

        // Mortar lines (horizontal gaps between rows)
        for (uint256 r = 0; r <= rows; r++) {
            uint256 yPos = offsetY + r * rowH;
            shapes = string(abi.encodePacked(shapes,
                '<rect x="', offsetX.toString(), '" y="', _subInt(yPos, 1).toString(),
                '" width="400" height="2" fill="', bg, '"/>'
            ));
        }

        return string(abi.encodePacked('<g>', shapes, '</g>'));
    }

    // ============ Animated Illusion 10: Rotating Spiral ============

    function _rotatingSpiral(
        uint16 complexity,
        string memory c0,
        string memory c1,
        string memory c2,
        string memory bg
    ) internal pure returns (string memory) {
        // Archimedean spiral that rotates continuously — hypnotic motion aftereffect
        uint256 numArms = 3 + (complexity % 3); // 3-5 arms
        uint256 numSteps = complexity + 10; // 20-39 points per arm
        uint256 maxR = 180;
        string memory shapes = "";

        for (uint256 arm = 0; arm < numArms; arm++) {
            string memory path = "M 250 250";
            for (uint256 i = 1; i <= numSteps; i++) {
                uint256 t = (i * 1000) / numSteps;
                uint256 r = (maxR * t) / 1000;
                // angle = arm_offset + t * 3 full turns
                int256 angle = int256(arm * 360 / numArms) + int256((t * 1080) / 1000);
                (int256 dx, int256 dy) = _polar(r, uint256(angle));
                uint256 px = _addInt(250, dx);
                uint256 py = _addInt(250, dy);
                path = string(abi.encodePacked(path, ' L ', px.toString(), ' ', py.toString()));
            }
            string memory color;
            if (arm % 3 == 0) color = c0;
            else if (arm % 3 == 1) color = c1;
            else color = c2;

            shapes = string(abi.encodePacked(shapes,
                '<path d="', path, '" fill="none" stroke="', color,
                '" stroke-width="4" stroke-linecap="round"/>'
            ));
        }

        // Wrap entire spiral in a rotating group (no translate — path already uses absolute coords)
        shapes = string(abi.encodePacked(
            '<g>',
            shapes,
            '<animateTransform attributeName="transform" type="rotate" ',
            'from="0 250 250" to="360 250 250" dur="8s" repeatCount="indefinite"/>',
            '</g>'
        ));

        // Central glowing dot
        shapes = string(abi.encodePacked(shapes,
            '<circle cx="250" cy="250" r="8" fill="', c0, '">',
            '<animate attributeName="r" values="8;12;8" dur="2s" repeatCount="indefinite"/>',
            '</circle>'
        ));

        return shapes;
    }

    // ============ Animated Illusion 11: Pulsating Rings ============

    function _pulsatingRings(
        uint16 complexity,
        string memory c0,
        string memory c1,
        string memory c2,
        string memory bg
    ) internal pure returns (string memory) {
        // Concentric rings that expand and contract in a breathing pattern
        uint256 numRings = complexity + 5; // 25-34 rings
        uint256 maxR = 200;
        string memory shapes = "";

        for (uint256 i = 0; i < numRings; i++) {
            uint256 baseR = (maxR * (i + 1)) / numRings;
            string memory color;
            if (i % 3 == 0) color = c0;
            else if (i % 3 == 1) color = c1;
            else color = c2;

            // Each ring pulses with a slight delay — wave effect
            uint256 dur = 3 + (i % 3); // 3-5 second cycles
            uint256 delay = (i * 200) / 1000; // staggered start

            shapes = string(abi.encodePacked(shapes,
                '<circle cx="250" cy="250" r="', baseR.toString(),
                '" fill="none" stroke="', color, '" stroke-width="4">',
                '<animate attributeName="r" values="', baseR.toString(), ';',
                (baseR + 15).toString(), ';', baseR.toString(),
                '" dur="', dur.toString(), 's" begin="', delay.toString(),
                's" repeatCount="indefinite"/>',
                '<animate attributeName="opacity" values="1;0.4;1" dur="',
                dur.toString(), 's" begin="', delay.toString(),
                's" repeatCount="indefinite"/>',
                '</circle>'
            ));
        }

        // Central pulsing core
        shapes = string(abi.encodePacked(shapes,
            '<circle cx="250" cy="250" r="12" fill="', c0, '">',
            '<animate attributeName="r" values="12;20;12" dur="2s" repeatCount="indefinite"/>',
            '<animate attributeName="opacity" values="1;0.6;1" dur="2s" repeatCount="indefinite"/>',
            '</circle>'
        ));

        return string(abi.encodePacked('<g>', shapes, '</g>'));
    }

    // ============ Animated Illusion 12: Rotating Impossible Shape ============

    function _rotatingImpossible(
        string memory c0,
        string memory c1,
        string memory c2,
        string memory bg
    ) internal pure returns (string memory) {
        // Impossible cube / Necker cube that rotates and flips perspective
        return string(abi.encodePacked(
            '<g transform="translate(250 250)">',
            // Outer cube frame (Necker cube ambiguity)
            '<g>',
            // Front face
            '<polygon points="-80,-80 80,-80 80,80 -80,80" fill="none" stroke="', c0, '" stroke-width="4"/>',
            // Back face (offset, creating ambiguity)
            '<polygon points="-50,-110 110,-110 110,50 -50,50" fill="none" stroke="', c1, '" stroke-width="4"/>',
            // Connecting edges (the impossible part — they swap which is front/back)
            '<line x1="-80" y1="-80" x2="-50" y2="-110" stroke="', c2, '" stroke-width="4"/>',
            '<line x1="80" y1="-80" x2="110" y2="-110" stroke="', c2, '" stroke-width="4"/>',
            '<line x1="-80" y1="80" x2="-50" y2="50" stroke="', c2, '" stroke-width="4"/>',
            '<line x1="80" y1="80" x2="110" y2="50" stroke="', c2, '" stroke-width="4"/>',
            // Inner connecting beams that make it impossible
            '<polygon points="-80,-80 80,-80 110,-110 -50,-110" fill="', c0, '" opacity="0.3"/>',
            '<polygon points="-80,80 80,80 110,50 -50,50" fill="', c1, '" opacity="0.3"/>',
            '<polygon points="-80,-80 -80,80 -50,50 -50,-110" fill="', c2, '" opacity="0.2"/>',
            '<polygon points="80,-80 80,80 110,50 110,-110" fill="', c0, '" opacity="0.2"/>',
            // Continuous rotation
            '<animateTransform attributeName="transform" type="rotate" ',
            'from="0" to="360" dur="12s" repeatCount="indefinite"/>',
            '</g>',
            // Counter-rotating inner frame
            '<g>',
            '<polygon points="-40,-40 40,-40 40,40 -40,40" fill="none" stroke="', c1, '" stroke-width="3" opacity="0.5">',
            '<animateTransform attributeName="transform" type="rotate" ',
            'from="360" to="0" dur="8s" repeatCount="indefinite"/>',
            '</polygon>',
            '<polygon points="-20,-20 20,-20 20,20 -20,20" fill="', c0, '" opacity="0.3">',
            '<animateTransform attributeName="transform" type="rotate" ',
            'from="0" to="360" dur="5s" repeatCount="indefinite"/>',
            '</polygon>',
            '</g>',
            '</g>',
            // Outer rotating accent ring
            '<circle cx="250" cy="250" r="210" fill="none" stroke="', c2, '" stroke-width="1" opacity="0.2" stroke-dasharray="8 16">',
            '<animateTransform attributeName="transform" type="rotate" from="0 250 250" to="360 250 250" dur="20s" repeatCount="indefinite"/>',
            '</circle>'
        ));
    }

    function _addInt(uint256 base, int256 offset) internal pure returns (uint256) {
        if (offset >= 0) {
            return base + uint256(offset);
        } else {
            uint256 absOffset = uint256(-offset);
            return base >= absOffset ? base - absOffset : 0;
        }
    }

    function _subInt(uint256 base, int256 offset) internal pure returns (uint256) {
        if (offset >= 0) {
            return base >= uint256(offset) ? base - uint256(offset) : 0;
        } else {
            return base + uint256(-offset);
        }
    }

    // ============ Required Overrides ============

    function supportsInterface(bytes4 interfaceId) public view override(ERC721) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}

XEBBAirdrop.sol Monthly Airdrop

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

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/interfaces/IERC721Receiver.sol";
import "./XEBBArtNFT.sol";
import "./XEBBStaking.sol";

/**
 * @title XEBBAirdrop
 * @notice Monthly NFT art airdrop contract for XEBB token holders.
 *
 * Features:
 *   - Monthly snapshot-based eligibility (min 10,000 XEBB)
 *   - Random recipient selection using block hash + seed entropy
 *   - Rarity distribution: 60% Common, 30% Rare, 10% Legendary
 *   - Staking multipliers from XEBBStaking contract
 *   - 50-200 recipients per drop (owner-configurable)
 *
 * Note: For production, replace _getRandom with Chainlink VRF v2.
 */
contract XEBBAirdrop is Ownable, ReentrancyGuard, IERC721Receiver {
    using SafeERC20 for IERC20;

    // ============ Structs ============

    struct AirdropRound {
        uint256 snapshotBlock;
        uint256 executedAt;
        uint256 recipientCount;
        uint256 totalMinted;
        bool executed;
    }

    struct EligibilityCheck {
        address holder;
        uint256 balance;
        uint256 stakedAmount;
        uint8 stakingTier;
    }

    // ============ State ============

    IERC20 public immutable xebbToken;
    XEBBArtNFT public immutable artNFT;
    XEBBStaking public immutable staking;

    /// @dev Minimum XEBB balance required for airdrop eligibility (10,000 XEBB).
    uint256 public constant MIN_BALANCE = 10_000 * 10 ** 18;

    /// @dev Maximum recipients per round.
    uint256 public maxRecipients = 200;

    /// @dev Minimum recipients per round.
    uint256 public minRecipients = 50;

    /// @dev Mapping of round ID to round data.
    mapping(uint256 => AirdropRound) public rounds;

    /// @dev Current round ID.
    uint256 public currentRound;

    /// @dev Whether an address is an authorized airdrop operator.
    mapping(address => bool) public operators;

    /// @dev Nonce for random number generation.
    uint256 private _nonce;

    // ============ Events ============

    event RoundScheduled(uint256 indexed roundId, uint256 snapshotBlock);
    event RoundExecuted(uint256 indexed roundId, uint256 recipientCount, uint256 totalMinted);
    event AirdropSent(address indexed recipient, uint256 tokenId, uint8 rarity);
    event OperatorSet(address indexed operator, bool status);
    event RecipientLimitUpdated(uint256 min, uint256 max);

    // ============ Constructor ============

    constructor(
        address _xebbToken,
        address _artNFT,
        address _staking
    ) Ownable(msg.sender) {
        xebbToken = IERC20(_xebbToken);
        artNFT = XEBBArtNFT(_artNFT);
        staking = XEBBStaking(_staking);
    }

    // ============ Modifiers ============

    modifier onlyOperator() {
        require(msg.sender == owner() || operators[msg.sender], "XEBBAirdrop: not operator");
        _;
    }

    // ============ External Functions ============

    /**
     * @notice Schedules a new airdrop round with a snapshot at the current block.
     */
    function scheduleRound() external onlyOperator returns (uint256 roundId) {
        roundId = ++currentRound;
        rounds[roundId] = AirdropRound({
            snapshotBlock: block.number,
            executedAt: 0,
            recipientCount: 0,
            totalMinted: 0,
            executed: false
        });

        emit RoundScheduled(roundId, block.number);
    }

    /**
     * @notice Executes an airdrop round by distributing art NFTs to eligible holders.
     * @param roundId The round ID to execute.
     * @param recipients Pre-selected eligible recipient addresses.
     * @param rarities Rarity tiers for each recipient (must match recipients length).
     */
    function executeRound(
        uint256 roundId,
        address[] calldata recipients,
        uint8[] calldata rarities
    ) external onlyOperator nonReentrant {
        AirdropRound storage round = rounds[roundId];
        require(round.snapshotBlock > 0, "XEBBAirdrop: round not scheduled");
        require(!round.executed, "XEBBAirdrop: round already executed");
        require(recipients.length == rarities.length, "XEBBAirdrop: length mismatch");
        require(recipients.length >= minRecipients, "XEBBAirdrop: below min recipients");
        require(recipients.length <= maxRecipients, "XEBBAirdrop: above max recipients");

        uint256 seed = _getRandomSeed(roundId);
        uint256 totalMinted = 0;

        for (uint256 i = 0; i < recipients.length; i++) {
            // Verify eligibility at snapshot block
            require(
                _isEligible(recipients[i], round.snapshotBlock),
                "XEBBAirdrop: recipient not eligible"
            );
            require(rarities[i] <= 2, "XEBBAirdrop: invalid rarity");

            uint256 itemSeed = seed + i * 1000;
            artNFT.mint(recipients[i], rarities[i], itemSeed);
            totalMinted++;

            emit AirdropSent(recipients[i], totalMinted, rarities[i]);
        }

        round.executed = true;
        round.executedAt = block.timestamp;
        round.recipientCount = recipients.length;
        round.totalMinted = totalMinted;

        emit RoundExecuted(roundId, recipients.length, totalMinted);
    }

    /**
     * @notice Checks if an address is eligible for airdrop at a given block.
     * @param holder The address to check.
     * @param snapshotBlock The block number of the snapshot.
     * @return True if eligible (balance + staked >= MIN_BALANCE).
     */
    function isEligible(address holder, uint256 snapshotBlock) external view returns (bool) {
        return _isEligible(holder, snapshotBlock);
    }

    /**
     * @notice Returns the airdrop weight for a holder (balance + staked with multiplier).
     * @param holder The address to check.
     */
    function getAirdropWeight(address holder) external view returns (uint256) {
        uint256 balance = xebbToken.balanceOf(holder);
        uint256 stakedWeight = staking.getAirdropWeight(holder);
        return balance + stakedWeight;
    }

    /**
     * @notice Returns the round data.
     */
    function getRound(uint256 roundId) external view returns (AirdropRound memory) {
        return rounds[roundId];
    }

    /**
     * @notice Sets operator status (owner only).
     */
    function setOperator(address operator, bool status) external onlyOwner {
        operators[operator] = status;
        emit OperatorSet(operator, status);
    }

    /**
     * @notice Updates recipient limits (owner only).
     */
    function setRecipientLimits(uint256 _min, uint256 _max) external onlyOwner {
        require(_min > 0 && _max >= _min, "XEBBAirdrop: invalid limits");
        minRecipients = _min;
        maxRecipients = _max;
        emit RecipientLimitUpdated(_min, _max);
    }

    /**
     * @notice Generates a random rarity based on probability distribution.
     *         60% Common, 30% Rare, 10% Legendary.
     * @param seed Random seed.
     */
    function rollRarity(uint256 seed) public pure returns (uint8) {
        uint256 roll = seed % 100;
        if (roll < 60) return 0; // Common (60%)
        if (roll < 90) return 1; // Rare (30%)
        return 2;                // Legendary (10%)
    }

    // ============ IERC721Receiver ============

    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
    ) external pure override returns (bytes4) {
        return this.onERC721Received.selector;
    }

    // ============ Internal Functions ============

    function _isEligible(address holder, uint256 snapshotBlock) internal view returns (bool) {
        // For simplicity, check current balance + staked
        // In production, use a snapshot contract for historical balances
        uint256 balance = xebbToken.balanceOf(holder);
        uint256 stakedAmount = staking.getAirdropWeight(holder);
        return (balance + stakedAmount) >= MIN_BALANCE;
    }

    function _getRandomSeed(uint256 roundId) internal returns (uint256) {
        _nonce++;
        return uint256(keccak256(abi.encodePacked(
            block.prevrandao,
            block.timestamp,
            roundId,
            _nonce,
            msg.sender
        )));
    }

    /// @dev Burns a percentage of the contract's XEBB balance. Called during round execution.
    /// @dev Uses low-level call to burn tokens — requires XEBBToken to be ERC20Burnable.
    function _executeMonthlyBurn(uint256 roundId) internal returns (uint256 burnedAmount) {
        uint256 balance = xebbToken.balanceOf(address(this));
        if (balance == 0 || burnRateBps == 0) return 0;

        burnedAmount = (balance * burnRateBps) / 10000;
        if (burnedAmount == 0) return 0;

        // Call burn(amount) on the token contract — burns from this contract's balance
        (bool success, ) = address(xebbToken).call(
            abi.encodeWithSignature("burn(uint256)", burnedAmount)
        );
        require(success, "XEBBAirdrop: burn failed");

        totalBurned += burnedAmount;
        emit MonthlyBurn(roundId, burnedAmount);
    }
}

XEBBGovernance.sol Governance

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
import "@openzeppelin/contracts/governance/TimelockController.sol";

/**
 * @title XEBBGovernance
 * @notice On-chain governance for XEBB Token protocol.
 *
 * Parameters:
 *   - Proposal threshold: 1,000,000 XEBB (1% of supply)
 *   - Quorum: 10% of total supply
 *   - Voting period: 72 hours
 *   - Timelock: 48 hours
 *   - Voting power: Proportional to token balance (via ERC20Votes)
 *   - Delegation: Supported
 */
contract XEBBGovernance is
    Governor,
    GovernorCountingSimple,
    GovernorVotes,
    GovernorVotesQuorumFraction,
    GovernorTimelockControl
{
    // 72 hours in blocks (~2s blocks on Base)
    uint256 public constant VOTING_PERIOD = 129_600; // 72h / 2s
    uint256 public constant VOTING_DELAY = 7_200;   // 4 hours / 2s
    uint256 public constant PROPOSAL_THRESHOLD = 1_000_000 * 10 ** 18; // 1M XEBB

    constructor(
        IVotes token,
        TimelockController timelock
    )
        Governor("XEBB Governance")
        GovernorVotes(token)
        GovernorVotesQuorumFraction(10) // 10% quorum
        GovernorTimelockControl(timelock)
    {}

    function votingDelay() public pure override returns (uint256) {
        return VOTING_DELAY;
    }

    function votingPeriod() public pure override returns (uint256) {
        return VOTING_PERIOD;
    }

    function proposalThreshold() public pure override returns (uint256) {
        return PROPOSAL_THRESHOLD;
    }

    // Required overrides for multiple inheritance
    function state(uint256 proposalId)
        public
        view
        override(Governor, GovernorTimelockControl)
        returns (ProposalState)
    {
        return super.state(proposalId);
    }

    function _executeOperations(
        uint256 proposalId,
        address[] memory targets,
        uint256[] memory values,
        bytes[] memory calldatas,
        bytes32 descriptionHash
    ) internal override(Governor, GovernorTimelockControl) {
        super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);
    }

    function _queueOperations(
        uint256 proposalId,
        address[] memory targets,
        uint256[] memory values,
        bytes[] memory calldatas,
        bytes32 descriptionHash
    ) internal override(Governor, GovernorTimelockControl) returns (uint48) {
        return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash);
    }

    function proposalNeedsQueuing(uint256 proposalId)
        public
        view
        override(Governor, GovernorTimelockControl)
        returns (bool)
    {
        return super.proposalNeedsQueuing(proposalId);
    }

    function _cancel(
        address[] memory targets,
        uint256[] memory values,
        bytes[] memory calldatas,
        bytes32 descriptionHash
    ) internal override(Governor, GovernorTimelockControl) returns (uint256) {
        return super._cancel(targets, values, calldatas, descriptionHash);
    }

    function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) {
        return super._executor();
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(Governor)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

XEBBMining.sol Browser PoW Mining

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
 * @title XEBBMining
 * @notice Browser-based proof-of-work mining for XEBB Token.
 *         Users mine in their browser (Web Worker hashing keccak256),
 *         then submit valid proofs on-chain to claim XEBB rewards.
 *
 * Mechanics:
 *   - 10 XEBB per valid proof
 *   - 30 XEBB max per wallet per day
 *   - 1,370 XEBB global daily emission cap
 *   - 10-minute cooldown between claims
 *   - 22-bit difficulty (adjustable by owner, 18-28 range)
 *   - Daily epochs with blockhash-derived challenges
 *   - Replay prevention via used-proof tracking
 *
 * The mining reserve is pre-funded with XEBB tokens.
 * No new tokens are minted — supply remains fixed at 100M.
 */
contract XEBBMining is Ownable {
    IERC20 public immutable token;

    // ============ Mining Parameters ============

    uint256 public constant REWARD_PER_PROOF = 10 ether;       // 10 XEBB
    uint256 public constant MAX_PER_WALLET_PER_DAY = 30 ether;  // 30 XEBB/day
    uint256 public constant DAILY_EMISSION_CAP = 1370 ether;    // 1370 XEBB/day global
    uint256 public constant CLAIM_COOLDOWN = 10 minutes;
    uint256 public constant MIN_DIFFICULTY = 18; // bits
    uint256 public constant MAX_DIFFICULTY = 28; // bits

    uint256 public difficultyBits = 22;

    // ============ State ============

    mapping(address => uint256) public lastClaimTime;
    mapping(address => uint256) public minedToday;        // resets per epoch
    mapping(address => uint256) public lastEpochMined;     // tracks which epoch the wallet last mined in
    uint256 public epochGlobalMined;                       // total mined this epoch
    uint256 public currentEpoch;                           // current day epoch
    bytes32 public epochChallenge;                         // challenge hash for current epoch

    mapping(bytes32 => bool) public usedProofs;            // replay prevention

    uint256 public totalMined;                              // lifetime total mined
    uint256 public miningReserve;                          // remaining tokens for mining

    // ============ Events ============

    event ProofSubmitted(address indexed miner, uint256 nonce, bytes32 hash, uint256 reward);
    event DifficultyAdjusted(uint256 oldBits, uint256 newBits);
    event ReserveFunded(uint256 amount);
    event EmergencyWithdraw(address indexed to, uint256 amount);

    // ============ Constructor ============

    constructor(address _token) Ownable(msg.sender) {
        token = IERC20(_token);
        _updateEpoch();
    }

    // ============ Epoch Management ============

    function _currentEpochId() internal view returns (uint256) {
        return block.timestamp / 1 days;
    }

    function _updateEpoch() internal {
        uint256 newEpoch = _currentEpochId();
        if (newEpoch != currentEpoch) {
            currentEpoch = newEpoch;
            epochGlobalMined = 0;
            // Challenge derived from previous blockhash + epoch
            // Can't use future blockhash, so use the most recent one
            bytes32 blockHash = blockhash(block.number - 1);
            epochChallenge = keccak256(abi.encodePacked(blockHash, newEpoch));
        }
    }

    function getChallenge() external view returns (bytes32) {
        uint256 epoch = _currentEpochId();
        if (epoch != currentEpoch) {
            // New epoch — compute what the challenge will be
            bytes32 blockHash = blockhash(block.number - 1);
            return keccak256(abi.encodePacked(blockHash, epoch));
        }
        return epochChallenge;
    }

    function getCurrentEpoch() external view returns (uint256) {
        return _currentEpochId();
    }

    // ============ Mining ============

    /**
     * @notice Submit a valid proof-of-work to claim mining rewards.
     * @param nonce The nonce that produces a valid hash below the difficulty target.
     *
     * The hash is computed as:
     *   keccak256(abi.encodePacked(chainId, address(this), msg.sender, currentEpoch, epochChallenge, nonce))
     *
     * The hash must have `difficultyBits` leading zero bits.
     */
    function submitProof(uint256 nonce) external {
        _updateEpoch();

        // Check cooldown
        require(
            block.timestamp >= lastClaimTime[msg.sender] + CLAIM_COOLDOWN,
            "XEBBMining: cooldown active"
        );

        // Check daily wallet cap
        uint256 walletMined = (lastEpochMined[msg.sender] == currentEpoch)
            ? minedToday[msg.sender]
            : 0;
        require(
            walletMined + REWARD_PER_PROOF <= MAX_PER_WALLET_PER_DAY,
            "XEBBMining: daily wallet cap reached"
        );

        // Check global daily cap
        require(
            epochGlobalMined + REWARD_PER_PROOF <= DAILY_EMISSION_CAP,
            "XEBBMining: daily global cap reached"
        );

        // Check reserve
        require(
            miningReserve >= REWARD_PER_PROOF,
            "XEBBMining: mining reserve exhausted"
        );

        // Verify proof of work
        bytes32 hash = keccak256(abi.encodePacked(
            block.chainid,
            address(this),
            msg.sender,
            currentEpoch,
            epochChallenge,
            nonce
        ));

        // Check leading zero bits
        require(_hasLeadingZeros(hash, difficultyBits), "XEBBMining: invalid proof");

        // Prevent replay
        require(!usedProofs[hash], "XEBBMining: proof already used");
        usedProofs[hash] = true;

        // Update state
        lastClaimTime[msg.sender] = block.timestamp;
        if (lastEpochMined[msg.sender] != currentEpoch) {
            lastEpochMined[msg.sender] = currentEpoch;
            minedToday[msg.sender] = 0;
        }
        minedToday[msg.sender] += REWARD_PER_PROOF;
        epochGlobalMined += REWARD_PER_PROOF;
        miningReserve -= REWARD_PER_PROOF;
        totalMined += REWARD_PER_PROOF;

        // Transfer reward
        require(
            token.transfer(msg.sender, REWARD_PER_PROOF),
            "XEBBMining: transfer failed"
        );

        emit ProofSubmitted(msg.sender, nonce, hash, REWARD_PER_PROOF);
    }

    // ============ View Functions ============

    function getMiningStats(address miner) external view returns (
        uint256 walletMinedToday,
        uint256 cooldownRemaining,
        uint256 globalMinedToday,
        uint256 reserveRemaining
    ) {
        uint256 epoch = _currentEpochId();
        walletMinedToday = (lastEpochMined[miner] == epoch) ? minedToday[miner] : 0;
        cooldownRemaining = (lastClaimTime[miner] + CLAIM_COOLDOWN > block.timestamp)
            ? (lastClaimTime[miner] + CLAIM_COOLDOWN - block.timestamp)
            : 0;
        globalMinedToday = (epoch == currentEpoch) ? epochGlobalMined : 0;
        reserveRemaining = miningReserve;
    }

    function getDifficulty() external view returns (uint256) {
        return difficultyBits;
    }

    // ============ Admin ============

    /**
     * @notice Fund the mining reserve with XEBB tokens.
     * @param amount Token amount (in wei) to deposit.
     */
    function fundReserve(uint256 amount) external onlyOwner {
        require(
            token.transferFrom(msg.sender, address(this), amount),
            "XEBBMining: transferFrom failed"
        );
        miningReserve += amount;
        emit ReserveFunded(amount);
    }

    /**
     * @notice Adjust mining difficulty (18-28 bits range).
     */
    function setDifficulty(uint256 bits) external onlyOwner {
        require(bits >= MIN_DIFFICULTY && bits <= MAX_DIFFICULTY, "XEBBMining: out of range");
        emit DifficultyAdjusted(difficultyBits, bits);
        difficultyBits = bits;
    }

    /**
     * @notice Emergency withdraw remaining reserve (owner only).
     */
    function emergencyWithdraw() external onlyOwner {
        uint256 balance = miningReserve;
        miningReserve = 0;
        require(token.transfer(msg.sender, balance), "XEBBMining: withdrawal failed");
        emit EmergencyWithdraw(msg.sender, balance);
    }

    // ============ Internal: Leading Zeros Check ============

    /**
     * @dev Returns true if `hash` has at least `bits` leading zero bits.
     */
    function _hasLeadingZeros(bytes32 hash, uint256 bits) internal pure returns (bool) {
        // Convert hash to uint256 — leading zeros in bytes32 = leading zeros in uint256
        uint256 value = uint256(hash);

        // Check that value has at least `bits` leading zero bits
        // value must be < 2^(256 - bits)
        uint256 target = type(uint256).max >> bits;
        return value <= target;
    }
}