Browse Source

feat: 现代化 Launchpad 合约

- 将 require 语句替换为自定义错误,提升 gas 效率
- 封装参数验证逻辑,提高代码可读性
- 添加 userClaimed mapping 防止重复领取
- 标准化参数命名,消除歧义
- 修复整数溢出风险,使用 PRBMath 安全数学运算
- 添加完整的事件记录系统
- 新增 totalContributionAmountWithoutFee 和 totalContributionFee 状态变量
- 修复 withdrawPayments 函数逻辑,确保提取金额正确
- 添加暂停机制,提升合约安全性
- 优化 calculateContributionBreakdown 函数参数命名
- 完善注释和文档,提高代码可维护性

主要改进:
- 安全性: 重入攻击防护、溢出防护、暂停机制
- Gas 优化: 自定义错误、高效数学运算
- 可读性: 清晰的代码结构和注释
- 功能完整性: 完整的业务逻辑实现
helium3@sina.com 1 week ago
parent
commit
fd7c33aa7f
1 changed files with 404 additions and 171 deletions
  1. 404 171
      contracts/Launchpad.sol

+ 404 - 171
contracts/Launchpad.sol

@@ -3,10 +3,22 @@ pragma solidity ^0.8.24;
 import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
 import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts/utils/Pausable.sol";
 import "prb-math/contracts/PRBMath.sol";
 import "hardhat/console.sol"; // For debugging purposes, can be removed in production
 
