444 lines
17 KiB
Solidity
444 lines
17 KiB
Solidity
// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.20;
|
||
|
||
/**
|
||
* @title XICPresale
|
||
* @notice NAC XIC Token Presale Contract — 购买即时发放版本 v2
|
||
* @dev 用户支付 USDT 后,XIC 代币在同一笔交易内立即转入用户钱包
|
||
*
|
||
* 核心参数:
|
||
* - 预售总量:2,500,000,000 XIC(25亿)
|
||
* - 价格:$0.02 USDT / XIC
|
||
* - 预售硬顶:$50,000,000 USDT(5000万)
|
||
* - 无最小/最大单笔限制
|
||
* - 预售时长:180天(半年),到期自动停止
|
||
* - 预售结束后未售出 XIC 可由 Owner 回收
|
||
* - 支持 USDT 和 BNB 两种支付方式
|
||
*
|
||
* 合约关联:
|
||
* - XIC Token: 0x59FF34dD59680a7125782b1f6df2A86ed46F5A24
|
||
* - BSC USDT: 0x55d398326f99059fF775485246999027B3197955
|
||
*/
|
||
|
||
interface IERC20 {
|
||
function transfer(address to, uint256 amount) external returns (bool);
|
||
function transferFrom(address from, address to, uint256 amount) external returns (bool);
|
||
function balanceOf(address account) external view returns (uint256);
|
||
function allowance(address owner, address spender) external view returns (uint256);
|
||
function decimals() external view returns (uint8);
|
||
}
|
||
|
||
interface IPriceOracle {
|
||
function getBNBPrice() external view returns (uint256); // BNB/USD price, 18 decimals
|
||
}
|
||
|
||
contract XICPresale {
|
||
|
||
// ─── Constants ─────────────────────────────────────────────────────────────
|
||
|
||
uint256 public constant PRESALE_DURATION = 180 days; // 预售时长:半年
|
||
|
||
// ─── State Variables ───────────────────────────────────────────────────────
|
||
|
||
address public owner;
|
||
address public wallet; // USDT/BNB 收款钱包地址
|
||
|
||
IERC20 public immutable xicToken; // XIC 代币合约
|
||
IERC20 public immutable usdt; // BSC USDT 合约
|
||
|
||
// 价格:$0.02 USDT per XIC(18 decimals 精度)
|
||
uint256 public tokenPrice = 2e16; // 0.02 * 1e18
|
||
|
||
// 预售总量:25亿 XIC
|
||
uint256 public hardCap = 2_500_000_000 * 1e18;
|
||
|
||
// 预售时间
|
||
uint256 public presaleStartTime; // 预售开始时间(Unix 时间戳)
|
||
uint256 public presaleEndTime; // 预售结束时间(= startTime + 180 days)
|
||
bool public presaleStarted; // 是否已启动
|
||
|
||
// 预售状态(Owner 可手动暂停/恢复)
|
||
bool public presalePaused = false;
|
||
|
||
// 统计
|
||
uint256 public totalTokensSold; // 已售 XIC 总量(18 decimals)
|
||
uint256 public totalRaised; // 已筹 USDT 总量(6 decimals)
|
||
|
||
// 用户购买记录
|
||
mapping(address => uint256) public userPurchases; // 用户购买的 XIC 总量
|
||
mapping(address => uint256) public userSpent; // 用户花费的 USDT 总量
|
||
|
||
// BNB 价格预言机
|
||
address public priceOracle;
|
||
|
||
// ─── Events ────────────────────────────────────────────────────────────────
|
||
|
||
event PresaleStarted(uint256 startTime, uint256 endTime);
|
||
event PresalePaused(bool paused);
|
||
event PresaleEnded(uint256 totalSold, uint256 totalRaised);
|
||
event TokensPurchased(
|
||
address indexed buyer,
|
||
uint256 usdtAmount,
|
||
uint256 tokenAmount,
|
||
string paymentMethod
|
||
);
|
||
event UnsoldTokensRecovered(uint256 amount);
|
||
event WalletChanged(address newWallet);
|
||
event TokenPriceChanged(uint256 newPrice);
|
||
event HardCapChanged(uint256 newHardCap);
|
||
event EmergencyWithdraw(address token, uint256 amount);
|
||
|
||
// ─── Modifiers ─────────────────────────────────────────────────────────────
|
||
|
||
modifier onlyOwner() {
|
||
require(msg.sender == owner, "Presale: caller is not owner");
|
||
_;
|
||
}
|
||
|
||
modifier whenActive() {
|
||
require(presaleStarted, "Presale: not started yet");
|
||
require(!presalePaused, "Presale: presale is paused");
|
||
require(block.timestamp <= presaleEndTime, "Presale: presale has ended");
|
||
require(totalTokensSold < hardCap, "Presale: hard cap reached");
|
||
_;
|
||
}
|
||
|
||
modifier afterPresale() {
|
||
require(
|
||
presaleStarted && (block.timestamp > presaleEndTime || totalTokensSold >= hardCap),
|
||
"Presale: presale still active"
|
||
);
|
||
_;
|
||
}
|
||
|
||
// ─── Constructor ───────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* @param _xicToken XIC 代币合约地址
|
||
* @param _usdt BSC USDT 合约地址
|
||
* @param _wallet 收款钱包地址(接收 USDT 和 BNB)
|
||
* @param _oracle BNB 价格预言机地址(可为 address(0) 表示不支持 BNB 购买)
|
||
*/
|
||
constructor(
|
||
address _xicToken,
|
||
address _usdt,
|
||
address _wallet,
|
||
address _oracle
|
||
) {
|
||
require(_xicToken != address(0), "Invalid XIC token address");
|
||
require(_usdt != address(0), "Invalid USDT address");
|
||
require(_wallet != address(0), "Invalid wallet address");
|
||
|
||
owner = msg.sender;
|
||
xicToken = IERC20(_xicToken);
|
||
usdt = IERC20(_usdt);
|
||
wallet = _wallet;
|
||
priceOracle = _oracle;
|
||
}
|
||
|
||
// ─── Owner: Start Presale ──────────────────────────────────────────────────
|
||
|
||
/**
|
||
* @notice 启动预售(Owner 调用一次,之后自动计时 180 天)
|
||
* @dev 调用前请确保合约已持有足够的 XIC(至少 25亿 XIC)
|
||
*/
|
||
function startPresale() external onlyOwner {
|
||
require(!presaleStarted, "Presale: already started");
|
||
|
||
uint256 xicBalance = xicToken.balanceOf(address(this));
|
||
require(xicBalance >= hardCap, "Presale: insufficient XIC in contract");
|
||
|
||
presaleStarted = true;
|
||
presaleStartTime = block.timestamp;
|
||
presaleEndTime = block.timestamp + PRESALE_DURATION;
|
||
|
||
emit PresaleStarted(presaleStartTime, presaleEndTime);
|
||
}
|
||
|
||
/**
|
||
* @notice 暂停 / 恢复预售(紧急情况使用)
|
||
*/
|
||
function setPaused(bool _paused) external onlyOwner {
|
||
presalePaused = _paused;
|
||
emit PresalePaused(_paused);
|
||
}
|
||
|
||
// ─── Core Purchase Functions ───────────────────────────────────────────────
|
||
|
||
/**
|
||
* @notice 用 USDT 购买 XIC(即时发放)
|
||
* @param usdtAmount USDT 数量(6 decimals,例如 100 USDT = 100_000_000)
|
||
*
|
||
* 购买流程:
|
||
* 1. 用户调用 USDT.approve(presaleAddress, usdtAmount)
|
||
* 2. 调用此函数
|
||
* 3. 合约将 USDT 从用户转入 wallet
|
||
* 4. 合约立即将 XIC 转入用户钱包
|
||
*/
|
||
function buyWithUSDT(uint256 usdtAmount) external whenActive {
|
||
require(usdtAmount > 0, "Presale: amount must be > 0");
|
||
|
||
// 将 USDT (6d) 转换为 18d 精度,再计算 XIC 数量
|
||
// XIC = usdtAmount * 1e12 * 1e18 / tokenPrice
|
||
uint256 tokenAmount = (usdtAmount * 1e12 * 1e18) / tokenPrice;
|
||
|
||
require(tokenAmount > 0, "Presale: token amount too small");
|
||
require(
|
||
totalTokensSold + tokenAmount <= hardCap,
|
||
"Presale: exceeds hard cap"
|
||
);
|
||
|
||
// 检查合约 XIC 余额
|
||
require(
|
||
xicToken.balanceOf(address(this)) >= tokenAmount,
|
||
"Presale: insufficient XIC in contract"
|
||
);
|
||
|
||
// 检查用户 USDT 授权和余额
|
||
require(
|
||
usdt.allowance(msg.sender, address(this)) >= usdtAmount,
|
||
"Presale: insufficient USDT allowance"
|
||
);
|
||
require(
|
||
usdt.balanceOf(msg.sender) >= usdtAmount,
|
||
"Presale: insufficient USDT balance"
|
||
);
|
||
|
||
// 1. 收取 USDT → wallet
|
||
require(
|
||
usdt.transferFrom(msg.sender, wallet, usdtAmount),
|
||
"Presale: USDT transfer failed"
|
||
);
|
||
|
||
// 2. 立即发放 XIC → 买家
|
||
require(
|
||
xicToken.transfer(msg.sender, tokenAmount),
|
||
"Presale: XIC transfer failed"
|
||
);
|
||
|
||
// 更新统计
|
||
totalTokensSold += tokenAmount;
|
||
totalRaised += usdtAmount;
|
||
userPurchases[msg.sender] += tokenAmount;
|
||
userSpent[msg.sender] += usdtAmount;
|
||
|
||
emit TokensPurchased(msg.sender, usdtAmount, tokenAmount, "USDT");
|
||
}
|
||
|
||
/**
|
||
* @notice 用 BNB 购买 XIC(即时发放)
|
||
* @dev 需要价格预言机,直接发送 BNB 到合约地址也可触发
|
||
*/
|
||
function buyWithBNB() external payable whenActive {
|
||
require(msg.value > 0, "Presale: BNB amount must be > 0");
|
||
require(priceOracle != address(0), "Presale: BNB purchase not supported");
|
||
|
||
uint256 bnbPriceUSD = IPriceOracle(priceOracle).getBNBPrice();
|
||
require(bnbPriceUSD > 0, "Presale: invalid BNB price");
|
||
|
||
// BNB 价值(18d)→ XIC 数量
|
||
uint256 usdValue18 = (msg.value * bnbPriceUSD) / 1e18;
|
||
uint256 tokenAmount = (usdValue18 * 1e18) / tokenPrice;
|
||
|
||
require(tokenAmount > 0, "Presale: token amount too small");
|
||
require(
|
||
totalTokensSold + tokenAmount <= hardCap,
|
||
"Presale: exceeds hard cap"
|
||
);
|
||
require(
|
||
xicToken.balanceOf(address(this)) >= tokenAmount,
|
||
"Presale: insufficient XIC in contract"
|
||
);
|
||
|
||
// 1. 转发 BNB → wallet
|
||
(bool bnbOk, ) = wallet.call{value: msg.value}("");
|
||
require(bnbOk, "Presale: BNB transfer to wallet failed");
|
||
|
||
// 2. 立即发放 XIC → 买家
|
||
require(
|
||
xicToken.transfer(msg.sender, tokenAmount),
|
||
"Presale: XIC transfer failed"
|
||
);
|
||
|
||
// 等值 USDT(6d)用于统计
|
||
uint256 usdtEquiv = usdValue18 / 1e12;
|
||
|
||
totalTokensSold += tokenAmount;
|
||
totalRaised += usdtEquiv;
|
||
userPurchases[msg.sender] += tokenAmount;
|
||
userSpent[msg.sender] += usdtEquiv;
|
||
|
||
emit TokensPurchased(msg.sender, usdtEquiv, tokenAmount, "BNB");
|
||
}
|
||
|
||
// ─── Post-Presale: Recover Unsold Tokens ──────────────────────────────────
|
||
|
||
/**
|
||
* @notice 预售结束后,Owner 回收合约内未售出的 XIC
|
||
* @dev 只能在预售结束后(超时或售罄)调用
|
||
*/
|
||
function recoverUnsoldTokens() external onlyOwner afterPresale {
|
||
uint256 remaining = xicToken.balanceOf(address(this));
|
||
require(remaining > 0, "Presale: no unsold tokens to recover");
|
||
|
||
require(
|
||
xicToken.transfer(owner, remaining),
|
||
"Presale: recovery transfer failed"
|
||
);
|
||
|
||
emit UnsoldTokensRecovered(remaining);
|
||
emit PresaleEnded(totalTokensSold, totalRaised);
|
||
}
|
||
|
||
// ─── View Functions ────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* @notice 计算指定 USDT 金额可购买的 XIC 数量
|
||
* @param usdtAmount USDT 金额(6 decimals)
|
||
*/
|
||
function calculateTokenAmount(uint256 usdtAmount) external view returns (uint256) {
|
||
return (usdtAmount * 1e12 * 1e18) / tokenPrice;
|
||
}
|
||
|
||
/**
|
||
* @notice 计算指定 BNB 金额可购买的 XIC 数量
|
||
*/
|
||
function calculateTokenAmountForBNB(uint256 bnbAmount) external view returns (uint256) {
|
||
if (priceOracle == address(0)) return 0;
|
||
uint256 bnbPriceUSD = IPriceOracle(priceOracle).getBNBPrice();
|
||
if (bnbPriceUSD == 0) return 0;
|
||
uint256 usdValue18 = (bnbAmount * bnbPriceUSD) / 1e18;
|
||
return (usdValue18 * 1e18) / tokenPrice;
|
||
}
|
||
|
||
/**
|
||
* @notice 合约当前持有的 XIC 余额(可售量)
|
||
*/
|
||
function availableXIC() external view returns (uint256) {
|
||
return xicToken.balanceOf(address(this));
|
||
}
|
||
|
||
/**
|
||
* @notice 预售进度(已售 / 硬顶,basis points 0-10000)
|
||
*/
|
||
function presaleProgress() external view returns (
|
||
uint256 sold,
|
||
uint256 cap,
|
||
uint256 progressBps
|
||
) {
|
||
sold = totalTokensSold;
|
||
cap = hardCap;
|
||
progressBps = cap > 0 ? (sold * 10000) / cap : 0;
|
||
}
|
||
|
||
/**
|
||
* @notice 预售剩余时间(秒)
|
||
*/
|
||
function timeRemaining() external view returns (uint256) {
|
||
if (!presaleStarted || block.timestamp >= presaleEndTime) return 0;
|
||
return presaleEndTime - block.timestamp;
|
||
}
|
||
|
||
/**
|
||
* @notice 预售是否当前可购买
|
||
*/
|
||
function isPresaleActive() external view returns (bool) {
|
||
return presaleStarted
|
||
&& !presalePaused
|
||
&& block.timestamp <= presaleEndTime
|
||
&& totalTokensSold < hardCap;
|
||
}
|
||
|
||
/**
|
||
* @notice 获取 BNB 当前价格(USD,18 decimals)
|
||
*/
|
||
function getBNBPrice() external view returns (uint256) {
|
||
if (priceOracle == address(0)) return 0;
|
||
return IPriceOracle(priceOracle).getBNBPrice();
|
||
}
|
||
|
||
// ─── Owner Admin Functions ─────────────────────────────────────────────────
|
||
|
||
function setWallet(address _wallet) external onlyOwner {
|
||
require(_wallet != address(0), "Invalid wallet address");
|
||
wallet = _wallet;
|
||
emit WalletChanged(_wallet);
|
||
}
|
||
|
||
function setTokenPrice(uint256 _tokenPrice) external onlyOwner {
|
||
require(_tokenPrice > 0, "Invalid token price");
|
||
tokenPrice = _tokenPrice;
|
||
emit TokenPriceChanged(_tokenPrice);
|
||
}
|
||
|
||
function setHardCap(uint256 _hardCap) external onlyOwner {
|
||
require(_hardCap >= totalTokensSold, "Hard cap below sold amount");
|
||
hardCap = _hardCap;
|
||
emit HardCapChanged(_hardCap);
|
||
}
|
||
|
||
function setPriceOracle(address _oracle) external onlyOwner {
|
||
priceOracle = _oracle;
|
||
}
|
||
|
||
function transferOwnership(address newOwner) external onlyOwner {
|
||
require(newOwner != address(0), "Invalid owner address");
|
||
owner = newOwner;
|
||
}
|
||
|
||
/**
|
||
* @notice 紧急提取合约内的任意代币(误转入的情况)
|
||
* @dev 不可在预售进行中提取 XIC,防止影响购买
|
||
*/
|
||
function emergencyWithdraw(address token, uint256 amount) external onlyOwner {
|
||
// 预售进行中,禁止提取 XIC(防止合约无法发放)
|
||
if (
|
||
token == address(xicToken)
|
||
&& presaleStarted
|
||
&& block.timestamp <= presaleEndTime
|
||
&& totalTokensSold < hardCap
|
||
) {
|
||
revert("Presale: cannot withdraw XIC during active presale");
|
||
}
|
||
IERC20 tokenContract = IERC20(token);
|
||
require(tokenContract.balanceOf(address(this)) >= amount, "Insufficient balance");
|
||
require(tokenContract.transfer(owner, amount), "Transfer failed");
|
||
emit EmergencyWithdraw(token, amount);
|
||
}
|
||
|
||
/**
|
||
* @notice 提取合约内误转入的 BNB
|
||
*/
|
||
function withdrawBNB() external onlyOwner {
|
||
uint256 balance = address(this).balance;
|
||
require(balance > 0, "No BNB to withdraw");
|
||
(bool ok, ) = owner.call{value: balance}("");
|
||
require(ok, "BNB withdrawal failed");
|
||
}
|
||
|
||
// ─── Receive ───────────────────────────────────────────────────────────────
|
||
|
||
receive() external payable {
|
||
if (presaleStarted && !presalePaused && priceOracle != address(0)) {
|
||
// 直接发送 BNB 触发购买
|
||
require(msg.value > 0, "No BNB sent");
|
||
uint256 bnbPriceUSD = IPriceOracle(priceOracle).getBNBPrice();
|
||
require(bnbPriceUSD > 0, "Invalid BNB price");
|
||
uint256 usdValue18 = (msg.value * bnbPriceUSD) / 1e18;
|
||
uint256 tokenAmount = (usdValue18 * 1e18) / tokenPrice;
|
||
require(tokenAmount > 0, "Token amount too small");
|
||
require(totalTokensSold + tokenAmount <= hardCap, "Hard cap reached");
|
||
require(xicToken.balanceOf(address(this)) >= tokenAmount, "Insufficient XIC");
|
||
(bool bnbOk, ) = wallet.call{value: msg.value}("");
|
||
require(bnbOk, "BNB transfer failed");
|
||
require(xicToken.transfer(msg.sender, tokenAmount), "XIC transfer failed");
|
||
uint256 usdtEquiv = usdValue18 / 1e12;
|
||
totalTokensSold += tokenAmount;
|
||
totalRaised += usdtEquiv;
|
||
userPurchases[msg.sender] += tokenAmount;
|
||
userSpent[msg.sender] += usdtEquiv;
|
||
emit TokensPurchased(msg.sender, usdtEquiv, tokenAmount, "BNB");
|
||
}
|
||
}
|
||
}
|