使用 Solidity 在以太坊区块链上编写 ERC20 代币预售智能合约的综合指南
介绍
本文将为您提供全面的指南,逐步构建接受 ETH 和主要稳定币的预售合约。
主要特点
- 多种支付方式(ETH、USDT、USDC、DAI)
- 早期投资者奖金制度
- 分阶段代币购买活动
先决条件
- 安全帽开发环境
- Openzeppelin 合约
- 以太坊开发经验
- ERC20 代币的基本了解
代币特征
- 类型:ERC20
- 姓名:银凤凰
- 符号:SPX
- 十进制:18
- 总供应量:1000亿
预售功能
- 预售供应量:100亿(10%)
- 预售期:30天
- 预售阶段:4
- 软顶:500000 USDT
- 硬顶:1020000 USDT
- 每个阶段的价格和代币数量:
阶段 | 价格 | 代币数量 |
---|---|---|
1 | 0.00008 USDT | 30亿 |
2 | 0.00010 USDT | 40亿 |
3 | 0.00012 USDT | 20亿 |
4 | 0.00014 USDT | 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);
}
estimatedTokenAmountAvailableWithCoin
该函数使用 Uniswap V2 路由器和函数计算用户可以使用特定的 eth 数量购买多少代币。
辅助函数用于计算 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 在内的多种支付方式。
该实现方式可作为未来预售合约的绝佳模板,在安全性、功能性和用户可访问性之间取得平衡。
其架构确保公平分配,并通过结构合理的验证和分配机制保护投资者和项目所有者的利益。