// SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; /// @dev Deliberately excludes transfer-tax and rebasing tokens. An external quote /// token can still freeze, upgrade or misreport balances; exact deltas are not an audit. abstract contract ExactTransfers { using SafeERC20 for IERC20; function _pull(IERC20 asset, address from, uint256 amount) internal { uint256 here = asset.balanceOf(address(this)); uint256 there = asset.balanceOf(from); asset.safeTransferFrom(from, address(this), amount); require(asset.balanceOf(address(this)) == here + amount && asset.balanceOf(from) + amount == there, "Non-exact transfer"); } function _push(IERC20 asset, address to, uint256 amount) internal { uint256 here = asset.balanceOf(address(this)); uint256 there = asset.balanceOf(to); asset.safeTransfer(to, amount); require(asset.balanceOf(to) == there + amount && asset.balanceOf(address(this)) + amount == here, "Non-exact transfer"); } } interface ILaunch { function community() external view returns(address); } /// @notice A named community space. Approval is permanent; it never grants custody. contract CoharyxCommunity { address public immutable owner; address public immutable quoteAsset; uint8 public immutable quoteDecimals; string public name; string public metadataJSON; mapping(address => bool) public approved; event ProjectApproved(address indexed project); constructor(string memory name_, address quote_, string memory metadata_) { require(bytes(name_).length > 0 && bytes(name_).length <= 60, "Name length"); require(quote_.code.length > 0, "Quote must be a contract"); uint8 d = IERC20Metadata(quote_).decimals(); require(d >= 6 && d <= 18, "Quote decimals 6-18"); require(bytes(metadata_).length <= 2048, "Metadata too long"); owner = msg.sender; quoteAsset = quote_; quoteDecimals = d; name = name_; metadataJSON = metadata_; } function approveProject(address project) external { require(msg.sender == owner, "Community owner only"); require(project.code.length > 0 && ILaunch(project).community() == address(this), "Wrong community"); require(!approved[project], "Already approved"); approved[project] = true; emit ProjectApproved(project); } } contract CoharyxToken is ERC20 { constructor(string memory name_, string memory symbol_, uint256 supply_) ERC20(name_, symbol_) { require(bytes(name_).length > 0 && bytes(name_).length <= 60, "Name length"); require(bytes(symbol_).length > 0 && bytes(symbol_).length <= 12, "Symbol length"); require(supply_ >= 1e18 && supply_ <= 1e33 && supply_ % 1e18 == 0, "Whole supply 1-1e15"); _mint(msg.sender, supply_); } } /// @notice Isolated ERC20/ERC20 constant-product pool. No reserve withdrawal, /// owner override, rescue, mint, upgrade or external router execution. contract CoharyxPool is ReentrancyGuard, ExactTransfers { address public immutable initializer; IERC20 public immutable token; IERC20 public immutable quoteAsset; address public immutable creator; address public immutable communityOwner; bool public initialized; uint256 public reserveQuote; uint256 public reserveToken; uint256 public creatorFees; uint256 public communityFees; uint256 public constant FEE_BPS = 30; event Trade(address indexed trader, bool isBuy, uint256 amountIn, uint256 amountOut, uint256 quoteReserve, uint256 tokenReserve); event FeesClaimed(address indexed recipient, uint256 amount); constructor(address token_, address quote_, address creator_, address communityOwner_) { require(token_ != quote_ && token_ != address(0) && quote_ != address(0), "Invalid pair"); require(creator_ != address(0) && communityOwner_ != address(0), "Invalid recipient"); initializer=msg.sender;token=IERC20(token_);quoteAsset=IERC20(quote_);creator=creator_;communityOwner=communityOwner_; } function initialize(uint256 quoteAmount, uint256 tokenAmount) external { require(msg.sender == initializer && !initialized, "Initialize once"); require(quoteAmount > 0 && tokenAmount > 0, "Empty pool"); require(quoteAsset.balanceOf(address(this)) >= quoteAmount && token.balanceOf(address(this)) >= tokenAmount, "Not funded"); initialized=true; reserveQuote=quoteAmount; reserveToken=tokenAmount; } function quoteBuy(uint256 amount) public view returns(uint256 out, uint256 fee) { require(initialized, "Pool not live");fee=Math.mulDiv(amount,FEE_BPS,10000); out=Math.mulDiv(reserveToken,amount-fee,reserveQuote+amount-fee); } function quoteSell(uint256 amount) public view returns(uint256 out, uint256 fee) { require(initialized, "Pool not live");uint256 gross=Math.mulDiv(reserveQuote,amount,reserveToken+amount); fee=Math.mulDiv(gross,FEE_BPS,10000);out=gross-fee; } function _fees(uint256 fee) internal {uint256 share=fee/3;communityFees+=share;creatorFees+=fee-share;} function buy(uint256 amount, uint256 minimum, uint256 deadline) external nonReentrant returns(uint256 out) { require(block.timestamp<=deadline && amount>0 && amount<=type(uint128).max && minimum>0,"Expired or invalid amount"); uint256 fee;(out,fee)=quoteBuy(amount);require(out>=minimum,"Slippage"); _pull(quoteAsset,msg.sender,amount); reserveQuote+=amount-fee;reserveToken-=out;_fees(fee);_push(token,msg.sender,out); emit Trade(msg.sender,true,amount,out,reserveQuote,reserveToken); } function sell(uint256 amount, uint256 minimum, uint256 deadline) external nonReentrant returns(uint256 out) { require(block.timestamp<=deadline && amount>0 && amount<=type(uint128).max && minimum>0,"Expired or invalid amount"); uint256 fee;(out,fee)=quoteSell(amount);require(out>=minimum,"Slippage"); _pull(token,msg.sender,amount); reserveToken+=amount;reserveQuote-=out+fee;_fees(fee);_push(quoteAsset,msg.sender,out); emit Trade(msg.sender,false,amount,out,reserveQuote,reserveToken); } function claimFees() external nonReentrant { uint256 amount; if(msg.sender==creator){amount+=creatorFees;creatorFees=0;} if(msg.sender==communityOwner){amount+=communityFees;communityFees=0;} require(amount>0,"No fees for this account");_push(quoteAsset,msg.sender,amount); emit FeesClaimed(msg.sender,amount); } } /// @notice Fixed-window, capped community raise. Exactly half of the supply is /// allocated pro-rata to contributors; half seeds the permanent pool. All raised /// quote assets seed the pool. Nobody can withdraw a project treasury allocation. contract CoharyxLaunch is ReentrancyGuard, ExactTransfers { CoharyxCommunity public immutable community; CoharyxToken public immutable token; IERC20 public immutable quoteAsset; address public immutable creator; uint256 public immutable target; uint256 public immutable cap; uint256 public immutable deadline; uint256 public immutable saleSupply; uint256 public immutable poolSupply; uint256 public constant FINALIZE_GRACE=7 days; string public metadataJSON; uint256 public totalRaised; uint256 public participantCount; uint256 public refundedTotal; bool public finalized; CoharyxPool public pool; mapping(address=>uint256) public contributions; mapping(address=>bool) public claimed; mapping(address=>bool) public refunded; event Contributed(address indexed account,uint256 amount,uint256 total); event Launched(address indexed pool,address indexed token,uint256 raised); event Refunded(address indexed account,uint256 amount); event Claimed(address indexed account,uint256 amount); constructor(string memory name_,string memory symbol_,uint256 supply_,address community_,uint256 target_,uint256 cap_,uint256 deadline_,string memory metadata_) { require(community_.code.length>0,"Community required"); CoharyxCommunity c=CoharyxCommunity(community_); require(c.quoteAsset().code.length>0 && c.owner()!=address(0),"Invalid community"); require(target_>0 && cap_>=target_ && cap_<=type(uint128).max,"Invalid target or cap"); require(deadline_>=block.timestamp+1 hours && deadline_<=block.timestamp+30 days,"Window 1 hour-30 days"); require(bytes(metadata_).length<=2048,"Metadata too long"); community=c;quoteAsset=IERC20(c.quoteAsset());creator=msg.sender; target=target_;cap=cap_;deadline=deadline_;metadataJSON=metadata_; token=new CoharyxToken(name_,symbol_,supply_);saleSupply=supply_/2;poolSupply=supply_-saleSupply; } /// @return 0 accepting (subject to community approval), 1 ready to finalize, /// 2 pool live, 3 refunds. Chain time, not local browser time, determines state. function phase() public view returns(uint8) { if(finalized)return 2; if(block.timestamp=deadline+FINALIZE_GRACE)return 3; return 1; } function contribute(uint256 amount) external nonReentrant { require(phase()==0 && community.approved(address(this)),"Not open or not approved"); require(amount>0 && amount<=cap-totalRaised,"Amount exceeds remaining cap"); require(Math.mulDiv(contributions[msg.sender]+amount,saleSupply,cap)>0,"Contribution too small"); _pull(quoteAsset,msg.sender,amount); if(contributions[msg.sender]==0)participantCount++; contributions[msg.sender]+=amount;totalRaised+=amount; emit Contributed(msg.sender,amount,totalRaised); } function finalize() external nonReentrant { require(phase()==1,"Not ready"); finalized=true; pool=new CoharyxPool(address(token),address(quoteAsset),creator,community.owner()); _push(quoteAsset,address(pool),totalRaised);_push(IERC20(address(token)),address(pool),poolSupply); pool.initialize(totalRaised,poolSupply); emit Launched(address(pool),address(token),totalRaised); } function claim() external nonReentrant returns(uint256 amount) { require(finalized && !claimed[msg.sender] && contributions[msg.sender]>0,"Nothing to claim"); claimed[msg.sender]=true;amount=Math.mulDiv(contributions[msg.sender],saleSupply,totalRaised); require(amount>0,"Allocation rounds to zero");_push(IERC20(address(token)),msg.sender,amount); emit Claimed(msg.sender,amount); } function refund() external nonReentrant { require(phase()==3 && !refunded[msg.sender] && contributions[msg.sender]>0,"Refund not available"); refunded[msg.sender]=true;uint256 amount=contributions[msg.sender];refundedTotal+=amount; _push(quoteAsset,msg.sender,amount);emit Refunded(msg.sender,amount); } }