// 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 {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {DegenZFunTaxToken} from "./DegenZFunTaxToken.sol"; import {IBondingRouter} from "./interfaces/IBondingRouter.sol"; import {IUniswapV2Factory} from "../interfaces/IUniswapV2Factory.sol"; interface IDegenZV2TaxLaunchpad { function finishGraduation(address token, address pair) external; } /// @title DegenZV2TaxBondingCurve — Fun bonding curve that graduates to Uniswap V2 (tax tokens). /// @notice Curve + creator are tax-exempt. Protocol trade fee is 0 (fee taken by DegenZFeeRouter off-chain path). contract DegenZV2TaxBondingCurve 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 = 1.5 ether; uint256 public constant INITIAL_VIRTUAL_TOKEN = 1_073_000_000 * 1e18; address public constant DEAD = 0x000000000000000000000000000000000000dEaD; DegenZFunTaxToken public immutable token; IBondingRouter public immutable router; address public immutable weth; address public immutable factory; address public immutable treasury; address public immutable creator; string public metadataUri; uint8 public immutable tokenType; // 1 = tax, 2 = reward uint256 public virtualEthReserve; uint256 public virtualTokenReserve; uint256 public realEthReserve; uint256 public realTokenReserve; uint256 public immutable graduationMarketCapEth; uint256 public immutable treasuryGraduationFeeBps; 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 ZeroAmount(); error SlippageExceeded(); error GraduationFailed(); error EthTransferFailed(); error OnlyFactory(); modifier onlyFactory() { if (msg.sender != factory) revert OnlyFactory(); _; } constructor( string memory name_, string memory symbol_, string memory metadataUri_, address creator_, address factory_, address router_, address weth_, address treasury_, address marketingWallet_, uint256 buyTaxBps_, uint256 sellTaxBps_, uint256 graduationMarketCapEth_, uint256 treasuryGraduationFeeBps_, uint8 tokenType_ ) { require( creator_ != address(0) && factory_ != address(0) && router_ != address(0) && weth_ != address(0) && treasury_ != address(0) && marketingWallet_ != address(0), "zero addr" ); require(graduationMarketCapEth_ > 0, "mcap=0"); require(treasuryGraduationFeeBps_ <= 2000, "fee too high"); require(tokenType_ == 1 || tokenType_ == 2, "type"); creator = creator_; factory = factory_; router = IBondingRouter(router_); weth = weth_; treasury = treasury_; metadataUri = metadataUri_; graduationMarketCapEth = graduationMarketCapEth_; treasuryGraduationFeeBps = treasuryGraduationFeeBps_; tokenType = tokenType_; token = new DegenZFunTaxToken( name_, symbol_, address(this), factory_, marketingWallet_, buyTaxBps_, sellTaxBps_, TOTAL_SUPPLY ); virtualEthReserve = INITIAL_VIRTUAL_ETH; virtualTokenReserve = INITIAL_VIRTUAL_TOKEN; realTokenReserve = TOKENS_FOR_CURVE; } function buy(uint256 minTokensOut) external payable nonReentrant { if (graduated) revert AlreadyGraduated(); if (msg.value == 0) revert ZeroAmount(); // Protocol trade fee = 0 on-curve (collected by DegenZFeeRouter for site/bot). uint256 ethForCurve = msg.value; uint256 tokensOut = _quoteBuy(ethForCurve); if (tokensOut < minTokensOut) revert SlippageExceeded(); _applyBuy(ethForCurve, tokensOut); uint256 balBefore = IERC20(address(token)).balanceOf(msg.sender); IERC20(address(token)).transfer(msg.sender, tokensOut); uint256 received = IERC20(address(token)).balanceOf(msg.sender) - balBefore; // Curve is exempt so received == tokensOut; assert soft. if (received < minTokensOut) revert SlippageExceeded(); emit Bought(msg.sender, msg.value, 0, received, marketCapEth()); if (marketCapEth() >= graduationMarketCapEth) { _graduate(); } } /// @notice FoT-aware sell: credits tokens actually received (curve is exempt so full amount). function sell(uint256 tokenAmount, uint256 minEthOut) external nonReentrant { if (graduated) revert AlreadyGraduated(); if (tokenAmount == 0) revert ZeroAmount(); uint256 balBefore = IERC20(address(token)).balanceOf(address(this)); IERC20(address(token)).transferFrom(msg.sender, address(this), tokenAmount); uint256 received = IERC20(address(token)).balanceOf(address(this)) - balBefore; if (received == 0) revert ZeroAmount(); uint256 ethOut = _quoteSell(received); _applySell(received, ethOut); if (ethOut < minEthOut) revert SlippageExceeded(); (bool ok,) = msg.sender.call{value: ethOut}(""); if (!ok) revert EthTransferFailed(); emit Sold(msg.sender, received, 0, ethOut, marketCapEth()); } 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) external view returns (uint256) { return _quoteBuy(ethIn); } function quoteSell(uint256 tokenIn) external view returns (uint256) { return _quoteSell(tokenIn); } function _quoteBuy(uint256 ethIn) internal 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) internal 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); uint256 lpTokens = TOKENS_FOR_LP; if (lpTokens == 0 || ethForLp == 0) revert GraduationFailed(); IERC20(address(token)).approve(address(router), lpTokens); (,, uint256 liquidity) = router.addLiquidityETH{value: ethForLp}( address(token), lpTokens, 0, 0, DEAD, block.timestamp + 600 ); if (liquidity == 0) revert GraduationFailed(); address v2Factory = router.factory(); pair = IUniswapV2Factory(v2Factory).getPair(address(token), weth); require(pair != address(0), "no pair"); IDegenZV2TaxLaunchpad(factory).finishGraduation(address(token), pair); realEthReserve = 0; emit Graduated(pair, ethForLp, lpTokens, treasuryFee); } receive() external payable {} }