myco-bonding-curve/reference/MycoBondingCurve.sol

335 lines
10 KiB
Solidity

// 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);
}
}