// SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; /** * @title TokenPurchase * @dev Contract providing purchase functionality for tokens with price set by admin */ contract TokenPurchase is Ownable { IERC20Metadata public token; // Token contract interface to be sold IERC20Metadata public paymentToken; // Token used for payment (if address(0), then native currency is used) uint256 public currentPrice; // Current price (in 10**18 format, 1 = 10**18) address public paymentReceiver; // Address to receive payments uint256 public minPurchaseAmount; // Minimum token amount that can be purchased // Price update event event PriceUpdated(uint256 newPrice); // Token purchase event event TokensPurchased( address indexed buyer, uint256 amount, uint256 totalCost ); // Minimum purchase amount update event event MinPurchaseAmountUpdated(uint256 newMinAmount); /** * @dev Constructor * @param _initialOwner Initial admin address * @param _initialPaymentReceiver Initial payment receiver address * @param _token Token to be sold (can be address(0) to set later) * @param _paymentToken Token used for payment (address(0) for native currency) * @param _initialPrice Initial price (can be 0 to set later) */ constructor( address _initialOwner, address _initialPaymentReceiver, address _token, address _paymentToken, uint256 _initialPrice ) Ownable(_initialOwner) { require( _initialPaymentReceiver != address(0), "TokenPurchase: Payment receiver cannot be zero address" ); paymentReceiver = _initialPaymentReceiver; // Set token to be sold if provided if (_token != address(0)) { token = IERC20Metadata(_token); } // Set payment token if provided (address(0) means use native currency) if (_paymentToken != address(0)) { paymentToken = IERC20Metadata(_paymentToken); } // Set initial price if provided if (_initialPrice > 0) { currentPrice = _initialPrice; emit PriceUpdated(_initialPrice); } // Default minimum purchase amount set to 0 minPurchaseAmount = 0; } /** * @dev Set token contract address * @param _token New token address */ function setToken(address _token) external onlyOwner { require( _token != address(0), "TokenPurchase: Token cannot be zero address" ); token = IERC20Metadata(_token); } /** * @dev Set payment token address * @param _paymentToken New payment token address (address(0) for native currency) */ function setPaymentToken(address _paymentToken) external onlyOwner { // address(0) is allowed for native currency if (_paymentToken != address(0)) { paymentToken = IERC20Metadata(_paymentToken); } else { paymentToken = IERC20Metadata(address(0)); } } /** * @dev Set payment receiver address * @param _paymentReceiver New payment receiver address */ function setPaymentReceiver(address _paymentReceiver) external onlyOwner { require( _paymentReceiver != address(0), "TokenPurchase: Payment receiver cannot be zero address" ); paymentReceiver = _paymentReceiver; } /** * @dev Set the current price * @param _price New price (in 10**18 format, 1 = 10**18) */ function setPrice(uint256 _price) external onlyOwner { require(_price > 0, "TokenPurchase: Price must be greater than zero"); currentPrice = _price; emit PriceUpdated(_price); } /** * @dev Set the minimum purchase amount * @param _minAmount New minimum purchase amount */ function setMinPurchaseAmount(uint256 _minAmount) external onlyOwner { minPurchaseAmount = _minAmount; emit MinPurchaseAmountUpdated(_minAmount); } /** * @dev Get the current price * @return Current price */ function getPrice() external view returns (uint256) { return currentPrice; } /** * @dev Purchase tokens * @param _amount Token amount to purchase (in smallest unit) */ function purchase(uint256 _amount) external payable { require(address(token) != address(0), "TokenPurchase: Token not set"); require(currentPrice > 0, "TokenPurchase: Price not set"); require(_amount > 0, "TokenPurchase: Amount must be greater than zero"); require( _amount >= minPurchaseAmount, "TokenPurchase: Amount below minimum purchase amount" ); // Get token decimals uint8 tokenDecimals = token.decimals(); // Calculate payment token decimals (18 for native currency) uint8 paymentDecimals = address(paymentToken) == address(0) ? 18 : paymentToken.decimals(); // Calculate total cost with proper decimal adjustment // Price is in 10**18 format (1 = 10**18) // Solidity 0.8+ has built-in overflow checking uint256 priceTimeAmount = currentPrice * _amount; uint256 totalCost = priceTimeAmount / 10 ** 18; // Ensure the calculation didn't result in zero due to precision loss require( totalCost > 0, "TokenPurchase: Cost calculation resulted in zero" ); // Adjust for the difference in decimal places between token and payment token if (tokenDecimals != paymentDecimals) { if (tokenDecimals > paymentDecimals) { totalCost = totalCost / 10 ** (tokenDecimals - paymentDecimals); } else { totalCost = totalCost * 10 ** (paymentDecimals - tokenDecimals); } } // Ensure final cost is not zero after all adjustments require( totalCost > 0, "TokenPurchase: Final cost calculation resulted in zero" ); // Check if contract has enough tokens require( token.balanceOf(address(this)) >= _amount, "TokenPurchase: Insufficient token balance" ); // Handle payment based on payment token type if (address(paymentToken) == address(0)) { // Native currency payment require( msg.value >= totalCost, "TokenPurchase: Insufficient payment" ); // Forward payment to payment receiver payable(paymentReceiver).transfer(totalCost); // Refund excess payment if any if (msg.value > totalCost) { payable(msg.sender).transfer(msg.value - totalCost); } } else { // ERC20 token payment require( msg.value == 0, "TokenPurchase: ETH not accepted for token payments" ); // Transfer payment tokens from buyer to payment receiver require( paymentToken.transferFrom( msg.sender, paymentReceiver, totalCost ), "TokenPurchase: Payment token transfer failed" ); } // Transfer tokens from contract to buyer require( token.transfer(msg.sender, _amount), "TokenPurchase: Token transfer failed" ); emit TokensPurchased(msg.sender, _amount, totalCost); } /** * @dev Allow admin to withdraw tokens from the contract * @param _amount Amount of tokens to withdraw */ function withdrawTokens(uint256 _amount) external onlyOwner { require(address(token) != address(0), "TokenPurchase: Token not set"); require( token.balanceOf(address(this)) >= _amount, "TokenPurchase: Insufficient token balance" ); require( token.transfer(paymentReceiver, _amount), "TokenPurchase: Token transfer failed" ); } }