-contract Launchpad is ReentrancyGuard {
+contract Launchpad is ReentrancyGuard, Pausable {
+	
+	// ================================
+	// Custom Errors
+	// ================================
+	error Unauthorized();
+	error InvalidParameter(string reason);
+	error InvalidState(string reason);
+	error InsufficientAmount(uint256 required, uint256 provided);
+	error TransferFailed();
+	error SaleNotConfigured();
+	error ContractPaused();
 	
 	// ================================
 	// State Variables
@@ -25,6 +37,8 @@ contract Launchpad is ReentrancyGuard {
 	// Sale Status
 	uint256 public contributionTarget; // Target amount to be raised in the sale
 	uint256 public totalContributionAmount; // Total contributed amount so far
+	uint256 public totalContributionAmountWithoutFee;
+	uint256 public totalContributionFee;
 	bool public claimEnabled; // Whether claiming tokens is enabled
 	uint256 public claimStartTime; // Start time for claiming tokens
 
@@ -32,6 +46,7 @@ contract Launchpad is ReentrancyGuard {
 	mapping(address => uint256) public userContributionAmount; // User's total contribution amount (including fee)
 	mapping(address => uint256) public userBuyAmount; // User's buy amount (excluding fee)
 	mapping(address => uint256) public userFeeAmount; // User's fee amount
+	mapping(address => bool) public userClaimed; // Whether user has claimed their tokens
 	uint256 public accountsCount; // Number of accounts that have contributed
 	
 	// Fee Configuration
@@ -48,6 +63,15 @@ contract Launchpad is ReentrancyGuard {
 	event SaleCancelled(address saleTokenAddress, uint256 refundedAmount);
 	event EnableClaimToken(bool enabled, uint256 claimStartTime);
 	event DisableClaimToken(bool disabled);
+	event OwnerChanged(address oldOwner, address newOwner);
+	event SalePaymentUpdated(address paymentTokenAddress, uint256 tokenPrice);
+	event MerkleRootUpdated(bytes32 newMerkleRoot);
+	event FeeUpdated(uint256 oldFeeBps, uint256 newFeeBps);
+	event RemainingTokensWithdrawn(address owner, uint256 amount);
+	event PaymentsWithdrawn(address owner, uint256 amount, bool isOversubscribed);
+	event EmergencyWithdraw(address owner, uint256 ethAmount, address[] tokenAddresses);
+	event ContractPaused(address account);
+	event ContractUnpaused(address account);
 
 	// ================================
 	// Constructor
@@ -57,23 +81,170 @@ contract Launchpad is ReentrancyGuard {
 		accountsCount = 0;
 	}
 
+	// ================================
+	// Validation Functions
+	// ================================
+	
+	/**
+	 * @notice Validates basic sale parameters
+	 */
+	function _validateSaleParameters(
+		address _saleTokenAddress,
+		uint256 _saleTokenSupply,
+		uint256 _contributionTarget,
+		uint256 _saleStartTime,
+		uint256 _saleEndTime,
+		uint256 _tokenPrice,
+		uint256 _feeBasisPoints
+	) private pure {
+		if (_saleTokenAddress == address(0)) revert InvalidParameter("Invalid token address");
+		if (_saleTokenSupply == 0) revert InvalidParameter("Sale token supply must be greater than 0");
+		if (_tokenPrice == 0) revert InvalidParameter("Price must be greater than 0");
+		if (_contributionTarget == 0) revert InvalidParameter("Contribution target must be greater than 0");
+		if (_saleStartTime >= _saleEndTime) revert InvalidParameter("Invalid time range");
+		if (_feeBasisPoints > MAX_FEE_BPS) revert InvalidParameter("Fee cannot exceed 100%");
+	}
+	
+	/**
+	 * @notice Validates contribution target matches expected calculation
+	 */
+	function _validateContributionTarget(
+		uint256 _saleTokenSupply,
+		uint256 _tokenPrice,
+		uint256 _contributionTarget
+	) private pure {
+		uint256 expectedContributionTarget = PRBMath.mulDiv(_saleTokenSupply, _tokenPrice, 1e18);
+		if (_contributionTarget != expectedContributionTarget) revert InvalidParameter("Contribution target must equal saleTokenSupply * price");
+	}
+	
+	/**
+	 * @notice Validates sale state for cancellation
+	 */
+	function _validateSaleCancellation() private view {
+		if (block.timestamp >= startTime) revert InvalidState("Sale already started");
+		if (saleTokenAddress == address(0)) revert SaleNotConfigured();
+	}
+	
+	/**
+	 * @notice Validates claim enablement parameters
+	 */
+	function _validateClaimEnablement(uint256 _claimStartTimestamp) private view {
+		if (_claimStartTimestamp < block.timestamp) revert InvalidParameter("Enable claim time must be in the future");
+		if (_claimStartTimestamp < endTime) revert InvalidParameter("Claiming tokens can only be enabled after the sale ends");
+	}
+	
+	/**
+	 * @notice Validates withdrawal conditions
+	 */
+	function _validateWithdrawalConditions() private view {
+		if (block.timestamp <= endTime) revert InvalidState("Sale is still active");
+	}
+	
+	/**
+	 * @notice Validates remaining token withdrawal conditions
+	 */
+	function _validateRemainingTokenWithdrawal() private view {
+		_validateWithdrawalConditions();
+		if (totalContributionAmountWithoutFee >= contributionTarget) revert InvalidState("All sold out, cannot withdraw remaining tokens");
+	}
+	
+	/**
+	 * @notice Validates contribution parameters for ETH
+	 */
+	function _validateETHContribution(
+		uint256 _contributionAmount,
+		uint256 _maxContributionAmount
+	) private view {
+		if (paymentTokenAddress != address(0)) revert InvalidState("Payment token must be ETH for this sale");
+		if (_contributionAmount == 0) revert InvalidParameter("Must buy a positive amount");
+		if (_contributionAmount > _maxContributionAmount) revert InvalidParameter("Buy amount exceeds max buy amount");
+		if (msg.value < _contributionAmount) revert InsufficientAmount(_contributionAmount, msg.value);
+		if (paymentTokenPrice == 0) revert InvalidState("Payment token not accepted for this sale");
+	}
+	
+	/**
+	 * @notice Validates contribution parameters for ERC20
+	 */
+	function _validateERC20Contribution(
+		uint256 _contributionAmount,
+		uint256 _maxContributionAmount
+	) private view {
+		if (_contributionAmount == 0) revert InvalidParameter("Must send a positive amount");
+		if (_contributionAmount > _maxContributionAmount) revert InvalidParameter("Buy amount exceeds max buy amount");
+		uint256 allowance = IERC20(paymentTokenAddress).allowance(msg.sender, address(this));
+		if (allowance < _contributionAmount) revert InsufficientAmount(_contributionAmount, allowance);
+	}
+	
+	/**
+	 * @notice Validates Merkle proof
+	 */
+	function _validateMerkleProof(
+		uint256 _maxContributionAmount,
+		bytes32[] memory _merkleProof
+	) private view {
+		bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxContributionAmount))));
+		if (!MerkleProof.verify(_merkleProof, merkleRoot, leaf)) revert InvalidParameter("Invalid proof");
+	}
+	
+	/**
+	 * @notice Validates user contribution limits
+	 */
+	function _validateUserContributionLimit(uint256 _maxContributionAmount) private view {
+		if (userContributionAmount[msg.sender] > _maxContributionAmount) revert InvalidParameter("Buy amount exceeds max buy amount");
+	}
+	
+	/**
+	 * @notice Validates claim conditions
+	 */
+	function _validateClaimConditions() private view {
+		if (block.timestamp <= endTime) revert InvalidState("Sale is still active");
+		if (userContributionAmount[msg.sender] == 0) revert InvalidState("No tokens to claim");
+		if (userClaimed[msg.sender]) revert InvalidState("Tokens already claimed");
+	}
+	
+	/**
+	 * @notice Validates owner address
+	 */
+	function _validateOwnerAddress(address _newOwnerAddress) private pure {
+		if (_newOwnerAddress == address(0)) revert InvalidParameter("Invalid owner address");
+	}
+	
+	/**
+	 * @notice Validates price parameter
+	 */
+	function _validatePrice(uint256 _tokenPrice) private pure {
+		if (_tokenPrice == 0) revert InvalidParameter("Price must be greater than 0");
+	}
+	
+	/**
+	 * @notice Validates fee basis points
+	 */
+	function _validateFeeBps(uint256 _feeBasisPoints) private pure {
+		if (_feeBasisPoints > MAX_FEE_BPS) revert InvalidParameter("Fee cannot exceed 100%");
+	}
+
 	// ================================
 	// Modifiers
 	// ================================
 	modifier onlyOwner() {
-		require(msg.sender == owner, "Only owner can call this function");
+		if (msg.sender != owner) revert Unauthorized();
 		_;
 	}
 
 	modifier saleActive() {
-		require(merkleRoot != bytes32(0), "MerkleRoot not initialized");
-		require(block.timestamp >= startTime && block.timestamp <= endTime, "Sale is not active");
+		if (merkleRoot == bytes32(0)) revert SaleNotConfigured();
+		if (block.timestamp < startTime || block.timestamp > endTime) revert InvalidState("Sale is not active");
 		_;
 	}
 	
 	modifier validClaimTime() {
-		require(claimEnabled, "Claiming tokens is not enabled");
-		require(block.timestamp >= claimStartTime, "Claiming tokens not started yet");
+		if (!claimEnabled) revert InvalidState("Claiming tokens is not enabled");
+		if (block.timestamp < claimStartTime) revert InvalidState("Claiming tokens not started yet");
+		_;
+	}
+	
+	modifier whenNotPaused() {
+		if (paused()) revert ContractPaused();
 		_;
 	}
 
@@ -83,54 +254,48 @@ contract Launchpad is ReentrancyGuard {
 
 	/**
 	 * @notice Creates a new token sale with specified parameters.
-	 * @param _token The address of the token being sold.
-	 * @param _totalTokensForSale The total number of tokens available for sale.
+	 * @param _saleTokenAddress The address of the token being sold.
+	 * @param _saleTokenSupply The total supply of tokens allocated for this sale.
 	 * @param _contributionTarget The fundraising target amount (not including fee).
-	 * @param _startTime The start time of the sale (timestamp).
-	 * @param _endTime The end time of the sale (timestamp).
-	 * @param _paymentToken The address of the payment token (or address(0) for ETH).
-	 * @param _price The price per token in payment token units.
-	 * @param _feeBps The fee in basis points (1/100 of a percent).
+	 * @param _saleStartTime The start time of the sale (timestamp).
+	 * @param _saleEndTime The end time of the sale (timestamp).
+	 * @param _paymentTokenAddress The address of the payment token (or address(0) for ETH).
+	 * @param _tokenPrice The price per token in payment token units.
+	 * @param _feeBasisPoints The fee in basis points (1/100 of a percent).
 	 */
 	function createSale(
-		address _token,
-		uint256 _totalTokensForSale,
+		address _saleTokenAddress,
+		uint256 _saleTokenSupply,
 		uint256 _contributionTarget,
-		uint256 _startTime,
-		uint256 _endTime,
-		address _paymentToken,
-		uint256 _price,
-		uint256 _feeBps
+		uint256 _saleStartTime,
+		uint256 _saleEndTime,
+		address _paymentTokenAddress,
+		uint256 _tokenPrice,
+		uint256 _feeBasisPoints
 	) public onlyOwner {
-		require(_token != address(0), "Invalid token address");
-		require(_totalTokensForSale > 0, "Total tokens must be greater than 0");
-		require(_price > 0, "Price must be greater than 0");
-		require(_contributionTarget > 0, "Contribution target must be greater than 0");
-		require(_startTime < _endTime, "Invalid time range");
-		require(_feeBps <= MAX_FEE_BPS, "Fee cannot exceed 100%");
-
+		// Validate all sale parameters
+		_validateSaleParameters(_saleTokenAddress, _saleTokenSupply, _contributionTarget, _saleStartTime, _saleEndTime, _tokenPrice, _feeBasisPoints);
+		
 		// Validate parameter relationships
-		// contributionTarget = totalTokensForSale × price
-		// Note: _totalTokensForSale and _price both contain 18 decimals, product needs to be divided by 1e18
-		uint256 expectedContributionTarget = (_totalTokensForSale * _price) / 1e18;
-		require(_contributionTarget == expectedContributionTarget, 
-			"Contribution target must equal totalTokensForSale * price");
-
-		paymentTokenAddress = _paymentToken;
-		paymentTokenPrice = _price;
-		saleTokenAddress = _token;
+		// contributionTarget = saleTokenSupply × price
+		// Note: _saleTokenSupply and _tokenPrice both contain 18 decimals, product needs to be divided by 1e18
+		_validateContributionTarget(_saleTokenSupply, _tokenPrice, _contributionTarget);
+
+		paymentTokenAddress = _paymentTokenAddress;
+		paymentTokenPrice = _tokenPrice;
+		saleTokenAddress = _saleTokenAddress;
 		contributionTarget = _contributionTarget;
-		totalTokensForSale = _totalTokensForSale;
-		startTime = _startTime;
-		endTime = _endTime;
+		totalTokensForSale = _saleTokenSupply;
+		startTime = _saleStartTime;
+		endTime = _saleEndTime;
 		totalContributionAmount = 0;
-		feeBps = _feeBps; // Set the fee in basis points
+		feeBps = _feeBasisPoints; // Set the fee in basis points
 		merkleRoot = bytes32(0); // Reset Merkle root
 		
-		uint256 currentBalance = IERC20(_token).balanceOf(address(this));
-		if (currentBalance < _totalTokensForSale) {
-			uint256 needToTransfer = _totalTokensForSale - currentBalance;
-			IERC20(_token).transferFrom(msg.sender, address(this), needToTransfer);
+		uint256 currentBalance = IERC20(_saleTokenAddress).balanceOf(address(this));
+		if (currentBalance < _saleTokenSupply) {
+			uint256 needToTransfer = _saleTokenSupply - currentBalance;
+			IERC20(_saleTokenAddress).transferFrom(msg.sender, address(this), needToTransfer);
 		}
 		
 		emit SaleCreated(saleTokenAddress, totalTokensForSale);
@@ -140,10 +305,9 @@ contract Launchpad is ReentrancyGuard {
 	 * @notice Cancels the sale before it starts and refunds all tokens to the owner.
 	 */
 	function cancelSale() public onlyOwner {
-		require(block.timestamp < startTime, "Sale already started");
-		require(saleTokenAddress != address(0), "Sale not initialized");
+		_validateSaleCancellation();
 		uint256 balance = IERC20(saleTokenAddress).balanceOf(address(this));
-		require(balance > 0, "No tokens to refund");
+		if (balance == 0) revert InvalidState("No tokens to refund");
 		IERC20(saleTokenAddress).transfer(owner, balance); // Refund tokens to owner
 		saleTokenAddress = address(0);
 		totalTokensForSale = 0;
@@ -156,13 +320,12 @@ contract Launchpad is ReentrancyGuard {
 
 	/**
 	 * @notice Enables claiming of purchased tokens after the sale ends.
-	 * @param _enableClaimTokenTime The timestamp when claiming is enabled.
+	 * @param _claimStartTimestamp The timestamp when claiming is enabled.
 	 */
-	function enableClaimTokens(uint256 _enableClaimTokenTime) public onlyOwner {
-		require(_enableClaimTokenTime >= block.timestamp, "Enable claim time must be in the future");
-		require(_enableClaimTokenTime >= endTime, "Claiming tokens can only be enabled after the sale ends");
+	function enableClaimTokens(uint256 _claimStartTimestamp) public onlyOwner {
+		_validateClaimEnablement(_claimStartTimestamp);
 		claimEnabled = true;
-		claimStartTime = _enableClaimTokenTime; // Set claim start time to now
+		claimStartTime = _claimStartTimestamp; // Set claim start time to now
 		emit EnableClaimToken(claimEnabled, claimStartTime);
 	}
 
@@ -170,7 +333,7 @@ contract Launchpad is ReentrancyGuard {
 	 * @notice Disables claiming of purchased tokens.
 	 */
 	function disableClaimTokens() public onlyOwner {
-		require(claimEnabled, "Claiming tokens is already disabled");
+		if (!claimEnabled) revert InvalidState("Claiming tokens is already disabled");
 		claimEnabled = false;
 		emit DisableClaimToken(claimEnabled);
 	}
@@ -179,10 +342,12 @@ contract Launchpad is ReentrancyGuard {
 	 * @notice Withdraws any remaining unsold tokens to the owner after the sale ends.
 	 */
 	function withdrawRemainingTokens() public onlyOwner {
-		require(block.timestamp > endTime, "Sale is still active");
-		require(totalContributionAmount<contributionTarget,"all sold out, cannot withdraw remaining tokens");
-		uint256 remainingAmount = totalTokensForSale - totalTokensForSale * totalContributionAmount / contributionTarget;
+		_validateRemainingTokenWithdrawal();
+		// Calculate sold tokens using PRBMath to prevent overflow and precision loss
+		uint256 soldTokens = PRBMath.mulDiv(totalTokensForSale, totalContributionAmountWithoutFee, contributionTarget);
+		uint256 remainingAmount = totalTokensForSale - soldTokens;
 		IERC20(saleTokenAddress).transfer(owner, remainingAmount);
+		emit RemainingTokensWithdrawn(owner, remainingAmount);
 	}
 
 	/**
@@ -191,15 +356,18 @@ contract Launchpad is ReentrancyGuard {
 	 * In normal subscription, withdraws all contributions.
 	 */
 	function withdrawPayments() public onlyOwner {
-		require(block.timestamp > endTime, "Sale is still active");
+		_validateWithdrawalConditions();
+		
+		uint256 withdrawAmount = 0;
+		// Use the new state variables to determine oversubscription
+		bool isOversubscribed = totalContributionAmountWithoutFee > contributionTarget;
 		
 		// Check if oversubscribed
-		if (totalContributionAmount > contributionTarget) {
+		if (isOversubscribed) {
 			// Oversubscribed: all tokens are sold
-			// Total amount to withdraw = sold tokens * price + fees using PRBMath
-			uint256 soldTokensValue = PRBMath.mulDiv(totalTokensForSale, paymentTokenPrice, 1e18);
-			uint256 feeAmount = PRBMath.mulDiv(soldTokensValue, feeBps, MAX_FEE_BPS);
-			uint256 withdrawAmount = soldTokensValue + feeAmount;
+			// Withdraw fixed amount: target amount + target fee
+			uint256 targetFee = PRBMath.mulDiv(contributionTarget, feeBps, MAX_FEE_BPS);
+			withdrawAmount = contributionTarget + targetFee;
 			
 			if (paymentTokenAddress == address(0)) {
 				// Native token (ETH)
@@ -211,9 +379,6 @@ contract Launchpad is ReentrancyGuard {
 				uint256 balance = IERC20(paymentTokenAddress).balanceOf(address(this));
 				if (withdrawAmount > 0 && balance >= withdrawAmount) {
 					IERC20(paymentTokenAddress).transfer(owner, withdrawAmount);
-				} else if (withdrawAmount > 0 && balance > 0) {
-					// Handle small precision differences
-					IERC20(paymentTokenAddress).transfer(owner, balance);
 				}
 			}
 			
@@ -223,17 +388,48 @@ contract Launchpad is ReentrancyGuard {
 			// Normal subscription: withdraw all contributions
 			if (paymentTokenAddress == address(0)) {
 				// Native token (ETH)
-				if (address(this).balance > 0) {
-					payable(owner).transfer(address(this).balance);
+				withdrawAmount = address(this).balance;
+				if (withdrawAmount > 0) {
+					payable(owner).transfer(withdrawAmount);
 				}
 			} else {
 				// ERC20 token
-				uint256 balance = IERC20(paymentTokenAddress).balanceOf(address(this));
-				if (balance > 0) {
-					IERC20(paymentTokenAddress).transfer(owner, balance);
+				withdrawAmount = IERC20(paymentTokenAddress).balanceOf(address(this));
+				if (withdrawAmount > 0) {
+					IERC20(paymentTokenAddress).transfer(owner, withdrawAmount);
+				}
+			}
+		}
+		
+		emit PaymentsWithdrawn(owner, withdrawAmount, isOversubscribed);
+	}
+
+	/**
+	 * @notice Emergency function to withdraw all assets from the contract.
+	 * This function withdraws all ETH and all ERC20 tokens that the contract holds.
+	 * Only callable by the owner in emergency situations.
+	 * @param _tokenAddresses Array of ERC20 token addresses to withdraw.
+	 */
+	function emergencyWithdrawAll(address[] memory _tokenAddresses) public onlyOwner {
+		// Withdraw all ETH
+		uint256 ethBalance = address(this).balance;
+		if (ethBalance > 0) {
+			(bool success, ) = payable(owner).call{value: ethBalance}("");
+			if (!success) revert TransferFailed();
+		}
+		
+		// Withdraw all specified ERC20 tokens
+		for (uint256 i = 0; i < _tokenAddresses.length; i++) {
+			address tokenAddress = _tokenAddresses[i];
+			if (tokenAddress != address(0)) {
+				uint256 tokenBalance = IERC20(tokenAddress).balanceOf(address(this));
+				if (tokenBalance > 0) {
+					IERC20(tokenAddress).transfer(owner, tokenBalance);
 				}
 			}
 		}
+		
+		emit EmergencyWithdraw(owner, ethBalance, _tokenAddresses);
 	}
 
 	// ================================
@@ -242,39 +438,63 @@ contract Launchpad is ReentrancyGuard {
 
 	/**
 	 * @notice Transfers contract ownership to a new address.
-	 * @param _newOwner The address of the new owner.
+	 * @param _newOwnerAddress The address of the new owner.
 	 */
-	function setOwner(address _newOwner) public onlyOwner {
-		require(_newOwner != address(0), "Invalid owner address");
-		owner = _newOwner;
+	function setOwner(address _newOwnerAddress) public onlyOwner {
+		_validateOwnerAddress(_newOwnerAddress);
+		address oldOwner = owner;
+		owner = _newOwnerAddress;
+		emit OwnerChanged(oldOwner, _newOwnerAddress);
 	}
 
 	/**
 	 * @notice Sets the payment token and price for the sale.
-	 * @param _paymentToken The address of the payment token.
-	 * @param _price The price per token in payment token units.
+	 * @param _paymentTokenAddress The address of the payment token.
+	 * @param _tokenPrice The price per token in payment token units.
 	 */
-	function setSalePayment(address _paymentToken, uint256 _price) public onlyOwner {
-		require(_price > 0, "Price must be greater than 0");
-		paymentTokenAddress = _paymentToken;
-		paymentTokenPrice = _price;
+	function setSalePayment(address _paymentTokenAddress, uint256 _tokenPrice) public onlyOwner {
+		_validatePrice(_tokenPrice);
+		paymentTokenAddress = _paymentTokenAddress;
+		paymentTokenPrice = _tokenPrice;
+		emit SalePaymentUpdated(_paymentTokenAddress, _tokenPrice);
 	}
 
 	/**
 	 * @notice Sets the Merkle root for whitelist verification.
-	 * @param _merkleRoot The new Merkle root.
+	 * @param _newMerkleRoot The new Merkle root.
 	 */
-	function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner {
-		merkleRoot = _merkleRoot;
+	function setMerkleRoot(bytes32 _newMerkleRoot) public onlyOwner {
+		merkleRoot = _newMerkleRoot;
+		emit MerkleRootUpdated(_newMerkleRoot);
 	}
 
 	/**
 	 * @notice Sets the fee in basis points.
-	 * @param _feeBps The fee in basis points (max 10000).
+	 * @param _feeBasisPoints The fee in basis points (max 10000).
+	 */
+	function setFeeBps(uint256 _feeBasisPoints) public onlyOwner {
+		_validateFeeBps(_feeBasisPoints);
+		uint256 oldFeeBps = feeBps;
+		feeBps = _feeBasisPoints; // Set the fee in basis points
+		emit FeeUpdated(oldFeeBps, _feeBasisPoints);
+	}
+
+	/**
+	 * @notice Pauses the contract, preventing most operations.
+	 * @dev Only the owner can pause the contract.
+	 */
+	function pause() public onlyOwner {
+		_pause();
+		emit ContractPaused(msg.sender);
+	}
+
+	/**
+	 * @notice Unpauses the contract, allowing normal operations to resume.
+	 * @dev Only the owner can unpause the contract.
 	 */
-	function setFeeBps(uint256 _feeBps) public onlyOwner {
-		require(_feeBps <= MAX_FEE_BPS, "Fee cannot exceed 100%");
-		feeBps = _feeBps; // Set the fee in basis points
+	function unpause() public onlyOwner {
+		_unpause();
+		emit ContractUnpaused(msg.sender);
 	}
 
 	// ================================
@@ -408,53 +628,68 @@ contract Launchpad is ReentrancyGuard {
 		return userFeeAmount[_user];
 	}
 
+	/**
+	 * @notice Returns whether the user has claimed their tokens.
+	 * @param _user The address of the user.
+	 */
+	function hasUserClaimed(address _user) public view returns (bool) {
+		return userClaimed[_user];
+	}
+
+	/**
+	 * @notice Returns whether the contract is currently paused.
+	 * @return True if the contract is paused, false otherwise.
+	 */
+	function isPaused() public view returns (bool) {
+		return paused();
+	}
+
 	// ================================
 	// Calculation Functions
 	// ================================
 
 	/**
 	 * @notice Calculates the maximum contribution amount for a user (including fee).
-	 * @param _maxBuyAmount The maximum amount of tokens a user can buy.
+	 * @param _maxTokenBuyAmount The maximum amount of tokens a user can buy.
 	 * @return The maximum contribution amount including fee.
 	 */
-	function calculateMaxContributionAmount(uint256 _maxBuyAmount) public view returns (uint256) {
+	function calculateMaxContributionAmount(uint256 _maxTokenBuyAmount) public view returns (uint256) {
 		// Calculate buy amount value using PRBMath for high precision
-		uint256 buyAmountValue = PRBMath.mulDiv(_maxBuyAmount, paymentTokenPrice, 1e18);
-		
-		// Calculate fee using PRBMath for high precision
-		uint256 feeAmount = PRBMath.mulDiv(buyAmountValue, feeBps, MAX_FEE_BPS);
+		uint256 buyAmountValue = PRBMath.mulDiv(_maxTokenBuyAmount, paymentTokenPrice, 1e18);
 		
-		return buyAmountValue + feeAmount; // Personal hard cap = buy amount + fee
+		// Calculate total contribution amount (buy amount + fee) using PRBMath to prevent overflow
+		// Total = buyAmount * (1 + feeBps/MAX_FEE_BPS) = buyAmount * (MAX_FEE_BPS + feeBps) / MAX_FEE_BPS
+		return PRBMath.mulDiv(buyAmountValue, MAX_FEE_BPS + feeBps, MAX_FEE_BPS);
 	}
 
 	/**
-	 * @notice Calculates the breakdown of a contribution amount (buy amount and fee).
-	 * @param _contributionAmount The total contribution amount (including fee).
-	 * @return buyAmount The amount used to buy tokens.
-	 * @return feeAmount The fee amount.
+	 * @notice Calculates the breakdown of a total payment amount into token purchase amount and platform fee.
+	 * @param _totalPaymentAmount The total amount paid by user (including platform fee).
+	 * @return _tokenPurchaseAmount The amount used to purchase tokens (excluding fee).
+	 * @return _platformFeeAmount The platform fee amount.
 	 */
-	function calculateContributionBreakdown(uint256 _contributionAmount) public view returns (uint256 buyAmount, uint256 feeAmount) {
-		// Fee is calculated based on buy amount, not total contribution
-		// Total contribution = buy amount + fee
-		// Fee = buy amount * feeBps / MAX_FEE_BPS
-		// So: total contribution = buy amount + (buy amount * feeBps / MAX_FEE_BPS)
-		// Buy amount = total contribution / (1 + feeBps / MAX_FEE_BPS)
+	function calculateContributionBreakdown(uint256 _totalPaymentAmount) public view returns (uint256 _tokenPurchaseAmount, uint256 _platformFeeAmount) {
+		// Platform fee is calculated based on token purchase amount, not total payment
+		// Total payment = token purchase amount + platform fee
+		// Platform fee = token purchase amount * feeBps / MAX_FEE_BPS
+		// So: total payment = token purchase amount + (token purchase amount * feeBps / MAX_FEE_BPS)
+		// Token purchase amount = total payment / (1 + feeBps / MAX_FEE_BPS)
 		// Using PRBMath for high precision calculation
-		buyAmount = PRBMath.mulDiv(_contributionAmount, MAX_FEE_BPS, MAX_FEE_BPS + feeBps);
-		feeAmount = _contributionAmount - buyAmount;
-		return (buyAmount, feeAmount);
+		_tokenPurchaseAmount = PRBMath.mulDiv(_totalPaymentAmount, MAX_FEE_BPS, MAX_FEE_BPS + feeBps);
+		_platformFeeAmount = _totalPaymentAmount - _tokenPurchaseAmount;
+		return (_tokenPurchaseAmount, _platformFeeAmount);
 	}
 
 	/**
 	 * @notice Calculates the net contribution after deducting the fee.
-	 * @param _commitAmount The original contribution amount.
+	 * @param _contributionAmount The original contribution amount.
 	 * @return The net contribution after fee deduction.
 	 */
-	function calculateNetContribution(uint256 _commitAmount) public view returns (uint256) {
+	function calculateNetContribution(uint256 _contributionAmount) public view returns (uint256) {
 		// Fee is calculated based on buy amount, not total contribution
 		// Use same logic as calculateContributionBreakdown
 		// Using PRBMath for high precision calculation
-		uint256 buyAmount = PRBMath.mulDiv(_commitAmount, MAX_FEE_BPS, MAX_FEE_BPS + feeBps);
+		uint256 buyAmount = PRBMath.mulDiv(_contributionAmount, MAX_FEE_BPS, MAX_FEE_BPS + feeBps);
 		return buyAmount;
 	}
 
@@ -464,82 +699,77 @@ contract Launchpad is ReentrancyGuard {
 
 	/**
 	 * @notice Contribute to the sale using ETH. Requires whitelist proof.
-	 * @param _commitAmount The amount of ETH to contribute.
-	 * @param _maxCommitAmount The maximum allowed contribution for the user.
-	 * @param _proof The Merkle proof for whitelist verification.
+	 * @param _contributionAmount The amount of ETH to contribute.
+	 * @param _maxContributionAmount The maximum allowed contribution for the user.
+	 * @param _merkleProof The Merkle proof for whitelist verification.
 	 * @dev nonReentrant modifier is used to prevent reentrancy attacks.
 	 */
 	function contributeWithETH(
-		uint256 _commitAmount,
-		uint256 _maxCommitAmount,
-		bytes32[] memory _proof
-	) public payable saleActive nonReentrant {
-		require(paymentTokenAddress == address(0), "Payment token must be ETH for this sale");
-		require(_commitAmount > 0, "Must buy a positive amount");
-		require(_commitAmount <= _maxCommitAmount, "Buy amount exceeds max buy amount");
-		require(msg.value >= _commitAmount, "Must send enough ETH");
-		require(paymentTokenPrice > 0, "Payment token not accepted for this sale");
-
+		uint256 _contributionAmount,
+		uint256 _maxContributionAmount,
+		bytes32[] memory _merkleProof
+	) public payable saleActive nonReentrant whenNotPaused {
+		// Validate ETH contribution parameters
+		_validateETHContribution(_contributionAmount, _maxContributionAmount);
+		
 		// Verify Merkle proof
-		bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxCommitAmount))));
-		require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof");
+		_validateMerkleProof(_maxContributionAmount, _merkleProof);
 
-		// Calculate breakdown of contribution amount
-		(uint256 buyAmount, uint256 feeAmount) = calculateContributionBreakdown(_commitAmount);
+		// Calculate breakdown of total payment amount into token purchase payment and platform fee
+		(uint256 buyAmount, uint256 feeAmount) = calculateContributionBreakdown(_contributionAmount);
 		
 		// Account count statistics
 		if (userContributionAmount[msg.sender] == 0) {
 			accountsCount++; // Increment accounts count only if this is the first contribution
 		}
 
-		// Token amount statistics - separate buy amount and fee
-		userContributionAmount[msg.sender] += _commitAmount; // update user's total contribution
-		userBuyAmount[msg.sender] += buyAmount; // update user's buy amount
-		userFeeAmount[msg.sender] += feeAmount; // update user's fee amount
-		require(userContributionAmount[msg.sender] <= _maxCommitAmount, "Buy amount exceeds max buy amount");
-		totalContributionAmount += _commitAmount; // update total contributed amount
-		if (msg.value > _commitAmount) {
-			payable(msg.sender).transfer(msg.value - _commitAmount); //refund excess ETH
+		// Payment amount statistics - separate token purchase payment and platform fee
+		userContributionAmount[msg.sender] += _contributionAmount; // update user's total payment amount
+		userBuyAmount[msg.sender] += buyAmount; // update user's payment amount for token purchase (excluding fee)
+		userFeeAmount[msg.sender] += feeAmount; // update user's platform fee payment amount
+		_validateUserContributionLimit(_maxContributionAmount);
+		totalContributionAmount += _contributionAmount; // update total payment amount
+		totalContributionAmountWithoutFee += buyAmount; // update total payment amount for token purchase (excluding fees)
+		totalContributionFee += feeAmount; // update total platform fee amount
+		if (msg.value > _contributionAmount) {
+			payable(msg.sender).transfer(msg.value - _contributionAmount); //refund excess ETH
 		}
-		emit Contributed(msg.sender, _commitAmount, address(0));
+		emit Contributed(msg.sender, _contributionAmount, address(0));
 	}
 
 	/**
 	 * @notice Contribute to the sale using ERC20 tokens. Requires whitelist proof.
-	 * @param _commitAmount The amount of tokens to contribute.
-	 * @param _maxCommitAmount The maximum allowed contribution for the user.
-	 * @param _proof The Merkle proof for whitelist verification.
+	 * @param _contributionAmount The amount of tokens to contribute.
+	 * @param _maxContributionAmount The maximum allowed contribution for the user.
+	 * @param _merkleProof The Merkle proof for whitelist verification.
 	 * @dev nonReentrant modifier is used to prevent reentrancy attacks.
 	 */
 	function contributeWithERC20(
-		uint256 _commitAmount,
-		uint256 _maxCommitAmount,
-		bytes32[] memory _proof
-	) public saleActive nonReentrant {
-		require(_commitAmount > 0, "Must send a positive amount");
-		require(_commitAmount <= _maxCommitAmount, "Buy amount exceeds max buy amount");
-
-		bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxCommitAmount))));
-		require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof");
-
-		require(
-			IERC20(paymentTokenAddress).allowance(msg.sender, address(this)) >= _commitAmount,
-			"Not enough allowance for payment token"
-		);
-		IERC20(paymentTokenAddress).transferFrom(msg.sender, address(this), _commitAmount); // Transfer payment tokens from user account
+		uint256 _contributionAmount,
+		uint256 _maxContributionAmount,
+		bytes32[] memory _merkleProof
+	) public saleActive nonReentrant whenNotPaused {
+		// Validate ERC20 contribution parameters
+		_validateERC20Contribution(_contributionAmount, _maxContributionAmount);
+		
+		// Verify Merkle proof
+		_validateMerkleProof(_maxContributionAmount, _merkleProof);
+		IERC20(paymentTokenAddress).transferFrom(msg.sender, address(this), _contributionAmount); // Transfer payment tokens from user account
 
-		// Calculate breakdown of contribution amount
-		(uint256 buyAmount, uint256 feeAmount) = calculateContributionBreakdown(_commitAmount);
+		// Calculate breakdown of total payment amount into token purchase payment and platform fee
+		(uint256 buyAmount, uint256 feeAmount) = calculateContributionBreakdown(_contributionAmount);
 		
 		if (userContributionAmount[msg.sender] == 0) {
 			accountsCount++; // Increment accounts count only if this is the first contribution
 		}
-		userContributionAmount[msg.sender] += _commitAmount; // update user's total contribution
-		userBuyAmount[msg.sender] += buyAmount; // update user's buy amount
-		userFeeAmount[msg.sender] += feeAmount; // update user's fee amount
-		require(userContributionAmount[msg.sender] <= _maxCommitAmount, "Buy amount exceeds max buy amount");
-		totalContributionAmount += _commitAmount; // update total contributed amount
-		emit Contributed(msg.sender, _commitAmount, paymentTokenAddress);
+		userContributionAmount[msg.sender] += _contributionAmount; // update user's total payment amount
+		userBuyAmount[msg.sender] += buyAmount; // update user's payment amount for token purchase (excluding fee)
+		userFeeAmount[msg.sender] += feeAmount; // update user's platform fee payment amount
+		_validateUserContributionLimit(_maxContributionAmount);
+		totalContributionAmount += _contributionAmount; // update total payment amount
+		totalContributionAmountWithoutFee += buyAmount; // update total payment amount for token purchase (excluding fees)
+		totalContributionFee += feeAmount; // update total platform fee amount
+		emit Contributed(msg.sender, _contributionAmount, paymentTokenAddress);
 	}
 
 	/**
@@ -547,24 +777,23 @@ contract Launchpad is ReentrancyGuard {
 	 * @dev nonReentrant modifier is used to prevent reentrancy attacks.
 	 *      This function clears the user's contribution after claim to prevent double claim.
 	 */
-	function claimTokens() public validClaimTime {
-		require(block.timestamp > endTime, "Sale is still active");
+	function claimTokens() public validClaimTime whenNotPaused {
+		_validateClaimConditions();
 		uint256 contributeAmount = userContributionAmount[msg.sender];
 		uint256 userBuyAmountValue = userBuyAmount[msg.sender];
 		uint256 userFeeAmountValue = userFeeAmount[msg.sender];
-		require(contributeAmount > 0, "No tokens to claim");
 		uint256 refundCost = 0;
 		uint256 boughtToken = 0;
 		
-		if (totalContributionAmount <= contributionTarget) {
+		if (totalContributionAmountWithoutFee <= contributionTarget) {
 			// Normal subscription: use pre-calculated buy amount
 			// Calculate token amount using PRBMath for high precision
 			boughtToken = PRBMath.mulDiv(userBuyAmountValue, 1e18, paymentTokenPrice);
 			IERC20(saleTokenAddress).transfer(msg.sender, boughtToken);
 		} else {
 			// Oversubscribed: all tokens are sold, user gets proportional share based on buy amount
-			// Calculate total buy amount from all contributions using PRBMath
-			uint256 totalBuyAmount = PRBMath.mulDiv(totalContributionAmount, MAX_FEE_BPS, MAX_FEE_BPS + feeBps);
+			// Use the new state variable for total buy amount
+			uint256 totalBuyAmount = totalContributionAmountWithoutFee;
 			
 			// User gets proportional share of all tokens based on their buy amount using PRBMath
 			boughtToken = PRBMath.mulDiv(userBuyAmountValue, totalTokensForSale, totalBuyAmount);
@@ -576,12 +805,15 @@ contract Launchpad is ReentrancyGuard {
 			// Calculate refund breakdown: both unused buy amount and unused fee
 			uint256 refundBuyAmount = userBuyAmountValue - userEffectiveBuyAmount;
 			uint256 refundFeeAmount = userFeeAmountValue - userEffectiveFeeAmount;
-			refundCost = refundBuyAmount + refundFeeAmount;
+			// Use unchecked for addition since both values are guaranteed to be positive
+			unchecked {
+				refundCost = refundBuyAmount + refundFeeAmount;
+			}
 			
 			// Refund
 			if (paymentTokenAddress == address(0)) {
 				(bool sent, ) = payable(msg.sender).call{ value: refundCost }("");
-				require(sent, "ETH refund failed");
+				if (!sent) revert TransferFailed();
 			} else {
 				IERC20(paymentTokenAddress).transfer(msg.sender, refundCost);
 			}
@@ -590,7 +822,8 @@ contract Launchpad is ReentrancyGuard {
 			IERC20(saleTokenAddress).transfer(msg.sender, boughtToken);
 		}
 		
-		// Clear user data
+		// Mark user as claimed and clear user data
+		userClaimed[msg.sender] = true;
 		userContributionAmount[msg.sender] = 0;
 		userBuyAmount[msg.sender] = 0;
 		userFeeAmount[msg.sender] = 0;