// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {MoonRushBondingToken} from "./MoonRushBondingToken.sol"; import {IBondingRouter} from "./interfaces/IBondingRouter.sol"; /// @title MoonRushBondingCurve — Pump.fun-style bonding curve with Uniswap V2 graduation. /// @notice Graduates at target market cap (~$20k ETH proxy), burns LP, renounces ownership. contract MoonRushBondingCurve is ReentrancyGuard { uint256 public constant TOTAL_SUPPLY = 1_000_000_000 * 1e18; uint256 public constant TOKENS_FOR_CURVE = 800_000_000 * 1e18; uint256 public constant TOKENS_FOR_LP = 200_000_000 * 1e18; uint256 public constant INITIAL_VIRTUAL_ETH = 0.001 ether; address public constant DEAD = 0x000000000000000000000000000000000000dEaD; MoonRushBondingToken public immutable token; IBondingRouter public immutable router; address public immutable treasury; address public immutable creator; string public metadataUri; uint256 public virtualEthReserve; uint256 public virtualTokenReserve; uint256 public realEthReserve; uint256 public realTokenReserve; uint256 public immutable graduationMarketCapEth; uint256 public immutable treasuryGraduationFeeBps; uint256 public immutable treasuryTradeFeeBps; bool public graduated; address public pair; event Bought(address indexed buyer, uint256 ethIn, uint256 treasuryFee, uint256 tokensOut, uint256 marketCapEth); event Sold(address indexed seller, uint256 tokensIn, uint256 treasuryFee, uint256 ethOut, uint256 marketCapEth); event Graduated(address indexed pair, uint256 ethLiquidity, uint256 tokenLiquidity, uint256 treasuryFee); error AlreadyGraduated(); error NotGraduated(); error ZeroAmount(); error SlippageExceeded(); error GraduationFailed(); error EthTransferFailed(); constructor( string memory name_, string memory symbol_, string memory metadataUri_, address creator_, address router_, address treasury_, uint256 graduationMarketCapEth_, uint256 treasuryGraduationFeeBps_, uint256 treasuryTradeFeeBps_ ) { require(creator_ != address(0) && router_ != address(0) && treasury_ != address(0), "zero addr"); require(graduationMarketCapEth_ > 0, "mcap=0"); require(treasuryGraduationFeeBps_ <= 2000, "fee too high"); require(treasuryTradeFeeBps_ <= 1000, "trade fee too high"); creator = creator_; router = IBondingRouter(router_); treasury = treasury_; metadataUri = metadataUri_; graduationMarketCapEth = graduationMarketCapEth_; treasuryGraduationFeeBps = treasuryGraduationFeeBps_; treasuryTradeFeeBps = treasuryTradeFeeBps_; token = new MoonRushBondingToken(name_, symbol_, address(this), TOTAL_SUPPLY); virtualEthReserve = INITIAL_VIRTUAL_ETH; virtualTokenReserve = TOKENS_FOR_CURVE; realTokenReserve = TOKENS_FOR_CURVE; } /// @notice Buy tokens with native ETH while on the bonding curve. function buy(uint256 minTokensOut) external payable nonReentrant { if (graduated) revert AlreadyGraduated(); if (msg.value == 0) revert ZeroAmount(); uint256 tradeFee = (msg.value * treasuryTradeFeeBps) / 10_000; uint256 ethForCurve = msg.value - tradeFee; if (tradeFee > 0) _sendTreasury(tradeFee); uint256 tokensOut = _quoteBuy(ethForCurve); if (tokensOut < minTokensOut) revert SlippageExceeded(); _applyBuy(ethForCurve, tokensOut); IERC20(address(token)).transfer(msg.sender, tokensOut); emit Bought(msg.sender, msg.value, tradeFee, tokensOut, marketCapEth()); if (marketCapEth() >= graduationMarketCapEth) { _graduate(); } } /// @notice Sell tokens back to the curve before graduation. function sell(uint256 tokenAmount, uint256 minEthOut) external nonReentrant { if (graduated) revert AlreadyGraduated(); if (tokenAmount == 0) revert ZeroAmount(); uint256 ethOut = _quoteSell(tokenAmount); IERC20(address(token)).transferFrom(msg.sender, address(this), tokenAmount); _applySell(tokenAmount, ethOut); uint256 tradeFee = (ethOut * treasuryTradeFeeBps) / 10_000; uint256 ethToSeller = ethOut - tradeFee; if (ethToSeller < minEthOut) revert SlippageExceeded(); if (tradeFee > 0) _sendTreasury(tradeFee); (bool ok,) = msg.sender.call{value: ethToSeller}(""); if (!ok) revert EthTransferFailed(); emit Sold(msg.sender, tokenAmount, tradeFee, ethToSeller, marketCapEth()); } /// @notice Anyone can trigger graduation once market cap threshold is met. function graduate() external nonReentrant { if (graduated) revert AlreadyGraduated(); if (marketCapEth() < graduationMarketCapEth) revert GraduationFailed(); _graduate(); } function marketCapEth() public view returns (uint256) { if (graduated) return 0; if (virtualTokenReserve == 0) return 0; return (virtualEthReserve * TOTAL_SUPPLY) / virtualTokenReserve; } function bondingProgressBps() external view returns (uint256) { if (graduated) return 10_000; uint256 mcap = marketCapEth(); if (mcap >= graduationMarketCapEth) return 10_000; return (mcap * 10_000) / graduationMarketCapEth; } function _quoteBuy(uint256 ethIn) public view returns (uint256 tokensOut) { if (graduated || ethIn == 0) return 0; uint256 k = virtualEthReserve * virtualTokenReserve; uint256 newVirtualEth = virtualEthReserve + ethIn; uint256 newVirtualToken = k / newVirtualEth; tokensOut = virtualTokenReserve - newVirtualToken; if (tokensOut > realTokenReserve) tokensOut = realTokenReserve; } function _quoteSell(uint256 tokenIn) public view returns (uint256 ethOut) { if (graduated || tokenIn == 0) return 0; uint256 k = virtualEthReserve * virtualTokenReserve; uint256 newVirtualToken = virtualTokenReserve + tokenIn; uint256 newVirtualEth = k / newVirtualToken; ethOut = virtualEthReserve - newVirtualEth; if (ethOut > realEthReserve) ethOut = realEthReserve; } function _applyBuy(uint256 ethIn, uint256 tokensOut) internal { uint256 k = virtualEthReserve * virtualTokenReserve; virtualEthReserve += ethIn; virtualTokenReserve = k / virtualEthReserve; realEthReserve += ethIn; realTokenReserve -= tokensOut; } function _applySell(uint256 tokenIn, uint256 ethOut) internal { uint256 k = virtualEthReserve * virtualTokenReserve; virtualTokenReserve += tokenIn; virtualEthReserve = k / virtualTokenReserve; realTokenReserve += tokenIn; realEthReserve -= ethOut; } function _sendTreasury(uint256 amount) internal { if (amount == 0) return; (bool ok,) = treasury.call{value: amount}(""); if (!ok) revert EthTransferFailed(); } function _graduate() internal { graduated = true; uint256 treasuryFee = (realEthReserve * treasuryGraduationFeeBps) / 10_000; uint256 ethForLp = realEthReserve - treasuryFee; if (treasuryFee > 0) { _sendTreasury(treasuryFee); } IERC20(address(token)).approve(address(router), TOKENS_FOR_LP); (,, uint256 liquidity) = router.addLiquidityETH{value: ethForLp}( address(token), TOKENS_FOR_LP, 0, 0, DEAD, block.timestamp + 600 ); if (liquidity == 0) revert GraduationFailed(); pair = _computePair(); // Renounce token ownership — no rug after graduation MoonRushBondingToken(address(token)).renounceOwnership(); emit Graduated(pair, ethForLp, TOKENS_FOR_LP, treasuryFee); } function _computePair() internal view returns (address) { // Pair address is deterministic via factory; read from router factory if needed. // For events we pass zero if unknown — frontend reads from Graduated tx logs. return address(0); } receive() external payable {} }