// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; /// @notice Fixed-supply BEP20 with native-currency dividends. Transfers preserve /// earned rewards; the market's inventory never participates in holder rewards. contract SparkDividendToken is ERC20, ReentrancyGuard { uint256 private constant MAGNITUDE = 2 ** 128; address public immutable market; uint256 public eligibleSupply; uint256 public magnifiedPerShare; uint256 public undistributed; uint256 public distributed; mapping(address => int256) private corrections; mapping(address => uint256) public withdrawn; event DividendDeposited(uint256 amount); event DividendClaimed(address indexed holder, address indexed to, uint256 amount); constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) { market = msg.sender; _mint(msg.sender, 1_000_000_000 ether); } function scaled(uint256 amount) private view returns (int256) { uint256 value = amount * magnifiedPerShare; require(value <= uint256(type(int256).max), "DIVIDEND_OVERFLOW"); return int256(value); } function _update(address from, address to, uint256 amount) internal override { if (from != address(0) && from != market) { corrections[from] += scaled(amount); eligibleSupply -= amount; } if (to != address(0) && to != market) { corrections[to] -= scaled(amount); eligibleSupply += amount; } super._update(from, to, amount); } function depositDividend() external payable { require(msg.sender == market, "MARKET_ONLY"); undistributed += msg.value; if (eligibleSupply > 0 && undistributed > 0) { uint256 amount = undistributed; undistributed = 0; magnifiedPerShare += Math.mulDiv(amount, MAGNITUDE, eligibleSupply); distributed += amount; emit DividendDeposited(amount); } } function accumulativeDividendOf(address holder) public view returns (uint256) { if (holder == market || holder == address(0)) return 0; int256 adjusted = scaled(balanceOf(holder)) + corrections[holder]; return adjusted > 0 ? uint256(adjusted) / MAGNITUDE : 0; } function claimable(address holder) public view returns (uint256) { return accumulativeDividendOf(holder) - withdrawn[holder]; } function claimDividend(address payable to) external nonReentrant { require(to != address(0), "ZERO_RECIPIENT"); uint256 amount = claimable(msg.sender); require(amount > 0, "NO_REWARDS"); withdrawn[msg.sender] += amount; (bool ok,) = to.call{value: amount}(""); require(ok, "NATIVE_TRANSFER"); emit DividendClaimed(msg.sender, to, amount); } } struct LaunchParams { string name; string symbol; string meta; address creator; uint16 creatorTaxBps; bool holderSharing; address[] exemptions; } interface ISparkFactoryConfig { function orderBook() external view returns (address); function isPool(address pool) external view returns (bool); } /// @notice A virtual-reserve curve that becomes a real-reserve, permanently /// locked native BNB pool at its target. Does not migrate to an external DEX. contract SparkCurveV2 is ReentrancyGuard { using SafeERC20 for IERC20; uint256 public constant SUPPLY = 1_000_000_000 ether; uint256 public constant BASE_FEE_BPS = 100; uint256 public constant SNIPE_WINDOW = 3; address public immutable factory; address public immutable orderBook; address public immutable launcher; address public immutable creator; address public immutable treasury; SparkDividendToken public immutable token; uint256 public immutable graduationTarget; uint256 public immutable createdAt; uint16 public immutable creatorTaxBps; bool public immutable holderSharing; string public metadata; uint256 public reserveBNB; uint256 public reserveToken = SUPPLY; uint256 public volumeBNB; uint256 public feeTotal; bool public graduated; mapping(address => uint256) public fees; mapping(address => bool) public exempt; event Trade(address indexed account, bool buy, uint256 bnb, uint256 tokens, uint256 fee); event Graduated(uint256 bnb, uint256 tokens); event Refunded(address indexed account, uint256 amount); event FeesClaimed(address indexed account, uint256 amount); constructor(LaunchParams memory p, address launcher_, address treasury_, uint256 target_) { require(block.chainid == 56 || block.chainid == 97 || block.chainid == 31337, "BSC_ONLY"); require(bytes(p.name).length > 0 && bytes(p.name).length <= 64, "NAME_LENGTH"); require(bytes(p.symbol).length > 0 && bytes(p.symbol).length <= 12, "SYMBOL_LENGTH"); require(bytes(p.meta).length <= 2048 && p.exemptions.length <= 32, "METADATA_LIMIT"); require(p.creator != address(0) && treasury_ != address(0), "ZERO_ADDRESS"); require(p.creatorTaxBps <= 1000, "TAX_LIMIT"); require(target_ >= 0.1 ether && target_ <= 100 ether && target_ % 2 == 0, "TARGET_RANGE"); factory = msg.sender; orderBook = ISparkFactoryConfig(msg.sender).orderBook(); launcher = launcher_; creator = p.creator; treasury = treasury_; creatorTaxBps = p.creatorTaxBps; holderSharing = p.holderSharing; graduationTarget = target_; metadata = p.meta; createdAt = block.timestamp; exempt[launcher_] = true; exempt[p.creator] = true; for (uint256 i; i < p.exemptions.length; ++i) { require(p.exemptions[i] != address(0), "ZERO_EXEMPTION"); exempt[p.exemptions[i]] = true; } token = new SparkDividendToken(p.name, p.symbol); } function pricingReserves() public view returns (uint256 bnb, uint256 tokens) { // Virtual quote = target / 2, virtual tokens = supply / 8. At the target, // 25% of actual supply remains; its real-pool price matches the curve price. return graduated ? (reserveBNB, reserveToken) : (reserveBNB + graduationTarget / 2, reserveToken + SUPPLY / 8); } function currentSnipeTaxBps(address recipient) public view returns (uint256) { if (exempt[recipient] || graduated || block.timestamp >= createdAt + SNIPE_WINDOW) return 0; uint256 raw = 9900 * (SNIPE_WINDOW - (block.timestamp - createdAt)) / SNIPE_WINDOW; return Math.min(raw, 10000 - BASE_FEE_BPS - creatorTaxBps - 100); } struct BuyQuote { uint256 output; uint256 spent; uint256 net; uint256 baseFee; uint256 creatorTax; uint256 snipeTax; uint256 refund; } function quoteBuy(uint256 input, address recipient) public view returns (BuyQuote memory q) { uint256 snipeBps = currentSnipeTaxBps(recipient); q.spent = input; q.baseFee = input * BASE_FEE_BPS / 10000; q.creatorTax = input * creatorTaxBps / 10000; q.snipeTax = input * snipeBps / 10000; q.net = input - q.baseFee - q.creatorTax - q.snipeTax; if (!graduated && q.net > graduationTarget - reserveBNB) { uint256 remaining = graduationTarget - reserveBNB; // Separate fee floors can make the theoretical gross exceed input by // a few wei. The original input is already sufficient in this branch. q.spent = Math.min(input, Math.mulDiv(remaining, 10000, 10000 - BASE_FEE_BPS - creatorTaxBps - snipeBps, Math.Rounding.Ceil)); q.baseFee = q.spent * BASE_FEE_BPS / 10000; q.creatorTax = q.spent * creatorTaxBps / 10000; q.snipeTax = q.spent * snipeBps / 10000; uint256 actualNet = q.spent - q.baseFee - q.creatorTax - q.snipeTax; q.baseFee += actualNet - remaining; q.net = remaining; q.refund = input - q.spent; } (uint256 bnb, uint256 tokens) = pricingReserves(); q.output = Math.mulDiv(tokens, q.net, bnb + q.net); } function quoteSell(uint256 amount) public view returns (uint256 output, uint256 baseFee, uint256 creatorTax) { (uint256 bnb, uint256 tokens) = pricingReserves(); uint256 gross = Math.mulDiv(bnb, amount, tokens + amount); require(gross <= reserveBNB, "RESERVE_LIMIT"); baseFee = gross * BASE_FEE_BPS / 10000; creatorTax = gross * creatorTaxBps / 10000; output = gross - baseFee - creatorTax; } function charge(uint256 baseFee, uint256 creatorTax, uint256 snipeTax) private { uint256 shared = baseFee + snipeTax; uint256 platform = shared * 30 / 100; uint256 creatorPart = shared - platform + creatorTax; fees[treasury] += platform; feeTotal += shared + creatorTax; if (holderSharing) token.depositDividend{value: creatorPart}(); else fees[creator] += creatorPart; } function buy(uint256 minimum, uint256 deadline) external payable nonReentrant { buyInternal(msg.sender, minimum, deadline); } function buyFor(address recipient, uint256 minimum, uint256 deadline) external payable nonReentrant { require(recipient != address(0), "ZERO_RECIPIENT"); buyInternal(recipient, minimum, deadline); } function initialBuy(address recipient, uint256 minimum, uint256 deadline) external payable nonReentrant { require(msg.sender == factory, "FACTORY_ONLY"); buyInternal(recipient, minimum, deadline); } function buyInternal(address recipient, uint256 minimum, uint256 deadline) private { require(block.timestamp <= deadline, "EXPIRED"); require(msg.value > 0, "ZERO_INPUT"); BuyQuote memory q = quoteBuy(msg.value, recipient); require(q.output > 0 && q.output >= minimum, "SLIPPAGE"); reserveBNB += q.net; reserveToken -= q.output; volumeBNB += q.spent; IERC20(address(token)).safeTransfer(recipient, q.output); charge(q.baseFee, q.creatorTax, q.snipeTax); if (!graduated && reserveBNB == graduationTarget) { graduated = true; emit Graduated(reserveBNB, reserveToken); } if (q.refund > 0) { (bool ok,) = payable(recipient).call{value: q.refund}(""); require(ok, "REFUND_FAILED"); emit Refunded(recipient, q.refund); } emit Trade(recipient, true, q.spent, q.output, q.baseFee + q.creatorTax + q.snipeTax); } function sell(uint256 amount, uint256 minimum, uint256 deadline) external nonReentrant { sellInternal(msg.sender, amount, minimum, deadline); } function sellFor(address owner, uint256 amount, uint256 minimum, uint256 deadline) external nonReentrant { require(msg.sender == orderBook, "ORDER_BOOK_ONLY"); sellInternal(owner, amount, minimum, deadline); } function sellInternal(address owner, uint256 amount, uint256 minimum, uint256 deadline) private { require(block.timestamp <= deadline, "EXPIRED"); require(amount > 0, "ZERO_INPUT"); (uint256 output, uint256 baseFee, uint256 creatorTax) = quoteSell(amount); require(output > 0 && output >= minimum, "SLIPPAGE"); uint256 gross = output + baseFee + creatorTax; reserveBNB -= gross; reserveToken += amount; volumeBNB += gross; // Seller receives dividends earned on this trade before the sold balance leaves. charge(baseFee, creatorTax, 0); IERC20(address(token)).safeTransferFrom(owner, address(this), amount); (bool ok,) = payable(owner).call{value: output}(""); require(ok, "NATIVE_TRANSFER"); emit Trade(owner, false, output, amount, baseFee + creatorTax); } function claimFees(address payable to) external nonReentrant { require(to != address(0), "ZERO_RECIPIENT"); uint256 amount = fees[msg.sender]; require(amount > 0, "NO_FEES"); fees[msg.sender] = 0; (bool ok,) = to.call{value: amount}(""); require(ok, "NATIVE_TRANSFER"); emit FeesClaimed(msg.sender, amount); } } /// @notice Permissionless limit execution. Buy deposits remain refundable; sell /// tokens stay in the maker wallet and use an approval to their own market. contract SparkOrderBook is ReentrancyGuard { address public immutable factory; uint256 public nextId = 1; uint256 public escrowBNB; struct Order { uint256 id; address maker; address pool; bool buy; uint256 amount; uint256 minimum; uint256 expiry; uint8 status; } mapping(uint256 => Order) public orders; mapping(address => uint256[]) private ownerIds; mapping(address => uint256) public openCount; event OrderCreated(uint256 indexed id, address indexed maker, address indexed pool, bool buy, uint256 amount, uint256 minimum, uint256 expiry); event OrderExecuted(uint256 indexed id, address indexed executor); event OrderCancelled(uint256 indexed id); constructor(address factory_) { factory = factory_; } function place(address pool, bool buy, uint256 amount, uint256 minimum, uint256 expiry) external payable nonReentrant returns (uint256 id) { require(ISparkFactoryConfig(factory).isPool(pool), "UNKNOWN_POOL"); require(amount > 0 && minimum > 0, "ZERO_AMOUNT"); require(expiry > block.timestamp && expiry <= block.timestamp + 30 days, "EXPIRY_RANGE"); require(msg.value == (buy ? amount : 0), "INCORRECT_VALUE"); require(openCount[msg.sender] < 64, "ORDER_LIMIT"); if (!buy) { IERC20 t = IERC20(address(SparkCurveV2(pool).token())); require(t.balanceOf(msg.sender) >= amount && t.allowance(msg.sender, pool) >= amount, "TOKEN_APPROVAL"); } id = nextId++; orders[id] = Order(id, msg.sender, pool, buy, amount, minimum, expiry, 0); ownerIds[msg.sender].push(id); openCount[msg.sender]++; if (buy) escrowBNB += amount; emit OrderCreated(id, msg.sender, pool, buy, amount, minimum, expiry); } function executable(uint256 id) public view returns (bool) { Order memory o = orders[id]; if (o.maker == address(0) || o.status != 0 || block.timestamp > o.expiry) return false; SparkCurveV2 pool = SparkCurveV2(o.pool); if (o.buy) { try pool.quoteBuy(o.amount, o.maker) returns (SparkCurveV2.BuyQuote memory q) { return q.output >= o.minimum; } catch { return false; } } IERC20 t = IERC20(address(pool.token())); if (t.balanceOf(o.maker) < o.amount || t.allowance(o.maker, o.pool) < o.amount) return false; try pool.quoteSell(o.amount) returns (uint256 output, uint256, uint256) { return output >= o.minimum; } catch { return false; } } function execute(uint256 id) external nonReentrant { require(executable(id), "NOT_EXECUTABLE"); Order storage o = orders[id]; o.status = 1; openCount[o.maker]--; if (o.buy) { escrowBNB -= o.amount; SparkCurveV2(o.pool).buyFor{value:o.amount}(o.maker, o.minimum, o.expiry); } else SparkCurveV2(o.pool).sellFor(o.maker, o.amount, o.minimum, o.expiry); emit OrderExecuted(id, msg.sender); } function cancel(uint256 id, address payable to) external nonReentrant { Order storage o = orders[id]; require(o.maker == msg.sender && o.status == 0, "MAKER_ONLY"); require(to != address(0), "ZERO_RECIPIENT"); o.status = 2; openCount[o.maker]--; if (o.buy) { escrowBNB -= o.amount; (bool ok,) = to.call{value:o.amount}(""); require(ok, "REFUND_FAILED"); } emit OrderCancelled(id); } function orderCount(address maker) external view returns (uint256) { return ownerIds[maker].length; } function ordersFor(address maker, uint256 offset, uint256 limit) external view returns (Order[] memory result) { require(limit > 0 && limit <= 20, "LIMIT"); uint256 count = ownerIds[maker].length; if (offset >= count) return new Order[](0); uint256 length = Math.min(limit, count - offset); result = new Order[](length); for (uint256 i; i < length; ++i) result[i] = orders[ownerIds[maker][count - 1 - offset - i]]; } } contract SparkFactoryV2 is ReentrancyGuard { uint256 public constant version = 2; address public immutable treasury; uint256 public immutable launchFee; uint256 public immutable graduationTarget; uint256 public launchFees; address public immutable orderBook; mapping(address => bool) public isPool; SparkCurveV2[] public pools; event Launched(address indexed pool, address indexed token, address indexed launcher, address creator); constructor(address treasury_, uint256 fee_, uint256 target_) { require(block.chainid == 56 || block.chainid == 97 || block.chainid == 31337, "BSC_ONLY"); require(treasury_ != address(0), "ZERO_TREASURY"); require(fee_ <= 0.1 ether, "FEE_LIMIT"); require(target_ >= 0.1 ether && target_ <= 100 ether && target_ % 2 == 0, "TARGET_RANGE"); treasury = treasury_; launchFee = fee_; graduationTarget = target_; orderBook = address(new SparkOrderBook(address(this))); } function launch(LaunchParams calldata p, uint256 developerBuy, uint256 minTokens, uint256 deadline) external payable nonReentrant returns (address) { require(msg.value == launchFee + developerBuy, "INCORRECT_VALUE"); require(block.timestamp <= deadline, "EXPIRED"); LaunchParams memory params = p; if (params.creator == address(0)) params.creator = msg.sender; SparkCurveV2 pool = new SparkCurveV2(params, msg.sender, treasury, graduationTarget); pools.push(pool); isPool[address(pool)] = true; launchFees += launchFee; if (developerBuy > 0) pool.initialBuy{value: developerBuy}(msg.sender, minTokens, deadline); emit Launched(address(pool), address(pool.token()), msg.sender, params.creator); return address(pool); } function claimLaunchFees(address payable to) external nonReentrant { require(msg.sender == treasury && to != address(0), "TREASURY_ONLY"); uint256 amount = launchFees; require(amount > 0, "NO_FEES"); launchFees = 0; (bool ok,) = to.call{value: amount}(""); require(ok, "NATIVE_TRANSFER"); } function poolCount() external view returns (uint256) { return pools.length; } struct Market { address pool; address token; address creator; string name; string symbol; string meta; bool graduated; bool holderSharing; uint256 creatorTaxBps; uint256 bnb; uint256 tokens; uint256 volume; uint256 feeTotal; uint256 createdAt; uint256 balance; uint256 claimable; uint256 holderClaimable; } function markets(uint256 offset, uint256 limit, address account) external view returns (Market[] memory result) { require(limit > 0 && limit <= 20, "LIMIT"); uint256 count = pools.length; if (offset >= count) return new Market[](0); uint256 length = Math.min(limit, count - offset); result = new Market[](length); for (uint256 i; i < length; ++i) { SparkCurveV2 p = pools[count - 1 - offset - i]; SparkDividendToken t = p.token(); result[i] = Market(address(p), address(t), p.creator(), t.name(), t.symbol(), p.metadata(), p.graduated(), p.holderSharing(), p.creatorTaxBps(), p.reserveBNB(), p.reserveToken(), p.volumeBNB(), p.feeTotal(), p.createdAt(), t.balanceOf(account), p.fees(account), t.claimable(account)); } } }