Launchpad.sol 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  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 "prb-math/contracts/PRBMath.sol";
  6. import "hardhat/console.sol"; // For debugging purposes, can be removed in production
  7. contract Launchpad is ReentrancyGuard {
  8. // ================================
  9. // State Variables
  10. // ================================
  11. // Core Configuration
  12. address public owner;
  13. address public saleTokenAddress; // Sale token address
  14. uint256 public totalTokensForSale; // Total tokens for sale
  15. address public paymentTokenAddress; // Payment tokens accepted for the sale
  16. uint256 public paymentTokenPrice; // Payment token price in terms of sale token
  17. uint256 public startTime; // Beginning time of the sale
  18. uint256 public endTime; // Ending time of the sale
  19. bytes32 public merkleRoot; // Merkle root for whitelisting
  20. // Sale Status
  21. uint256 public contributionTarget; // Target amount to be raised in the sale
  22. uint256 public totalContributionAmount; // Total contributed amount so far
  23. bool public claimEnabled; // Whether claiming tokens is enabled
  24. uint256 public claimStartTime; // Start time for claiming tokens
  25. // User Data
  26. mapping(address => uint256) public userContributionAmount; // User's total contribution amount (including fee)
  27. mapping(address => uint256) public userBuyAmount; // User's buy amount (excluding fee)
  28. mapping(address => uint256) public userFeeAmount; // User's fee amount
  29. uint256 public accountsCount; // Number of accounts that have contributed
  30. // Fee Configuration
  31. uint256 public feeBps; // Fee in basis points (bps)
  32. uint256 public constant MAX_FEE_BPS = 10000; // 100% in basis points
  33. uint256 public constant DECIMALS = 1e18;
  34. // ================================
  35. // Events
  36. // ================================
  37. event SaleCreated(address saleTokenAddress, uint256 totalTokensForSale);
  38. event Contributed(address user, uint256 amount, address paymentTokenAddress);
  39. event TokensClaimed(address user, uint256 tokensClaimed, uint256 refundAmount);
  40. event SaleCancelled(address saleTokenAddress, uint256 refundedAmount);
  41. event EnableClaimToken(bool enabled, uint256 claimStartTime);
  42. event DisableClaimToken(bool disabled);
  43. // ================================
  44. // Constructor
  45. // ================================
  46. constructor() {
  47. owner = msg.sender;
  48. accountsCount = 0;
  49. }
  50. // ================================
  51. // Modifiers
  52. // ================================
  53. modifier onlyOwner() {
  54. require(msg.sender == owner, "Only owner can call this function");
  55. _;
  56. }
  57. modifier saleActive() {
  58. require(merkleRoot != bytes32(0), "MerkleRoot not initialized");
  59. require(block.timestamp >= startTime && block.timestamp <= endTime, "Sale is not active");
  60. _;
  61. }
  62. modifier validClaimTime() {
  63. require(claimEnabled, "Claiming tokens is not enabled");
  64. require(block.timestamp >= claimStartTime, "Claiming tokens not started yet");
  65. _;
  66. }
  67. // ================================
  68. // Owner Functions
  69. // ================================
  70. /**
  71. * @notice Creates a new token sale with specified parameters.
  72. * @param _token The address of the token being sold.
  73. * @param _totalTokensForSale The total number of tokens available for sale.
  74. * @param _contributionTarget The fundraising target amount (not including fee).
  75. * @param _startTime The start time of the sale (timestamp).
  76. * @param _endTime The end time of the sale (timestamp).
  77. * @param _paymentToken The address of the payment token (or address(0) for ETH).
  78. * @param _price The price per token in payment token units.
  79. * @param _feeBps The fee in basis points (1/100 of a percent).
  80. */
  81. function createSale(
  82. address _token,
  83. uint256 _totalTokensForSale,
  84. uint256 _contributionTarget,
  85. uint256 _startTime,
  86. uint256 _endTime,
  87. address _paymentToken,
  88. uint256 _price,
  89. uint256 _feeBps
  90. ) public onlyOwner {
  91. require(_token != address(0), "Invalid token address");
  92. require(_totalTokensForSale > 0, "Total tokens must be greater than 0");
  93. require(_price > 0, "Price must be greater than 0");
  94. require(_contributionTarget > 0, "Contribution target must be greater than 0");
  95. require(_startTime < _endTime, "Invalid time range");
  96. require(_feeBps <= MAX_FEE_BPS, "Fee cannot exceed 100%");
  97. // Validate parameter relationships
  98. // contributionTarget = totalTokensForSale × price
  99. // Note: _totalTokensForSale and _price both contain 18 decimals, product needs to be divided by 1e18
  100. uint256 expectedContributionTarget = (_totalTokensForSale * _price) / 1e18;
  101. require(_contributionTarget == expectedContributionTarget,
  102. "Contribution target must equal totalTokensForSale * price");
  103. paymentTokenAddress = _paymentToken;
  104. paymentTokenPrice = _price;
  105. saleTokenAddress = _token;
  106. contributionTarget = _contributionTarget;
  107. totalTokensForSale = _totalTokensForSale;
  108. startTime = _startTime;
  109. endTime = _endTime;
  110. totalContributionAmount = 0;
  111. feeBps = _feeBps; // Set the fee in basis points
  112. merkleRoot = bytes32(0); // Reset Merkle root
  113. uint256 currentBalance = IERC20(_token).balanceOf(address(this));
  114. if (currentBalance < _totalTokensForSale) {
  115. uint256 needToTransfer = _totalTokensForSale - currentBalance;
  116. IERC20(_token).transferFrom(msg.sender, address(this), needToTransfer);
  117. }
  118. emit SaleCreated(saleTokenAddress, totalTokensForSale);
  119. }
  120. /**
  121. * @notice Cancels the sale before it starts and refunds all tokens to the owner.
  122. */
  123. function cancelSale() public onlyOwner {
  124. require(block.timestamp < startTime, "Sale already started");
  125. require(saleTokenAddress != address(0), "Sale not initialized");
  126. uint256 balance = IERC20(saleTokenAddress).balanceOf(address(this));
  127. require(balance > 0, "No tokens to refund");
  128. IERC20(saleTokenAddress).transfer(owner, balance); // Refund tokens to owner
  129. saleTokenAddress = address(0);
  130. totalTokensForSale = 0;
  131. totalContributionAmount = 0;
  132. startTime = 0;
  133. endTime = 0;
  134. merkleRoot = bytes32(0); // Reset Merkle root
  135. emit SaleCancelled(saleTokenAddress, balance);
  136. }
  137. /**
  138. * @notice Enables claiming of purchased tokens after the sale ends.
  139. * @param _enableClaimTokenTime The timestamp when claiming is enabled.
  140. */
  141. function enableClaimTokens(uint256 _enableClaimTokenTime) public onlyOwner {
  142. require(_enableClaimTokenTime >= block.timestamp, "Enable claim time must be in the future");
  143. require(_enableClaimTokenTime >= endTime, "Claiming tokens can only be enabled after the sale ends");
  144. claimEnabled = true;
  145. claimStartTime = _enableClaimTokenTime; // Set claim start time to now
  146. emit EnableClaimToken(claimEnabled, claimStartTime);
  147. }
  148. /**
  149. * @notice Disables claiming of purchased tokens.
  150. */
  151. function disableClaimTokens() public onlyOwner {
  152. require(claimEnabled, "Claiming tokens is already disabled");
  153. claimEnabled = false;
  154. emit DisableClaimToken(claimEnabled);
  155. }
  156. /**
  157. * @notice Withdraws any remaining unsold tokens to the owner after the sale ends.
  158. */
  159. function withdrawRemainingTokens() public onlyOwner {
  160. require(block.timestamp > endTime, "Sale is still active");
  161. require(totalContributionAmount<contributionTarget,"all sold out, cannot withdraw remaining tokens");
  162. uint256 remainingAmount = totalTokensForSale - totalTokensForSale * totalContributionAmount / contributionTarget;
  163. IERC20(saleTokenAddress).transfer(owner, remainingAmount);
  164. }
  165. /**
  166. * @notice Withdraws payment tokens from the contract to the owner.
  167. * In case of oversubscription, withdraws the total amount for sold tokens plus fees.
  168. * In normal subscription, withdraws all contributions.
  169. */
  170. function withdrawPayments() public onlyOwner {
  171. require(block.timestamp > endTime, "Sale is still active");
  172. // Check if oversubscribed
  173. if (totalContributionAmount > contributionTarget) {
  174. // Oversubscribed: all tokens are sold
  175. // Total amount to withdraw = sold tokens * price + fees using PRBMath
  176. uint256 soldTokensValue = PRBMath.mulDiv(totalTokensForSale, paymentTokenPrice, 1e18);
  177. uint256 feeAmount = PRBMath.mulDiv(soldTokensValue, feeBps, MAX_FEE_BPS);
  178. uint256 withdrawAmount = soldTokensValue + feeAmount;
  179. if (paymentTokenAddress == address(0)) {
  180. // Native token (ETH)
  181. if (withdrawAmount > 0 && address(this).balance >= withdrawAmount) {
  182. payable(owner).transfer(withdrawAmount);
  183. }
  184. } else {
  185. // ERC20 token
  186. uint256 balance = IERC20(paymentTokenAddress).balanceOf(address(this));
  187. if (withdrawAmount > 0 && balance >= withdrawAmount) {
  188. IERC20(paymentTokenAddress).transfer(owner, withdrawAmount);
  189. } else if (withdrawAmount > 0 && balance > 0) {
  190. // Handle small precision differences
  191. IERC20(paymentTokenAddress).transfer(owner, balance);
  192. }
  193. }
  194. // Keep in contract:
  195. // Refund portion: totalContributionAmount - withdrawAmount
  196. } else {
  197. // Normal subscription: withdraw all contributions
  198. if (paymentTokenAddress == address(0)) {
  199. // Native token (ETH)
  200. if (address(this).balance > 0) {
  201. payable(owner).transfer(address(this).balance);
  202. }
  203. } else {
  204. // ERC20 token
  205. uint256 balance = IERC20(paymentTokenAddress).balanceOf(address(this));
  206. if (balance > 0) {
  207. IERC20(paymentTokenAddress).transfer(owner, balance);
  208. }
  209. }
  210. }
  211. }
  212. // ================================
  213. // Configuration Functions
  214. // ================================
  215. /**
  216. * @notice Transfers contract ownership to a new address.
  217. * @param _newOwner The address of the new owner.
  218. */
  219. function setOwner(address _newOwner) public onlyOwner {
  220. require(_newOwner != address(0), "Invalid owner address");
  221. owner = _newOwner;
  222. }
  223. /**
  224. * @notice Sets the payment token and price for the sale.
  225. * @param _paymentToken The address of the payment token.
  226. * @param _price The price per token in payment token units.
  227. */
  228. function setSalePayment(address _paymentToken, uint256 _price) public onlyOwner {
  229. require(_price > 0, "Price must be greater than 0");
  230. paymentTokenAddress = _paymentToken;
  231. paymentTokenPrice = _price;
  232. }
  233. /**
  234. * @notice Sets the Merkle root for whitelist verification.
  235. * @param _merkleRoot The new Merkle root.
  236. */
  237. function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner {
  238. merkleRoot = _merkleRoot;
  239. }
  240. /**
  241. * @notice Sets the fee in basis points.
  242. * @param _feeBps The fee in basis points (max 10000).
  243. */
  244. function setFeeBps(uint256 _feeBps) public onlyOwner {
  245. require(_feeBps <= MAX_FEE_BPS, "Fee cannot exceed 100%");
  246. feeBps = _feeBps; // Set the fee in basis points
  247. }
  248. // ================================
  249. // Getter Functions
  250. // ================================
  251. /**
  252. * @notice Returns the address of the contract owner.
  253. */
  254. function getOwner() public view returns (address) {
  255. return owner;
  256. }
  257. /**
  258. * @notice Returns the address of the sale token.
  259. */
  260. function getSaleToken() public view returns (address) {
  261. return saleTokenAddress;
  262. }
  263. /**
  264. * @notice Returns the total number of tokens for sale.
  265. */
  266. function getTotalTokensForSale() public view returns (uint256) {
  267. return totalTokensForSale;
  268. }
  269. /**
  270. * @notice Returns the total contributed amount so far.
  271. */
  272. function getTotalContributeAmount() public view returns (uint256) {
  273. return totalContributionAmount;
  274. }
  275. /**
  276. * @notice Returns the sale start time.
  277. */
  278. function getStartTime() public view returns (uint256) {
  279. return startTime;
  280. }
  281. /**
  282. * @notice Returns the sale end time.
  283. */
  284. function getEndTime() public view returns (uint256) {
  285. return endTime;
  286. }
  287. /**
  288. * @notice Returns the payment token price.
  289. */
  290. function getPaymentTokenPrice() public view returns (uint256) {
  291. return paymentTokenPrice;
  292. }
  293. /**
  294. * @notice Returns the payment token address.
  295. */
  296. function getPaymentTokens() public view returns (address) {
  297. return paymentTokenAddress;
  298. }
  299. /**
  300. * @notice Returns the claim start time.
  301. */
  302. function getClaimStartTime() public view returns (uint256) {
  303. return claimStartTime;
  304. }
  305. /**
  306. * @notice Returns whether claiming is enabled.
  307. */
  308. function isClaimEnabled() public view returns (bool) {
  309. return claimEnabled;
  310. }
  311. /**
  312. * @notice Returns all sale details as a tuple.
  313. */
  314. function getSaleDetails()
  315. public
  316. view
  317. returns (
  318. address _saleTokenAddress,
  319. uint256 _totalTokensForSale,
  320. uint256 _totalContributionAmount,
  321. uint256 _contributionTarget,
  322. uint256 _startTime,
  323. uint256 _endTime,
  324. bytes32 _merkleRoot,
  325. address _paymentTokenAddress,
  326. uint256 _paymentTokenPrice,
  327. uint256 _feeBps
  328. )
  329. {
  330. return (
  331. saleTokenAddress,
  332. totalTokensForSale,
  333. totalContributionAmount,
  334. contributionTarget,
  335. startTime,
  336. endTime,
  337. merkleRoot,
  338. paymentTokenAddress,
  339. paymentTokenPrice,
  340. feeBps
  341. );
  342. }
  343. /**
  344. * @notice Returns the claimable tokens for a user.
  345. * @param _user The address of the user.
  346. */
  347. function getClaimableTokens(address _user) public view returns (uint256) {
  348. return userContributionAmount[_user];
  349. }
  350. /**
  351. * @notice Returns the user's buy amount (excluding fee).
  352. * @param _user The address of the user.
  353. */
  354. function getUserBuyAmount(address _user) public view returns (uint256) {
  355. return userBuyAmount[_user];
  356. }
  357. /**
  358. * @notice Returns the user's fee amount.
  359. * @param _user The address of the user.
  360. */
  361. function getUserFeeAmount(address _user) public view returns (uint256) {
  362. return userFeeAmount[_user];
  363. }
  364. // ================================
  365. // Calculation Functions
  366. // ================================
  367. /**
  368. * @notice Calculates the maximum contribution amount for a user (including fee).
  369. * @param _maxBuyAmount The maximum amount of tokens a user can buy.
  370. * @return The maximum contribution amount including fee.
  371. */
  372. function calculateMaxContributionAmount(uint256 _maxBuyAmount) public view returns (uint256) {
  373. // Calculate buy amount value using PRBMath for high precision
  374. uint256 buyAmountValue = PRBMath.mulDiv(_maxBuyAmount, paymentTokenPrice, 1e18);
  375. // Calculate fee using PRBMath for high precision
  376. uint256 feeAmount = PRBMath.mulDiv(buyAmountValue, feeBps, MAX_FEE_BPS);
  377. return buyAmountValue + feeAmount; // Personal hard cap = buy amount + fee
  378. }
  379. /**
  380. * @notice Calculates the breakdown of a contribution amount (buy amount and fee).
  381. * @param _contributionAmount The total contribution amount (including fee).
  382. * @return buyAmount The amount used to buy tokens.
  383. * @return feeAmount The fee amount.
  384. */
  385. function calculateContributionBreakdown(uint256 _contributionAmount) public view returns (uint256 buyAmount, uint256 feeAmount) {
  386. // Fee is calculated based on buy amount, not total contribution
  387. // Total contribution = buy amount + fee
  388. // Fee = buy amount * feeBps / MAX_FEE_BPS
  389. // So: total contribution = buy amount + (buy amount * feeBps / MAX_FEE_BPS)
  390. // Buy amount = total contribution / (1 + feeBps / MAX_FEE_BPS)
  391. // Using PRBMath for high precision calculation
  392. buyAmount = PRBMath.mulDiv(_contributionAmount, MAX_FEE_BPS, MAX_FEE_BPS + feeBps);
  393. feeAmount = _contributionAmount - buyAmount;
  394. return (buyAmount, feeAmount);
  395. }
  396. /**
  397. * @notice Calculates the net contribution after deducting the fee.
  398. * @param _commitAmount The original contribution amount.
  399. * @return The net contribution after fee deduction.
  400. */
  401. function calculateNetContribution(uint256 _commitAmount) public view returns (uint256) {
  402. // Fee is calculated based on buy amount, not total contribution
  403. // Use same logic as calculateContributionBreakdown
  404. // Using PRBMath for high precision calculation
  405. uint256 buyAmount = PRBMath.mulDiv(_commitAmount, MAX_FEE_BPS, MAX_FEE_BPS + feeBps);
  406. return buyAmount;
  407. }
  408. // ================================
  409. // Main Functions
  410. // ================================
  411. /**
  412. * @notice Contribute to the sale using ETH. Requires whitelist proof.
  413. * @param _commitAmount The amount of ETH to contribute.
  414. * @param _maxCommitAmount The maximum allowed contribution for the user.
  415. * @param _proof The Merkle proof for whitelist verification.
  416. * @dev nonReentrant modifier is used to prevent reentrancy attacks.
  417. */
  418. function contributeWithETH(
  419. uint256 _commitAmount,
  420. uint256 _maxCommitAmount,
  421. bytes32[] memory _proof
  422. ) public payable saleActive nonReentrant {
  423. require(paymentTokenAddress == address(0), "Payment token must be ETH for this sale");
  424. require(_commitAmount > 0, "Must buy a positive amount");
  425. require(_commitAmount <= _maxCommitAmount, "Buy amount exceeds max buy amount");
  426. require(msg.value >= _commitAmount, "Must send enough ETH");
  427. require(paymentTokenPrice > 0, "Payment token not accepted for this sale");
  428. // Verify Merkle proof
  429. bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxCommitAmount))));
  430. require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof");
  431. // Calculate breakdown of contribution amount
  432. (uint256 buyAmount, uint256 feeAmount) = calculateContributionBreakdown(_commitAmount);
  433. // Account count statistics
  434. if (userContributionAmount[msg.sender] == 0) {
  435. accountsCount++; // Increment accounts count only if this is the first contribution
  436. }
  437. // Token amount statistics - separate buy amount and fee
  438. userContributionAmount[msg.sender] += _commitAmount; // update user's total contribution
  439. userBuyAmount[msg.sender] += buyAmount; // update user's buy amount
  440. userFeeAmount[msg.sender] += feeAmount; // update user's fee amount
  441. require(userContributionAmount[msg.sender] <= _maxCommitAmount, "Buy amount exceeds max buy amount");
  442. totalContributionAmount += _commitAmount; // update total contributed amount
  443. if (msg.value > _commitAmount) {
  444. payable(msg.sender).transfer(msg.value - _commitAmount); //refund excess ETH
  445. }
  446. emit Contributed(msg.sender, _commitAmount, address(0));
  447. }
  448. /**
  449. * @notice Contribute to the sale using ERC20 tokens. Requires whitelist proof.
  450. * @param _commitAmount The amount of tokens to contribute.
  451. * @param _maxCommitAmount The maximum allowed contribution for the user.
  452. * @param _proof The Merkle proof for whitelist verification.
  453. * @dev nonReentrant modifier is used to prevent reentrancy attacks.
  454. */
  455. function contributeWithERC20(
  456. uint256 _commitAmount,
  457. uint256 _maxCommitAmount,
  458. bytes32[] memory _proof
  459. ) public saleActive nonReentrant {
  460. require(_commitAmount > 0, "Must send a positive amount");
  461. require(_commitAmount <= _maxCommitAmount, "Buy amount exceeds max buy amount");
  462. bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxCommitAmount))));
  463. require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof");
  464. require(
  465. IERC20(paymentTokenAddress).allowance(msg.sender, address(this)) >= _commitAmount,
  466. "Not enough allowance for payment token"
  467. );
  468. IERC20(paymentTokenAddress).transferFrom(msg.sender, address(this), _commitAmount); // Transfer payment tokens from user account
  469. // Calculate breakdown of contribution amount
  470. (uint256 buyAmount, uint256 feeAmount) = calculateContributionBreakdown(_commitAmount);
  471. if (userContributionAmount[msg.sender] == 0) {
  472. accountsCount++; // Increment accounts count only if this is the first contribution
  473. }
  474. userContributionAmount[msg.sender] += _commitAmount; // update user's total contribution
  475. userBuyAmount[msg.sender] += buyAmount; // update user's buy amount
  476. userFeeAmount[msg.sender] += feeAmount; // update user's fee amount
  477. require(userContributionAmount[msg.sender] <= _maxCommitAmount, "Buy amount exceeds max buy amount");
  478. totalContributionAmount += _commitAmount; // update total contributed amount
  479. emit Contributed(msg.sender, _commitAmount, paymentTokenAddress);
  480. }
  481. /**
  482. * @notice Claim purchased tokens and receive refund if sale is oversubscribed.
  483. * @dev nonReentrant modifier is used to prevent reentrancy attacks.
  484. * This function clears the user's contribution after claim to prevent double claim.
  485. */
  486. function claimTokens() public validClaimTime {
  487. require(block.timestamp > endTime, "Sale is still active");
  488. uint256 contributeAmount = userContributionAmount[msg.sender];
  489. uint256 userBuyAmountValue = userBuyAmount[msg.sender];
  490. uint256 userFeeAmountValue = userFeeAmount[msg.sender];
  491. require(contributeAmount > 0, "No tokens to claim");
  492. uint256 refundCost = 0;
  493. uint256 boughtToken = 0;
  494. if (totalContributionAmount <= contributionTarget) {
  495. // Normal subscription: use pre-calculated buy amount
  496. // Calculate token amount using PRBMath for high precision
  497. boughtToken = PRBMath.mulDiv(userBuyAmountValue, 1e18, paymentTokenPrice);
  498. IERC20(saleTokenAddress).transfer(msg.sender, boughtToken);
  499. } else {
  500. // Oversubscribed: all tokens are sold, user gets proportional share based on buy amount
  501. // Calculate total buy amount from all contributions using PRBMath
  502. uint256 totalBuyAmount = PRBMath.mulDiv(totalContributionAmount, MAX_FEE_BPS, MAX_FEE_BPS + feeBps);
  503. // User gets proportional share of all tokens based on their buy amount using PRBMath
  504. boughtToken = PRBMath.mulDiv(userBuyAmountValue, totalTokensForSale, totalBuyAmount);
  505. // Calculate how much the user actually pays for these tokens using PRBMath
  506. uint256 userEffectiveBuyAmount = PRBMath.mulDiv(boughtToken, paymentTokenPrice, 1e18);
  507. uint256 userEffectiveFeeAmount = PRBMath.mulDiv(userEffectiveBuyAmount, feeBps, MAX_FEE_BPS);
  508. // Calculate refund breakdown: both unused buy amount and unused fee
  509. uint256 refundBuyAmount = userBuyAmountValue - userEffectiveBuyAmount;
  510. uint256 refundFeeAmount = userFeeAmountValue - userEffectiveFeeAmount;
  511. refundCost = refundBuyAmount + refundFeeAmount;
  512. // Refund
  513. if (paymentTokenAddress == address(0)) {
  514. (bool sent, ) = payable(msg.sender).call{ value: refundCost }("");
  515. require(sent, "ETH refund failed");
  516. } else {
  517. IERC20(paymentTokenAddress).transfer(msg.sender, refundCost);
  518. }
  519. // Transfer tokens
  520. IERC20(saleTokenAddress).transfer(msg.sender, boughtToken);
  521. }
  522. // Clear user data
  523. userContributionAmount[msg.sender] = 0;
  524. userBuyAmount[msg.sender] = 0;
  525. userFeeAmount[msg.sender] = 0;
  526. emit TokensClaimed(msg.sender, boughtToken, refundCost);
  527. }
  528. }