使用 Solidity 在以太坊区块链上编写 ERC20 代币预售智能合约的全面指南
介绍
本文将为您提供一份全面的指南,一步一步教您如何构建一个接受 ETH 和主流稳定币的预售合约。
主要特点
- 多种支付方式(ETH、USDT、USDC、DAI)
- 早期投资者奖励制度
- 分阶段的代币购买活动
先决条件
- Hardhat 开发环境
- Openzeppelin 合同
- 以太坊开发经验
- 对ERC20代币的基本了解
代币功能
- 类型:ERC20
- 名称:银凤凰
- 股票代码:SPX
- 十进制:18
- 总供应量:1000亿
预售功能
- 预售供应量:100亿(10%)
- 预售期:30天
- 预售阶段:4
- 软上限:500000 USDT
- 硬顶:1020000 USDT
- 各阶段的价格和代币数量:
| 阶段 | 价格 | 代币金额 |
|---|---|---|
| 1 | 0.00008 美元 | 30亿 |
| 2 | 0.00010 美元 | 40亿 |
| 3 | 0.00012 美元 | 20亿 |
| 4 | 0.00014 美元 | 10亿 |
- 可购买代币的选项:ETH、USDT、USDC、DAI
- 申领时间:第二次公开拍卖结束后
- 购买代币的最低金额:100 USDT
在软顶达到之前购买代币的投资者将被列入早期投资者名单,如果预售结束后仍有未售出的代币,他们可以获得奖励代币。
如何逐步实施
主要功能:
- buyWithETH
- 用稳定币购买
- 用于计算可用 ETH 或稳定币代币数量的辅助函数,反之亦然。
- 宣称
- 提取
- 退款
- 一些辅助函数,例如 set 和 get 函数
使用 ETH 购买 SPX 代币
function buyWithETH() external payable whenNotPaused nonReentrant {
require(
block.timestamp >= startTime && block.timestamp <= endTime,
"Invalid time for buying the token"
);
uint256 _estimatedTokenAmount = estimatedTokenAmountAvailableWithETH(
msg.value
);
uint256 _tokensAvailable = tokensAvailable();
require(
_estimatedTokenAmount <= _tokensAvailable &&
_estimatedTokenAmount > 0,
"Invalid token amount to buy"
);
uint256 minUSDTOutput = (_estimatedTokenAmount * 90) / 100;
// Swap ETH for USDT
address[] memory path = new address[](2);
path[0] = router.WETH();
path[1] = USDT;
uint256[] memory amounts = router.swapExactETHForTokens{
value: msg.value
}(minUSDTOutput, path, address(this), block.timestamp + 15 minutes);
// Ensure the swap was successful
require(amounts.length > 1, "Swap failed, no USDT received");
uint256 _usdtAmount = amounts[1];
// Calculate final token amount
uint256 _tokenAmount = estimatedTokenAmountAvailableWithCoin(
_usdtAmount,
USDTInterface
);
//Update investor records
_updateInvestorRecords(
msg.sender,
_tokenAmount,
USDTInterface,
_usdtAmount
);
//Update presale stats
_updatePresaleStats(_tokenAmount, _usdtAmount, 6);
emit TokensBought(
msg.sender,
_tokenAmount,
_usdtAmount,
block.timestamp
);
}
首先,该函数检查预售是否正在进行。
接下来,它估算投资者可以用特定数量的 ETH 购买多少代币,并检查该数量是否可用。
然后,它使用 Uniswap V2 路由器将 ETH 兑换成 USDT 以购买 SPX 代币,并返回等值的 USDT。
接下来,它计算投资者可以用兑换后的 USDT 购买多少代币。
最后,它更新投资者和投资状态。
function _updateInvestorRecords(
address investor_,
uint256 tokenAmount_,
IERC20 coin_,
uint256 coinAmount_
) private {
if (investorTokenBalance[investor_] == 0) {
investors.push(investor_);
if (fundsRaised < softcap && !earlyInvestorsMapping[investor_]) {
earlyInvestorsMapping[investor_] = true;
earlyInvestors.push(investor_);
}
}
investorTokenBalance[investor_] += tokenAmount_;
investments[investor_][address(coin_)] += coinAmount_;
}
接下来,它会更新预售状态。
function _updatePresaleStats(
uint256 tokenAmount_,
uint256 coinAmount_,
uint8 coinDecimals_
) private {
totalTokensSold += tokenAmount_;
fundsRaised += coinAmount_ / (10 ** (coinDecimals_ - 6));
}
最后,它会发出TokensBought事件。
使用稳定币购买SPX代币
function _buyWithCoin(
IERC20 coin_,
uint256 tokenAmount_
) internal checkSaleState(tokenAmount_) whenNotPaused nonReentrant {
uint256 _coinAmount = estimatedCoinAmountForTokenAmount(
tokenAmount_,
coin_
);
uint8 _coinDecimals = getCoinDecimals(coin_);
//Check allowances and balances
require(
_coinAmount <= coin_.allowance(msg.sender, address(this)),
"Insufficient allowance"
);
require(
_coinAmount <= coin_.balanceOf(msg.sender),
"Insufficient balance."
);
//Send the coin to the contract
SafeERC20.safeTransferFrom(
coin_,
msg.sender,
address(this),
_coinAmount
);
//Update the investor status
_updateInvestorRecords(msg.sender, tokenAmount_, coin_, _coinAmount);
// Update presale stats
_updatePresaleStats(tokenAmount_, _coinAmount, _coinDecimals);
emit TokensBought(
msg.sender,
tokenAmount_,
_coinAmount,
block.timestamp
);
}
首先,该函数会检查预售是否正在进行、投资者想要购买的代币数量是否可用等等(根据条件调整)。
接下来,它会计算购买这些代币所需的代币数量,并检查投资者的账户余额和授权额度是否充足。
然后,它会将稳定币转入预售合约。
之后,它会更新投资者和投资状态以及预售状态,最后发出TokensBought事件。
使用特定稳定币购买代币的每个函数都可以写成如下形式:
function buyWithUSDT(uint256 tokenAmount_) external whenNotPaused {
_buyWithCoin(USDTInterface, tokenAmount_);
}
用于计算 SPX 代币与 ETH 之间互换的辅助函数
function estimatedTokenAmountAvailableWithETH(
uint256 ethAmount_
) public view returns (uint256) {
// Swap ETH for USDT
address[] memory path = new address[](2);
path[0] = router.WETH();
path[1] = USDT;
uint256[] memory amounts = router.getAmountsOut(ethAmount_, path);
require(amounts.length > 1, "Invalid path");
uint256 _usdtAmount = amounts[1];
// Calculate token amount
return
estimatedTokenAmountAvailableWithCoin(_usdtAmount, USDTInterface);
}
该函数使用 Uniswap V2 路由器和函数计算用户使用特定数量的 ETH 可以购买多少代币estimatedTokenAmountAvailableWithCoin。
用于计算SPX代币与稳定币数量之间相互转换的辅助函数
function estimatedTokenAmountAvailableWithCoin(
uint256 coinAmount_,
IERC20 coin_
) public view returns (uint256) {
uint256 tokenAmount = 0;
uint256 remainingCoinAmount = coinAmount_;
uint8 _coinDecimals = getCoinDecimals(coin_);
for (uint8 i = 0; i < thresholds.length; i++) {
// Get the current token price at the index
uint256 _priceAtCurrentTier = getCurrentTokenPriceForIndex(i);
uint256 _currentThreshold = thresholds[i];
// Determine the number of tokens available at this tier
uint256 numTokensAvailableAtTier = _currentThreshold >
totalTokensSold
? _currentThreshold - totalTokensSold
: 0;
// Calculate the maximum number of tokens that can be bought with the remaining coin amount
uint256 maxTokensAffordable = (remainingCoinAmount *
(10 ** (18 - _coinDecimals + 6))) / _priceAtCurrentTier;
// Determine how many tokens can actually be bought at this tier
uint256 tokensToBuyAtTier = numTokensAvailableAtTier <
maxTokensAffordable
? numTokensAvailableAtTier
: maxTokensAffordable;
// Update amounts
tokenAmount += tokensToBuyAtTier;
remainingCoinAmount -=
(tokensToBuyAtTier * _priceAtCurrentTier) /
(10 ** (18 - _coinDecimals + 6));
// If there is no remaining coin amount, break out of the loop
if (remainingCoinAmount == 0) {
break;
}
}
return tokenAmount;
}
此功能确保:
- 在不同价格层级上进行准确的代币计算
- 针对不同稳定币的正确小数处理方法
- 每个层级的最大代币可用数量限制
- 有效利用剩余购买金额
该实现方案支持预售的分级定价结构,同时保持代币计算的精确性。
索赔功能
function claim(address investor_) external nonReentrant {
require(
block.timestamp > claimTime && claimTime > 0,
"It's not claiming time yet."
);
require(
fundsRaised >= softcap,
"Can not claim as softcap not reached. Instead you can be refunded."
);
uint256 _tokenAmountforUser = getTokenAmountForInvestor(investor_);
uint256 _bonusTokenAmount = getBonusTokenAmount();
if (isEarlyInvestors(investor_))
_tokenAmountforUser += _bonusTokenAmount;
require(_tokenAmountforUser > 0, "No tokens claim.");
investorTokenBalance[investor_] = 0;
earlyInvestorsMapping[investor_] = false;
SafeERC20.safeTransfer(token, investor_, _tokenAmountforUser);
emit TokensClaimed(investor_, _tokenAmountforUser);
}
此功能
- 检查索赔时间和软上限要求
- 计算包括奖励在内的总代币数
- 重置投资者余额和早期投资者身份
SafeERC20代币转移的用途- 发出
TokensClaimed事件
取款功能
function withdraw() external onlyOwner nonReentrant {
require(
block.timestamp > endTime,
"Cannot withdraw because presale is still in progress."
);
require(wallet != address(0), "Wallet not set");
require(
fundsRaised > softcap,
"Can not withdraw as softcap not reached."
);
uint256 _usdtBalance = USDTInterface.balanceOf(address(this));
uint256 _usdcBalance = USDCInterface.balanceOf(address(this));
uint256 _daiBalance = DAIInterface.balanceOf(address(this));
require(
_usdtBalance > 0 && _usdcBalance > 0 && _daiBalance > 0,
"No funds to withdraw"
);
if (_usdtBalance > 0)
SafeERC20.safeTransfer(USDTInterface, wallet, _usdtBalance);
if (_usdcBalance > 0)
SafeERC20.safeTransfer(USDCInterface, wallet, _usdcBalance);
if (_daiBalance > 0)
SafeERC20.safeTransfer(DAIInterface, wallet, _daiBalance);
}
此功能
- 验证是否已设置预定义的多重签名钱包地址
- 确保预售已经结束
- 核实是否存在充足资金
SafeERC20转账用途
退款功能
function refund() external onlyOwner nonReentrant {
require(
block.timestamp > endTime,
"Cannot refund because presale is still in progress."
);
require(fundsRaised < softcap, "Softcap reached, refund not available");
// refund all funds to investors
for (uint256 i = 0; i < investors.length; i++) {
address investor = investors[i];
//Refund USDT
uint256 _usdtAmount = investments[investor][address(USDTInterface)];
if (_usdtAmount > 0) {
investments[investor][address(USDTInterface)] = 0;
SafeERC20.safeTransfer(USDTInterface, investor, _usdtAmount);
emit FundsRefunded(investor, _usdtAmount, block.timestamp);
}
//Refund USDC
uint256 _usdcAmount = investments[investor][address(USDCInterface)];
if (_usdcAmount > 0) {
investments[investor][address(USDCInterface)] = 0;
SafeERC20.safeTransfer(USDCInterface, investor, _usdcAmount);
emit FundsRefunded(investor, _usdcAmount, block.timestamp);
}
//Refund DAI
uint256 _daiAmount = investments[investor][address(DAIInterface)];
if (_daiAmount > 0) {
investments[investor][address(DAIInterface)] = 0;
SafeERC20.safeTransfer(DAIInterface, investor, _daiAmount);
emit FundsRefunded(investor, _daiAmount, block.timestamp);
}
}
fundsRaised = 0;
delete investors;
}
此功能
- 循环遍及所有投资者
- 对每种稳定币分别进行检查和退款。
- 将投资记录重置为零
- 发出
FundsRefunded事件 - 清除全局状态(
fundsRaised和investors数组)
结论
此 SPX 代币预售智能合约展示了一个强大而灵活的实现方案,可有效处理包括 ETH、USDT、USDC 和 DAI 在内的多种支付方式。
该方案可作为未来预售合约的优秀模板,在安全性、功能性和用户易用性之间实现了平衡。
其架构通过结构完善的验证和分配机制,确保了公平分配,同时保障了投资者和项目所有者的利益。