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

2025-06-07

使用 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
    );
}
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

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;
    }
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/stevendev0822/compressive-guide-to-write-erc20-token-presale-smart-contract-on-ethereum-blockchain-using-solidity-1h6e
PREV
Domain-driven Design (DDD): File Structure Project Current File Structure Domain DDD (Domain-driven Design) Modules Shared Infrastructure Layer vs Domain Project File Structure using DDD Final considerations Keep in touch
NEXT
为什么我为 Web Components 编写了一个微型库 挑战自己 摆脱痛苦 完全控制 分发 装饰器 输入 Readymade 示例 Readymade 现在是 v1