Solidity实战:手搓一个Meme币发射平台需要几步?

5 阅读5分钟

前言

Meme币的爆发式流行催生了新一代去中心化发行平台的需求。本文将深入剖析一个完整的MemePump工厂合约实现,涵盖ERC-20代币发行、联合曲线定价、自动化毕业机制等核心模块,并提供完整的Hardhat测试与部署方案。免责声明:纯技术分享,不构成投资建议。智能合约有风险,上主网前务必审计。操作后果自负。

一、架构概览

本项目采用工厂模式 + 联合曲线的双合约架构:

┌─────────────────┐      铸造全部代币      ┌─────────────────┐
│  MemeToken      │ ◄──────────────────── │ MemePumpFactory │
│  (ERC-20)       │                       │  (工厂/市场)    │
└─────────────────┘      控制流通/定价      └─────────────────┘
                                │
                                ▼
                        达到20ETH募资目标
                                │
                                ▼
                           触发毕业
                      (迁移至UniswapDEX)

核心设计哲学:

  • 公平发射:所有代币初始由工厂持有,无预挖、无团队预留
  • 价格发现:线性联合曲线确保早期买家享有价格优势
  • 自动毕业:达到募资目标后自动进入下一阶段

二、智能合约开发、测试、部署

2.1 智能合约

2.1.1 meme币

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.5.0
pragma solidity ^0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MemeToken is ERC20 {
    address public factory;

    constructor(
        string memory name, 
        string memory symbol, 
        uint256 totalSupply,
        address _factory
    ) ERC20(name, symbol) {
        factory = _factory;
        // 初始将所有代币铸造给工厂合约,由工厂控制 Bonding Curve 销售
        _mint(_factory, totalSupply);
    }
}

2.1.2 平台工厂与联合曲线合约

关键决策:
  • 10亿总量:Meme币标准配置,便于心理定价(如$0.0001/枚)
  • 20ETH毕业线:平衡早期流动性需求与DEX上线门槛
  • 1%手续费:维持平台运营的可持续收入模型
数学模型解析:这是一个线性递增价格曲线:

P(s)=0.0001+s×10131018=104+1022s (ETH)P(s) = 0.0001 + \frac{s \times 10^{-13}}{10^{18}} = 10^{-4} + 10^{-22}s \text{ (ETH)}

曲线特性:
  • 初始低价:0.0001 ETH(约$0.25)降低参与门槛
  • 温和上涨:每售出100万枚仅涨价0.01%,抑制早期巨鲸
  • 线性可预测:买家可精确计算滑点,无突发价格跳跃
安全设计:
  • 重入防护:先更新状态后转账,符合Checks-Effects-Interactions模式
  • 精确计算:使用整数运算避免浮点误差,(amount / 1e18)确保单位统一
  • 自动触发:无需手动干预,达到阈值立即执行毕业
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/access/Ownable.sol";
import "./MemeToken.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract MemePumpFactory is Ownable {
    uint256 public constant TOTAL_SUPPLY = 1_000_000_000 * 10**18; // 10亿
    uint256 public constant FUNDING_GOAL = 20 ether; // 募资目标
    uint256 public constant FEE_PERCENT = 1; // 1% 手续费

    struct TokenInfo {
        address tokenAddress;
        uint256 raisedAmount;
        uint256 soldAmount;
        bool isGraduated;
    }

    // 状态变量
    mapping(address => TokenInfo) public tokens;
    address[] public allTokens; // 💡 修复点:记录所有创建的代币地址

    event TokenCreated(address indexed token, string name, string symbol);
    event AssetPurchased(address indexed token, address indexed buyer, uint256 amount, uint256 cost);

    constructor() Ownable(msg.sender) {}

    // 1. 一键发币
    function createMeme(string memory name, string memory symbol) external {
        MemeToken newToken = new MemeToken(name, symbol, TOTAL_SUPPLY, address(this));
        address tokenAddr = address(newToken);

        tokens[tokenAddr] = TokenInfo(tokenAddr, 0, 0, false);
        allTokens.push(tokenAddr); // 💡 修复点:存入数组供查询

        emit TokenCreated(tokenAddr, name, symbol);
    }

    // 💡 修复点:供测试脚本调用,获取最新创建的代币地址
    function getLastCreatedToken() external view returns (address) {
        require(allTokens.length > 0, "No tokens created");
        return allTokens[allTokens.length - 1];
    }

    // 💡 修复点:供前端/脚本获取代币总数
    function getTokenCount() external view returns (uint256) {
        return allTokens.length;
    }

    // 2. 联合曲线定价逻辑
    function getPrice(uint256 currentSupply) public pure returns (uint256) {
        return 0.0001 ether + (currentSupply * 0.0000001 ether / 1e18);
    }

    // 3. 买入代币
    function buy(address tokenAddr, uint256 amount) external payable {
        TokenInfo storage info = tokens[tokenAddr];
        require(!info.isGraduated, "Token already graduated");

        uint256 cost = getPrice(info.soldAmount) * (amount / 1e18);
        uint256 fee = (cost * FEE_PERCENT) / 100;
        
        require(msg.value >= cost + fee, "Insufficient ETH");

        info.raisedAmount += cost;
        info.soldAmount += amount;

        IERC20(tokenAddr).transfer(msg.sender, amount);
        
        emit AssetPurchased(tokenAddr, msg.sender, amount, cost);

        if (info.raisedAmount >= FUNDING_GOAL) {
            _graduate(tokenAddr);
        }
    }

    // 4. 毕业逻辑
    function _graduate(address tokenAddr) internal {
        TokenInfo storage info = tokens[tokenAddr];
        info.isGraduated = true;
        // 实际逻辑:迁移至 Uniswap...
    }

    // 提取平台手续费
    function withdrawFees() external onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "No fees to withdraw");
        payable(owner()).transfer(balance);
    }
}

