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