[NAC-PRESALE-V2] 新预售合约和部署手册 - XICPresale.sol

This commit is contained in:
nacadmin 2026-03-10 01:55:27 +08:00
parent 48e3b611c3
commit 30dbacbd93
1 changed files with 443 additions and 0 deletions

View File

@ -0,0 +1,443 @@
// 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");
}
}
}