123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- 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 saleTokenAddress; // Sale token address
- uint256 public totalTokensForSale; // Total tokens for sale
- address public paymentTokenAddress; // Payment tokens accepted for the sale
- uint256 public paymentTokenPrice; // Payment token price in terms of sale token
- uint256 public startTime; // Beginning time of the sale
- uint256 public endTime; // Ending time of the sale
- bytes32 public merkleRoot; // Merkle root for whitelisting
- uint256 public contributionTarget; // Target amount to be raised in the sale
- uint256 public totalContributionAmount; // Total contributed amount so far
- bool public claimEnabled; // Whether claiming tokens is enabled
- uint256 public claimStartTime; // Start time for claiming tokens
- mapping(address => uint256) public userContributionAmount; // User's contribution amount for claiming tokens
- uint256 public accountsCount; // Number of accounts that have contributed
- uint256 public feeBps; // Fee in basis points (bps)
- uint256 public constant MAX_FEE_BPS = 10000; // 100% in basis points
- uint256 public constant DECIMALS = 1e18;
- event SaleCreated(address saleTokenAddress, uint256 totalTokensForSale);
- event Contributed(address user, uint256 amount, address paymentTokenAddress);
- event TokensClaimed(address user, uint256 tokensClaimed, uint256 refundAmount);
- event SaleCancelled(address saleTokenAddress, 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");
- require(block.timestamp >= startTime && block.timestamp <= endTime, "Sale is not active");
- _;
- }
- modifier validClaimTime() {
- require(claimEnabled, "Claiming tokens is not enabled");
- require(block.timestamp >= claimStartTime, "Claiming tokens not started yet");
- _;
- }
- // Owner Functions
- /**
- * @notice Creates a new token sale with specified parameters.
- * @param _token The address of the token being sold.
- * @param _totalTokensForSale The total number of tokens available for sale.
- * @param _contributionTarget The fundraising target amount.
- * @param _startTime The start time of the sale (timestamp).
- * @param _endTime The end time of the sale (timestamp).
- * @param _paymentToken The address of the payment token (or address(0) for ETH).
- * @param _price The price per token in payment token units.
- * @param _feeBps The fee in basis points (1/100 of a percent).
- */
- function createSale(
- address _token,
- uint256 _totalTokensForSale,
- uint256 _contributionTarget,
- uint256 _startTime,
- uint256 _endTime,
- address _paymentToken,
- uint256 _price,
- uint256 _feeBps
- ) public onlyOwner {
- require(_token != address(0), "Invalid token address");
- require(_totalTokensForSale > 0, "Total tokens must be greater than 0");
- require(_startTime < _endTime, "Invalid time range");
- paymentTokenAddress = _paymentToken;
- paymentTokenPrice = _price;
- saleTokenAddress = _token;
- contributionTarget = _contributionTarget;
- totalTokensForSale = _totalTokensForSale;
- startTime = _startTime;
- endTime = _endTime;
- totalContributionAmount = 0;
- feeBps = _feeBps; // Set the fee in basis points
- merkleRoot = bytes32(0); // Reset Merkle root
- IERC20(_token).transferFrom(msg.sender, address(this), _totalTokensForSale); // Transfer tokens to the contracts
- emit SaleCreated(saleTokenAddress, totalTokensForSale);
- }
- /**
- * @notice Withdraws any remaining unsold tokens to the owner after the sale ends.
- */
- function withdrawRemainingTokens() public onlyOwner {
- require(block.timestamp > endTime, "Sale is still active");
- require(totalContributionAmount<contributionTarget,"all sold out, cannot withdraw remaining tokens");
- uint256 remainingAmount = totalTokensForSale - totalTokensForSale * totalContributionAmount / contributionTarget;
- IERC20(saleTokenAddress).transfer(owner, remainingAmount);
- }
- /**
- * @notice Withdraws all collected payments (ETH or ERC20) to the owner after the sale ends.
- */
- function withdrawPayments() public onlyOwner {
- require(block.timestamp > endTime, "Sale is still active");
- if (paymentTokenAddress == address(0)) {
- payable(owner).transfer(address(this).balance);
- } else {
- uint256 balance = IERC20(paymentTokenAddress).balanceOf(address(this));
- IERC20(paymentTokenAddress).transfer(owner, balance);
- }
- }
- /**
- * @notice Cancels the sale before it starts and refunds all tokens to the owner.
- */
- function cancelSale() public onlyOwner {
- require(block.timestamp < startTime, "Sale already started");
- require(saleTokenAddress != address(0), "Sale not initialized");
- uint256 balance = IERC20(saleTokenAddress).balanceOf(address(this));
- require(balance > 0, "No tokens to refund");
- IERC20(saleTokenAddress).transfer(owner, balance); // Refund tokens to owner
- saleTokenAddress = address(0);
- totalTokensForSale = 0;
- totalContributionAmount = 0;
- startTime = 0;
- endTime = 0;
- merkleRoot = bytes32(0); // Reset Merkle root
- emit SaleCancelled(saleTokenAddress, balance);
- }
- /**
- * @notice Enables claiming of purchased tokens after the sale ends.
- * @param _enableClaimTokenTime The timestamp when claiming is enabled.
- */
- 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");
- claimEnabled = true;
- claimStartTime = _enableClaimTokenTime; // Set claim start time to now
- emit EnableClaimToken(claimEnabled, claimStartTime);
- }
- /**
- * @notice Disables claiming of purchased tokens.
- */
- function disableClaimTokens() public onlyOwner {
- require(claimEnabled, "Claiming tokens is already disabled");
- claimEnabled = false;
- emit DisableClaimToken(claimEnabled);
- }
- // Setter
- /**
- * @notice Transfers contract ownership to a new address.
- * @param _newOwner The address of the new owner.
- */
- function setOwner(address _newOwner) public onlyOwner {
- require(_newOwner != address(0), "Invalid owner address");
- owner = _newOwner;
- }
- /**
- * @notice Sets the payment token and price for the sale.
- * @param _paymentToken The address of the payment token.
- * @param _price The price per token in payment token units.
- */
- function setSalePayment(address _paymentToken, uint256 _price) public onlyOwner {
- require(_price > 0, "Price must be greater than 0");
- paymentTokenAddress = _paymentToken;
- paymentTokenPrice = _price;
- }
- /**
- * @notice Sets the Merkle root for whitelist verification.
- * @param _merkleRoot The new Merkle root.
- */
- function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner {
- merkleRoot = _merkleRoot;
- }
- /**
- * @notice Sets the fee in basis points.
- * @param _feeBps The fee in basis points (max 10000).
- */
- function setFeeBps(uint256 _feeBps) public onlyOwner {
- require(_feeBps <= MAX_FEE_BPS, "Fee cannot exceed 100%");
- feeBps = _feeBps; // Set the fee in basis points
- }
- //Getter
- /**
- * @notice Returns the address of the contract owner.
- */
- function getOwner() public view returns (address) {
- return owner;
- }
- /**
- * @notice Returns the address of the sale token.
- */
- function getSaleToken() public view returns (address) {
- return saleTokenAddress;
- }
- /**
- * @notice Returns the total number of tokens for sale.
- */
- function getTotalTokensForSale() public view returns (uint256) {
- return totalTokensForSale;
- }
- /**
- * @notice Returns the total contributed amount so far.
- */
- function getTotalContributeAmount() public view returns (uint256) {
- return totalContributionAmount;
- }
- /**
- * @notice Returns the sale start time.
- */
- function getStartTime() public view returns (uint256) {
- return startTime;
- }
- /**
- * @notice Returns the sale end time.
- */
- function getEndTime() public view returns (uint256) {
- return endTime;
- }
- /**
- * @notice Returns all sale details as a tuple.
- */
- function getSaleDetails()
- public
- view
- returns (
- address saleTokenAddress,
- uint256 totalTokensForSale,
- uint256 totalContributeAmount,
- uint256 startTime,
- uint256 endTime,
- bytes32 merkleRoot,
- address paymentTokenAddress,
- uint256 salePaymentPrice
- )
- {
- return (
- saleTokenAddress,
- totalTokensForSale,
- totalContributionAmount,
- startTime,
- endTime,
- merkleRoot,
- paymentTokenAddress,
- paymentTokenPrice
- );
- }
- /**
- * @notice Returns the claimable tokens for a user.
- * @param _user The address of the user.
- */
- function getClaimableTokens(address _user) public view returns (uint256) {
- return userContributionAmount[_user];
- }
- /**
- * @notice Returns the payment token price.
- */
- function getPaymentTokenPrice() public view returns (uint256) {
- return paymentTokenPrice;
- }
- /**
- * @notice Returns the payment token address.
- */
- function getPaymentTokens() public view returns (address) {
- return paymentTokenAddress;
- }
- /**
- * @notice Returns the claim start time.
- */
- function getClaimStartTime() public view returns (uint256) {
- return claimStartTime;
- }
- /**
- * @notice Returns whether claiming is enabled.
- */
- function isClaimEnabled() public view returns (bool) {
- return claimEnabled;
- }
- //Main Function
- /**
- * @notice Calculates the net contribution after deducting the fee.
- * @param _commitAmount The original contribution amount.
- * @return The net contribution after fee deduction.
- */
- function calculateNetContribution(uint256 _commitAmount) public view returns (uint256) {
- // Calculate net contribution after fee
- uint256 net = (_commitAmount * (MAX_FEE_BPS - feeBps)) / MAX_FEE_BPS;
- return net;
- }
- /**
- * @notice Contribute to the sale using ETH. Requires whitelist proof.
- * @param _commitAmount The amount of ETH to contribute.
- * @param _maxCommitAmount The maximum allowed contribution for the user.
- * @param _proof The Merkle proof for whitelist verification.
- * @dev nonReentrant modifier is used to prevent reentrancy attacks.
- */
- function contributeWithETH(
- uint256 _commitAmount,
- uint256 _maxCommitAmount,
- bytes32[] memory _proof
- ) public payable saleActive nonReentrant {
- require(paymentTokenAddress == address(0), "Payment token must be ETH for this sale");
- require(_commitAmount > 0, "Must buy a positive amount");
- require(_commitAmount <= _maxCommitAmount, "Buy amount exceeds max buy amount");
- require(msg.value >= _commitAmount, "Must send enough ETH");
- require(paymentTokenPrice > 0, "Payment token not accepted for this sale");
- // 验证 Merkle 证明
- bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxCommitAmount))));
- require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof");
- //户数统计
- if (userContributionAmount[msg.sender] == 0) {
- accountsCount++; // Increment accounts count only if this is the first contribution
- }
- //币数统计
- userContributionAmount[msg.sender] += _commitAmount; // update user's commit tokens
- require(userContributionAmount[msg.sender] <= _maxCommitAmount, "Buy amount exceeds max buy amount");
- totalContributionAmount += _commitAmount; // update total contributed amount
- if (msg.value > _commitAmount) {
- payable(msg.sender).transfer(msg.value - _commitAmount); //refund excess ETH
- }
- emit Contributed(msg.sender, _commitAmount, address(0));
- }
- /**
- * @notice Contribute to the sale using ERC20 tokens. Requires whitelist proof.
- * @param _commitAmount The amount of tokens to contribute.
- * @param _maxCommitAmount The maximum allowed contribution for the user.
- * @param _proof The Merkle proof for whitelist verification.
- * @dev nonReentrant modifier is used to prevent reentrancy attacks.
- */
- function contributeWithERC20(
- uint256 _commitAmount,
- uint256 _maxCommitAmount,
- bytes32[] memory _proof
- ) public saleActive nonReentrant {
- require(_commitAmount > 0, "Must send a positive amount");
- require(_commitAmount <= _maxCommitAmount, "Buy amount exceeds max buy amount");
- bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxCommitAmount))));
- require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof");
- require(
- IERC20(paymentTokenAddress).allowance(msg.sender, address(this)) >= _commitAmount,
- "Not enough allowance for payment token"
- );
- if (userContributionAmount[msg.sender] == 0) {
- accountsCount++; // Increment accounts count only if this is the first contribution
- }
- userContributionAmount[msg.sender] += _commitAmount; // update user's claimable tokens
- require(userContributionAmount[msg.sender] <= _maxCommitAmount, "Buy amount exceeds max buy amount");
- totalContributionAmount += _commitAmount; // update total contributed amount
- IERC20(paymentTokenAddress).transferFrom(msg.sender, address(this), _commitAmount); // 从用户账户转移支付代币
- emit Contributed(msg.sender, _commitAmount, paymentTokenAddress);
- }
- /**
- * @notice Claim purchased tokens and receive refund if sale is oversubscribed.
- * @dev nonReentrant modifier is used to prevent reentrancy attacks.
- * This function clears the user's contribution after claim to prevent double claim.
- */
- function claimTokens() public validClaimTime {
- require(block.timestamp > endTime, "Sale is still active");
- uint256 contributeAmount = userContributionAmount[msg.sender];
- require(contributeAmount > 0, "No tokens to claim");
- uint256 refundCost = 0;
- uint256 boughtToken = 0;
- if (totalContributionAmount <= contributionTarget) {
- uint256 netContribution = calculateNetContribution(contributeAmount);
- boughtToken = (netContribution * DECIMALS) / paymentTokenPrice;
- IERC20(saleTokenAddress).transfer(msg.sender, boughtToken);
- } else {
- uint256 userValidContribution = (contributeAmount * contributionTarget) / totalContributionAmount;
- refundCost = contributeAmount - userValidContribution;
- uint256 netContribution = calculateNetContribution(userValidContribution);
- boughtToken = (netContribution * DECIMALS) / paymentTokenPrice;
- if (paymentTokenAddress == address(0)) {
- (bool sent, ) = payable(msg.sender).call{ value: refundCost }("");
- require(sent, "ETH refund failed");
- } else {
- IERC20(paymentTokenAddress).transfer(msg.sender, refundCost);
- }
- IERC20(saleTokenAddress).transfer(msg.sender, boughtToken);
- }
- userContributionAmount[msg.sender] = 0;
- emit TokensClaimed(msg.sender, boughtToken, refundCost);
- }
- }
|