Launchpad.sol 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826
  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 "@openzeppelin/contracts/utils/Pausable.sol";
  6. import "prb-math/contracts/PRBMath.sol";
  7. import "hardhat/console.sol"; // For debugging purposes, can be removed in production
  8. contract Launchpad is ReentrancyGuard, Pausable {
  9. // ================================
  10. // Custom Errors
  11. // ================================
  12. error Unauthorized();
  13. error InvalidParameter(string reason);
  14. error InvalidState(string reason);
  15. error InsufficientAmount(uint256 required, uint256 provided);
  16. error TransferFailed();
  17. error SaleNotConfigured();
  18. // ================================
  19. // State Variables
  20. // ================================
  21. // Core Configuration
  22. address public owner;
  23. address public saleTokenAddress; // Sale token address
  24. uint256 public totalTokensForSale; // Total tokens for sale
  25. address public paymentTokenAddress; // Payment tokens accepted for the sale
  26. uint256 public paymentTokenPrice; // Payment token price in terms of sale token
  27. uint256 public startTime; // Beginning time of the sale
  28. uint256 public endTime; // Ending time of the sale
  29. bytes32 public merkleRoot; // Merkle root for whitelisting
  30. // Sale Status
  31. uint256 public contributionTarget; // Target amount to be raised in the sale
  32. uint256 public totalContributionAmount; // Total contributed amount so far
  33. uint256 public totalContributionAmountWithoutFee;
  34. uint256 public totalContributionFee;
  35. bool public claimEnabled; // Whether claiming tokens is enabled
  36. uint256 public claimStartTime; // Start time for claiming tokens
  37. // User Data
  38. mapping(address => uint256) public userContributionAmount; // User's total contribution amount (including fee)
  39. mapping(address => uint256) public userBuyAmount; // User's buy amount (excluding fee)
  40. mapping(address => uint256) public userFeeAmount; // User's fee amount
  41. mapping(address => bool) public userClaimed; // Whether user has claimed their tokens
  42. uint256 public accountsCount; // Number of accounts that have contributed
  43. // Fee Configuration
  44. uint256 public feeBps; // Fee in basis points (bps)
  45. uint256 public constant MAX_FEE_BPS = 10000; // 100% in basis points
  46. uint256 public constant DECIMALS = 1e18;
  47. // ================================
  48. // Events
  49. // ================================
  50. event SaleCreated(address saleTokenAddress, uint256 totalTokensForSale);
  51. event Contributed(address user, uint256 amount, address paymentTokenAddress);
  52. event TokensClaimed(address user, uint256 tokensClaimed, uint256 refundAmount);
  53. event SaleCancelled(address saleTokenAddress, uint256 refundedAmount);
  54. event EnableClaimToken(bool enabled, uint256 claimStartTime);
  55. event DisableClaimToken(bool disabled);
  56. event OwnerChanged(address oldOwner, address newOwner);
  57. event SalePaymentUpdated(address paymentTokenAddress, uint256 tokenPrice);
  58. event MerkleRootUpdated(bytes32 newMerkleRoot);
  59. event FeeUpdated(uint256 oldFeeBps, uint256 newFeeBps);
  60. event RemainingTokensWithdrawn(address owner, uint256 amount);
  61. event PaymentsWithdrawn(address owner, uint256 amount, bool isOversubscribed);
  62. event EmergencyWithdraw(address owner, uint256 ethAmount, address[] tokenAddresses);
  63. event ContractPaused(address account);
  64. event ContractUnpaused(address account);
  65. // ================================
  66. // Constructor
  67. // ================================
  68. constructor() {
  69. owner = msg.sender;
  70. accountsCount = 0;
  71. }
  72. // ================================
  73. // Validation Functions
  74. // ================================
  75. /**
  76. * @notice Validates basic sale parameters
  77. */
  78. function _validateSaleParameters(
  79. address _saleTokenAddress,
  80. uint256 _saleTokenSupply,
  81. uint256 _contributionTarget,
  82. uint256 _saleStartTime,
  83. uint256 _saleEndTime,
  84. uint256 _tokenPrice,
  85. uint256 _feeBasisPoints
  86. ) private pure {
  87. if (_saleTokenAddress == address(0)) revert InvalidParameter("Invalid token address");
  88. if (_saleTokenSupply == 0) revert InvalidParameter("Sale token supply must be greater than 0");
  89. if (_tokenPrice == 0) revert InvalidParameter("Price must be greater than 0");
  90. if (_contributionTarget == 0) revert InvalidParameter("Contribution target must be greater than 0");
  91. if (_saleStartTime >= _saleEndTime) revert InvalidParameter("Invalid time range");
  92. if (_feeBasisPoints > MAX_FEE_BPS) revert InvalidParameter("Fee cannot exceed 100%");
  93. }
  94. /**
  95. * @notice Validates contribution target matches expected calculation
  96. */
  97. function _validateContributionTarget(
  98. uint256 _saleTokenSupply,
  99. uint256 _tokenPrice,
  100. uint256 _contributionTarget
  101. ) private pure {
  102. uint256 expectedContributionTarget = PRBMath.mulDiv(_saleTokenSupply, _tokenPrice, 1e18);
  103. if (_contributionTarget != expectedContributionTarget) revert InvalidParameter("Contribution target must equal saleTokenSupply * price");
  104. }
  105. /**
  106. * @notice Validates sale state for cancellation
  107. */
  108. function _validateSaleCancellation() private view {
  109. if (block.timestamp >= startTime) revert InvalidState("Sale already started");
  110. if (saleTokenAddress == address(0)) revert SaleNotConfigured();
  111. }
  112. /**
  113. * @notice Validates claim enablement parameters
  114. */
  115. function _validateClaimEnablement(uint256 _claimStartTimestamp) private view {
  116. if (_claimStartTimestamp < block.timestamp) revert InvalidParameter("Enable claim time must be in the future");
  117. if (_claimStartTimestamp < endTime) revert InvalidParameter("Claiming tokens can only be enabled after the sale ends");
  118. }
  119. /**
  120. * @notice Validates withdrawal conditions
  121. */
  122. function _validateWithdrawalConditions() private view {
  123. if (block.timestamp <= endTime) revert InvalidState("Sale is still active");
  124. }
  125. /**
  126. * @notice Validates remaining token withdrawal conditions
  127. */
  128. function _validateRemainingTokenWithdrawal() private view {
  129. _validateWithdrawalConditions();
  130. if (totalContributionAmountWithoutFee >= contributionTarget) revert InvalidState("All sold out, cannot withdraw remaining tokens");
  131. }
  132. /**
  133. * @notice Validates contribution parameters for ETH
  134. */
  135. function _validateETHContribution(
  136. uint256 _contributionAmount,
  137. uint256 _maxContributionAmount
  138. ) private view {
  139. if (paymentTokenAddress != address(0)) revert InvalidState("Payment token must be ETH for this sale");
  140. if (_contributionAmount == 0) revert InvalidParameter("Must buy a positive amount");
  141. if (_contributionAmount > _maxContributionAmount) revert InvalidParameter("Buy amount exceeds max buy amount");
  142. if (msg.value < _contributionAmount) revert InsufficientAmount(_contributionAmount, msg.value);
  143. if (paymentTokenPrice == 0) revert InvalidState("Payment token not accepted for this sale");
  144. }
  145. /**
  146. * @notice Validates contribution parameters for ERC20
  147. */
  148. function _validateERC20Contribution(
  149. uint256 _contributionAmount,
  150. uint256 _maxContributionAmount
  151. ) private view {
  152. if (_contributionAmount == 0) revert InvalidParameter("Must send a positive amount");
  153. if (_contributionAmount > _maxContributionAmount) revert InvalidParameter("Buy amount exceeds max buy amount");
  154. uint256 allowance = IERC20(paymentTokenAddress).allowance(msg.sender, address(this));
  155. if (allowance < _contributionAmount) revert InsufficientAmount(_contributionAmount, allowance);
  156. }
  157. /**
  158. * @notice Validates Merkle proof
  159. */
  160. function _validateMerkleProof(
  161. uint256 _maxContributionAmount,
  162. bytes32[] memory _merkleProof
  163. ) private view {
  164. bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxContributionAmount))));
  165. if (!MerkleProof.verify(_merkleProof, merkleRoot, leaf)) revert InvalidParameter("Invalid proof");
  166. }
  167. /**
  168. * @notice Validates user contribution limits
  169. */
  170. function _validateUserContributionLimit(uint256 _maxContributionAmount) private view {
  171. if (userContributionAmount[msg.sender] > _maxContributionAmount) revert InvalidParameter("Buy amount exceeds max buy amount");
  172. }
  173. /**
  174. * @notice Validates claim conditions
  175. */
  176. function _validateClaimConditions() private view {
  177. if (block.timestamp <= endTime) revert InvalidState("Sale is still active");
  178. if (userContributionAmount[msg.sender] == 0) revert InvalidState("No tokens to claim");
  179. if (userClaimed[msg.sender]) revert InvalidState("Tokens already claimed");
  180. }
  181. /**
  182. * @notice Validates owner address
  183. */
  184. function _validateOwnerAddress(address _newOwnerAddress) private pure {
  185. if (_newOwnerAddress == address(0)) revert InvalidParameter("Invalid owner address");
  186. }
  187. /**
  188. * @notice Validates price parameter
  189. */
  190. function _validatePrice(uint256 _tokenPrice) private pure {
  191. if (_tokenPrice == 0) revert InvalidParameter("Price must be greater than 0");
  192. }
  193. /**
  194. * @notice Validates fee basis points
  195. */
  196. function _validateFeeBps(uint256 _feeBasisPoints) private pure {
  197. if (_feeBasisPoints > MAX_FEE_BPS) revert InvalidParameter("Fee cannot exceed 100%");
  198. }
  199. // ================================
  200. // Modifiers
  201. // ================================
  202. modifier onlyOwner() {
  203. if (msg.sender != owner) revert Unauthorized();
  204. _;
  205. }
  206. modifier saleActive() {
  207. if (merkleRoot == bytes32(0)) revert SaleNotConfigured();
  208. if (block.timestamp < startTime || block.timestamp > endTime) revert InvalidState("Sale is not active");
  209. _;
  210. }
  211. modifier validClaimTime() {
  212. if (!claimEnabled) revert InvalidState("Claiming tokens is not enabled");
  213. if (block.timestamp < claimStartTime) revert InvalidState("Claiming tokens not started yet");
  214. _;
  215. }
  216. // ================================
  217. // Owner Functions
  218. // ================================
  219. /**
  220. * @notice Creates a new token sale with specified parameters.
  221. * @param _saleTokenAddress The address of the token being sold.
  222. * @param _saleTokenSupply The total supply of tokens allocated for this sale.
  223. * @param _contributionTarget The fundraising target amount (not including fee).
  224. * @param _saleStartTime The start time of the sale (timestamp).
  225. * @param _saleEndTime The end time of the sale (timestamp).
  226. * @param _paymentTokenAddress The address of the payment token (or address(0) for ETH).
  227. * @param _tokenPrice The price per token in payment token units.
  228. * @param _feeBasisPoints The fee in basis points (1/100 of a percent).
  229. */
  230. function createSale(
  231. address _saleTokenAddress,
  232. uint256 _saleTokenSupply,
  233. uint256 _contributionTarget,
  234. uint256 _saleStartTime,
  235. uint256 _saleEndTime,
  236. address _paymentTokenAddress,
  237. uint256 _tokenPrice,
  238. uint256 _feeBasisPoints
  239. ) public onlyOwner {
  240. // Validate all sale parameters
  241. _validateSaleParameters(_saleTokenAddress, _saleTokenSupply, _contributionTarget, _saleStartTime, _saleEndTime, _tokenPrice, _feeBasisPoints);
  242. // Validate parameter relationships
  243. // contributionTarget = saleTokenSupply × price
  244. // Note: _saleTokenSupply and _tokenPrice both contain 18 decimals, product needs to be divided by 1e18
  245. _validateContributionTarget(_saleTokenSupply, _tokenPrice, _contributionTarget);
  246. paymentTokenAddress = _paymentTokenAddress;
  247. paymentTokenPrice = _tokenPrice;
  248. saleTokenAddress = _saleTokenAddress;
  249. contributionTarget = _contributionTarget;
  250. totalTokensForSale = _saleTokenSupply;
  251. startTime = _saleStartTime;
  252. endTime = _saleEndTime;
  253. totalContributionAmount = 0;
  254. feeBps = _feeBasisPoints; // Set the fee in basis points
  255. merkleRoot = bytes32(0); // Reset Merkle root
  256. uint256 currentBalance = IERC20(_saleTokenAddress).balanceOf(address(this));
  257. if (currentBalance < _saleTokenSupply) {
  258. uint256 needToTransfer = _saleTokenSupply - currentBalance;
  259. IERC20(_saleTokenAddress).transferFrom(msg.sender, address(this), needToTransfer);
  260. }
  261. emit SaleCreated(saleTokenAddress, totalTokensForSale);
  262. }
  263. /**
  264. * @notice Cancels the sale before it starts and refunds all tokens to the owner.
  265. */
  266. function cancelSale() public onlyOwner {
  267. _validateSaleCancellation();
  268. uint256 balance = IERC20(saleTokenAddress).balanceOf(address(this));
  269. if (balance == 0) revert InvalidState("No tokens to refund");
  270. IERC20(saleTokenAddress).transfer(owner, balance); // Refund tokens to owner
  271. saleTokenAddress = address(0);
  272. totalTokensForSale = 0;
  273. totalContributionAmount = 0;
  274. startTime = 0;
  275. endTime = 0;
  276. merkleRoot = bytes32(0); // Reset Merkle root
  277. emit SaleCancelled(saleTokenAddress, balance);
  278. }
  279. /**
  280. * @notice Enables claiming of purchased tokens after the sale ends.
  281. * @param _claimStartTimestamp The timestamp when claiming is enabled.
  282. */
  283. function enableClaimTokens(uint256 _claimStartTimestamp) public onlyOwner {
  284. _validateClaimEnablement(_claimStartTimestamp);
  285. claimEnabled = true;
  286. claimStartTime = _claimStartTimestamp; // Set claim start time to now
  287. emit EnableClaimToken(claimEnabled, claimStartTime);
  288. }
  289. /**
  290. * @notice Disables claiming of purchased tokens.
  291. */
  292. function disableClaimTokens() public onlyOwner {
  293. if (!claimEnabled) revert InvalidState("Claiming tokens is already disabled");
  294. claimEnabled = false;
  295. emit DisableClaimToken(claimEnabled);
  296. }
  297. /**
  298. * @notice Withdraws any remaining unsold tokens to the owner after the sale ends.
  299. */
  300. function withdrawRemainingTokens() public onlyOwner {
  301. _validateRemainingTokenWithdrawal();
  302. // Calculate sold tokens using PRBMath to prevent overflow and precision loss
  303. uint256 soldTokens = PRBMath.mulDiv(totalTokensForSale, totalContributionAmountWithoutFee, contributionTarget);
  304. uint256 remainingAmount = totalTokensForSale - soldTokens;
  305. IERC20(saleTokenAddress).transfer(owner, remainingAmount);
  306. emit RemainingTokensWithdrawn(owner, remainingAmount);
  307. }
  308. /**
  309. * @notice Withdraws payment tokens from the contract to the owner.
  310. * In case of oversubscription, withdraws the total amount for sold tokens plus fees.
  311. * In normal subscription, withdraws all contributions.
  312. */
  313. function withdrawPayments() public onlyOwner {
  314. _validateWithdrawalConditions();
  315. uint256 withdrawAmount = 0;
  316. // Use the new state variables to determine oversubscription
  317. bool isOversubscribed = totalContributionAmountWithoutFee > contributionTarget;
  318. // Check if oversubscribed
  319. if (isOversubscribed) {
  320. // Oversubscribed: all tokens are sold
  321. // Withdraw fixed amount: target amount + target fee
  322. uint256 targetFee = PRBMath.mulDiv(contributionTarget, feeBps, MAX_FEE_BPS);
  323. withdrawAmount = contributionTarget + targetFee;
  324. if (paymentTokenAddress == address(0)) {
  325. // Native token (ETH)
  326. if (withdrawAmount > 0 && address(this).balance >= withdrawAmount) {
  327. payable(owner).transfer(withdrawAmount);
  328. }
  329. } else {
  330. // ERC20 token
  331. uint256 balance = IERC20(paymentTokenAddress).balanceOf(address(this));
  332. if (withdrawAmount > 0 && balance >= withdrawAmount) {
  333. IERC20(paymentTokenAddress).transfer(owner, withdrawAmount);
  334. }
  335. }
  336. // Keep in contract:
  337. // Refund portion: totalContributionAmount - withdrawAmount
  338. } else {
  339. // Normal subscription: withdraw all contributions
  340. if (paymentTokenAddress == address(0)) {
  341. // Native token (ETH)
  342. withdrawAmount = address(this).balance;
  343. if (withdrawAmount > 0) {
  344. payable(owner).transfer(withdrawAmount);
  345. }
  346. } else {
  347. // ERC20 token
  348. withdrawAmount = IERC20(paymentTokenAddress).balanceOf(address(this));
  349. if (withdrawAmount > 0) {
  350. IERC20(paymentTokenAddress).transfer(owner, withdrawAmount);
  351. }
  352. }
  353. }
  354. emit PaymentsWithdrawn(owner, withdrawAmount, isOversubscribed);
  355. }
  356. /**
  357. * @notice Emergency function to withdraw all assets from the contract.
  358. * This function withdraws all ETH and all ERC20 tokens that the contract holds.
  359. * Only callable by the owner in emergency situations.
  360. * @param _tokenAddresses Array of ERC20 token addresses to withdraw.
  361. */
  362. function emergencyWithdrawAll(address[] memory _tokenAddresses) public onlyOwner {
  363. // Withdraw all ETH
  364. uint256 ethBalance = address(this).balance;
  365. if (ethBalance > 0) {
  366. (bool success, ) = payable(owner).call{value: ethBalance}("");
  367. if (!success) revert TransferFailed();
  368. }
  369. // Withdraw all specified ERC20 tokens
  370. for (uint256 i = 0; i < _tokenAddresses.length; i++) {
  371. address tokenAddress = _tokenAddresses[i];
  372. if (tokenAddress != address(0)) {
  373. uint256 tokenBalance = IERC20(tokenAddress).balanceOf(address(this));
  374. if (tokenBalance > 0) {
  375. IERC20(tokenAddress).transfer(owner, tokenBalance);
  376. }
  377. }
  378. }
  379. emit EmergencyWithdraw(owner, ethBalance, _tokenAddresses);
  380. }
  381. // ================================
  382. // Configuration Functions
  383. // ================================
  384. /**
  385. * @notice Transfers contract ownership to a new address.
  386. * @param _newOwnerAddress The address of the new owner.
  387. */
  388. function setOwner(address _newOwnerAddress) public onlyOwner {
  389. _validateOwnerAddress(_newOwnerAddress);
  390. address oldOwner = owner;
  391. owner = _newOwnerAddress;
  392. emit OwnerChanged(oldOwner, _newOwnerAddress);
  393. }
  394. /**
  395. * @notice Sets the payment token and price for the sale.
  396. * @param _paymentTokenAddress The address of the payment token.
  397. * @param _tokenPrice The price per token in payment token units.
  398. */
  399. function setSalePayment(address _paymentTokenAddress, uint256 _tokenPrice) public onlyOwner {
  400. _validatePrice(_tokenPrice);
  401. paymentTokenAddress = _paymentTokenAddress;
  402. paymentTokenPrice = _tokenPrice;
  403. emit SalePaymentUpdated(_paymentTokenAddress, _tokenPrice);
  404. }
  405. /**
  406. * @notice Sets the Merkle root for whitelist verification.
  407. * @param _newMerkleRoot The new Merkle root.
  408. */
  409. function setMerkleRoot(bytes32 _newMerkleRoot) public onlyOwner {
  410. merkleRoot = _newMerkleRoot;
  411. emit MerkleRootUpdated(_newMerkleRoot);
  412. }
  413. /**
  414. * @notice Sets the fee in basis points.
  415. * @param _feeBasisPoints The fee in basis points (max 10000).
  416. */
  417. function setFeeBps(uint256 _feeBasisPoints) public onlyOwner {
  418. _validateFeeBps(_feeBasisPoints);
  419. uint256 oldFeeBps = feeBps;
  420. feeBps = _feeBasisPoints; // Set the fee in basis points
  421. emit FeeUpdated(oldFeeBps, _feeBasisPoints);
  422. }
  423. /**
  424. * @notice Pauses the contract, preventing most operations.
  425. * @dev Only the owner can pause the contract.
  426. */
  427. function pause() public onlyOwner {
  428. _pause();
  429. emit ContractPaused(msg.sender);
  430. }
  431. /**
  432. * @notice Unpauses the contract, allowing normal operations to resume.
  433. * @dev Only the owner can unpause the contract.
  434. */
  435. function unpause() public onlyOwner {
  436. _unpause();
  437. emit ContractUnpaused(msg.sender);
  438. }
  439. // ================================
  440. // Getter Functions
  441. // ================================
  442. /**
  443. * @notice Returns the address of the contract owner.
  444. */
  445. function getOwner() public view returns (address) {
  446. return owner;
  447. }
  448. /**
  449. * @notice Returns the address of the sale token.
  450. */
  451. function getSaleToken() public view returns (address) {
  452. return saleTokenAddress;
  453. }
  454. /**
  455. * @notice Returns the total number of tokens for sale.
  456. */
  457. function getTotalTokensForSale() public view returns (uint256) {
  458. return totalTokensForSale;
  459. }
  460. /**
  461. * @notice Returns the total contributed amount so far.
  462. */
  463. function getTotalContributeAmount() public view returns (uint256) {
  464. return totalContributionAmount;
  465. }
  466. /**
  467. * @notice Returns the sale start time.
  468. */
  469. function getStartTime() public view returns (uint256) {
  470. return startTime;
  471. }
  472. /**
  473. * @notice Returns the sale end time.
  474. */
  475. function getEndTime() public view returns (uint256) {
  476. return endTime;
  477. }
  478. /**
  479. * @notice Returns the payment token price.
  480. */
  481. function getPaymentTokenPrice() public view returns (uint256) {
  482. return paymentTokenPrice;
  483. }
  484. /**
  485. * @notice Returns the payment token address.
  486. */
  487. function getPaymentTokens() public view returns (address) {
  488. return paymentTokenAddress;
  489. }
  490. /**
  491. * @notice Returns the claim start time.
  492. */
  493. function getClaimStartTime() public view returns (uint256) {
  494. return claimStartTime;
  495. }
  496. /**
  497. * @notice Returns whether claiming is enabled.
  498. */
  499. function isClaimEnabled() public view returns (bool) {
  500. return claimEnabled;
  501. }
  502. /**
  503. * @notice Returns all sale details as a tuple.
  504. */
  505. function getSaleDetails()
  506. public
  507. view
  508. returns (
  509. address _saleTokenAddress,
  510. uint256 _totalTokensForSale,
  511. uint256 _totalContributionAmount,
  512. uint256 _contributionTarget,
  513. uint256 _startTime,
  514. uint256 _endTime,
  515. bytes32 _merkleRoot,
  516. address _paymentTokenAddress,
  517. uint256 _paymentTokenPrice,
  518. uint256 _feeBps
  519. )
  520. {
  521. return (
  522. saleTokenAddress,
  523. totalTokensForSale,
  524. totalContributionAmount,
  525. contributionTarget,
  526. startTime,
  527. endTime,
  528. merkleRoot,
  529. paymentTokenAddress,
  530. paymentTokenPrice,
  531. feeBps
  532. );
  533. }
  534. /**
  535. * @notice Returns the claimable tokens for a user.
  536. * @param _user The address of the user.
  537. */
  538. function getClaimableTokens(address _user) public view returns (uint256) {
  539. return userContributionAmount[_user];
  540. }
  541. /**
  542. * @notice Returns the user's buy amount (excluding fee).
  543. * @param _user The address of the user.
  544. */
  545. function getUserBuyAmount(address _user) public view returns (uint256) {
  546. return userBuyAmount[_user];
  547. }
  548. /**
  549. * @notice Returns the user's fee amount.
  550. * @param _user The address of the user.
  551. */
  552. function getUserFeeAmount(address _user) public view returns (uint256) {
  553. return userFeeAmount[_user];
  554. }
  555. /**
  556. * @notice Returns whether the user has claimed their tokens.
  557. * @param _user The address of the user.
  558. */
  559. function hasUserClaimed(address _user) public view returns (bool) {
  560. return userClaimed[_user];
  561. }
  562. /**
  563. * @notice Returns whether the contract is currently paused.
  564. * @return True if the contract is paused, false otherwise.
  565. */
  566. function isPaused() public view returns (bool) {
  567. return paused();
  568. }
  569. // ================================
  570. // Calculation Functions
  571. // ================================
  572. /**
  573. * @notice Calculates the maximum contribution amount for a user (including fee).
  574. * @param _maxTokenBuyAmount The maximum amount of tokens a user can buy.
  575. * @return The maximum contribution amount including fee.
  576. */
  577. function calculateMaxContributionAmount(uint256 _maxTokenBuyAmount) public view returns (uint256) {
  578. // Calculate buy amount value using PRBMath for high precision
  579. uint256 buyAmountValue = PRBMath.mulDiv(_maxTokenBuyAmount, paymentTokenPrice, 1e18);
  580. // Calculate total contribution amount (buy amount + fee) using PRBMath to prevent overflow
  581. // Total = buyAmount * (1 + feeBps/MAX_FEE_BPS) = buyAmount * (MAX_FEE_BPS + feeBps) / MAX_FEE_BPS
  582. return PRBMath.mulDiv(buyAmountValue, MAX_FEE_BPS + feeBps, MAX_FEE_BPS);
  583. }
  584. /**
  585. * @notice Calculates the breakdown of a total payment amount into token purchase amount and platform fee.
  586. * @param _totalPaymentAmount The total amount paid by user (including platform fee).
  587. * @return _tokenPurchaseAmount The amount used to purchase tokens (excluding fee).
  588. * @return _platformFeeAmount The platform fee amount.
  589. */
  590. function calculateContributionBreakdown(uint256 _totalPaymentAmount) public view returns (uint256 _tokenPurchaseAmount, uint256 _platformFeeAmount) {
  591. // Platform fee is calculated based on token purchase amount, not total payment
  592. // Total payment = token purchase amount + platform fee
  593. // Platform fee = token purchase amount * feeBps / MAX_FEE_BPS
  594. // So: total payment = token purchase amount + (token purchase amount * feeBps / MAX_FEE_BPS)
  595. // Token purchase amount = total payment / (1 + feeBps / MAX_FEE_BPS)
  596. // Using PRBMath for high precision calculation
  597. _tokenPurchaseAmount = PRBMath.mulDiv(_totalPaymentAmount, MAX_FEE_BPS, MAX_FEE_BPS + feeBps);
  598. _platformFeeAmount = _totalPaymentAmount - _tokenPurchaseAmount;
  599. return (_tokenPurchaseAmount, _platformFeeAmount);
  600. }
  601. /**
  602. * @notice Calculates the net contribution after deducting the fee.
  603. * @param _contributionAmount The original contribution amount.
  604. * @return The net contribution after fee deduction.
  605. */
  606. function calculateNetContribution(uint256 _contributionAmount) public view returns (uint256) {
  607. // Fee is calculated based on buy amount, not total contribution
  608. // Use same logic as calculateContributionBreakdown
  609. // Using PRBMath for high precision calculation
  610. uint256 buyAmount = PRBMath.mulDiv(_contributionAmount, MAX_FEE_BPS, MAX_FEE_BPS + feeBps);
  611. return buyAmount;
  612. }
  613. // ================================
  614. // Main Functions
  615. // ================================
  616. /**
  617. * @notice Contribute to the sale using ETH. Requires whitelist proof.
  618. * @param _contributionAmount The amount of ETH to contribute.
  619. * @param _maxContributionAmount The maximum allowed contribution for the user.
  620. * @param _merkleProof The Merkle proof for whitelist verification.
  621. * @dev nonReentrant modifier is used to prevent reentrancy attacks.
  622. */
  623. function contributeWithETH(
  624. uint256 _contributionAmount,
  625. uint256 _maxContributionAmount,
  626. bytes32[] memory _merkleProof
  627. ) public payable saleActive nonReentrant whenNotPaused {
  628. // Validate ETH contribution parameters
  629. _validateETHContribution(_contributionAmount, _maxContributionAmount);
  630. // Verify Merkle proof
  631. _validateMerkleProof(_maxContributionAmount, _merkleProof);
  632. // Calculate breakdown of total payment amount into token purchase payment and platform fee
  633. (uint256 buyAmount, uint256 feeAmount) = calculateContributionBreakdown(_contributionAmount);
  634. // Account count statistics
  635. if (userContributionAmount[msg.sender] == 0) {
  636. accountsCount++; // Increment accounts count only if this is the first contribution
  637. }
  638. // Payment amount statistics - separate token purchase payment and platform fee
  639. userContributionAmount[msg.sender] += _contributionAmount; // update user's total payment amount
  640. userBuyAmount[msg.sender] += buyAmount; // update user's payment amount for token purchase (excluding fee)
  641. userFeeAmount[msg.sender] += feeAmount; // update user's platform fee payment amount
  642. _validateUserContributionLimit(_maxContributionAmount);
  643. totalContributionAmount += _contributionAmount; // update total payment amount
  644. totalContributionAmountWithoutFee += buyAmount; // update total payment amount for token purchase (excluding fees)
  645. totalContributionFee += feeAmount; // update total platform fee amount
  646. if (msg.value > _contributionAmount) {
  647. payable(msg.sender).transfer(msg.value - _contributionAmount); //refund excess ETH
  648. }
  649. emit Contributed(msg.sender, _contributionAmount, address(0));
  650. }
  651. /**
  652. * @notice Contribute to the sale using ERC20 tokens. Requires whitelist proof.
  653. * @param _contributionAmount The amount of tokens to contribute.
  654. * @param _maxContributionAmount The maximum allowed contribution for the user.
  655. * @param _merkleProof The Merkle proof for whitelist verification.
  656. * @dev nonReentrant modifier is used to prevent reentrancy attacks.
  657. */
  658. function contributeWithERC20(
  659. uint256 _contributionAmount,
  660. uint256 _maxContributionAmount,
  661. bytes32[] memory _merkleProof
  662. ) public saleActive nonReentrant whenNotPaused {
  663. // Validate ERC20 contribution parameters
  664. _validateERC20Contribution(_contributionAmount, _maxContributionAmount);
  665. // Verify Merkle proof
  666. _validateMerkleProof(_maxContributionAmount, _merkleProof);
  667. IERC20(paymentTokenAddress).transferFrom(msg.sender, address(this), _contributionAmount); // Transfer payment tokens from user account
  668. // Calculate breakdown of total payment amount into token purchase payment and platform fee
  669. (uint256 buyAmount, uint256 feeAmount) = calculateContributionBreakdown(_contributionAmount);
  670. if (userContributionAmount[msg.sender] == 0) {
  671. accountsCount++; // Increment accounts count only if this is the first contribution
  672. }
  673. userContributionAmount[msg.sender] += _contributionAmount; // update user's total payment amount
  674. userBuyAmount[msg.sender] += buyAmount; // update user's payment amount for token purchase (excluding fee)
  675. userFeeAmount[msg.sender] += feeAmount; // update user's platform fee payment amount
  676. _validateUserContributionLimit(_maxContributionAmount);
  677. totalContributionAmount += _contributionAmount; // update total payment amount
  678. totalContributionAmountWithoutFee += buyAmount; // update total payment amount for token purchase (excluding fees)
  679. totalContributionFee += feeAmount; // update total platform fee amount
  680. emit Contributed(msg.sender, _contributionAmount, paymentTokenAddress);
  681. }
  682. /**
  683. * @notice Claim purchased tokens and receive refund if sale is oversubscribed.
  684. * @dev nonReentrant modifier is used to prevent reentrancy attacks.
  685. * This function clears the user's contribution after claim to prevent double claim.
  686. */
  687. function claimTokens() public validClaimTime whenNotPaused {
  688. _validateClaimConditions();
  689. uint256 contributeAmount = userContributionAmount[msg.sender];
  690. uint256 userBuyAmountValue = userBuyAmount[msg.sender];
  691. uint256 userFeeAmountValue = userFeeAmount[msg.sender];
  692. uint256 refundCost = 0;
  693. uint256 boughtToken = 0;
  694. if (totalContributionAmountWithoutFee <= contributionTarget) {
  695. // Normal subscription: use pre-calculated buy amount
  696. // Calculate token amount using PRBMath for high precision
  697. boughtToken = PRBMath.mulDiv(userBuyAmountValue, 1e18, paymentTokenPrice);
  698. IERC20(saleTokenAddress).transfer(msg.sender, boughtToken);
  699. } else {
  700. // Oversubscribed: all tokens are sold, user gets proportional share based on buy amount
  701. // Use the new state variable for total buy amount
  702. uint256 totalBuyAmount = totalContributionAmountWithoutFee;
  703. // User gets proportional share of all tokens based on their buy amount using PRBMath
  704. boughtToken = PRBMath.mulDiv(userBuyAmountValue, totalTokensForSale, totalBuyAmount);
  705. // Calculate how much the user actually pays for these tokens using PRBMath
  706. uint256 userEffectiveBuyAmount = PRBMath.mulDiv(boughtToken, paymentTokenPrice, 1e18);
  707. uint256 userEffectiveFeeAmount = PRBMath.mulDiv(userEffectiveBuyAmount, feeBps, MAX_FEE_BPS);
  708. // Calculate refund breakdown: both unused buy amount and unused fee
  709. uint256 refundBuyAmount = userBuyAmountValue - userEffectiveBuyAmount;
  710. uint256 refundFeeAmount = userFeeAmountValue - userEffectiveFeeAmount;
  711. // Use unchecked for addition since both values are guaranteed to be positive
  712. unchecked {
  713. refundCost = refundBuyAmount + refundFeeAmount;
  714. }
  715. // Refund
  716. if (paymentTokenAddress == address(0)) {
  717. (bool sent, ) = payable(msg.sender).call{ value: refundCost }("");
  718. if (!sent) revert TransferFailed();
  719. } else {
  720. IERC20(paymentTokenAddress).transfer(msg.sender, refundCost);
  721. }
  722. // Transfer tokens
  723. IERC20(saleTokenAddress).transfer(msg.sender, boughtToken);
  724. }
  725. // Mark user as claimed and clear user data
  726. userClaimed[msg.sender] = true;
  727. userContributionAmount[msg.sender] = 0;
  728. userBuyAmount[msg.sender] = 0;
  729. userFeeAmount[msg.sender] = 0;
  730. emit TokensClaimed(msg.sender, boughtToken, refundCost);
  731. }
  732. }