|
@@ -3,11 +3,17 @@ pragma solidity ^0.8.24;
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
|
|
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
|
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
|
|
|
+import "prb-math/contracts/PRBMath.sol";
|
|
import "hardhat/console.sol"; // For debugging purposes, can be removed in production
|
|
import "hardhat/console.sol"; // For debugging purposes, can be removed in production
|
|
|
|
|
|
contract Launchpad is ReentrancyGuard {
|
|
contract Launchpad is ReentrancyGuard {
|
|
|
|
+
|
|
|
|
+ // ================================
|
|
|
|
+ // State Variables
|
|
|
|
+ // ================================
|
|
|
|
+
|
|
|
|
+ // Core Configuration
|
|
address public owner;
|
|
address public owner;
|
|
-
|
|
|
|
address public saleTokenAddress; // Sale token address
|
|
address public saleTokenAddress; // Sale token address
|
|
uint256 public totalTokensForSale; // Total tokens for sale
|
|
uint256 public totalTokensForSale; // Total tokens for sale
|
|
address public paymentTokenAddress; // Payment tokens accepted for the sale
|
|
address public paymentTokenAddress; // Payment tokens accepted for the sale
|
|
@@ -16,17 +22,26 @@ contract Launchpad is ReentrancyGuard {
|
|
uint256 public endTime; // Ending time of the sale
|
|
uint256 public endTime; // Ending time of the sale
|
|
bytes32 public merkleRoot; // Merkle root for whitelisting
|
|
bytes32 public merkleRoot; // Merkle root for whitelisting
|
|
|
|
|
|
|
|
+ // Sale Status
|
|
uint256 public contributionTarget; // Target amount to be raised in the sale
|
|
uint256 public contributionTarget; // Target amount to be raised in the sale
|
|
uint256 public totalContributionAmount; // Total contributed amount so far
|
|
uint256 public totalContributionAmount; // Total contributed amount so far
|
|
bool public claimEnabled; // Whether claiming tokens is enabled
|
|
bool public claimEnabled; // Whether claiming tokens is enabled
|
|
uint256 public claimStartTime; // Start time for claiming tokens
|
|
uint256 public claimStartTime; // Start time for claiming tokens
|
|
|
|
|
|
- mapping(address => uint256) public userContributionAmount; // User's contribution amount for claiming tokens
|
|
|
|
|
|
+ // User Data
|
|
|
|
+ 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
|
|
uint256 public accountsCount; // Number of accounts that have contributed
|
|
uint256 public accountsCount; // Number of accounts that have contributed
|
|
|
|
+
|
|
|
|
+ // Fee Configuration
|
|
uint256 public feeBps; // Fee in basis points (bps)
|
|
uint256 public feeBps; // Fee in basis points (bps)
|
|
uint256 public constant MAX_FEE_BPS = 10000; // 100% in basis points
|
|
uint256 public constant MAX_FEE_BPS = 10000; // 100% in basis points
|
|
uint256 public constant DECIMALS = 1e18;
|
|
uint256 public constant DECIMALS = 1e18;
|
|
|
|
|
|
|
|
+ // ================================
|
|
|
|
+ // Events
|
|
|
|
+ // ================================
|
|
event SaleCreated(address saleTokenAddress, uint256 totalTokensForSale);
|
|
event SaleCreated(address saleTokenAddress, uint256 totalTokensForSale);
|
|
event Contributed(address user, uint256 amount, address paymentTokenAddress);
|
|
event Contributed(address user, uint256 amount, address paymentTokenAddress);
|
|
event TokensClaimed(address user, uint256 tokensClaimed, uint256 refundAmount);
|
|
event TokensClaimed(address user, uint256 tokensClaimed, uint256 refundAmount);
|
|
@@ -34,12 +49,17 @@ contract Launchpad is ReentrancyGuard {
|
|
event EnableClaimToken(bool enabled, uint256 claimStartTime);
|
|
event EnableClaimToken(bool enabled, uint256 claimStartTime);
|
|
event DisableClaimToken(bool disabled);
|
|
event DisableClaimToken(bool disabled);
|
|
|
|
|
|
|
|
+ // ================================
|
|
|
|
+ // Constructor
|
|
|
|
+ // ================================
|
|
constructor() {
|
|
constructor() {
|
|
owner = msg.sender;
|
|
owner = msg.sender;
|
|
accountsCount = 0;
|
|
accountsCount = 0;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ // ================================
|
|
// Modifiers
|
|
// Modifiers
|
|
|
|
+ // ================================
|
|
modifier onlyOwner() {
|
|
modifier onlyOwner() {
|
|
require(msg.sender == owner, "Only owner can call this function");
|
|
require(msg.sender == owner, "Only owner can call this function");
|
|
_;
|
|
_;
|
|
@@ -50,18 +70,22 @@ contract Launchpad is ReentrancyGuard {
|
|
require(block.timestamp >= startTime && block.timestamp <= endTime, "Sale is not active");
|
|
require(block.timestamp >= startTime && block.timestamp <= endTime, "Sale is not active");
|
|
_;
|
|
_;
|
|
}
|
|
}
|
|
|
|
+
|
|
modifier validClaimTime() {
|
|
modifier validClaimTime() {
|
|
require(claimEnabled, "Claiming tokens is not enabled");
|
|
require(claimEnabled, "Claiming tokens is not enabled");
|
|
require(block.timestamp >= claimStartTime, "Claiming tokens not started yet");
|
|
require(block.timestamp >= claimStartTime, "Claiming tokens not started yet");
|
|
_;
|
|
_;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ // ================================
|
|
// Owner Functions
|
|
// Owner Functions
|
|
|
|
+ // ================================
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* @notice Creates a new token sale with specified parameters.
|
|
* @notice Creates a new token sale with specified parameters.
|
|
* @param _token The address of the token being sold.
|
|
* @param _token The address of the token being sold.
|
|
* @param _totalTokensForSale The total number of tokens available for sale.
|
|
* @param _totalTokensForSale The total number of tokens available for sale.
|
|
- * @param _contributionTarget The fundraising target amount.
|
|
|
|
|
|
+ * @param _contributionTarget The fundraising target amount (not including fee).
|
|
* @param _startTime The start time of the sale (timestamp).
|
|
* @param _startTime The start time of the sale (timestamp).
|
|
* @param _endTime The end 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 _paymentToken The address of the payment token (or address(0) for ETH).
|
|
@@ -80,7 +104,17 @@ contract Launchpad is ReentrancyGuard {
|
|
) public onlyOwner {
|
|
) public onlyOwner {
|
|
require(_token != address(0), "Invalid token address");
|
|
require(_token != address(0), "Invalid token address");
|
|
require(_totalTokensForSale > 0, "Total tokens must be greater than 0");
|
|
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(_startTime < _endTime, "Invalid time range");
|
|
|
|
+ require(_feeBps <= MAX_FEE_BPS, "Fee cannot exceed 100%");
|
|
|
|
+
|
|
|
|
+ // 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;
|
|
paymentTokenAddress = _paymentToken;
|
|
paymentTokenPrice = _price;
|
|
paymentTokenPrice = _price;
|
|
@@ -92,32 +126,14 @@ contract Launchpad is ReentrancyGuard {
|
|
totalContributionAmount = 0;
|
|
totalContributionAmount = 0;
|
|
feeBps = _feeBps; // Set the fee in basis points
|
|
feeBps = _feeBps; // Set the fee in basis points
|
|
merkleRoot = bytes32(0); // Reset Merkle root
|
|
merkleRoot = bytes32(0); // Reset Merkle root
|
|
- IERC20(_token).transferFrom(msg.sender, address(this), _totalTokensForSale); // Transfer tokens to the contracts
|
|
|
|
-
|
|
|
|
- emit SaleCreated(saleTokenAddress, totalTokensForSale);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * @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;
|
|
|
|
- IERC20(saleTokenAddress).transfer(owner, remainingAmount);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * @notice Withdraws all collected payments (ETH or ERC20) to the owner after the sale ends.
|
|
|
|
- */
|
|
|
|
- function withdrawPayments() public onlyOwner {
|
|
|
|
- require(block.timestamp > endTime, "Sale is still active");
|
|
|
|
- if (paymentTokenAddress == address(0)) {
|
|
|
|
- payable(owner).transfer(address(this).balance);
|
|
|
|
- } else {
|
|
|
|
- uint256 balance = IERC20(paymentTokenAddress).balanceOf(address(this));
|
|
|
|
- IERC20(paymentTokenAddress).transfer(owner, balance);
|
|
|
|
|
|
+
|
|
|
|
+ uint256 currentBalance = IERC20(_token).balanceOf(address(this));
|
|
|
|
+ if (currentBalance < _totalTokensForSale) {
|
|
|
|
+ uint256 needToTransfer = _totalTokensForSale - currentBalance;
|
|
|
|
+ IERC20(_token).transferFrom(msg.sender, address(this), needToTransfer);
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ emit SaleCreated(saleTokenAddress, totalTokensForSale);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -159,7 +175,71 @@ contract Launchpad is ReentrancyGuard {
|
|
emit DisableClaimToken(claimEnabled);
|
|
emit DisableClaimToken(claimEnabled);
|
|
}
|
|
}
|
|
|
|
|
|
- // Setter
|
|
|
|
|
|
+ /**
|
|
|
|
+ * @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;
|
|
|
|
+ IERC20(saleTokenAddress).transfer(owner, remainingAmount);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * @notice Withdraws payment tokens from the contract to the owner.
|
|
|
|
+ * In case of oversubscription, withdraws the total amount for sold tokens plus fees.
|
|
|
|
+ * In normal subscription, withdraws all contributions.
|
|
|
|
+ */
|
|
|
|
+ function withdrawPayments() public onlyOwner {
|
|
|
|
+ require(block.timestamp > endTime, "Sale is still active");
|
|
|
|
+
|
|
|
|
+ // Check if oversubscribed
|
|
|
|
+ if (totalContributionAmount > contributionTarget) {
|
|
|
|
+ // 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;
|
|
|
|
+
|
|
|
|
+ if (paymentTokenAddress == address(0)) {
|
|
|
|
+ // Native token (ETH)
|
|
|
|
+ if (withdrawAmount > 0 && address(this).balance >= withdrawAmount) {
|
|
|
|
+ payable(owner).transfer(withdrawAmount);
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ // ERC20 token
|
|
|
|
+ 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);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Keep in contract:
|
|
|
|
+ // Refund portion: totalContributionAmount - withdrawAmount
|
|
|
|
+ } else {
|
|
|
|
+ // Normal subscription: withdraw all contributions
|
|
|
|
+ if (paymentTokenAddress == address(0)) {
|
|
|
|
+ // Native token (ETH)
|
|
|
|
+ if (address(this).balance > 0) {
|
|
|
|
+ payable(owner).transfer(address(this).balance);
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ // ERC20 token
|
|
|
|
+ uint256 balance = IERC20(paymentTokenAddress).balanceOf(address(this));
|
|
|
|
+ if (balance > 0) {
|
|
|
|
+ IERC20(paymentTokenAddress).transfer(owner, balance);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // ================================
|
|
|
|
+ // Configuration Functions
|
|
|
|
+ // ================================
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* @notice Transfers contract ownership to a new address.
|
|
* @notice Transfers contract ownership to a new address.
|
|
* @param _newOwner The address of the new owner.
|
|
* @param _newOwner The address of the new owner.
|
|
@@ -197,7 +277,9 @@ contract Launchpad is ReentrancyGuard {
|
|
feeBps = _feeBps; // Set the fee in basis points
|
|
feeBps = _feeBps; // Set the fee in basis points
|
|
}
|
|
}
|
|
|
|
|
|
- //Getter
|
|
|
|
|
|
+ // ================================
|
|
|
|
+ // Getter Functions
|
|
|
|
+ // ================================
|
|
|
|
|
|
/**
|
|
/**
|
|
* @notice Returns the address of the contract owner.
|
|
* @notice Returns the address of the contract owner.
|
|
@@ -205,36 +287,70 @@ contract Launchpad is ReentrancyGuard {
|
|
function getOwner() public view returns (address) {
|
|
function getOwner() public view returns (address) {
|
|
return owner;
|
|
return owner;
|
|
}
|
|
}
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* @notice Returns the address of the sale token.
|
|
* @notice Returns the address of the sale token.
|
|
*/
|
|
*/
|
|
function getSaleToken() public view returns (address) {
|
|
function getSaleToken() public view returns (address) {
|
|
return saleTokenAddress;
|
|
return saleTokenAddress;
|
|
}
|
|
}
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* @notice Returns the total number of tokens for sale.
|
|
* @notice Returns the total number of tokens for sale.
|
|
*/
|
|
*/
|
|
function getTotalTokensForSale() public view returns (uint256) {
|
|
function getTotalTokensForSale() public view returns (uint256) {
|
|
return totalTokensForSale;
|
|
return totalTokensForSale;
|
|
}
|
|
}
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* @notice Returns the total contributed amount so far.
|
|
* @notice Returns the total contributed amount so far.
|
|
*/
|
|
*/
|
|
function getTotalContributeAmount() public view returns (uint256) {
|
|
function getTotalContributeAmount() public view returns (uint256) {
|
|
return totalContributionAmount;
|
|
return totalContributionAmount;
|
|
}
|
|
}
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* @notice Returns the sale start time.
|
|
* @notice Returns the sale start time.
|
|
*/
|
|
*/
|
|
function getStartTime() public view returns (uint256) {
|
|
function getStartTime() public view returns (uint256) {
|
|
return startTime;
|
|
return startTime;
|
|
}
|
|
}
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* @notice Returns the sale end time.
|
|
* @notice Returns the sale end time.
|
|
*/
|
|
*/
|
|
function getEndTime() public view returns (uint256) {
|
|
function getEndTime() public view returns (uint256) {
|
|
return endTime;
|
|
return endTime;
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * @notice Returns the payment token price.
|
|
|
|
+ */
|
|
|
|
+ function getPaymentTokenPrice() public view returns (uint256) {
|
|
|
|
+ return paymentTokenPrice;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * @notice Returns the payment token address.
|
|
|
|
+ */
|
|
|
|
+ function getPaymentTokens() public view returns (address) {
|
|
|
|
+ return paymentTokenAddress;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * @notice Returns the claim start time.
|
|
|
|
+ */
|
|
|
|
+ function getClaimStartTime() public view returns (uint256) {
|
|
|
|
+ return claimStartTime;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * @notice Returns whether claiming is enabled.
|
|
|
|
+ */
|
|
|
|
+ function isClaimEnabled() public view returns (bool) {
|
|
|
|
+ return claimEnabled;
|
|
|
|
+ }
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* @notice Returns all sale details as a tuple.
|
|
* @notice Returns all sale details as a tuple.
|
|
*/
|
|
*/
|
|
@@ -242,25 +358,29 @@ contract Launchpad is ReentrancyGuard {
|
|
public
|
|
public
|
|
view
|
|
view
|
|
returns (
|
|
returns (
|
|
- address saleTokenAddress,
|
|
|
|
- uint256 totalTokensForSale,
|
|
|
|
- uint256 totalContributeAmount,
|
|
|
|
- uint256 startTime,
|
|
|
|
- uint256 endTime,
|
|
|
|
- bytes32 merkleRoot,
|
|
|
|
- address paymentTokenAddress,
|
|
|
|
- uint256 salePaymentPrice
|
|
|
|
|
|
+ address _saleTokenAddress,
|
|
|
|
+ uint256 _totalTokensForSale,
|
|
|
|
+ uint256 _totalContributionAmount,
|
|
|
|
+ uint256 _contributionTarget,
|
|
|
|
+ uint256 _startTime,
|
|
|
|
+ uint256 _endTime,
|
|
|
|
+ bytes32 _merkleRoot,
|
|
|
|
+ address _paymentTokenAddress,
|
|
|
|
+ uint256 _paymentTokenPrice,
|
|
|
|
+ uint256 _feeBps
|
|
)
|
|
)
|
|
{
|
|
{
|
|
return (
|
|
return (
|
|
saleTokenAddress,
|
|
saleTokenAddress,
|
|
totalTokensForSale,
|
|
totalTokensForSale,
|
|
totalContributionAmount,
|
|
totalContributionAmount,
|
|
|
|
+ contributionTarget,
|
|
startTime,
|
|
startTime,
|
|
endTime,
|
|
endTime,
|
|
merkleRoot,
|
|
merkleRoot,
|
|
paymentTokenAddress,
|
|
paymentTokenAddress,
|
|
- paymentTokenPrice
|
|
|
|
|
|
+ paymentTokenPrice,
|
|
|
|
+ feeBps
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
@@ -273,42 +393,75 @@ contract Launchpad is ReentrancyGuard {
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
/**
|
|
- * @notice Returns the payment token price.
|
|
|
|
|
|
+ * @notice Returns the user's buy amount (excluding fee).
|
|
|
|
+ * @param _user The address of the user.
|
|
*/
|
|
*/
|
|
- function getPaymentTokenPrice() public view returns (uint256) {
|
|
|
|
- return paymentTokenPrice;
|
|
|
|
|
|
+ function getUserBuyAmount(address _user) public view returns (uint256) {
|
|
|
|
+ return userBuyAmount[_user];
|
|
}
|
|
}
|
|
|
|
+
|
|
/**
|
|
/**
|
|
- * @notice Returns the payment token address.
|
|
|
|
|
|
+ * @notice Returns the user's fee amount.
|
|
|
|
+ * @param _user The address of the user.
|
|
*/
|
|
*/
|
|
- function getPaymentTokens() public view returns (address) {
|
|
|
|
- return paymentTokenAddress;
|
|
|
|
|
|
+ function getUserFeeAmount(address _user) public view returns (uint256) {
|
|
|
|
+ return userFeeAmount[_user];
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ // ================================
|
|
|
|
+ // Calculation Functions
|
|
|
|
+ // ================================
|
|
|
|
+
|
|
/**
|
|
/**
|
|
- * @notice Returns the claim start time.
|
|
|
|
|
|
+ * @notice Calculates the maximum contribution amount for a user (including fee).
|
|
|
|
+ * @param _maxBuyAmount The maximum amount of tokens a user can buy.
|
|
|
|
+ * @return The maximum contribution amount including fee.
|
|
*/
|
|
*/
|
|
- function getClaimStartTime() public view returns (uint256) {
|
|
|
|
- return claimStartTime;
|
|
|
|
|
|
+ function calculateMaxContributionAmount(uint256 _maxBuyAmount) 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);
|
|
|
|
+
|
|
|
|
+ return buyAmountValue + feeAmount; // Personal hard cap = buy amount + fee
|
|
}
|
|
}
|
|
|
|
+
|
|
/**
|
|
/**
|
|
- * @notice Returns whether claiming is enabled.
|
|
|
|
|
|
+ * @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.
|
|
*/
|
|
*/
|
|
- function isClaimEnabled() public view returns (bool) {
|
|
|
|
- return claimEnabled;
|
|
|
|
|
|
+ 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)
|
|
|
|
+ // Using PRBMath for high precision calculation
|
|
|
|
+ buyAmount = PRBMath.mulDiv(_contributionAmount, MAX_FEE_BPS, MAX_FEE_BPS + feeBps);
|
|
|
|
+ feeAmount = _contributionAmount - buyAmount;
|
|
|
|
+ return (buyAmount, feeAmount);
|
|
}
|
|
}
|
|
|
|
|
|
- //Main Function
|
|
|
|
/**
|
|
/**
|
|
* @notice Calculates the net contribution after deducting the fee.
|
|
* @notice Calculates the net contribution after deducting the fee.
|
|
* @param _commitAmount The original contribution amount.
|
|
* @param _commitAmount The original contribution amount.
|
|
* @return The net contribution after fee deduction.
|
|
* @return The net contribution after fee deduction.
|
|
*/
|
|
*/
|
|
function calculateNetContribution(uint256 _commitAmount) public view returns (uint256) {
|
|
function calculateNetContribution(uint256 _commitAmount) public view returns (uint256) {
|
|
- // Calculate net contribution after fee
|
|
|
|
- uint256 net = (_commitAmount * (MAX_FEE_BPS - feeBps)) / MAX_FEE_BPS;
|
|
|
|
- return net;
|
|
|
|
|
|
+ // 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);
|
|
|
|
+ return buyAmount;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ // ================================
|
|
|
|
+ // Main Functions
|
|
|
|
+ // ================================
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* @notice Contribute to the sale using ETH. Requires whitelist proof.
|
|
* @notice Contribute to the sale using ETH. Requires whitelist proof.
|
|
* @param _commitAmount The amount of ETH to contribute.
|
|
* @param _commitAmount The amount of ETH to contribute.
|
|
@@ -327,17 +480,22 @@ contract Launchpad is ReentrancyGuard {
|
|
require(msg.value >= _commitAmount, "Must send enough ETH");
|
|
require(msg.value >= _commitAmount, "Must send enough ETH");
|
|
require(paymentTokenPrice > 0, "Payment token not accepted for this sale");
|
|
require(paymentTokenPrice > 0, "Payment token not accepted for this sale");
|
|
|
|
|
|
- // 验证 Merkle 证明
|
|
|
|
|
|
+ // Verify Merkle proof
|
|
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxCommitAmount))));
|
|
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxCommitAmount))));
|
|
require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof");
|
|
require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof");
|
|
|
|
|
|
- //户数统计
|
|
|
|
|
|
+ // Calculate breakdown of contribution amount
|
|
|
|
+ (uint256 buyAmount, uint256 feeAmount) = calculateContributionBreakdown(_commitAmount);
|
|
|
|
+
|
|
|
|
+ // Account count statistics
|
|
if (userContributionAmount[msg.sender] == 0) {
|
|
if (userContributionAmount[msg.sender] == 0) {
|
|
accountsCount++; // Increment accounts count only if this is the first contribution
|
|
accountsCount++; // Increment accounts count only if this is the first contribution
|
|
}
|
|
}
|
|
|
|
|
|
- //币数统计
|
|
|
|
- userContributionAmount[msg.sender] += _commitAmount; // update user's commit tokens
|
|
|
|
|
|
+ // 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");
|
|
require(userContributionAmount[msg.sender] <= _maxCommitAmount, "Buy amount exceeds max buy amount");
|
|
totalContributionAmount += _commitAmount; // update total contributed amount
|
|
totalContributionAmount += _commitAmount; // update total contributed amount
|
|
if (msg.value > _commitAmount) {
|
|
if (msg.value > _commitAmount) {
|
|
@@ -368,13 +526,19 @@ contract Launchpad is ReentrancyGuard {
|
|
IERC20(paymentTokenAddress).allowance(msg.sender, address(this)) >= _commitAmount,
|
|
IERC20(paymentTokenAddress).allowance(msg.sender, address(this)) >= _commitAmount,
|
|
"Not enough allowance for payment token"
|
|
"Not enough allowance for payment token"
|
|
);
|
|
);
|
|
|
|
+ IERC20(paymentTokenAddress).transferFrom(msg.sender, address(this), _commitAmount); // Transfer payment tokens from user account
|
|
|
|
+
|
|
|
|
+ // Calculate breakdown of contribution amount
|
|
|
|
+ (uint256 buyAmount, uint256 feeAmount) = calculateContributionBreakdown(_commitAmount);
|
|
|
|
+
|
|
if (userContributionAmount[msg.sender] == 0) {
|
|
if (userContributionAmount[msg.sender] == 0) {
|
|
accountsCount++; // Increment accounts count only if this is the first contribution
|
|
accountsCount++; // Increment accounts count only if this is the first contribution
|
|
}
|
|
}
|
|
- userContributionAmount[msg.sender] += _commitAmount; // update user's claimable tokens
|
|
|
|
|
|
+ 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");
|
|
require(userContributionAmount[msg.sender] <= _maxCommitAmount, "Buy amount exceeds max buy amount");
|
|
totalContributionAmount += _commitAmount; // update total contributed amount
|
|
totalContributionAmount += _commitAmount; // update total contributed amount
|
|
- IERC20(paymentTokenAddress).transferFrom(msg.sender, address(this), _commitAmount); // 从用户账户转移支付代币
|
|
|
|
emit Contributed(msg.sender, _commitAmount, paymentTokenAddress);
|
|
emit Contributed(msg.sender, _commitAmount, paymentTokenAddress);
|
|
}
|
|
}
|
|
|
|
|
|
@@ -386,27 +550,50 @@ contract Launchpad is ReentrancyGuard {
|
|
function claimTokens() public validClaimTime {
|
|
function claimTokens() public validClaimTime {
|
|
require(block.timestamp > endTime, "Sale is still active");
|
|
require(block.timestamp > endTime, "Sale is still active");
|
|
uint256 contributeAmount = userContributionAmount[msg.sender];
|
|
uint256 contributeAmount = userContributionAmount[msg.sender];
|
|
|
|
+ uint256 userBuyAmountValue = userBuyAmount[msg.sender];
|
|
|
|
+ uint256 userFeeAmountValue = userFeeAmount[msg.sender];
|
|
require(contributeAmount > 0, "No tokens to claim");
|
|
require(contributeAmount > 0, "No tokens to claim");
|
|
uint256 refundCost = 0;
|
|
uint256 refundCost = 0;
|
|
uint256 boughtToken = 0;
|
|
uint256 boughtToken = 0;
|
|
|
|
+
|
|
if (totalContributionAmount <= contributionTarget) {
|
|
if (totalContributionAmount <= contributionTarget) {
|
|
- uint256 netContribution = calculateNetContribution(contributeAmount);
|
|
|
|
- boughtToken = (netContribution * DECIMALS) / paymentTokenPrice;
|
|
|
|
|
|
+ // 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);
|
|
IERC20(saleTokenAddress).transfer(msg.sender, boughtToken);
|
|
} else {
|
|
} else {
|
|
- uint256 userValidContribution = (contributeAmount * contributionTarget) / totalContributionAmount;
|
|
|
|
- refundCost = contributeAmount - userValidContribution;
|
|
|
|
- uint256 netContribution = calculateNetContribution(userValidContribution);
|
|
|
|
- boughtToken = (netContribution * DECIMALS) / paymentTokenPrice;
|
|
|
|
|
|
+ // 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);
|
|
|
|
+
|
|
|
|
+ // User gets proportional share of all tokens based on their buy amount using PRBMath
|
|
|
|
+ boughtToken = PRBMath.mulDiv(userBuyAmountValue, totalTokensForSale, totalBuyAmount);
|
|
|
|
+
|
|
|
|
+ // Calculate how much the user actually pays for these tokens using PRBMath
|
|
|
|
+ uint256 userEffectiveBuyAmount = PRBMath.mulDiv(boughtToken, paymentTokenPrice, 1e18);
|
|
|
|
+ uint256 userEffectiveFeeAmount = PRBMath.mulDiv(userEffectiveBuyAmount, feeBps, MAX_FEE_BPS);
|
|
|
|
+
|
|
|
|
+ // Calculate refund breakdown: both unused buy amount and unused fee
|
|
|
|
+ uint256 refundBuyAmount = userBuyAmountValue - userEffectiveBuyAmount;
|
|
|
|
+ uint256 refundFeeAmount = userFeeAmountValue - userEffectiveFeeAmount;
|
|
|
|
+ refundCost = refundBuyAmount + refundFeeAmount;
|
|
|
|
+
|
|
|
|
+ // Refund
|
|
if (paymentTokenAddress == address(0)) {
|
|
if (paymentTokenAddress == address(0)) {
|
|
(bool sent, ) = payable(msg.sender).call{ value: refundCost }("");
|
|
(bool sent, ) = payable(msg.sender).call{ value: refundCost }("");
|
|
require(sent, "ETH refund failed");
|
|
require(sent, "ETH refund failed");
|
|
} else {
|
|
} else {
|
|
IERC20(paymentTokenAddress).transfer(msg.sender, refundCost);
|
|
IERC20(paymentTokenAddress).transfer(msg.sender, refundCost);
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ // Transfer tokens
|
|
IERC20(saleTokenAddress).transfer(msg.sender, boughtToken);
|
|
IERC20(saleTokenAddress).transfer(msg.sender, boughtToken);
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ // Clear user data
|
|
userContributionAmount[msg.sender] = 0;
|
|
userContributionAmount[msg.sender] = 0;
|
|
|
|
+ userBuyAmount[msg.sender] = 0;
|
|
|
|
+ userFeeAmount[msg.sender] = 0;
|
|
emit TokensClaimed(msg.sender, boughtToken, refundCost);
|
|
emit TokensClaimed(msg.sender, boughtToken, refundCost);
|
|
}
|
|
}
|
|
}
|
|
}
|