// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "./MycoToken.sol"; /** * @title MycoBondingCurve * @notice Polynomial bonding curve for $MYCO token * @dev Price = basePrice + coefficient * supply^exponent * * Users buy/sell $MYCO against USDC through this curve. * - Buy: Send USDC, receive $MYCO (minted) * - Sell: Send $MYCO (burned), receive USDC */ contract MycoBondingCurve is ReentrancyGuard, Ownable { using SafeERC20 for IERC20; // =================== // State Variables // =================== /// @notice The $MYCO token MycoToken public immutable mycoToken; /// @notice USDC token (6 decimals on Base) IERC20 public immutable usdc; /// @notice Base price in USDC (6 decimals) - starting price when supply is 0 uint256 public basePrice; /// @notice Coefficient for price growth (6 decimals) uint256 public coefficient; /// @notice Exponent for curve steepness (1 = linear, 2 = quadratic) uint256 public exponent; /// @notice Protocol fee in basis points (100 = 1%) uint256 public feePercentage; /// @notice Treasury address for protocol fees address public treasury; /// @notice Total USDC held in reserve uint256 public reserveBalance; /// @notice Accumulated protocol fees uint256 public accumulatedFees; // =================== // Events // =================== event TokensPurchased( address indexed buyer, uint256 usdcAmount, uint256 tokensMinted, uint256 newSupply, uint256 newPrice ); event TokensSold( address indexed seller, uint256 tokensBurned, uint256 usdcReturned, uint256 newSupply, uint256 newPrice ); event FeesWithdrawn(address indexed to, uint256 amount); event CurveParametersUpdated(uint256 basePrice, uint256 coefficient, uint256 exponent); event FeePercentageUpdated(uint256 oldFee, uint256 newFee); event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury); // =================== // Errors // =================== error ZeroAmount(); error ZeroAddress(); error SlippageExceeded(); error InsufficientReserve(); error InvalidParameters(); error FeeTooHigh(); // =================== // Constructor // =================== /** * @notice Deploy the bonding curve * @param _mycoToken Address of the $MYCO token * @param _usdc Address of USDC token * @param _treasury Treasury address for fees * @param _basePrice Starting price (6 decimals) * @param _coefficient Price growth coefficient * @param _exponent Curve exponent (1 or 2) * @param _feePercentage Fee in basis points */ constructor( address _mycoToken, address _usdc, address _treasury, uint256 _basePrice, uint256 _coefficient, uint256 _exponent, uint256 _feePercentage ) Ownable(msg.sender) { if (_mycoToken == address(0) || _usdc == address(0) || _treasury == address(0)) { revert ZeroAddress(); } if (_exponent == 0 || _exponent > 3) revert InvalidParameters(); if (_feePercentage > 1000) revert FeeTooHigh(); // Max 10% mycoToken = MycoToken(_mycoToken); usdc = IERC20(_usdc); treasury = _treasury; basePrice = _basePrice; coefficient = _coefficient; exponent = _exponent; feePercentage = _feePercentage; } // =================== // View Functions // =================== /** * @notice Calculate the current token price * @return price Current price in USDC (6 decimals) */ function getCurrentPrice() public view returns (uint256) { return getPrice(mycoToken.totalSupply()); } /** * @notice Calculate price at a given supply * @param supply Token supply (18 decimals) * @return price Price in USDC (6 decimals) */ function getPrice(uint256 supply) public view returns (uint256) { // Convert supply from 18 decimals to a reasonable number for calculation // Using supply in millions of tokens for price calculation uint256 supplyInMillions = supply / 1e24; // 1e18 * 1e6 = 1 million tokens uint256 priceComponent = coefficient; for (uint256 i = 0; i < exponent; i++) { priceComponent = priceComponent * supplyInMillions / 1e6; } return basePrice + priceComponent; } /** * @notice Calculate tokens received for USDC input * @param usdcAmount Amount of USDC to spend * @return tokensOut Amount of tokens to receive */ function calculateBuyReturn(uint256 usdcAmount) public view returns (uint256 tokensOut) { if (usdcAmount == 0) return 0; uint256 currentSupply = mycoToken.totalSupply(); uint256 currentPrice = getPrice(currentSupply); // Simple approximation: tokens = usdcAmount / averagePrice // For more accuracy, integrate the curve uint256 estimatedTokens = (usdcAmount * 1e18) / currentPrice; // Calculate ending price and average uint256 endPrice = getPrice(currentSupply + estimatedTokens); uint256 avgPrice = (currentPrice + endPrice) / 2; tokensOut = (usdcAmount * 1e18) / avgPrice; } /** * @notice Calculate USDC received for token input * @param tokenAmount Amount of tokens to sell * @return usdcOut Amount of USDC to receive (after fees) */ function calculateSellReturn(uint256 tokenAmount) public view returns (uint256 usdcOut) { if (tokenAmount == 0) return 0; uint256 currentSupply = mycoToken.totalSupply(); if (tokenAmount > currentSupply) return 0; uint256 currentPrice = getPrice(currentSupply); uint256 endPrice = getPrice(currentSupply - tokenAmount); uint256 avgPrice = (currentPrice + endPrice) / 2; uint256 grossUsdc = (tokenAmount * avgPrice) / 1e18; uint256 fee = (grossUsdc * feePercentage) / 10000; usdcOut = grossUsdc - fee; } // =================== // Buy/Sell Functions // =================== /** * @notice Buy tokens with USDC * @param usdcAmount Amount of USDC to spend * @param minTokensOut Minimum tokens to receive (slippage protection) * @return tokensMinted Amount of tokens minted */ function buy(uint256 usdcAmount, uint256 minTokensOut) external nonReentrant returns (uint256 tokensMinted) { if (usdcAmount == 0) revert ZeroAmount(); tokensMinted = calculateBuyReturn(usdcAmount); if (tokensMinted < minTokensOut) revert SlippageExceeded(); // Transfer USDC from buyer usdc.safeTransferFrom(msg.sender, address(this), usdcAmount); reserveBalance += usdcAmount; // Mint tokens to buyer mycoToken.mint(msg.sender, tokensMinted); emit TokensPurchased( msg.sender, usdcAmount, tokensMinted, mycoToken.totalSupply(), getCurrentPrice() ); } /** * @notice Sell tokens for USDC * @param tokenAmount Amount of tokens to sell * @param minUsdcOut Minimum USDC to receive (slippage protection) * @return usdcReturned Amount of USDC returned */ function sell(uint256 tokenAmount, uint256 minUsdcOut) external nonReentrant returns (uint256 usdcReturned) { if (tokenAmount == 0) revert ZeroAmount(); uint256 currentSupply = mycoToken.totalSupply(); uint256 currentPrice = getPrice(currentSupply); uint256 endPrice = getPrice(currentSupply - tokenAmount); uint256 avgPrice = (currentPrice + endPrice) / 2; uint256 grossUsdc = (tokenAmount * avgPrice) / 1e18; uint256 fee = (grossUsdc * feePercentage) / 10000; usdcReturned = grossUsdc - fee; if (usdcReturned < minUsdcOut) revert SlippageExceeded(); if (usdcReturned > reserveBalance) revert InsufficientReserve(); // Burn tokens from seller mycoToken.burnFrom(msg.sender, tokenAmount); // Update reserves and fees reserveBalance -= grossUsdc; accumulatedFees += fee; // Transfer USDC to seller usdc.safeTransfer(msg.sender, usdcReturned); emit TokensSold( msg.sender, tokenAmount, usdcReturned, mycoToken.totalSupply(), getCurrentPrice() ); } // =================== // Admin Functions // =================== /** * @notice Withdraw accumulated fees to treasury */ function withdrawFees() external { uint256 fees = accumulatedFees; if (fees == 0) revert ZeroAmount(); accumulatedFees = 0; usdc.safeTransfer(treasury, fees); emit FeesWithdrawn(treasury, fees); } /** * @notice Update curve parameters (only owner) * @param _basePrice New base price * @param _coefficient New coefficient * @param _exponent New exponent */ function updateCurveParameters( uint256 _basePrice, uint256 _coefficient, uint256 _exponent ) external onlyOwner { if (_exponent == 0 || _exponent > 3) revert InvalidParameters(); basePrice = _basePrice; coefficient = _coefficient; exponent = _exponent; emit CurveParametersUpdated(_basePrice, _coefficient, _exponent); } /** * @notice Update fee percentage (only owner) * @param _feePercentage New fee in basis points */ function updateFeePercentage(uint256 _feePercentage) external onlyOwner { if (_feePercentage > 1000) revert FeeTooHigh(); uint256 oldFee = feePercentage; feePercentage = _feePercentage; emit FeePercentageUpdated(oldFee, _feePercentage); } /** * @notice Update treasury address (only owner) * @param _treasury New treasury address */ function updateTreasury(address _treasury) external onlyOwner { if (_treasury == address(0)) revert ZeroAddress(); address oldTreasury = treasury; treasury = _treasury; emit TreasuryUpdated(oldTreasury, _treasury); } }