335 lines
10 KiB
Solidity
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);
|
|
}
|
|
}
|