pragma solidity ^0.8.24; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "hardhat/console.sol"; // For debugging purposes, can be removed in production contract Launchpad is ReentrancyGuard { address public owner; address public token; // sale token address uint256 public totalTokens; // total tokens for sale uint256 public tokensSold; // tokens sold so far uint256 public startTime; // beginning time of the sale uint256 public endTime; // ending time of the sale bytes32 public merkleRoot; // Merkle root for whitelisting bool public enableClaimToken; // enable claim token uint256 public claimStartTime; // start time for claiming tokens address public paymentToken; // payment tokens accepted for the sale uint256 public salePaymentPrice; // payment token price in terms of sale token mapping(address => uint256) public claimableTokensAmount; // user contributions for claiming tokens uint256 public accountsCount; // number of accounts that have contributed event SaleCreated(address token, uint256 totalTokens); event Contributed(address user, uint256 amount, address paymentToken); event TokensClaimed(address user, uint256 amount); event SaleCancelled(address token, uint256 refundedAmount); event EnableClaimToken(bool enabled, uint256 claimStartTime); event DisableClaimToken(bool disabled); constructor() { owner = msg.sender; accountsCount = 0; } // Modifiers modifier onlyOwner() { require(msg.sender == owner, "Only owner can call this function"); _; } modifier saleActive() { require(merkleRoot != bytes32(0), "MerkleRoot not initialized"); console.log("Claim start time:", startTime); console.log("Current time:", block.timestamp); console.log("Claiming tokens enabled:", endTime); require(block.timestamp >= startTime && block.timestamp <= endTime, "Sale is not active"); _; } modifier validClaimTime() { require(enableClaimToken, "Claiming tokens is not enabled"); require(block.timestamp >= claimStartTime, "Claiming tokens not started yet"); _; } // Owner Functions function createSale( address _token, uint256 _totalTokens, uint256 _startTime, uint256 _endTime, address _paymentToken, uint256 _price ) public onlyOwner { require(_token != address(0), "Invalid token address"); require(_totalTokens > 0, "Total tokens must be greater than 0"); require(_startTime < _endTime, "Invalid time range"); paymentToken = _paymentToken; salePaymentPrice = _price; token = _token; totalTokens = _totalTokens; startTime = _startTime; endTime = _endTime; tokensSold = 0; merkleRoot = bytes32(0); // Reset Merkle root IERC20(_token).transferFrom(msg.sender, address(this), _totalTokens); // Transfer tokens to the contracts emit SaleCreated(token, totalTokens); } function withdrawRemainingTokens() public onlyOwner { require(block.timestamp > endTime, "Sale is still active"); uint256 remaining = IERC20(token).balanceOf(address(this)); IERC20(token).transfer(owner, remaining); } function withdrawPayments() public onlyOwner { require(block.timestamp > endTime, "Sale is still active"); if (paymentToken == address(0)) { payable(owner).transfer(address(this).balance); } else { uint256 balance = IERC20(paymentToken).balanceOf(address(this)); IERC20(paymentToken).transfer(owner, balance); } } function cancelSale() public onlyOwner { require(block.timestamp < startTime, "Sale already started"); require(token != address(0), "Sale not initialized"); uint256 balance = IERC20(token).balanceOf(address(this)); require(balance > 0, "No tokens to refund"); IERC20(token).transfer(owner, balance); // Refund tokens to owner token = address(0); totalTokens = 0; tokensSold = 0; startTime = 0; endTime = 0; merkleRoot = bytes32(0); // Reset Merkle root emit SaleCancelled(token, balance); } function enableClaimTokens(uint256 _enableClaimTokenTime) public onlyOwner { require(_enableClaimTokenTime >= block.timestamp, "Enable claim time must be in the future"); require(_enableClaimTokenTime >= endTime, "Claiming tokens can only be enabled after the sale ends"); enableClaimToken = true; claimStartTime = _enableClaimTokenTime; // Set claim start time to now emit EnableClaimToken(enableClaimToken, claimStartTime); } function disableClaimTokens() public onlyOwner { require(enableClaimToken, "Claiming tokens is already disabled"); enableClaimToken = false; emit DisableClaimToken(enableClaimToken); } // Setter function setOwner(address _newOwner) public onlyOwner { require(_newOwner != address(0), "Invalid owner address"); owner = _newOwner; } function setSalePayment(address _paymentToken, uint256 _price) public onlyOwner { require(_price > 0, "Price must be greater than 0"); paymentToken = _paymentToken; salePaymentPrice = _price; } function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner { merkleRoot = _merkleRoot; } //Getter function getOwner() public view returns (address) { return owner; } function getSaleToken() public view returns (address) { return token; } function getTotalTokens() public view returns (uint256) { return totalTokens; } function getTokensSold() public view returns (uint256) { return tokensSold; } function getStartTime() public view returns (uint256) { return startTime; } function getEndTime() public view returns (uint256) { return endTime; } function getSaleDetails() public view returns ( address token, uint256 totalTokens, uint256 tokensSold, uint256 startTime, uint256 endTime, bytes32 merkleRoot, address paymentToken, uint256 salePaymentPrice ) { return (token, totalTokens, tokensSold, startTime, endTime, merkleRoot, paymentToken, salePaymentPrice); } function getClaimableTokens(address _user) public view returns (uint256) { return claimableTokensAmount[_user]; } function getPaymentTokenPrice() public view returns (uint256) { return salePaymentPrice; } function getPaymentTokens() public view returns (address) { return paymentToken; } function getClaimStartTime() public view returns (uint256) { return claimStartTime; } function isClaimEnabled() public view returns (bool) { return enableClaimToken; } //Main Function //MUST TEST ! function contributeETH( uint256 _buyAmount, uint256 _maxBuyAmount, bytes32[] memory _proof ) public payable saleActive nonReentrant { require(paymentToken == address(0), "Payment token must be ETH for this sale"); require(msg.value > 0, "Must send ETH"); require(_buyAmount > 0, "Must buy a positive amount"); require(_buyAmount <= _maxBuyAmount, "Buy amount exceeds max buy amount"); require(salePaymentPrice > 0, "Payment token not accepted for this sale"); // 验证 Merkle 证明 bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxBuyAmount)))); require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof"); uint256 cost = (salePaymentPrice / 10 ** 18) * _buyAmount; // Assuming salePaymentPrice is in wei require(msg.value >= cost, "Not enough ETH sent"); tokensSold += _buyAmount; if (claimableTokensAmount[msg.sender] == 0) { accountsCount++; // Increment accounts count only if this is the first contribution } claimableTokensAmount[msg.sender] += _buyAmount; // update user's claimable tokens require(claimableTokensAmount[msg.sender] <= _maxBuyAmount, "Buy amount exceeds max buy amount"); payable(msg.sender).transfer(msg.value - cost); //refund excess ETH emit Contributed(msg.sender, _buyAmount, address(0)); } function contributeERC20( uint256 _buyAmount, uint256 _maxBuyAmount, bytes32[] memory _proof ) public saleActive nonReentrant { require(_buyAmount > 0, "Must send a positive amount"); require(_buyAmount <= _maxBuyAmount, "Buy amount exceeds max buy amount"); bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxBuyAmount)))); require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof"); uint256 price = salePaymentPrice; require(price > 0, "Payment token not accepted for this sale"); uint256 cost = (price / 10 ** 18) * _buyAmount; // Assuming salePaymentPrice is in wei require( IERC20(paymentToken).allowance(msg.sender, address(this)) >= cost, "Not enough allowance for payment token" ); tokensSold += _buyAmount; if (claimableTokensAmount[msg.sender] == 0) { accountsCount++; // Increment accounts count only if this is the first contribution } claimableTokensAmount[msg.sender] += _buyAmount; // update user's claimable tokens console.log("Claimable tokens amount for user:", claimableTokensAmount[msg.sender]); require(claimableTokensAmount[msg.sender] <= _maxBuyAmount, "Buy amount exceeds max buy amount"); IERC20(paymentToken).transferFrom(msg.sender, address(this), cost); // 从用户账户转移支付代币 emit Contributed(msg.sender, _buyAmount, paymentToken); } function claimTokens() public validClaimTime { require(block.timestamp > endTime, "Sale is still active"); uint256 amount = claimableTokensAmount[msg.sender]; require(amount > 0, "No tokens to claim"); if (tokensSold <= totalTokens) { IERC20(token).transfer(msg.sender, amount); // 分发销售代币 emit TokensClaimed(msg.sender, amount); } else { uint256 boughtAmountAvg = totalTokens / accountsCount; // 平均每个账户购买的代币数量 uint256 refundTokenAmount = amount - boughtAmountAvg; // 计算退款金额 uint256 refundAmount = (refundAmount / 10 ** 18) * salePaymentPrice; if (paymentToken == address(0)) { payable(msg.sender).transfer(refundAmount); // 退款ETH } else { IERC20(paymentToken).transfer(msg.sender, refundAmount); // 退款ERC20代币 } IERC20(token).transfer(msg.sender, boughtAmountAvg); // 分发销售代币 } } }