Launchpad.sol 30 KB

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