Launchpad.sol 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. pragma solidity ^0.8.24;
  2. import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
  3. import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
  4. import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
  5. import "hardhat/console.sol"; // For debugging purposes, can be removed in production
  6. contract Launchpad is ReentrancyGuard {
  7. address public owner;
  8. address public saleTokenAddress; // Sale token address
  9. uint256 public totalTokensForSale; // Total tokens for sale
  10. address public paymentTokenAddress; // Payment tokens accepted for the sale
  11. uint256 public paymentTokenPrice; // Payment token price in terms of sale token
  12. uint256 public startTime; // Beginning time of the sale
  13. uint256 public endTime; // Ending time of the sale
  14. bytes32 public merkleRoot; // Merkle root for whitelisting
  15. uint256 public contributionTarget; // Target amount to be raised in the sale
  16. uint256 public totalContributionAmount; // Total contributed amount so far
  17. bool public claimEnabled; // Whether claiming tokens is enabled
  18. uint256 public claimStartTime; // Start time for claiming tokens
  19. mapping(address => uint256) public userContributionAmount; // User's contribution amount for claiming tokens
  20. uint256 public accountsCount; // Number of accounts that have contributed
  21. uint256 public feeBps; // Fee in basis points (bps)
  22. uint256 public constant MAX_FEE_BPS = 10000; // 100% in basis points
  23. uint256 public constant DECIMALS = 1e18;
  24. event SaleCreated(address saleTokenAddress, uint256 totalTokensForSale);
  25. event Contributed(address user, uint256 amount, address paymentTokenAddress);
  26. event TokensClaimed(address user, uint256 tokensClaimed, uint256 refundAmount);
  27. event SaleCancelled(address saleTokenAddress, uint256 refundedAmount);
  28. event EnableClaimToken(bool enabled, uint256 claimStartTime);
  29. event DisableClaimToken(bool disabled);
  30. constructor() {
  31. owner = msg.sender;
  32. accountsCount = 0;
  33. }
  34. // Modifiers
  35. modifier onlyOwner() {
  36. require(msg.sender == owner, "Only owner can call this function");
  37. _;
  38. }
  39. modifier saleActive() {
  40. require(merkleRoot != bytes32(0), "MerkleRoot not initialized");
  41. require(block.timestamp >= startTime && block.timestamp <= endTime, "Sale is not active");
  42. _;
  43. }
  44. modifier validClaimTime() {
  45. require(claimEnabled, "Claiming tokens is not enabled");
  46. require(block.timestamp >= claimStartTime, "Claiming tokens not started yet");
  47. _;
  48. }
  49. // Owner Functions
  50. /**
  51. * @notice Creates a new token sale with specified parameters.
  52. * @param _token The address of the token being sold.
  53. * @param _totalTokensForSale The total number of tokens available for sale.
  54. * @param _contributionTarget The fundraising target amount.
  55. * @param _startTime The start time of the sale (timestamp).
  56. * @param _endTime The end time of the sale (timestamp).
  57. * @param _paymentToken The address of the payment token (or address(0) for ETH).
  58. * @param _price The price per token in payment token units.
  59. * @param _feeBps The fee in basis points (1/100 of a percent).
  60. */
  61. function createSale(
  62. address _token,
  63. uint256 _totalTokensForSale,
  64. uint256 _contributionTarget,
  65. uint256 _startTime,
  66. uint256 _endTime,
  67. address _paymentToken,
  68. uint256 _price,
  69. uint256 _feeBps
  70. ) public onlyOwner {
  71. require(_token != address(0), "Invalid token address");
  72. require(_totalTokensForSale > 0, "Total tokens must be greater than 0");
  73. require(_startTime < _endTime, "Invalid time range");
  74. paymentTokenAddress = _paymentToken;
  75. paymentTokenPrice = _price;
  76. saleTokenAddress = _token;
  77. contributionTarget = _contributionTarget;
  78. totalTokensForSale = _totalTokensForSale;
  79. startTime = _startTime;
  80. endTime = _endTime;
  81. totalContributionAmount = 0;
  82. feeBps = _feeBps; // Set the fee in basis points
  83. merkleRoot = bytes32(0); // Reset Merkle root
  84. IERC20(_token).transferFrom(msg.sender, address(this), _totalTokensForSale); // Transfer tokens to the contracts
  85. emit SaleCreated(saleTokenAddress, totalTokensForSale);
  86. }
  87. /**
  88. * @notice Withdraws any remaining unsold tokens to the owner after the sale ends.
  89. */
  90. function withdrawRemainingTokens() public onlyOwner {
  91. require(block.timestamp > endTime, "Sale is still active");
  92. require(totalContributionAmount<contributionTarget,"all sold out, cannot withdraw remaining tokens");
  93. uint256 remainingAmount = totalTokensForSale - totalTokensForSale * totalContributionAmount / contributionTarget;
  94. IERC20(saleTokenAddress).transfer(owner, remainingAmount);
  95. }
  96. /**
  97. * @notice Withdraws all collected payments (ETH or ERC20) to the owner after the sale ends.
  98. */
  99. function withdrawPayments() public onlyOwner {
  100. require(block.timestamp > endTime, "Sale is still active");
  101. if (paymentTokenAddress == address(0)) {
  102. payable(owner).transfer(address(this).balance);
  103. } else {
  104. uint256 balance = IERC20(paymentTokenAddress).balanceOf(address(this));
  105. IERC20(paymentTokenAddress).transfer(owner, balance);
  106. }
  107. }
  108. /**
  109. * @notice Cancels the sale before it starts and refunds all tokens to the owner.
  110. */
  111. function cancelSale() public onlyOwner {
  112. require(block.timestamp < startTime, "Sale already started");
  113. require(saleTokenAddress != address(0), "Sale not initialized");
  114. uint256 balance = IERC20(saleTokenAddress).balanceOf(address(this));
  115. require(balance > 0, "No tokens to refund");
  116. IERC20(saleTokenAddress).transfer(owner, balance); // Refund tokens to owner
  117. saleTokenAddress = address(0);
  118. totalTokensForSale = 0;
  119. totalContributionAmount = 0;
  120. startTime = 0;
  121. endTime = 0;
  122. merkleRoot = bytes32(0); // Reset Merkle root
  123. emit SaleCancelled(saleTokenAddress, balance);
  124. }
  125. /**
  126. * @notice Enables claiming of purchased tokens after the sale ends.
  127. * @param _enableClaimTokenTime The timestamp when claiming is enabled.
  128. */
  129. function enableClaimTokens(uint256 _enableClaimTokenTime) public onlyOwner {
  130. require(_enableClaimTokenTime >= block.timestamp, "Enable claim time must be in the future");
  131. require(_enableClaimTokenTime >= endTime, "Claiming tokens can only be enabled after the sale ends");
  132. claimEnabled = true;
  133. claimStartTime = _enableClaimTokenTime; // Set claim start time to now
  134. emit EnableClaimToken(claimEnabled, claimStartTime);
  135. }
  136. /**
  137. * @notice Disables claiming of purchased tokens.
  138. */
  139. function disableClaimTokens() public onlyOwner {
  140. require(claimEnabled, "Claiming tokens is already disabled");
  141. claimEnabled = false;
  142. emit DisableClaimToken(claimEnabled);
  143. }
  144. // Setter
  145. /**
  146. * @notice Transfers contract ownership to a new address.
  147. * @param _newOwner The address of the new owner.
  148. */
  149. function setOwner(address _newOwner) public onlyOwner {
  150. require(_newOwner != address(0), "Invalid owner address");
  151. owner = _newOwner;
  152. }
  153. /**
  154. * @notice Sets the payment token and price for the sale.
  155. * @param _paymentToken The address of the payment token.
  156. * @param _price The price per token in payment token units.
  157. */
  158. function setSalePayment(address _paymentToken, uint256 _price) public onlyOwner {
  159. require(_price > 0, "Price must be greater than 0");
  160. paymentTokenAddress = _paymentToken;
  161. paymentTokenPrice = _price;
  162. }
  163. /**
  164. * @notice Sets the Merkle root for whitelist verification.
  165. * @param _merkleRoot The new Merkle root.
  166. */
  167. function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner {
  168. merkleRoot = _merkleRoot;
  169. }
  170. /**
  171. * @notice Sets the fee in basis points.
  172. * @param _feeBps The fee in basis points (max 10000).
  173. */
  174. function setFeeBps(uint256 _feeBps) public onlyOwner {
  175. require(_feeBps <= MAX_FEE_BPS, "Fee cannot exceed 100%");
  176. feeBps = _feeBps; // Set the fee in basis points
  177. }
  178. //Getter
  179. /**
  180. * @notice Returns the address of the contract owner.
  181. */
  182. function getOwner() public view returns (address) {
  183. return owner;
  184. }
  185. /**
  186. * @notice Returns the address of the sale token.
  187. */
  188. function getSaleToken() public view returns (address) {
  189. return saleTokenAddress;
  190. }
  191. /**
  192. * @notice Returns the total number of tokens for sale.
  193. */
  194. function getTotalTokensForSale() public view returns (uint256) {
  195. return totalTokensForSale;
  196. }
  197. /**
  198. * @notice Returns the total contributed amount so far.
  199. */
  200. function getTotalContributeAmount() public view returns (uint256) {
  201. return totalContributionAmount;
  202. }
  203. /**
  204. * @notice Returns the sale start time.
  205. */
  206. function getStartTime() public view returns (uint256) {
  207. return startTime;
  208. }
  209. /**
  210. * @notice Returns the sale end time.
  211. */
  212. function getEndTime() public view returns (uint256) {
  213. return endTime;
  214. }
  215. /**
  216. * @notice Returns all sale details as a tuple.
  217. */
  218. function getSaleDetails()
  219. public
  220. view
  221. returns (
  222. address saleTokenAddress,
  223. uint256 totalTokensForSale,
  224. uint256 totalContributeAmount,
  225. uint256 startTime,
  226. uint256 endTime,
  227. bytes32 merkleRoot,
  228. address paymentTokenAddress,
  229. uint256 salePaymentPrice
  230. )
  231. {
  232. return (
  233. saleTokenAddress,
  234. totalTokensForSale,
  235. totalContributionAmount,
  236. startTime,
  237. endTime,
  238. merkleRoot,
  239. paymentTokenAddress,
  240. paymentTokenPrice
  241. );
  242. }
  243. /**
  244. * @notice Returns the claimable tokens for a user.
  245. * @param _user The address of the user.
  246. */
  247. function getClaimableTokens(address _user) public view returns (uint256) {
  248. return userContributionAmount[_user];
  249. }
  250. /**
  251. * @notice Returns the payment token price.
  252. */
  253. function getPaymentTokenPrice() public view returns (uint256) {
  254. return paymentTokenPrice;
  255. }
  256. /**
  257. * @notice Returns the payment token address.
  258. */
  259. function getPaymentTokens() public view returns (address) {
  260. return paymentTokenAddress;
  261. }
  262. /**
  263. * @notice Returns the claim start time.
  264. */
  265. function getClaimStartTime() public view returns (uint256) {
  266. return claimStartTime;
  267. }
  268. /**
  269. * @notice Returns whether claiming is enabled.
  270. */
  271. function isClaimEnabled() public view returns (bool) {
  272. return claimEnabled;
  273. }
  274. //Main Function
  275. /**
  276. * @notice Calculates the net contribution after deducting the fee.
  277. * @param _commitAmount The original contribution amount.
  278. * @return The net contribution after fee deduction.
  279. */
  280. function calculateNetContribution(uint256 _commitAmount) public view returns (uint256) {
  281. // Calculate net contribution after fee
  282. uint256 net = (_commitAmount * (MAX_FEE_BPS - feeBps)) / MAX_FEE_BPS;
  283. return net;
  284. }
  285. /**
  286. * @notice Contribute to the sale using ETH. Requires whitelist proof.
  287. * @param _commitAmount The amount of ETH to contribute.
  288. * @param _maxCommitAmount The maximum allowed contribution for the user.
  289. * @param _proof The Merkle proof for whitelist verification.
  290. * @dev nonReentrant modifier is used to prevent reentrancy attacks.
  291. */
  292. function contributeWithETH(
  293. uint256 _commitAmount,
  294. uint256 _maxCommitAmount,
  295. bytes32[] memory _proof
  296. ) public payable saleActive nonReentrant {
  297. require(paymentTokenAddress == address(0), "Payment token must be ETH for this sale");
  298. require(_commitAmount > 0, "Must buy a positive amount");
  299. require(_commitAmount <= _maxCommitAmount, "Buy amount exceeds max buy amount");
  300. require(msg.value >= _commitAmount, "Must send enough ETH");
  301. require(paymentTokenPrice > 0, "Payment token not accepted for this sale");
  302. // 验证 Merkle 证明
  303. bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxCommitAmount))));
  304. require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof");
  305. //户数统计
  306. if (userContributionAmount[msg.sender] == 0) {
  307. accountsCount++; // Increment accounts count only if this is the first contribution
  308. }
  309. //币数统计
  310. userContributionAmount[msg.sender] += _commitAmount; // update user's commit tokens
  311. require(userContributionAmount[msg.sender] <= _maxCommitAmount, "Buy amount exceeds max buy amount");
  312. totalContributionAmount += _commitAmount; // update total contributed amount
  313. if (msg.value > _commitAmount) {
  314. payable(msg.sender).transfer(msg.value - _commitAmount); //refund excess ETH
  315. }
  316. emit Contributed(msg.sender, _commitAmount, address(0));
  317. }
  318. /**
  319. * @notice Contribute to the sale using ERC20 tokens. Requires whitelist proof.
  320. * @param _commitAmount The amount of tokens to contribute.
  321. * @param _maxCommitAmount The maximum allowed contribution for the user.
  322. * @param _proof The Merkle proof for whitelist verification.
  323. * @dev nonReentrant modifier is used to prevent reentrancy attacks.
  324. */
  325. function contributeWithERC20(
  326. uint256 _commitAmount,
  327. uint256 _maxCommitAmount,
  328. bytes32[] memory _proof
  329. ) public saleActive nonReentrant {
  330. require(_commitAmount > 0, "Must send a positive amount");
  331. require(_commitAmount <= _maxCommitAmount, "Buy amount exceeds max buy amount");
  332. bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxCommitAmount))));
  333. require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof");
  334. require(
  335. IERC20(paymentTokenAddress).allowance(msg.sender, address(this)) >= _commitAmount,
  336. "Not enough allowance for payment token"
  337. );
  338. if (userContributionAmount[msg.sender] == 0) {
  339. accountsCount++; // Increment accounts count only if this is the first contribution
  340. }
  341. userContributionAmount[msg.sender] += _commitAmount; // update user's claimable tokens
  342. require(userContributionAmount[msg.sender] <= _maxCommitAmount, "Buy amount exceeds max buy amount");
  343. totalContributionAmount += _commitAmount; // update total contributed amount
  344. IERC20(paymentTokenAddress).transferFrom(msg.sender, address(this), _commitAmount); // 从用户账户转移支付代币
  345. emit Contributed(msg.sender, _commitAmount, paymentTokenAddress);
  346. }
  347. /**
  348. * @notice Claim purchased tokens and receive refund if sale is oversubscribed.
  349. * @dev nonReentrant modifier is used to prevent reentrancy attacks.
  350. * This function clears the user's contribution after claim to prevent double claim.
  351. */
  352. function claimTokens() public validClaimTime {
  353. require(block.timestamp > endTime, "Sale is still active");
  354. uint256 contributeAmount = userContributionAmount[msg.sender];
  355. require(contributeAmount > 0, "No tokens to claim");
  356. uint256 refundCost = 0;
  357. uint256 boughtToken = 0;
  358. if (totalContributionAmount <= contributionTarget) {
  359. uint256 netContribution = calculateNetContribution(contributeAmount);
  360. boughtToken = (netContribution * DECIMALS) / paymentTokenPrice;
  361. IERC20(saleTokenAddress).transfer(msg.sender, boughtToken);
  362. } else {
  363. uint256 userValidContribution = (contributeAmount * contributionTarget) / totalContributionAmount;
  364. refundCost = contributeAmount - userValidContribution;
  365. uint256 netContribution = calculateNetContribution(userValidContribution);
  366. boughtToken = (netContribution * DECIMALS) / paymentTokenPrice;
  367. if (paymentTokenAddress == address(0)) {
  368. (bool sent, ) = payable(msg.sender).call{ value: refundCost }("");
  369. require(sent, "ETH refund failed");
  370. } else {
  371. IERC20(paymentTokenAddress).transfer(msg.sender, refundCost);
  372. }
  373. IERC20(saleTokenAddress).transfer(msg.sender, boughtToken);
  374. }
  375. userContributionAmount[msg.sender] = 0;
  376. emit TokensClaimed(msg.sender, boughtToken, refundCost);
  377. }
  378. }