// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.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"; interface IAgentRegistry { function handleOf(address account) external view returns (string memory); function ownerOfHandle(string calldata handle) external view returns (address); } interface IV2Factory { function getPair(address a, address b) external view returns (address); function createPair(address a, address b) external returns (address); } interface IV2Pair is IERC20 { function token0() external view returns (address); function token1() external view returns (address); function getReserves() external view returns (uint112, uint112, uint32); function price0CumulativeLast() external view returns (uint256); function price1CumulativeLast() external view returns (uint256); function mint(address to) external returns (uint256); function sync() external; } /// @notice Fixed supply. Fees apply to transfers to/from the official pair only. /// No owner, upgrades, arbitrary fee exemptions, additional minting or confiscation. contract MIAToken is ERC20 { address public immutable launch; address public immutable marketingWallet; address public officialPair; bool public tradingOpen; uint256 public constant BUY_TAX_BPS = 100; uint256 public constant SELL_TAX_BPS = 200; event TradingOpened(address indexed pair); event MarketingTax(address indexed payer, uint256 amount, bool sell); error OnlyLaunch(); error InvalidState(); constructor(address marketing, uint256 supply) ERC20("MIA", "MIA") { require(marketing != address(0) && supply > 0, "config"); launch = msg.sender; marketingWallet = marketing; _mint(msg.sender, supply); } function openTrading(address pair) external { if (msg.sender != launch) revert OnlyLaunch(); if (tradingOpen || pair == address(0)) revert InvalidState(); officialPair = pair; tradingOpen = true; emit TradingOpened(pair); } function burnUnused(uint256 amount) external { if (msg.sender != launch) revert OnlyLaunch(); _burn(launch, amount); } function _update(address from, address to, uint256 amount) internal override { if (from == address(0) || from == launch) { super._update(from, to, amount); return; } if (!tradingOpen) revert InvalidState(); uint256 bps = to == officialPair ? SELL_TAX_BPS : (from == officialPair ? BUY_TAX_BPS : 0); uint256 fee = Math.mulDiv(amount, bps, 10_000); if (fee > 0) { super._update(from, marketingWallet, fee); emit MarketingTax(from, fee, to == officialPair); } super._update(from, to, amount - fee); } } /// @notice MIA subscription sale. No BNB deposits; approve USDT then call mint. /// @dev Uses a real V2 pair directly; protocol liquidity is sent to a dead address. contract MIALaunch is ReentrancyGuard { using SafeERC20 for IERC20; uint256 public constant U = 1e18; uint256 public constant SHARE_CAP = 27_500; uint256 public constant BOOTSTRAP_SHARES = 5_000; uint256 public constant RAISE_CAP = 50_000e18; uint256 public constant OPEN_THRESHOLD = 5_000e18; uint256 public constant IDENTITY_QUOTA = 10; uint256 public constant TX_SHARE_LIMIT = 10; uint256 public constant START_DELAY = 1 hours; uint256 public constant SALE_DURATION = 7 days; uint256 public constant DISCOUNT_BPS = 300; uint256 public constant LIQUIDITY_BPS = 8_000; uint256 public constant ORACLE_WINDOW = 600; uint256 public constant ORACLE_MAX_AGE = 3_600; uint256 public constant MAX_PRICE_DEVIATION_BPS = 100; uint256 public constant MAX_MINT_POOL_BPS = 100; uint256 public constant MIN_POOL_USDT = 1_000e18; address public constant LP_LOCK = 0x000000000000000000000000000000000000dEaD; uint256 private constant Q112 = 2 ** 112; MIAToken public immutable token; IERC20 public immutable usdt; IAgentRegistry public immutable agentRegistry; IV2Pair public immutable pair; address public immutable marketingWallet; address public immutable pauseGuardian; uint256 public immutable initialTokensPerShare; uint256 public immutable startsAt; uint256 public immutable endsAt; bool public immutable tokenIs0; uint256 public totalSharesMinted; uint256 public totalRaised; uint256 public saleInventory; uint256 public lpInventory; uint256 public earlyLiability; uint256 public marketingDue; uint256 public marketingPaid; uint256 public liquidityAddedUSDT; uint256 public liquidityAddedMIA; bool public poolOpened; bool public mintPaused; bool public finalized; mapping(bytes32 => uint256) public mintedByIdentity; mapping(address => uint256) public claimable; mapping(address => uint256) public refundable; uint256 public observationCumulative; uint32 public observationTime; uint256 public twapPrice; uint256 public twapUpdatedAt; event Subscribed(address indexed buyer, bytes32 indexed identity, uint256 shares, uint256 cost, uint256 tokens); event PoolOpened(address indexed pair, uint256 openingPrice, uint256 externalUSDT); event LiquidityAdded(uint256 usdtAmount, uint256 tokenAmount, uint256 lpAmount); event Claimed(address indexed buyer, uint256 tokens); event Refunded(address indexed buyer, uint256 usdtAmount); event MarketingPaid(address indexed wallet, uint256 usdtAmount); event ObservationUpdated(uint256 price, uint256 timestamp); event MintPauseChanged(bool paused); event Finalized(uint256 unusedBurned); error InvalidConfig(); error SaleUnavailable(); error InvalidShares(); error AgentRequired(); error AgentRegistryUnavailable(); error QuotaExceeded(); error MaxCostExceeded(); error TokensBelowMinimum(); error InventoryInsufficient(); error PriceNotReady(); error PriceDeviation(); error PoolTooSmall(); error MintTooLarge(); error InvalidPool(); error NoClaim(); error OnlyGuardian(); /// @param supply Fixed MIA supply in 18-decimal atomic units. /// @param saleAllocation MIA allocated to subscribers; remainder backs liquidity. /// @param bootstrapTokens MIA per early share, in 18-decimal atomic units. constructor( address usdtAddress, address registryAddress, address factoryAddress, address marketing, address guardian, uint256 supply, uint256 saleAllocation, uint256 bootstrapTokens, uint256 delaySeconds, uint256 durationSeconds ) { if (usdtAddress.code.length == 0 || registryAddress.code.length == 0 || factoryAddress.code.length == 0 || marketing == address(0) || marketing == LP_LOCK || guardian == address(0) || bootstrapTokens == 0 || bootstrapTokens > 1e30 || supply > 1e33 || saleAllocation >= supply || saleAllocation < 5_000 * bootstrapTokens || supply - saleAllocation < 4_000 * bootstrapTokens || delaySeconds != START_DELAY || durationSeconds != SALE_DURATION || IERC20Metadata(usdtAddress).decimals() != 18) revert InvalidConfig(); usdt = IERC20(usdtAddress); agentRegistry = IAgentRegistry(registryAddress); marketingWallet = marketing; pauseGuardian = guardian; initialTokensPerShare = bootstrapTokens; startsAt = block.timestamp + START_DELAY; endsAt = startsAt + SALE_DURATION; saleInventory = saleAllocation; lpInventory = supply - saleAllocation; MIAToken created = new MIAToken(marketing, supply); token = created; address p = IV2Factory(factoryAddress).getPair(address(created), usdtAddress); if (p == address(0)) p = IV2Factory(factoryAddress).createPair(address(created), usdtAddress); pair = IV2Pair(p); tokenIs0 = IV2Pair(p).token0() == address(created); if ((tokenIs0 ? IV2Pair(p).token1() : IV2Pair(p).token0()) != usdtAddress || IV2Pair(p).totalSupply() != 0) revert InvalidPool(); } function cumulativeCost(uint256 n) public pure returns (uint256) { if (n > SHARE_CAP) revert InvalidShares(); if (n <= BOOTSTRAP_SHARES) return n * U; uint256 x = n - BOOTSTRAP_SHARES; uint256 k = x / 2_500; uint256 r = x % 2_500; // k*(k+11) avoids unsigned k-1 underflow for the first post-open tier. uint256 cents = 500_000 + 2_500 * 10 * k * (k + 11) + r * (120 + 20 * k); return cents * 1e16; } function costFor(uint256 sold, uint256 shares) public pure returns (uint256) { if (shares == 0 || shares > TX_SHARE_LIMIT || sold > SHARE_CAP || shares > SHARE_CAP - sold) revert InvalidShares(); return cumulativeCost(sold + shares) - cumulativeCost(sold); } function nextSharePrice() external view returns (uint256) { return totalSharesMinted == SHARE_CAP ? 0 : costFor(totalSharesMinted, 1); } function identityOf(address buyer) public view returns (bytes32) { string memory handle; try agentRegistry.handleOf{gas: 100_000}(buyer) returns (string memory h) { handle = h; } catch { revert AgentRegistryUnavailable(); } if (bytes(handle).length == 0 || bytes(handle).length > 128) revert AgentRequired(); try agentRegistry.ownerOfHandle{gas: 100_000}(handle) returns (address owner) { if (owner != buyer) revert AgentRequired(); } catch { revert AgentRegistryUnavailable(); } return keccak256(bytes(handle)); } function _checkSale() private view { if (block.timestamp < startsAt || block.timestamp >= endsAt || mintPaused || finalized || totalSharesMinted == SHARE_CAP) revert SaleUnavailable(); } function _reserves() private view returns (uint256 mia, uint256 stable) { (uint112 r0, uint112 r1,) = pair.getReserves(); return tokenIs0 ? (r0, r1) : (r1, r0); } function _spot() private view returns (uint256 price, uint256 stable) { uint256 mia; (mia, stable) = _reserves(); if (mia == 0 || stable == 0) revert InvalidPool(); price = Math.mulDiv(stable, U, mia); if (price == 0) revert InvalidPool(); } function guardedPrice(uint256 mintCost) public view returns (uint256 price) { uint256 stable; (price, stable) = _spot(); if (twapUpdatedAt == 0 || block.timestamp - twapUpdatedAt > ORACLE_MAX_AGE) revert PriceNotReady(); uint256 delta = price > twapPrice ? price - twapPrice : twapPrice - price; if (delta > Math.mulDiv(twapPrice, MAX_PRICE_DEVIATION_BPS, 10_000)) revert PriceDeviation(); if (stable < MIN_POOL_USDT) revert PoolTooSmall(); if (mintCost > Math.mulDiv(stable, MAX_MINT_POOL_BPS, 10_000)) revert MintTooLarge(); } function discountedTokens(uint256 cost, uint256 price) public pure returns (uint256) { if (price == 0) revert InvalidPool(); return Math.mulDiv(cost, U * 10_000, price * (10_000 - DISCOUNT_BPS)); } function quoteMint(address buyer, uint256 shares) external view returns (uint256 cost, uint256 tokensOut, uint256 remainingQuota) { _checkSale(); bytes32 key = identityOf(buyer); remainingQuota = IDENTITY_QUOTA - mintedByIdentity[key]; if (shares > remainingQuota) revert QuotaExceeded(); cost = costFor(totalSharesMinted, shares); if (poolOpened) { tokensOut = discountedTokens(cost, guardedPrice(cost)); } else { uint256 early = Math.min(shares, BOOTSTRAP_SHARES - totalSharesMinted); tokensOut = early * initialTokensPerShare; uint256 laterCost = cost - early * U; if (laterCost > 0) { // Any pre-opening USDT donation raises, rather than lowers, the initial price. uint256 openingPrice = Math.mulDiv(4_000 * U + usdt.balanceOf(address(pair)), U, 4_000 * initialTokensPerShare); tokensOut += discountedTokens(laterCost, openingPrice); } } if (tokensOut > saleInventory) revert InventoryInsufficient(); } function mint(uint256 shares, uint256 maxCost, uint256 minTokensOut, uint256 deadline) external nonReentrant { _checkSale(); if (block.timestamp > deadline) revert SaleUnavailable(); bytes32 key = identityOf(msg.sender); if (mintedByIdentity[key] + shares > IDENTITY_QUOTA) revert QuotaExceeded(); uint256 oldShares = totalSharesMinted; uint256 cost = costFor(oldShares, shares); if (cost > maxCost) revert MaxCostExceeded(); // Sync donations before evaluating the guarded price; they cannot be ignored in an execution quote. uint256 price; if (poolOpened) { pair.sync(); price = guardedPrice(cost); } uint256 beforeBalance = usdt.balanceOf(address(this)); usdt.safeTransferFrom(msg.sender, address(this), cost); if (usdt.balanceOf(address(this)) - beforeBalance != cost) revert InvalidConfig(); mintedByIdentity[key] += shares; totalSharesMinted = oldShares + shares; totalRaised += cost; assert(totalRaised == cumulativeCost(totalSharesMinted) && totalRaised <= RAISE_CAP); uint256 totalTokens; uint256 laterCost = cost; if (!poolOpened) { uint256 early = Math.min(shares, BOOTSTRAP_SHARES - oldShares); totalTokens = early * initialTokensPerShare; _useSaleInventory(totalTokens); claimable[msg.sender] += totalTokens; earlyLiability += totalTokens; refundable[msg.sender] += early * U; laterCost -= early * U; if (totalSharesMinted >= BOOTSTRAP_SHARES) { _openPool(); (price,) = _spot(); } } if (laterCost > 0) { uint256 output = discountedTokens(laterCost, price); _useSaleInventory(output); uint256 lpUSDT = Math.mulDiv(laterCost, LIQUIDITY_BPS, 10_000); _addLiquidity(lpUSDT); marketingDue += laterCost - lpUSDT; IERC20(address(token)).safeTransfer(msg.sender, output); totalTokens += output; } if (totalTokens < minTokensOut) revert TokensBelowMinimum(); emit Subscribed(msg.sender, key, shares, cost, totalTokens); } function _useSaleInventory(uint256 amount) private { if (amount == 0 || amount > saleInventory) revert InventoryInsufficient(); saleInventory -= amount; } function _openPool() private { if (poolOpened || pair.totalSupply() != 0 || token.balanceOf(address(pair)) != 0) revert InvalidPool(); uint256 donation = usdt.balanceOf(address(pair)); uint256 seed = 4_000 * initialTokensPerShare; if (seed > lpInventory) revert InventoryInsufficient(); lpInventory -= seed; IERC20(address(token)).safeTransfer(address(pair), seed); usdt.safeTransfer(address(pair), 4_000 * U); uint256 lp = pair.mint(LP_LOCK); if (lp == 0) revert InvalidPool(); liquidityAddedUSDT += 4_000 * U; liquidityAddedMIA += seed; marketingDue += 1_000 * U; poolOpened = true; token.openTrading(address(pair)); (observationCumulative, observationTime) = _currentCumulative(); (uint256 p,) = _spot(); emit LiquidityAdded(4_000 * U, seed, lp); emit PoolOpened(address(pair), p, donation); } function _addLiquidity(uint256 amount) private { (uint256 mia, uint256 stable) = _reserves(); if (mia == 0 || stable == 0) revert InvalidPool(); uint256 tokensNeeded = Math.mulDiv(amount, mia, stable, Math.Rounding.Ceil); if (tokensNeeded == 0 || tokensNeeded > lpInventory) revert InventoryInsufficient(); uint256 minimumLP = Math.min(Math.mulDiv(amount, pair.totalSupply(), stable), Math.mulDiv(tokensNeeded, pair.totalSupply(), mia)); if (minimumLP == 0) revert InvalidPool(); lpInventory -= tokensNeeded; IERC20(address(token)).safeTransfer(address(pair), tokensNeeded); usdt.safeTransfer(address(pair), amount); uint256 lp = pair.mint(LP_LOCK); if (lp < minimumLP) revert InvalidPool(); liquidityAddedUSDT += amount; liquidityAddedMIA += tokensNeeded; emit LiquidityAdded(amount, tokensNeeded, lp); } function _currentCumulative() private view returns (uint256 cumulative, uint32 now32) { cumulative = tokenIs0 ? pair.price0CumulativeLast() : pair.price1CumulativeLast(); (uint112 r0, uint112 r1, uint32 last) = pair.getReserves(); now32 = uint32(block.timestamp); if (r0 != 0 && r1 != 0) { uint256 ratio = tokenIs0 ? (uint256(r1) << 112) / r0 : (uint256(r0) << 112) / r1; unchecked { cumulative += ratio * uint32(now32 - last); } } } function updateObservation() external { if (!poolOpened) revert PriceNotReady(); (uint256 cumulative, uint32 timestamp) = _currentCumulative(); uint32 elapsed; unchecked { elapsed = timestamp - observationTime; } if (elapsed < ORACLE_WINDOW) revert PriceNotReady(); if (elapsed > ORACLE_MAX_AGE) { twapUpdatedAt = 0; } else { uint256 diff; unchecked { diff = cumulative - observationCumulative; } twapPrice = Math.mulDiv(diff / elapsed, U, Q112); if (twapPrice == 0) revert InvalidPool(); twapUpdatedAt = block.timestamp; emit ObservationUpdated(twapPrice, block.timestamp); } observationCumulative = cumulative; observationTime = timestamp; } function claim() external nonReentrant { uint256 amount = claimable[msg.sender]; if (!poolOpened || amount == 0) revert NoClaim(); claimable[msg.sender] = 0; earlyLiability -= amount; IERC20(address(token)).safeTransfer(msg.sender, amount); emit Claimed(msg.sender, amount); } function refund() external nonReentrant { if (poolOpened || block.timestamp < endsAt) revert SaleUnavailable(); uint256 amount = refundable[msg.sender]; if (amount == 0) revert NoClaim(); refundable[msg.sender] = 0; uint256 cancelled = claimable[msg.sender]; claimable[msg.sender] = 0; earlyLiability -= cancelled; token.burnUnused(cancelled); usdt.safeTransfer(msg.sender, amount); emit Refunded(msg.sender, amount); } function claimMarketing() external nonReentrant { uint256 amount = marketingDue; if (!poolOpened || amount == 0) revert NoClaim(); marketingDue = 0; marketingPaid += amount; usdt.safeTransfer(marketingWallet, amount); emit MarketingPaid(marketingWallet, amount); } function setMintPaused(bool paused) external { if (msg.sender != pauseGuardian) revert OnlyGuardian(); mintPaused = paused; emit MintPauseChanged(paused); } function finalize() external nonReentrant { if (finalized || (totalSharesMinted != SHARE_CAP && block.timestamp < endsAt)) revert SaleUnavailable(); finalized = true; uint256 unused = saleInventory + lpInventory; saleInventory = 0; lpInventory = 0; token.burnUnused(unused); emit Finalized(unused); } }