2.2 测试脚本

测试用例

  • 创建代币 -> 买入 -> 验证状态
  • 达到募资目标并触发毕业逻辑
  • 管理员提取手续费且合约余额清空
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { parseEther, formatEther } from 'viem';
import { network } from "hardhat";

describe("MemePump 平台核心逻辑测试", function () {
  let publicClient: any;
  let factoryContract: any;
  let owner: any, user1: any;

  beforeEach(async function () {
    const { viem } = await network.connect();
    publicClient = await viem.getPublicClient();
    [owner, user1] = await viem.getWalletClients();
    factoryContract = await viem.deployContract("MemePumpFactory", []);
  });

  it("应该成功:创建代币 -> 买入 -> 验证状态", async function () {
    await factoryContract.write.createMeme(["Dogecoin 3.0", "DOGE3"], { account: user1.account });
    const tokenAddress = await factoryContract.read.getLastCreatedToken();

    // --- 修复点:直接调用合约的 getPrice 获取单价 ---
    const buyAmount = parseEther("1000"); 
    const unitPrice = await factoryContract.read.getPrice([0n]); // 获取初始价格
    
    // 合约逻辑: cost = price * (amount / 1e18)
    const cost = (unitPrice * buyAmount) / parseEther("1");
    const fee = (cost * 1n) / 100n; // 1%
    const totalRequired = cost + fee + 100n; // 多给 100 wei 容错

    const hash = await factoryContract.write.buy([tokenAddress, buyAmount], {
      account: user1.account,
      value: totalRequired
    });
    await publicClient.waitForTransactionReceipt({ hash });

    const tokenInfo = await factoryContract.read.tokens([tokenAddress]);
    // index 1 是 raisedAmount, 2 是 soldAmount
    assert.equal(tokenInfo[2], buyAmount, "已售数量不匹配");
    assert.equal(tokenInfo[1], cost, "筹集金额不匹配");
  });

  it("应该成功:达到募资目标并触发毕业逻辑", async function () {
    await factoryContract.write.createMeme(["MoonToken", "MOON"], { account: owner.account });
    const tokenAddress = await factoryContract.read.getLastCreatedToken();

    // --- 修复点:确保发送的 ETH 确实能覆盖 20 ETH 的目标 ---
    // 假设我们直接买入足够大的量来触发 20 ETH
    // 由于价格随供应增加,发送 21 ETH 配合一个足够大的 amount 肯定能触发
    const buyAmount = parseEther("200000"); // 20万个
    const fundingGoalPlusFee = parseEther("21"); 

    await factoryContract.write.buy([tokenAddress, buyAmount], {
      account: owner.account,
      value: fundingGoalPlusFee
    });

    const tokenInfo = await factoryContract.read.tokens([tokenAddress]);
    // index 3 是 isGraduated
    assert.strictEqual(tokenInfo[3], true, "募集达标后 isGraduated 应为 true");
  });

  it("应该成功:管理员提取手续费且合约余额清空", async function () {
    await factoryContract.write.createMeme(["FeeToken", "FEE"], { account: user1.account });
    const tokenAddress = await factoryContract.read.getLastCreatedToken();
    
    // 产生手续费
    await factoryContract.write.buy([tokenAddress, parseEther("100")], {
      account: user1.account,
      value: parseEther("1") 
    });

    const contractBalanceBefore = await publicClient.getBalance({ address: factoryContract.address });
    assert.ok(contractBalanceBefore > 0n, "合约应当持有手续费");

    await factoryContract.write.withdrawFees({ account: owner.account });

    const contractBalanceAfter = await publicClient.getBalance({ address: factoryContract.address });
    assert.equal(contractBalanceAfter, 0n, "提取后合约余额应归零");
  });
});

2.3 部署脚本

// scripts/deploy.js
import { network, artifacts } from "hardhat";
async function main() {
  // 连接网络
  const { viem } = await network.connect({ network: network.name });//指定网络进行链接
  
  // 获取客户端
  const [deployer] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();
 
  const deployerAddress = deployer.account.address;
   console.log("部署者的地址:", deployerAddress);
  // 加载合约
  const MemePumpFactoryArtifact = await artifacts.readArtifact("MemePumpFactory");

  // 部署(构造函数参数:recipient, initialOwner)
  const hash = await deployer.deployContract({
    abi: MemePumpFactoryArtifact.abi,//获取abi
    bytecode: MemePumpFactoryArtifact.bytecode,//硬编码
    args: [],//process.env.RECIPIENT, process.env.OWNER
  });

  // 等待确认并打印地址
  const MemePumpFactoryReceipt = await publicClient.waitForTransactionReceipt({ hash });
  console.log("平台工厂与联合曲线合约地址:", MemePumpFactoryReceipt.contractAddress);
  
}
main().catch(console.error);

结语

本文展示的MemePump架构提供了一个最小可行但功能完整的Meme币发射平台实现。联合曲线机制确保了公平的价格发现,工厂模式实现了代币生命周期的标准化管理,而自动毕业机制则为项目提供了清晰的进阶路径。