// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @title BotCurve /// @notice ERC-20 token with a deterministic, reserve-backed linear bonding curve. /// @dev This contract has no owner, proxy, pause, blacklist, or discretionary mint path. contract BotCurve { string public constant name = "BotCurve"; string public constant symbol = "BOTC"; uint8 public constant decimals = 18; uint256 public constant WAD = 1e18; uint256 public constant MAX_SUPPLY = 1_000_000_000 * WAD; uint256 public constant MIN_TRADE = 1 * WAD; uint256 public constant BASE_PRICE_WEI = 1_000_000_000_000; uint256 public constant SLOPE_WEI = 1_000_000; uint256 public constant FEE_BPS = 20; uint256 public constant BPS_DENOMINATOR = 10_000; uint256 public totalSupply; uint256 public reserveBalance; uint256 public accruedFees; address public immutable feeRecipient; mapping(address account => uint256 balance) public balanceOf; mapping(address owner => mapping(address spender => uint256 amount)) public allowance; uint256 private _entered; event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); event Bought( address indexed payer, address indexed recipient, uint256 tokenAmount, uint256 curveCost, uint256 protocolFee, uint256 supplyAfter, uint256 reserveAfter ); event Sold( address indexed seller, address indexed recipient, uint256 tokenAmount, uint256 grossRefund, uint256 protocolFee, uint256 supplyAfter, uint256 reserveAfter ); event FeesClaimed(address indexed recipient, uint256 amount); error ZeroAddress(); error Reentrancy(); error DeadlineExpired(uint256 deadline, uint256 currentTimestamp); error TradeBelowMinimum(uint256 amount, uint256 minimum); error SupplyCapExceeded(uint256 requestedSupply, uint256 maximumSupply); error InsufficientBalance(address account, uint256 balance, uint256 required); error InsufficientAllowance(address spender, uint256 allowanceAmount, uint256 required); error CostAboveMaximum(uint256 totalCost, uint256 maximumCost); error PayoutBelowMinimum(uint256 payout, uint256 minimumPayout); error InsufficientPayment(uint256 supplied, uint256 required); error InsufficientReserve(uint256 reserve, uint256 required); error Unauthorized(address caller); error NothingToClaim(); error NativeTransferFailed(address recipient, uint256 amount); error DirectNativeTransferDisabled(); modifier nonReentrant() { if (_entered != 0) revert Reentrancy(); _entered = 1; _; _entered = 0; } constructor(address treasury) { if (treasury == address(0)) revert ZeroAddress(); feeRecipient = treasury; } /// @notice Transfers BOTC without changing curve supply or reserve accounting. function transfer(address to, uint256 value) external returns (bool) { _transfer(msg.sender, to, value); return true; } /// @notice Sets an ERC-20 allowance. function approve(address spender, uint256 value) external returns (bool) { if (spender == address(0)) revert ZeroAddress(); allowance[msg.sender][spender] = value; emit Approval(msg.sender, spender, value); return true; } /// @notice Transfers BOTC using an ERC-20 allowance. function transferFrom(address from, address to, uint256 value) external returns (bool) { _spendAllowance(from, msg.sender, value); _transfer(from, to, value); return true; } /// @notice Current marginal curve price in wei for one whole BOTC. function spotPrice() public view returns (uint256) { return BASE_PRICE_WEI + (SLOPE_WEI * totalSupply) / WAD; } /// @notice Exact protected cost to mint `amount` BOTC at current supply. /// @dev Buy-side curve cost and fee round upward to protect reserve solvency. function quoteBuy(uint256 amount) public view returns (uint256 curveCost, uint256 protocolFee, uint256 totalCost) { _validateTradeAmount(amount); uint256 requestedSupply = totalSupply + amount; if (requestedSupply > MAX_SUPPLY) { revert SupplyCapExceeded(requestedSupply, MAX_SUPPLY); } curveCost = _curveCostForMint(totalSupply, amount); protocolFee = _ceilDiv(curveCost * FEE_BPS, BPS_DENOMINATOR); totalCost = curveCost + protocolFee; } /// @notice Exact protected payout to burn `amount` BOTC at current supply. /// @dev Sell-side curve refund and fee round downward. function quoteSell(uint256 amount) public view returns (uint256 grossRefund, uint256 protocolFee, uint256 netPayout) { _validateTradeAmount(amount); if (amount > totalSupply) { revert InsufficientBalance(address(this), totalSupply, amount); } grossRefund = _curveRefundForBurn(totalSupply, amount); protocolFee = (grossRefund * FEE_BPS) / BPS_DENOMINATOR; netPayout = grossRefund - protocolFee; } /// @notice Buys an exact BOTC amount with ETH. /// @param amount BOTC amount in 18-decimal units. /// @param recipient Address receiving BOTC. /// @param maxTotalCost Reverts if the current total cost is higher. /// @param deadline Reverts after this timestamp. function buy( uint256 amount, address recipient, uint256 maxTotalCost, uint256 deadline ) external payable nonReentrant returns (uint256 totalCost) { _validateDeadline(deadline); if (recipient == address(0)) revert ZeroAddress(); (uint256 curveCost, uint256 protocolFee, uint256 quotedTotal) = quoteBuy(amount); if (quotedTotal > maxTotalCost) { revert CostAboveMaximum(quotedTotal, maxTotalCost); } if (msg.value < quotedTotal) { revert InsufficientPayment(msg.value, quotedTotal); } _mint(recipient, amount); reserveBalance += curveCost; accruedFees += protocolFee; emit Bought( msg.sender, recipient, amount, curveCost, protocolFee, totalSupply, reserveBalance ); uint256 refund = msg.value - quotedTotal; if (refund != 0) _sendNative(payable(msg.sender), refund); return quotedTotal; } /// @notice Sells an exact BOTC amount back to the curve for ETH. /// @param amount BOTC amount in 18-decimal units. /// @param recipient Address receiving ETH. /// @param minPayout Reverts if the current net payout is lower. /// @param deadline Reverts after this timestamp. function sell( uint256 amount, address payable recipient, uint256 minPayout, uint256 deadline ) external nonReentrant returns (uint256 netPayout) { _validateDeadline(deadline); if (recipient == address(0)) revert ZeroAddress(); uint256 sellerBalance = balanceOf[msg.sender]; if (sellerBalance < amount) { revert InsufficientBalance(msg.sender, sellerBalance, amount); } (uint256 grossRefund, uint256 protocolFee, uint256 quotedPayout) = quoteSell(amount); if (quotedPayout < minPayout) { revert PayoutBelowMinimum(quotedPayout, minPayout); } if (reserveBalance < grossRefund) { revert InsufficientReserve(reserveBalance, grossRefund); } _burn(msg.sender, amount); reserveBalance -= grossRefund; accruedFees += protocolFee; emit Sold( msg.sender, recipient, amount, grossRefund, protocolFee, totalSupply, reserveBalance ); _sendNative(recipient, quotedPayout); return quotedPayout; } /// @notice Claims protocol fees without touching the recorded curve reserve. function claimFees() external nonReentrant returns (uint256 amount) { if (msg.sender != feeRecipient) revert Unauthorized(msg.sender); amount = accruedFees; if (amount == 0) revert NothingToClaim(); accruedFees = 0; emit FeesClaimed(feeRecipient, amount); _sendNative(payable(feeRecipient), amount); } /// @notice Returns machine-readable accounting values in one call. function marketState() external view returns ( uint256 supply, uint256 spotPriceWei, uint256 reserve, uint256 fees, uint256 excessNativeBalance ) { supply = totalSupply; spotPriceWei = spotPrice(); reserve = reserveBalance; fees = accruedFees; uint256 accounted = reserve + fees; excessNativeBalance = address(this).balance > accounted ? address(this).balance - accounted : 0; } receive() external payable { revert DirectNativeTransferDisabled(); } fallback() external payable { revert DirectNativeTransferDisabled(); } function _curveCostForMint(uint256 supply, uint256 amount) internal pure returns (uint256) { uint256 linear = _ceilDiv(BASE_PRICE_WEI * amount, WAD); uint256 areaNumerator = amount * ((2 * supply) + amount); uint256 tokenSquared = _ceilDiv(areaNumerator, 2 * WAD); uint256 quadratic = _ceilDiv(SLOPE_WEI * tokenSquared, WAD); return linear + quadratic; } function _curveRefundForBurn(uint256 supply, uint256 amount) internal pure returns (uint256) { uint256 linear = (BASE_PRICE_WEI * amount) / WAD; uint256 areaNumerator = amount * ((2 * supply) - amount); uint256 tokenSquared = areaNumerator / (2 * WAD); uint256 quadratic = (SLOPE_WEI * tokenSquared) / WAD; return linear + quadratic; } function _validateTradeAmount(uint256 amount) internal pure { if (amount < MIN_TRADE) revert TradeBelowMinimum(amount, MIN_TRADE); } function _validateDeadline(uint256 deadline) internal view { if (block.timestamp > deadline) { revert DeadlineExpired(deadline, block.timestamp); } } function _ceilDiv(uint256 numerator, uint256 denominator) internal pure returns (uint256) { if (numerator == 0) return 0; return ((numerator - 1) / denominator) + 1; } function _transfer(address from, address to, uint256 value) internal { if (from == address(0) || to == address(0)) revert ZeroAddress(); uint256 fromBalance = balanceOf[from]; if (fromBalance < value) { revert InsufficientBalance(from, fromBalance, value); } unchecked { balanceOf[from] = fromBalance - value; balanceOf[to] += value; } emit Transfer(from, to, value); } function _spendAllowance(address owner, address spender, uint256 value) internal { uint256 currentAllowance = allowance[owner][spender]; if (currentAllowance != type(uint256).max) { if (currentAllowance < value) { revert InsufficientAllowance(spender, currentAllowance, value); } unchecked { allowance[owner][spender] = currentAllowance - value; } emit Approval(owner, spender, allowance[owner][spender]); } } function _mint(address to, uint256 value) internal { if (to == address(0)) revert ZeroAddress(); totalSupply += value; unchecked { balanceOf[to] += value; } emit Transfer(address(0), to, value); } function _burn(address from, uint256 value) internal { if (from == address(0)) revert ZeroAddress(); uint256 fromBalance = balanceOf[from]; if (fromBalance < value) { revert InsufficientBalance(from, fromBalance, value); } unchecked { balanceOf[from] = fromBalance - value; totalSupply -= value; } emit Transfer(from, address(0), value); } function _sendNative(address payable recipient, uint256 amount) internal { (bool success, ) = recipient.call{value: amount}(""); if (!success) revert NativeTransferFailed(recipient, amount); } }