发布于 2026-01-05 2 阅读
0

使用 Solidity 在以太坊区块链上编写 ERC20 代币预售智能合约的全面指南

使用 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
    );
}
Enter fullscreen mode Exit fullscreen mode

首先,该函数检查预售是否正在进行。
接下来,它估算投资者可以用特定数量的 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_;
}
Enter fullscreen mode Exit fullscreen mode

接下来,它会更新预售状态。

function _updatePresaleStats(
    uint256 tokenAmount_,
    uint256 coinAmount_,
    uint8 coinDecimals_
) private {
    totalTokensSold += tokenAmount_;
    fundsRaised += coinAmount_ / (10 ** (coinDecimals_ - 6));
}
Enter fullscreen mode Exit fullscreen mode

最后,它会发出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
    );
}
Enter fullscreen mode Exit fullscreen mode

首先,该函数会检查预售是否正在进行、投资者想要购买的代币数量是否可用等等(根据条件调整)。
接下来,它会计算购买这些代币所需的代币数量,并检查投资者的账户余额和授权额度是否充足。
然后,它会将稳定币转入预售合约。
之后,它会更新投资者和投资状态以及预售状态,最后发出TokensBought事件。

使用特定稳定币购买代币的每个函数都可以写成如下形式:

function buyWithUSDT(uint256 tokenAmount_) external whenNotPaused {
    _buyWithCoin(USDTInterface, tokenAmount_);
}
Enter fullscreen mode Exit fullscreen mode
用于计算 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);
}
Enter fullscreen mode Exit fullscreen mode

该函数使用 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;
    }
Enter fullscreen mode Exit fullscreen mode

此功能确保:

  • 在不同价格层级上进行准确的代币计算
  • 针对不同稳定币的正确小数处理方法
  • 每个层级的最大代币可用数量限制
  • 有效利用剩余购买金额

该实现方案支持预售的分级定价结构,同时保持代币计算的精确性。

索赔功能
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);
}
Enter fullscreen mode Exit fullscreen mode

此功能

  • 检查索赔时间和软上限要求
  • 计算包括奖励在内的总代币数
  • 重置投资者余额和早期投资者身份
  • 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);
}
Enter fullscreen mode Exit fullscreen mode

此功能

  • 验证是否已设置预定义的多重签名钱包地址
  • 确保预售已经结束
  • 核实是否存在充足资金
  • 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;
}
Enter fullscreen mode Exit fullscreen mode

此功能

  • 循环遍及所有投资者
  • 对每种稳定币分别进行检查和退款。
  • 将投资记录重置为零
  • 发出FundsRefunded事件
  • 清除全局状态(fundsRaisedinvestors数组)

结论

此 SPX 代币预售智能合约展示了一个强大而灵活的实现方案,可有效处理包括 ETH、USDT、USDC 和 DAI 在内的多种支付方式。
该方案可作为未来预售合约的优秀模板,在安全性、功能性和用户易用性之间实现了平衡。
其架构通过结构完善的验证和分配机制,确保了公平分配,同时保障了投资者和项目所有者的利益。

文章来源:https://dev.to/imcrazysteven/compressive-guide-to-write-erc20-token-presale-smart-contract-on-ethereum-blockchain-using-solidity-1h6e