普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月28日首页

深度拆解 Grass 模式:基于 EIP-712 与 DePIN 架构的奖励分发系统实现

作者 木西
2026年2月27日 18:47

前言

2026 年的 Web3 赛道中,以 GrassDawn 为代表的 DePIN(去中心化物理基础设施网络)项目,开创了 “带宽即挖矿” 的全新范式。这类项目的核心技术难点,并非数据采集本身,而是如何安全、低成本地将链下贡献转化为链上代币奖励。本文将从架构设计到智能合约实现,完整还原一套工业级的 Grass 奖励分发系统。特此声明:本文不构成任何项目推荐与投资建议,仅对行业主流模式与核心运行逻辑做技术拆解与原理分析。

一、Grass概述

1. 项目本质

Solana 链上 DePIN+AI 项目,核心是用户共享家庭闲置带宽,为 AI 企业提供分布式、合规的数据采集服务,用户以带宽贡献获取 $GRASS 代币奖励

2. 核心亮点

  • 商业模式清晰:98% 收入来自 AI 模型训练数据,客户群体明确

  • 资方优质:总融资 450 万美元,种子轮由 Polychain Capital、Tribe Capital 领投

  • 技术可靠:采用 zk-SNARK 技术,保障数据真实性与可追溯性

3. 关键风险

  • 合规风险:严打多账号刷量、非住宅 IP 违规操作,违规会取消奖励资格

  • 行业依赖风险:收入高度依赖 AI 训练数据市场,行业波动影响生态收益

  • 代币与参与风险:$GRASS 价格受市场波动影响,空投、解锁规则以官方公告为准;节点需稳定在线,产生电费成本

二、 核心架构:链下计算,链上验证

Grass 的运作并非全过程上链,其架构分为三个关键层级:

  1. 感知层 (链下) :用户运行浏览器插件,贡献闲置带宽。
  2. 验证层 (后端) :项目方服务器(或验证节点)统计用户贡献,将其转化为累计积分(Total Earned)。
  3. 结算层 (合约) :用户发起提现请求,后端生成 EIP-712 签名凭证,合约验证签名并拨付代币。

三、 工业级合约实现 (Solidity 0.8.24)

基于 OpenZeppelin V5,我们构建了具备防重放攻击角色权限控制结构化签名验证的核心合约。

代币合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MockToken is ERC20 {
    constructor(string memory name, string memory symbol) ERC20(name, symbol) {
        // 初始给部署者薄利 100 万个,方便测试
        _mint(msg.sender, 1000000 * 10**decimals());
    }
}

GrassDistributor合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract GrassDistributor is EIP712, AccessControl {
    using ECDSA for bytes32;
    using MessageHashUtils for bytes32;

    bytes32 public constant VERIFIER_ROLE = keccak256("VERIFIER_ROLE");
    // EIP-712 结构化类型哈希
    bytes32 private constant CLAIM_TYPEHASH = 
        keccak256("ClaimReward(address user,uint256 totalEarned,uint256 nonce)");

    IERC20 public immutable rewardToken;
    mapping(address => uint256) public claimedAmount;
    mapping(address => uint256) public nonces;

    event RewardClaimed(address indexed user, uint256 amount, uint256 nonce);

    constructor(address _token, address _initialVerifier) 
        EIP712("GrassNetwork", "1") 
    {
        rewardToken = IERC20(_token);
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(VERIFIER_ROLE, _initialVerifier);
    }

    /**
     * @notice 领取累计奖励
     * @param totalEarned 后端验证的该用户历史总赚取量
     * @param signature 后端 Verifier 的 EIP-712 签名
     */
    function claim(uint256 totalEarned, bytes calldata signature) external {
        uint256 currentNonce = nonces[msg.sender];
        uint256 alreadyClaimed = claimedAmount[msg.sender];
        uint256 amountToClaim = totalEarned - alreadyClaimed;

        require(amountToClaim > 0, "Grass: No rewards to claim");

        // 1. 构建 EIP-712 结构化哈希
        bytes32 structHash = keccak256(
            abi.encode(CLAIM_TYPEHASH, msg.sender, totalEarned, currentNonce)
        );
        bytes32 hash = _hashTypedDataV4(structHash);

        // 2. 验证签名者是否有 VERIFIER_ROLE 权限
        address signer = hash.recover(signature);
        require(hasRole(VERIFIER_ROLE, signer), "Grass: Invalid verifier signature");

        // 3. 更新状态(先更新后转账,防重入)
        nonces[msg.sender] = currentNonce + 1;
        claimedAmount[msg.sender] = totalEarned;

        // 4. 转账
        require(rewardToken.transfer(msg.sender, amountToClaim), "Grass: Transfer failed");

        emit RewardClaimed(msg.sender, amountToClaim, currentNonce);
    }
}


四、 自动化签名分发 (后端逻辑)

基于 Viem 的后端签名实现 (signReward.ts)

import { createWalletClient, http, type Address } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrum } from 'viem/chains';

/**
 * Grass 自动化签名分发服务 (Viem 版)
 * @param userAddress 领奖用户地址
 * @param totalEarned 数据库中的累计积分 (单位: wei)
 * @param currentNonce 合约中该用户的最新 Nonce
 */
async function generateViemSignature(
  userAddress: Address, 
  totalEarned: bigint, 
  currentNonce: bigint
) {
  // 1. 初始化 Verifier 账户 (从环境变量获取私钥)
  const privateKey = process.env.VERIFIER_PRIVATE_KEY as `0x${string}`;
  const account = privateKeyToAccount(privateKey);

  // 2. 创建 Wallet Client (仅用于签名,无需连接真实节点)
  const client = createWalletClient({
    account,
    chain: arbitrum,
    transport: http()
  });

  // 3. 定义 EIP-712 结构 (必须与 Solidity 合约完全一致)
  const domain = {
    name: 'GrassNetwork',
    version: '1',
    chainId: 42161, // Arbitrum One
    verifyingContract: '0xYourContractAddress' as Address,
  } as const;

  const types = {
    ClaimReward: [
      { name: 'user', type: 'address' },
      { name: 'totalEarned', type: 'uint256' },
      { name: 'nonce', type: 'uint256' },
    ],
  } as const;

  // 4. 执行签名
  const signature = await client.signTypedData({
    domain,
    types,
    primaryType: 'ClaimReward',
    message: {
      user: userAddress,
      totalEarned: totalEarned,
      nonce: currentNonce,
    },
  });

  return {
    signature,
    user: userAddress,
    totalEarned: totalEarned.toString(),
    nonce: Number(currentNonce)
  };
}


五、 自动化测试 (Viem + Node:Assert)

测试用例

  1. 正常领取:验证 totalEarned 模式下余额增量是否正确。
  2. 防重放校验:确保旧签名(Nonce 0)在 Nonce 已变为 1 时无法再次被合约接受。
  3. 权限校验:普通用户伪造签名应被 VERIFIER_ROLE 机制拦截。
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { network } from "hardhat"; 
import { parseEther, type Address } from "viem";

describe("Grass DePIN 奖励系统全流程测试", function () {
    let token: any, distributor: any;
    let admin: any, user: any, verifier: any;
    let vClient: any, pClient: any;

    beforeEach(async function () {
        // 连接本地环境
        const { viem } = await (network as any).connect();
        vClient = viem;
        [admin, user, verifier] = await vClient.getWalletClients();
        pClient = await vClient.getPublicClient();

        // 1. 部署模拟 ERC20 (假设已存在 MockToken)
        token = await vClient.deployContract("MockToken", ["Grass Token", "GRASS"]);
        
        // 2. 部署分配器,初始化 verifier 角色
        distributor = await vClient.deployContract("GrassDistributor", [
            token.address, 
            verifier.account.address
        ]);

        // 3. 给分配器合约注入 10000 个代币作为奖池
        await token.write.transfer([distributor.address, parseEther("10000")]);
    });

    it("用例 1: 验证 EIP-712 签名并成功领取奖励", async function () {
        const totalEarned = parseEther("150"); // 后端统计该用户总共赚了 150
        const nonce = 0n;
        const chainId = await pClient.getChainId();

        // --- 模拟后端签名逻辑 ---
        const domain = {
            name: 'GrassNetwork',
            version: '1',
            chainId,
            verifyingContract: distributor.address as Address,
        } as const;

        const types = {
            ClaimReward: [
                { name: 'user', type: 'address' },
                { name: 'totalEarned', type: 'uint256' },
                { name: 'nonce', type: 'uint256' },
            ],
        } as const;

        // 使用 verifier 私钥签名
        const signature = await verifier.signTypedData({
            domain,
            types,
            primaryType: 'ClaimReward',
            message: {
                user: user.account.address,
                totalEarned,
                nonce,
            },
        });

        // --- 前端发起领取 ---
        const txHash = await distributor.write.claim([totalEarned, signature], { 
            account: user.account 
        });
        await pClient.waitForTransactionReceipt({ hash: txHash });

        // --- 断言校验 ---
        const userBalance = await token.read.balanceOf([user.account.address]);
        assert.strictEqual(userBalance, parseEther("150"), "用户应收到 150 个代币");
        
        const nextNonce = await distributor.read.nonces([user.account.address]);
        assert.strictEqual(nextNonce, 1n, "Nonce 应该自增");
    });
    
    it("用例 2: 防重放测试 (尝试使用旧签名再次领取)", async function () {
    const totalEarned = parseEther("150");
    const nonce = 0n;
    const chainId = await pClient.getChainId();

    const domain = { name: 'GrassNetwork', version: '1', chainId, verifyingContract: distributor.address };
    const types = { ClaimReward: [{ name: 'user', type: 'address' }, { name: 'totalEarned', type: 'uint256' }, { name: 'nonce', type: 'uint256' }] };
    
    // 1. 第一次正常领取
    const signature = await verifier.signTypedData({
        domain,
        types,
        primaryType: 'ClaimReward',
        message: { user: user.account.address, totalEarned, nonce }
    });
    
    const tx1 = await distributor.write.claim([totalEarned, signature], { account: user.account });
    await pClient.waitForTransactionReceipt({ hash: tx1 });

    // 2. 第二次尝试使用完全相同的签名再次领取
    // 注意:此时合约内的 nonces[user] 已经是 1 了,但签名里的 nonce 还是 0
    try {
        await distributor.write.claim([totalEarned, signature], { account: user.account });
        // 如果运行到这里,说明没有报错,测试应该失败
        assert.fail("应该因为 Nonce 不匹配而回滚");
    } catch (e: any) {
        // 打印错误看看具体信息,有助于调试
        // console.log(e.message); 
        
        // 修改断言:只要捕获到错误即代表拦截成功
        // 或者匹配具体的错误字符串 "Grass: Invalid nonce"
        assert.ok(true, "成功拦截了重放攻击");
    }
});

    it("用例 3: 权限测试 (非法签名者签名)", async function () {
        const totalEarned = parseEther("50");
        // 使用普通用户 user 代替 verifier 进行签名
        const signature = await user.signTypedData({
            domain: { name: 'GrassNetwork', version: '1', chainId: await pClient.getChainId(), verifyingContract: distributor.address },
            types: { ClaimReward: [{ name: 'user', type: 'address' }, { name: 'totalEarned', type: 'uint256' }, { name: 'nonce', type: 'uint256' }] },
            primaryType: 'ClaimReward',
            message: { user: user.account.address, totalEarned, nonce: 0n }
        });

        try {
            await distributor.write.claim([totalEarned, signature], { account: user.account });
            assert.fail("非 Verifier 签名应被拦截");
        } catch (e: any) {
            assert.ok(e.message.includes("Grass: Invalid verifier signature"));
        }
    });
});

六、部署脚本

// scripts/deploy.ts
import { network, artifacts } from "hardhat";
import {parseEther} from "viem"
async function main() {
   // 连接网络
  const { viem } = await network.connect({ network: network.name });//指定网络进行链接
  // 获取客户端
  const [deployer] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();
  // 部署代币
  const MockTokenRegistry = await artifacts.readArtifact("MockToken");

  const MockTokenRegistryHash = await deployer.deployContract({
    abi: MockTokenRegistry.abi,//获取abi
    bytecode: MockTokenRegistry.bytecode,//硬编码
    args: ["Grass Token", "GRASS"],//部署者地址作为初始治理者
  });
  // 等待确认并打印治理代币地址
  const MockTokenRegistryReceipt = await publicClient.getTransactionReceipt({ hash: MockTokenRegistryHash });
  console.log("代币合约地址:", MockTokenRegistryReceipt.contractAddress);


  // 部署时间锁合约
  const GrassDistributorRegistry  = await  artifacts.readArtifact("GrassDistributor");

  const GrassDistributorHash = await deployer.deployContract({
    abi: GrassDistributorRegistry.abi,//获取abi
    bytecode: GrassDistributorRegistry.bytecode,//硬编码
    args: [MockTokenRegistryReceipt.contractAddress,deployer.account.address],//
  });
    // 等待确认并打印地址
  const GrassDistributorReceipt = await publicClient.getTransactionReceipt({ hash: GrassDistributorHash });
  console.log("GrassDistributor合约地址:", await GrassDistributorReceipt.contractAddress);
 
  
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

七、2026 年的技术演进思考

  • Gas 优化:由于使用了 Solidity 0.8.24,项目方可以结合 Transient Storage (TSTORE)  在批量分发(Airdrop)时极大降低成本。
  • 隐私增强:未来的 Grass 可能引入 zk-SNARKs。用户证明自己贡献了带宽,而无需向项目方暴露具体的抓取内容,合约只需验证 ZK Proof 即可发放奖励。
  • 多链结算:通过 Chainlink CCIP 等跨链协议,实现用户在 Arbitrum 挖矿,但在 Base 链领取奖励。

结语

Grass 的成功不仅在于其“零成本”的营销,更在于其背后这套成熟的、基于 EIP-712 的“链下计算+链上结算”技术栈。对于开发者而言,掌握这套架构是进入 DePIN 和 RWA 赛道的入场券。

昨天以前首页

深度解析|Form Network:BNX 迁移模块化 L2 的全流程技术实践

作者 木西
2026年2月24日 10:00

前言

随着 Web3 社交协议(如 Lens、Farcaster)的爆发,底层区块链的性能和交互成本成为制约用户增长的瓶颈。Form Network 作为首个专为 SocialFi 设计的以太坊 Layer 2,由 BinaryX (BNX)  战略升级而来。它不仅解决了扩展性问题,还通过FORM 的 1:1 迁移,开启了社交资产化的新篇章。

一、 什么是 Form Network?

Form Network 是基于 OP Stack 构建的模块化 L2,利用 Celestia 作为数据可用性(DA)层。

  • 核心价值:将社交影响力金融化(Socialized Finance)。它让用户的每一次点赞、关注和内容创作都转化为链上可流动的资产。
  • 原生收益:引入 Form ETH,让存放在社交协议中的资金自动赚取底层收益。
  • 生态玩法:用户通过迁移旧有的 BNX获得BNX 获得 FORM,从而参与社交挖矿、治理投票以及早期 SocialFi 项目的 IGO(首次分发)。

二、 核心技术实现:1:1 智能迁移合约

在 Form Network 生态启动阶段,最关键的任务是引导 BNX持有者无缝迁移到BNX 持有者无缝迁移到 FORM。为了极致的 UX(用户体验),我们集成了 EIP-2612 Permit 和 Multicall 功能。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Multicall} from "@openzeppelin/contracts/utils/Multicall.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";

// 模拟旧币 BNX (需支持 Permit 以测试一键迁移)
contract BNXToken is ERC20, ERC20Permit {
    constructor() ERC20("BinaryX", "BNX") ERC20Permit("BinaryX") {
        _mint(msg.sender, 1000000 * 10**18);
    }
}

// Form Network 核心合约
contract FormNetworkCore is Multicall, AccessControl {
    using SafeERC20 for IERC20;

    bytes32 public constant MIGRATOR_ROLE = keccak256("MIGRATOR_ROLE");
    IERC20 public immutable bnx;
    ERC20 public immutable form;

    event Migrated(address indexed user, uint256 amount);

    constructor(address _bnx, address _form) {
        bnx = IERC20(_bnx);
        form = ERC20(_form);
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    // 1:1 迁移逻辑
    function migrate(uint256 amount) public {
        require(amount > 0, "Amount zero");
        bnx.safeTransferFrom(msg.sender, address(this), amount);
        // 实际场景中 FORM 由此合约 Mint 或预存
        form.transfer(msg.sender, amount);
        emit Migrated(msg.sender, amount);
    }

    // 辅助 Permit 调用,用于 Multicall 组合
    function applyPermit(
        address token, uint256 value, uint256 deadline, 
        uint8 v, bytes32 r, bytes32 s
    ) external {
        ERC20Permit(token).permit(msg.sender, address(this), value, deadline, v, r, s);
    }
}

三、 自动化测试:基于 Viem 的现代工作流

测试用例:Form Network 核心迁移闭环测试

  • 创新点测试:Multicall + Permit 实现 [无授权感] 迁移
  • 安全边界测试:非授权代币迁移应失败
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { network } from "hardhat"; 
import { parseEther, keccak256, stringToBytes, encodeFunctionData } from "viem";

describe("Form Network 核心迁移闭环测试", function () {
    let bnx: any, form: any, core: any;
    let admin: any, user: any;
    let publicClient: any;

    beforeEach(async function () {
        const { viem: v } = await (network as any).connect();
        [admin, user] = await v.getWalletClients();
        publicClient = await v.getPublicClient();

        // 1. 部署 BNX (旧币) 和 FORM (新币)
        bnx = await v.deployContract("BNXToken");
        form = await v.deployContract("BNXToken"); // 复用合约代码作为 FORM

        // 2. 部署核心迁移合约
        core = await v.deployContract("FormNetworkCore", [bnx.address, form.address]);

        // 3. 初始化:给用户发 BNX,给 Core 存入 FORM 储备
        await bnx.write.transfer([user.account.address, parseEther("1000")]);
        await form.write.transfer([core.address, parseEther("5000")]);
    });

    it("创新点测试:Multicall + Permit 实现 [无授权感] 迁移", async function () {
        const amount = parseEther("100");
        const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);

        // A. 准备 Permit 签名 (EIP-712)
        const domain = {
            name: "BinaryX",
            version: "1",
            chainId: await publicClient.getChainId(),
            verifyingContract: bnx.address
        };
        const types = {
            Permit: [
                { name: "owner", type: "address" },
                { name: "spender", type: "address" },
                { name: "value", type: "uint256" },
                { name: "nonce", type: "uint256" },
                { name: "deadline", type: "uint256" },
            ]
        };
        const nonce = await bnx.read.nonces([user.account.address]);
        const signature = await user.signTypedData({
            domain, types, primaryType: "Permit",
            message: { owner: user.account.address, spender: core.address, value: amount, nonce, deadline }
        });

        // B. 解析签名 r, s, v
        const r = signature.slice(0, 66);
        const s = `0x${signature.slice(66, 130)}`;
        const v = parseInt(signature.slice(130, 132), 16);

        // C. 组合 Multicall 调用 (1. applyPermit -> 2. migrate)
        const permitData = encodeFunctionData({
            abi: core.abi,
            functionName: "applyPermit",
            args: [bnx.address, amount, deadline, v, r, s]
        });
        const migrateData = encodeFunctionData({
            abi: core.abi,
            functionName: "migrate",
            args: [amount]
        });

        // D. 用户发起原子交易
        await core.write.multicall([[permitData, migrateData]], { account: user.account });

        // E. 验证结果:用户 BNX 减少,FORM 增加
        const bnxBal = await bnx.read.balanceOf([user.account.address]);
        const formBal = await form.read.balanceOf([user.account.address]);
        
        assert.strictEqual(bnxBal, parseEther("900"), "BNX 未正确扣除");
        assert.strictEqual(formBal, amount, "FORM 未正确发放");
    });

    it("安全边界测试:非授权代币迁移应失败", async function () {
        const fakeToken = admin.account.address; // 随便一个地址
        
        try {
            await core.write.migrate([parseEther("10")], { account: user.account });
            assert.fail("未授权转账应该回滚");
        } catch (e: any) {
            // Viem 会抛出合约执行错误
            assert.ok(e.message.includes("ERC20InsufficientAllowance") || e.message.includes("revert"), "错误原因不符合预期");
        }
    });
});

四、部署脚本

// 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 bnxartifact = await artifacts.readArtifact("BNXToken");
  const formartifact = await artifacts.readArtifact("BNXToken"); 
  const coreartifact = await artifacts.readArtifact("FormNetworkCore");
  // 部署(构造函数参数:recipient, initialOwner)
  const bnxhash = await deployer.deployContract({
    abi: bnxartifact.abi,//获取abi
    bytecode: bnxartifact.bytecode,//硬编码
    args: [],//process.env.RECIPIENT, process.env.OWNER
  });

  // 等待确认并打印地址
  const bnxreceipt = await publicClient.waitForTransactionReceipt({ hash: bnxhash });
  console.log("BNX合约地址:", bnxreceipt.contractAddress);
  const formhash = await deployer.deployContract({
    abi: formartifact.abi,//获取abi
    bytecode: formartifact.bytecode,//硬编码
    args: [],//process.env.RECIPIENT, process.env.OWNER
  });
  const formreceipt = await publicClient.waitForTransactionReceipt({ hash: formhash });
  console.log("Form合约地址:", formreceipt.contractAddress);
  const corehash = await deployer.deployContract({
    abi: coreartifact.abi,//获取abi
    bytecode: coreartifact.bytecode,//硬编码
    args: [bnxreceipt.contractAddress, formreceipt.contractAddress],//process.env.RECIPIENT, process.env.OWNER
  });
  const coreceipt = await publicClient.waitForTransactionReceipt({ hash: corehash });
  console.log("Core合约地址:", coreceipt.contractAddress);
}

main().catch(console.error);

五、 结语:SocialFi 的未来

Form Network 的价值不在于它又是一个 L2,而在于它通过技术手段降低了社交门槛。通过 Multicall 减少弹窗交互,通过 Permit 省去授权步骤,Web3 社交应用正在向 Web2 的丝滑体验靠拢。

STEPN相关内容延续篇:基于OpenZeppelinV5与Solidity0.8.24的创新点拆解

作者 木西
2026年2月22日 10:48

前言

本文作为上一篇STEPN相关内容的延续,将依托OpenZeppelinV5框架与Solidity0.8.24版本,重点拆解其核心创新点,具体涵盖Haus系统、能量系统、代币经济体系以及更简洁易用的交互体验四大模块,深入解析各创新点的设计逻辑与实现思路。

STEPN GO概述

STEPN GO 是由 FSL(Find Satoshi Lab)开发的全新 Web3 社交生活应用,被视为 STEPN 的“2.0 升级版”。它在延续“运动赚币(M2E)”核心逻辑的基础上,针对经济循环和社交门槛做了重大革新。

核心机制与创新点

  • Haus 系统 (社交与租借)

    • 允许老玩家将 NFT 运动鞋借出或赠送给好友,受邀者无需预先购买加密货币或 NFT 即可开始体验。
    • 该系统支持收益共享,降低了 Web2 用户进入 Web3 的技术门槛。
  • 能量系统 (NFT 焚烧机制)

    • 与原版通过增加鞋子持有量获取能量不同,STEPN GO 要求玩家焚烧(Burn)其他运动鞋 NFT 来获取或增加能量上限。
    • 这一改动建立了极强的NFT 通缩模型,旨在解决原版中 NFT 无限产出导致的价值贬值问题。
  • 代币经济 (GGT)

    • 引入了新的游戏代币 GGT (Go Game Token),作为主要的运动奖励代币。
    • 通过运动产出的 GGT 可用于升级、维修和服装合成等游戏内活动。
  • 更简单的交互体验

    • 支持 FSL ID,引入了类似 Web2 的账户登录方式(如人脸识别),消除了用户管理私钥和钱包的复杂流程。

STEPN和STEPN Go对比

从开发者和经济模型的角度来看,Stepn Go 是对原版 Stepn 痛点的全面升级,核心逻辑从“单币产出”转向了“资源平衡”和“社交门槛”。

核心差异

对比维度 Stepn Stepn Go
准入门槛与社交机制 独狼模式,购买 Sneaker NFT 即可参与,后期废除激活码,玩家间无强绑定 门票 / 抽奖模式,新手需老用户邀请或代币锁定抽奖获取鞋子,The Haus 组队 / 抽奖系统限制 Bot 增长,利益向老用户倾斜
经济循环(代币与消耗) 双币制(GST/GMT),GST 近乎无限产出,仅消耗代币,用户增长放缓后易通胀崩盘 双币制,新增「Burning for Energy」,强制焚烧 Sneaker NFT 换取能量,以 NFT 消耗构建强底层通缩模型
数学模型差异(HP 与维修) 后期新增 HP 衰减,维修主要消耗 GST,机制简单 HP 损耗与效率挂钩,强制执行自动维修 / 高额 HP 维护成本,GGT 大量回流 / 销毁
角色属性与收益计算 属性简单(Efficiency、Luck、Comfort、Resilience) 属性更丰富,新增套装属性、社交等级收益加成

技术实现上的关键点

  1. 增加 NFT 焚烧逻辑:  玩家需要调用一个 burnSneakerForEnergy 函数。
  2. 动态 HP 算法:  Stepn Go 的 HP 损耗通常不是线性的,而是根据等级和属性非线性变化。
  3. 多角色分利:  净收益(Net Reward)的一部分往往会分给“邀请人”(The Haus 房主)。

智能合于落地全流程

智能合约

  • StepnGo合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract GGTToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    constructor() ERC20("Go Game Token", "GGT") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }
    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) { _mint(to, amount); }
    function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}

contract StepnGoIntegrated is ERC721, AccessControl, ReentrancyGuard {
    GGTToken public immutable ggt;
    bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE");

    struct Sneaker { uint256 level; uint256 efficiency; uint256 hp; }
    struct HausLease { address guest; uint256 guestShare; }

    mapping(uint256 => Sneaker) public sneakers;
    mapping(uint256 => HausLease) public hausRegistry;
    mapping(address => uint256) public permanentEnergy;
    uint256 private _nextTokenId;

    constructor(address _ggt) ERC721("StepnGo Sneaker", "SNK") {
        ggt = GGTToken(_ggt);
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    function mintSneaker(address to, uint256 eff) external onlyRole(DEFAULT_ADMIN_ROLE) returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        sneakers[tokenId] = Sneaker(1, eff, 10000);
        return tokenId;
    }

    function setHausLease(uint256 tokenId, address guest, uint256 share) external {
        require(ownerOf(tokenId) == msg.sender, "Not owner");
        hausRegistry[tokenId] = HausLease(guest, share);
    }

    function burnForEnergy(uint256 tokenId) external {
        require(ownerOf(tokenId) == msg.sender, "Not owner");
        _burn(tokenId);
        permanentEnergy[msg.sender] += 1;
    }

    function settleWorkout(uint256 tokenId, uint256 km) external onlyRole(ORACLE_ROLE) nonReentrant {
        Sneaker storage snk = sneakers[tokenId];
        require(snk.hp > 1000, "Low HP");
        uint256 totalReward = km * snk.efficiency * 10**16; 
        snk.hp -= (km * 100);
        address host = ownerOf(tokenId);
        HausLease memory lease = hausRegistry[tokenId];
        if (lease.guest != address(0)) {
            uint256 guestAmt = (totalReward * lease.guestShare) / 100;
            ggt.mint(lease.guest, guestAmt);
            ggt.mint(host, totalReward - guestAmt);
        } else { ggt.mint(host, totalReward); }
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, AccessControl) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}
  • GGTToken合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract GGTToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor() ERC20("Go Game Token", "GGT") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) external onlyRole(MINTER_ROLE) {
        _burn(from, amount);
    }
}

contract StepnGoEngine is ReentrancyGuard, AccessControl {
    GGTToken public immutable ggt;
    
    struct SneakerStats {
        uint256 level;
        uint256 efficiency; // 影响产出
        uint256 hp;         // 10000 基数 (100.00%)
    }

    mapping(uint256 => SneakerStats) public sneakers;
    bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE");

    event WorkoutProcessed(uint256 indexed tokenId, uint256 netGGT, uint256 hpLoss);

    constructor(address _ggt) {
        ggt = GGTToken(_ggt);
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    // 核心数学模型:结算运动奖励并扣除维修费(HP 损耗)
    function settleGGT(uint256 tokenId, uint256 km) external onlyRole(ORACLE_ROLE) nonReentrant {
        SneakerStats storage snk = sneakers[tokenId];
        require(snk.hp > 1000, "HP too low, need repair"); // 低于 10% 无法运动

        // 1. 产出公式: Reward = km * Efficiency * log(Level) 简化版
        uint256 rawReward = km * snk.efficiency * 10**15; 

        // 2. HP 损耗公式: Loss = km * (Level^0.5) 
        uint256 hpLoss = km * 100; // 模拟每公里掉 1%
        
        if (snk.hp > hpLoss) {
            snk.hp -= hpLoss;
        } else {
            snk.hp = 0;
        }

        // 3. 自动维修逻辑 (经济循环核心):
        // 假设系统强制扣除 10% 的产出用于“销毁”以维持生态,模拟强制维修费
        uint256 maintenanceFee = rawReward / 10; 
        uint256 netReward = rawReward - maintenanceFee;

        ggt.mint(tx.origin, netReward); // 发放净收益
        // 模拟销毁:如果已经产生了 GGT,此处可以 burn 掉维修费部分
        
        emit WorkoutProcessed(tokenId, netReward, hpLoss);
    }

    function initializeSneaker(uint256 tokenId, uint256 level, uint256 eff) external onlyRole(DEFAULT_ADMIN_ROLE) {
        sneakers[tokenId] = SneakerStats(level, eff, 10000);
    }
}

测试脚本

  • StepnGo测试
    • Haus 租赁分润 + HP 损耗结算
    • 销毁运动鞋增加永久能量
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { network } from "hardhat"; // 或者直接从 global 获取
import { parseEther, keccak256, stringToBytes } from "viem";

describe("STEPN GO 核心业务闭环测试", function () {
    let core: any, ggt: any;
    let admin: any, host: any, guest: any;
    let publicClient: any;

    beforeEach(async function () {
        const { viem: v } = await (network as any).connect();
        [admin, host, guest] = await v.getWalletClients();
        publicClient = await v.getPublicClient();

        // 1. 部署 GGT 和 Core
        ggt = await v.deployContract("contracts/StepnGoIntegrated.sol:GGTToken");
        core = await v.deployContract("contracts/StepnGoIntegrated.sol:StepnGoIntegrated", [ggt.address]);

        // 2. 角色授权
        const MINTER_ROLE = keccak256(stringToBytes("MINTER_ROLE"));
        const ORACLE_ROLE = keccak256(stringToBytes("ORACLE_ROLE"));
        await ggt.write.grantRole([MINTER_ROLE, core.address]);
        await core.write.grantRole([ORACLE_ROLE, admin.account.address]);
    });

    it("创新点测试:Haus 租赁分润 + HP 损耗结算", async function () {
        // A. 铸造并设置 30% 分成给 Guest
        await core.write.mintSneaker([host.account.address, 50n]);
        await core.write.setHausLease([0n, guest.account.address, 30n], { account: host.account });

        // B. 结算 10km (奖励 5e18)
        await core.write.settleWorkout([0n, 10n]);

        // C. 验证 Guest 收到 1.5e18 (30%)
        const guestBalance = await ggt.read.balanceOf([guest.account.address]);
        assert.strictEqual(guestBalance, 1500000000000000000n, "Guest 分润金额不正确");

        // D. 验证 HP 损耗 (10000 - 10*100 = 9000)
        const snk = await core.read.sneakers([0n]);
        assert.strictEqual(snk[2], 9000n, "HP 损耗计算不正确");
    });

    it("创新点测试:销毁运动鞋增加永久能量", async function () {
        // A. 给 Host 铸造一双鞋
        await core.write.mintSneaker([host.account.address, 20n]);
        
        // B. Host 销毁该鞋
        await core.write.burnForEnergy([0n], { account: host.account });

        // C. 验证能量增加且 NFT 消失
        const energy = await core.read.permanentEnergy([host.account.address]);
        assert.strictEqual(energy, 1n, "能量点数未增加");

        try {
            await core.read.ownerOf([0n]);
            assert.fail("NFT 未被正确销毁");
        } catch (e: any) {
            assert.ok(e.message.includes("ERC721NonexistentToken"), "报错信息不符合预期");
        }
    });
});
  • GGTToken测试
    • 正确计算收益并扣除 HP
    • HP 低于 10% 时应拒绝运动
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { network } from "hardhat";
import { parseUnits, decodeEventLog, keccak256, toBytes, getAddress } from 'viem';

describe("StepnGo Engine Logic (Viem + Node Test)", function () {
    let ggt: any;
    let engine: any;
    let publicClient: any;
    let admin: any, oracle: any, user: any;

    const TOKEN_ID = 101n;
    // 权限哈希定义
    const MINTER_ROLE = keccak256(toBytes("MINTER_ROLE"));
    const ORACLE_ROLE = keccak256(toBytes("ORACLE_ROLE"));

    beforeEach(async function () {
        const { viem } = await (network as any).connect();
        publicClient = await viem.getPublicClient();
        [admin, oracle, user] = await viem.getWalletClients();

        // --- 修复点 1: 使用完全限定名解决 HHE1001 ---
        ggt = await viem.deployContract("contracts/GGT.sol:GGTToken", []);
        engine = await viem.deployContract("contracts/GGT.sol:StepnGoEngine", [ggt.address]);

        // 权限授权
        await ggt.write.grantRole([MINTER_ROLE, engine.address], { account: admin.account });
        await engine.write.grantRole([ORACLE_ROLE, oracle.account.address], { account: admin.account });

        // 初始化
        await engine.write.initializeSneaker([TOKEN_ID, 5n, 10n], { account: admin.account });
    });

    describe("Settlement & Economy", function () {
        it("应该正确计算收益并扣除 HP", async function () {
            const km = 10n;
            const txHash = await engine.write.settleGGT([TOKEN_ID, km], { account: oracle.account });
            const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });

            // 1. 验证 HP
            const [,, currentHP] = await engine.read.sneakers([TOKEN_ID]);
            assert.equal(currentHP, 9000n);

            // --- 修复点 2: 健壮解析事件 ---
            // 过滤出属于 WorkoutProcessed 的日志 (对比 topic0)
            const workoutEventTopic = keccak256(toBytes("WorkoutProcessed(uint256,uint256,uint256)"));
            const log = receipt.logs.find((l: any) => l.topics[0] === workoutEventTopic);
            
            if (!log) throw new Error("WorkoutProcessed event not found");

            const event = decodeEventLog({
                abi: engine.abi,
                eventName: 'WorkoutProcessed',
                data: log.data,
                topics: log.topics,
            });

            const expectedNet = parseUnits("90", 15);
            assert.equal((event.args as any).netGGT, expectedNet);
            
            // 验证 Oracle 余额 (tx.origin)
            const balance = await ggt.read.balanceOf([oracle.account.address]);
            assert.equal(balance, expectedNet);
        });

        it("当 HP 低于 10% 时应拒绝运动", async function () {
            // 消耗 HP 至 900
            await engine.write.settleGGT([TOKEN_ID, 91n], { account: oracle.account });

            // --- 修复点 3: 捕获异步报错 ---
            await assert.rejects(
                async () => {
                    await engine.write.settleGGT([TOKEN_ID, 1n], { account: oracle.account });
                },
                (err: any) => {
                    const msg = err.message || "";
                    return msg.includes("HP too low") || msg.includes("Transaction reverted");
                }
            );
        });
    });
});

部署脚本

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

  const GGTTokenArtifact = await artifacts.readArtifact("contracts/StepnGoIntegrated.sol:GGTToken");
  const StepnGoIntegratedArtifact = await artifacts.readArtifact("contracts/StepnGoIntegrated.sol:StepnGoIntegrated");    
  // 1. 部署合约并获取交易哈希
  const GGTTokenHash = await deployer.deployContract({
    abi: GGTTokenArtifact.abi,
    bytecode: GGTTokenArtifact.bytecode,
    args: [],
  });
  const GGTTokenReceipt = await publicClient.waitForTransactionReceipt({ 
     hash: GGTTokenHash 
   });
   console.log("GGTToken合约地址:", GGTTokenReceipt.contractAddress);
    // 2. 部署StepnGoIntegrated合约并获取交易哈希
  const StepnGoIntegratedHash = await deployer.deployContract({
    abi: StepnGoIntegratedArtifact.abi,
    bytecode: StepnGoIntegratedArtifact.bytecode,
    args: [GGTTokenReceipt.contractAddress],
  });
  const StepnGoIntegratedReceipt = await publicClient.waitForTransactionReceipt({ 
     hash: StepnGoIntegratedHash 
   });
   console.log("StepnGoIntegrated合约地址:", StepnGoIntegratedReceipt.contractAddress);
}

main().catch(console.error);

结语

本次围绕 STEPN 与 STEPN GO 核心差异的拆解,已完成从理论分析到基于 OpenZeppelin V5+Solidity 0.8.24 的代码落地。这一技术栈的选型,既依托 OpenZeppelin V5 的安全组件筑牢合约基础,也借助 Solidity 0.8.24 的特性适配不同场景需求 ——STEPN 合约聚焦「运动 - 激励」完整经济闭环,而 STEPN GO 则做了轻量化重构,剥离冗余逻辑以适配高频、轻量化的使用场景。

此次实践不仅厘清了两款产品的底层技术分野,也验证了成熟开源工具链在区块链应用开发中的核心价值:以产品定位为导向,通过精准的合约逻辑设计,让技术落地真正匹配产品的差异化诉求。

深度实战:用 Solidity 0.8.24 + OpenZeppelin V5 还原 STEPN 核心机制

作者 木西
2026年2月21日 09:51

前言

在 Web3 领域,STEPN 凭借“运动即挖矿(Move-to-Earn)”模式和复杂的代币经济学成为了现象级项目。本文将通过最新的 Solidity 0.8.24 特性与 OpenZeppelin V5 框架,带你手把手实现其最核心的三个系统:NFT 运动鞋管理动态能量恢复以及运动鞋繁殖(Breeding)

一、 STEPN 项目机制深度梳理

STEPN 成功背后的三个核心经济齿轮

1. 核心产品逻辑:Move-to-Earn

  • 能量系统 (Energy) :这是限制产出的“体力值”。1 能量对应 5 分钟运动产出,随时间自动恢复,有效防止了无限刷币。
  • 消耗机制 (Consumption) :运动会降低鞋子的耐久度 (Durability) ,用户必须支付 $GST 代币进行修鞋,否则产出效率会大幅下降。
  • 反作弊 (Anti-Cheating) :通过 GPS 追踪和步法分析,确保奖励发放给真实的户外运动者。

2. 双代币模型:GSTGMTGST与GMT

  • $GST (实用代币) :无限供应,用于日常消耗(修鞋、升级、繁殖)。
  • $GMT (治理代币) :总量有限,用于高级功能和生态投票,是项目的长期价值锚点。

3. NFT 数值体系

NFT 运动鞋拥有四大属性:效率 (Efficiency)  决定产出,幸运 (Luck)  决定宝箱掉落,舒适 (Comfort)  决定治理币产出,韧性 (Resilience)  决定维护成本。通过“繁殖 (Minting)”消耗代币产出新鞋,是用户增长的核心动力。

二、 核心合约设计:StepnManager.sol

我们将所有的核心逻辑集成在一个管理合约中。该设计的精髓在于 “惰性计算” ——不在后台跑昂贵的定时任务恢复能量,而是在用户交互时(如结算或繁殖)根据时间戳差值动态计算,极大节省了链上 Gas 成本。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract GSTToken is ERC20, Ownable {
    constructor(address initialOwner) ERC20("Green Satoshi Token", "GST") Ownable(initialOwner) {}
    function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); }
}

contract StepnManager is ERC721, Ownable, ReentrancyGuard {
    GSTToken public immutable gstToken;
    uint256 private _nextTokenId;

    struct Sneaker {
        uint256 level;
        uint256 mintCount;
        uint256 lastUpdate;
        uint256 lastEnergyUpdate;
        uint256 energyBase;
    }

    mapping(uint256 => Sneaker) public sneakers;

    uint256 public constant REWARD_PER_MIN = 1 ether; 
    uint256 public constant MINT_COST_GST = 100 ether;
    uint256 public constant ENERGY_RECOVERY_RATE = 6 hours;

    constructor() ERC721("STEPN Sneaker", "SNK") Ownable(msg.sender) {
        gstToken = new GSTToken(address(this));
    }

    // --- 测试辅助函数 ---
    function testMintGST(address to, uint256 amount) external {
        gstToken.mint(to, amount);
    }

    function mintSneaker(address to) external onlyOwner returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        sneakers[tokenId] = Sneaker(1, 0, block.timestamp, block.timestamp, 100);
        return tokenId;
    }

    function getEnergy(uint256 tokenId) public view returns (uint256) {
        Sneaker storage s = sneakers[tokenId];
        uint256 timePassed = block.timestamp - s.lastEnergyUpdate;
        uint256 recovered = (timePassed / ENERGY_RECOVERY_RATE) * 25;
        uint256 total = s.energyBase + recovered;
        return total > 100 ? 100 : total;
    }

    function completeRun(uint256 tokenId) external nonReentrant {
        require(ownerOf(tokenId) == msg.sender, "Not owner");
        uint256 currentEnergy = getEnergy(tokenId);
        require(currentEnergy >= 25, "Low energy");

        Sneaker storage s = sneakers[tokenId];
        uint256 timeElapsed = block.timestamp - s.lastUpdate;
        require(timeElapsed >= 60, "Too short");

        s.energyBase = currentEnergy - 25;
        s.lastEnergyUpdate = block.timestamp;
        s.lastUpdate = block.timestamp;

        uint256 reward = (timeElapsed / 60) * REWARD_PER_MIN;
        gstToken.mint(msg.sender, reward);
    }

    function breed(uint256 p1, uint256 p2) external nonReentrant {
        require(ownerOf(p1) == msg.sender && ownerOf(p2) == msg.sender, "Not owner");
        require(p1 != p2, "Same parents");
        require(sneakers[p1].mintCount < 7 && sneakers[p2].mintCount < 7, "Max mints");

        gstToken.transferFrom(msg.sender, address(this), MINT_COST_GST);

        sneakers[p1].mintCount++;
        sneakers[p2].mintCount++;

        uint256 childId = _nextTokenId++;
        _safeMint(msg.sender, childId);
        sneakers[childId] = Sneaker(1, 0, block.timestamp, block.timestamp, 100);
    }
}

三、 高性能测试环境搭建

测试用例:STEPN 全流程功能测试

  • 场景1:基础铸造与属性验证
  • 场景2:运动奖励与能量消耗
  • 场景3:能量随时间自动恢复
  • 场景4:运动鞋繁殖 (Breeding) 完整流程
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { parseEther, formatEther } from 'viem';
import { network } from "hardhat";

describe("STEPN 全流程功能测试", function () {
    let stepn: any, gst: any;
    let publicClient: any, owner: any, user: any;

    beforeEach(async function () {
        // @ts-ignore
        const { viem } = await (network as any).connect();
        publicClient = await viem.getPublicClient();
        [owner, user] = await viem.getWalletClients();

        stepn = await viem.deployContract("StepnManager");
        const gstAddress = await stepn.read.gstToken();
        gst = await viem.getContractAt("GSTToken", gstAddress);
    });

    it("场景1:基础铸造与属性验证", async function () {
        await stepn.write.mintSneaker([user.account.address]);
        const sneaker = await stepn.read.sneakers([0n]);
        // index 0 = level, index 1 = mintCount
        assert.equal(sneaker[0], 1n);
    });

    it("场景2:运动奖励与能量消耗", async function () {
        await stepn.write.mintSneaker([user.account.address]);
        
        await publicClient.request({ method: "evm_increaseTime", params: [120] });
        await publicClient.request({ method: "evm_mine" });

        await stepn.write.completeRun([0n], { account: user.account });

        const balance = await gst.read.balanceOf([user.account.address]);
        const energy = await stepn.read.getEnergy([0n]);

        assert.equal(balance, parseEther("2"));
        assert.equal(energy, 75n);
    });

    it("场景3:能量随时间自动恢复", async function () {
        await stepn.write.mintSneaker([user.account.address]);
        
        // 消耗能量
        await publicClient.request({ method: "evm_increaseTime", params: [60] });
        await publicClient.request({ method: "evm_mine" });
        await stepn.write.completeRun([0n], { account: user.account }); 

        // 快进 6 小时恢复 25 能量
        await publicClient.request({ method: "evm_increaseTime", params: [6 * 3600] });
        await publicClient.request({ method: "evm_mine" });

        const energy = await stepn.read.getEnergy([0n]);
        assert.equal(energy, 100n);
    });

    it("场景4:运动鞋繁殖 (Breeding) 完整流程", async function () {
        // 1. 准备两双鞋
        await stepn.write.mintSneaker([user.account.address]); 
        await stepn.write.mintSneaker([user.account.address]); 
        
        // 2. 使用辅助函数给 User 发放 100 GST
        await stepn.write.testMintGST([user.account.address, parseEther("100")]);
        
        // 3. 授权并繁殖
        await gst.write.approve([stepn.address, parseEther("100")], { account: user.account });
        await stepn.write.breed([0n, 1n], { account: user.account });

        // 4. 验证:User 应该有 3 双鞋 (0, 1, 2)
        const totalSneakers = await stepn.read.balanceOf([user.account.address]);
        assert.equal(totalSneakers, 3n);
        
        // 验证父代繁殖次数增加
        const parent0 = await stepn.read.sneakers([0n]);
        assert.equal(parent0[1], 1n); // index 1 is mintCount
    });
});

四、合约部署脚本

// scripts/deploy.js
import { network, artifacts } from "hardhat";
import { parseUnits } from "viem";
async function main() {
  // 连接网络
  const { viem } = await network.connect({ network: network.name });//指定网络进行链接
  
  // 获取客户端
  const [deployer, investor] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();
 
  const deployerAddress = deployer.account.address;
   console.log("部署者的地址:", deployerAddress);
  
  // 部署SoulboundIdentity合约
  const StepnManagerArtifact = await artifacts.readArtifact("StepnManager");
  const GSTTokenArtifact = await artifacts.readArtifact("GSTToken");    
  // 1. 部署合约并获取交易哈希
  const StepnManagerHash = await deployer.deployContract({
    abi: StepnManagerArtifact.abi,
    bytecode: StepnManagerArtifact.bytecode,
    args: [],
  });
  const StepnManagerReceipt = await publicClient.waitForTransactionReceipt({ 
     hash: StepnManagerHash 
   });
   console.log("StepnManager合约地址:", StepnManagerReceipt.contractAddress);
    // 2. 部署GSTToken合约并获取交易哈希
  const GSTTokenHash = await deployer.deployContract({
    abi: GSTTokenArtifact.abi,
    bytecode: GSTTokenArtifact.bytecode,
    args: [StepnManagerReceipt.contractAddress],
  });
  const GSTTokenReceipt = await publicClient.waitForTransactionReceipt({ 
     hash: GSTTokenHash 
   });
   console.log("GSTToken合约地址:", GSTTokenReceipt.contractAddress);
}

main().catch(console.error);

五、 总结

至此,我们成功实现了一个具备产出(运动奖励)消耗(繁殖费用)限制(能量系统) 三位一体的 Web3 核心原型。

  • 高性能实现:通过时间锚点逻辑规避了轮询带来的 Gas 浪费。
  • 鲁棒性验证:利用 EVM 时间操纵技术确保了数值系统的准确性。
  • 经济闭环:完整实现了从“NFT 持有”到“运动产出”再到“代币销毁繁殖”的循环。

这种“时间快照”+“数值建模”的设计模式,不仅是 Move-to-Earn 的基石,也是构建所有链上复杂数值游戏(GameFi)和资产线性释放系统的最佳实践。

实战|DeLinkedIn 全栈开发:Web3 身份验证 + 数字资产确权,搭建职场社交新生态

作者 木西
2026年2月17日 18:19

前言

本文主要整合往期发布的 DAO、SSI 身份、社区所有权社交 等相关内容,实现一个简洁的去中心化社区实例。延续以往风格 理论加代码实践相结合。

概述

在 2026 年,职场社交正在经历从“平台信用”向“加密证明”的范式转移。传统的 LinkedIn 依赖于用户自述的简历,而 DeLinkedIn 则是通过 SSI (自主主权身份)Social NFT (内容所有权)  和 DAO (去中心化治理)  的三位一体架构,构建了一个真实、透明且价值对等的职业生态。

核心架构理论:三权分立

  1. 身份层 (SSI/SBT):真实性的根基

    • 理论:简历不再是 PDF,而是由大学、前雇主或开源组织签发的灵魂绑定代币 (SBT)
    • 解决痛点:消除简历造假。只有持有特定技能凭证的用户才能进入高阶人才库。
  2. 社交层 (Community Ownership):内容即资产

    • 理论:每一次职场深度分享都铸造为 ERC-721 NFT
    • 解决痛点:创作者拥有粉丝关系和内容的所有权,平台无法通过流量抽成剥削职场博主。
  3. 治理层 (DAO/Token):共建者激励

    • 理论:平台由持有 Governance Token 的成员共有。优质内容的产出直接由 DAO 金库进行代币奖励。
    • 解决痛点:将“用户流量”转化为“社区股份”,实现利益共担。

猎头赏金:智能合约如何重构招聘经济学

  1. 企业发布(Post & Lock):企业发布职位并锁定一定数额的平台代币(赏金)到合约。
  2. 用户推荐(Referral):用户通过自己的 DID 身份推荐好友。
  3. 多签结算(Settlement):当好友入职通过试用期,企业或 DAO 触发结算,赏金自动拨付给推荐人。

DeLinkedIn的BountyLinkedIn合约将传统猎头的"人治"流程改造为无需信任的自动化协议

三步闭环

步骤 角色 链上动作 传统痛点 合约解决方案
1. 锁仓 企业 postJobBounty() 锁定赏金 口头承诺无保障 资金托管在合约,无法撤回
2. 推荐 专业人士 referCandidate() 记录关系 推荐关系难证明 DID身份绑定,链上可追溯
3. 结算 企业/DAO fulfillBounty() 自动拨付 结算周期长、扯皮多 条件触发,秒级到账

智能合约落地全流程

智能合约

  • 去中心化领英
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

// 身份接口:用于核验用户是否拥有“技能凭证”
interface ISoulboundIdentity {
    function balanceOf(address owner) external view returns (uint256);
}

contract DeLinkedIn is ERC721, AccessControl {
    bytes32 public constant MODERATOR_ROLE = keccak256("MODERATOR_ROLE");
    IERC20 public platformToken;
    ISoulboundIdentity public sbtIdentity;

    uint256 private _nextPostId;
    uint256 public constant POST_REWARD = 10 * 10**18; // 发帖奖励 10 Token

    struct WorkPost {
        address author;
        string metadataUri; // 职业动态内容
        bool isVerifiedPro; // 是否为核验专家
    }

    mapping(uint256 => WorkPost) public posts;

    error NotSkillCertified(); // 未获得技能认证(SSI 拦截)
    error rewardTransferFailed();

    constructor(address _token, address _sbt) ERC721("DeLinkedInPost", "DLP") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        platformToken = IERC20(_token);
        sbtIdentity = ISoulboundIdentity(_sbt);
    }
    function supportsInterface(bytes4 interfaceId)
            public
            view
            override(ERC721, AccessControl)
            returns (bool)
        {
            return super.supportsInterface(interfaceId);
        }
    /**
     * @dev 发布职场动态:只有持有 SSI 技能凭证的用户才能发布
     */
    function publishProfessionalInsight(string memory _uri) external {
        // 1. SSI 身份核验:检查用户是否持有灵魂绑定技能凭证
        if (sbtIdentity.balanceOf(msg.sender) == 0) {
            revert NotSkillCertified();
        }

        // 2. 社区所有权:内容 NFT 化
        uint256 tokenId = _nextPostId++;
        _safeMint(msg.sender, tokenId);
        posts[tokenId] = WorkPost(msg.sender, _uri, true);

        // 3. 经济激励:给创作者发放平台代币奖励(由 DAO 金库支持)
        bool success = platformToken.transfer(msg.sender, POST_REWARD);
        if (!success) revert rewardTransferFailed();
    }
}

  • 猎头赏金:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "./DeLinkedIn.sol"; // 继承之前的逻辑

contract BountyLinkedIn is DeLinkedIn {
    struct JobBounty {
        address employer;
        uint256 rewardAmount;
        bool isActive;
    }

    // 职位 ID => 赏金信息
    mapping(uint256 => JobBounty) public jobBounties;
    // 推荐记录:候选人地址 => 推荐人地址
    mapping(address => address) public referrals;

    event BountyPosted(uint256 jobId, uint256 amount);
    event BountyClaimed(uint256 jobId, address indexed referrer, address indexed candidate);

    constructor(address _token, address _sbt) DeLinkedIn(_token, _sbt) {}

    /**
     * @dev 企业发布赏金职位
     */
    function postJobBounty(uint256 _jobId, uint256 _amount) external {
        platformToken.transferFrom(msg.sender, address(this), _amount);
        jobBounties[_jobId] = JobBounty(msg.sender, _amount, true);
        emit BountyPosted(_jobId, _amount);
    }

    /**
     * @dev 用户提交推荐:记录推荐关系
     */
    function referCandidate(address _candidate) external {
        if (sbtIdentity.balanceOf(msg.sender) == 0) revert NotSkillCertified();
        referrals[_candidate] = msg.sender;
    }

    /**
     * @dev 企业确认入职,拨付赏金给推荐人
     */
    function fulfillBounty(uint256 _jobId, address _candidate) external {
        JobBounty storage bounty = jobBounties[_jobId];
        require(msg.sender == bounty.employer, "Only employer can fulfill");
        require(bounty.isActive, "Bounty not active");

        address referrer = referrals[_candidate];
        require(referrer != address(0), "No referrer found");

        bounty.isActive = false;
        platformToken.transfer(referrer, bounty.rewardAmount);

        emit BountyClaimed(_jobId, referrer, _candidate);
    }
}

测试脚本

测试用例:DeLinkedIn 综合项目测试 (SSI + Social + DAO)

  • 核验通过的专业人士应能发布动态并获得代币奖励
  • 未获得 SSI 认证的‘游客’尝试发布应被拒绝
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { network } from "hardhat";
import { parseEther } from 'viem';

describe("DeLinkedIn 综合项目测试 (SSI + Social + DAO)", function () {
    let delinkedIn: any, token: any, sbt: any;
    let publicClient, testClient;
    let admin: any, user: any, stranger: any;

    beforeEach(async function () {
        const { viem: v } = await (network as any).connect();
        publicClient = await v.getPublicClient();
        testClient = await v.getTestClient();
        [admin, user, stranger] = await v.getWalletClients();

        // 1. 部署基础设施
        token = await v.deployContract("contracts/DAO.sol:MyToken", []); 
        sbt = await v.deployContract("contracts/SoulboundIdentity.sol:SoulboundIdentity", []);
        
        // 2. 部署领英主合约
        delinkedIn = await v.deployContract("contracts/DeLinkedIn.sol:DeLinkedIn", [
            token.address, 
            sbt.address
        ]);

        // 3. 注入 DAO 奖励金库资金
        await token.write.transfer([delinkedIn.address, parseEther("1000")]);

        // 4. 为 SSI 合约设置签发者并给 user 签发一个技能凭证
        const ISSUER_ROLE = await sbt.read.ISSUER_ROLE();
        await sbt.write.grantRole([ISSUER_ROLE, admin.account.address]);
        await sbt.write.issueIdentity([user.account.address, "ipfs://Senior-Dev-Cert", 0n]);
    });

    it("核验通过的专业人士应能发布动态并获得代币奖励", async function () {
        const initialBalance = await token.read.balanceOf([user.account.address]);
        
        // 用户发布动态
        await delinkedIn.write.publishProfessionalInsight(["ipfs://My-Web3-Insight"], { account: user.account });

        // 验证 1: 内容所有权 (NFT)
        const nftBalance = await delinkedIn.read.balanceOf([user.account.address]);
        assert.equal(nftBalance, 1n, "应获得内容所有权 NFT");

        // 验证 2: 经济激励 (Token)
        const finalBalance = await token.read.balanceOf([user.account.address]);
        assert.equal(finalBalance - initialBalance, parseEther("10"), "应获得 10 枚代币奖励");
    });

    it("未获得 SSI 认证的‘游客’尝试发布应被拒绝", async function () {
        await assert.rejects(
            delinkedIn.write.publishProfessionalInsight(["ipfs://Fake-Insight"], { account: stranger.account }),
            /NotSkillCertified/,
            "未认证用户必须被 SSI 逻辑拦截"
        );
    });
});

测试用例:猎头赏金流程测试

  • 发布赏金 -> 推荐好友 -> 入职结算的闭环
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { network} from "hardhat";
import { parseEther } from 'viem';

describe("DeLinkedIn 猎头赏金流程测试", function () {
    let bountyContract: any, token: any, sbt: any;
    let publicClient;
    let employer: any, referrer: any, candidate: any;

    beforeEach(async function () {
        const { viem: v } = await (network as any).connect();
        publicClient = await v.getPublicClient();
        [employer, referrer, candidate] = await v.getWalletClients();

        // 部署
        token = await v.deployContract("contracts/DAO.sol:MyToken", []);
        sbt = await v.deployContract("contracts/SoulboundIdentity.sol:SoulboundIdentity", []);
        bountyContract = await v.deployContract("contracts/BountyLinkedIn.sol:BountyLinkedIn", [token.address, sbt.address]);

        // 初始化:给推荐人签发技能身份,给企业发钱
        const ISSUER_ROLE = await sbt.read.ISSUER_ROLE();
        await sbt.write.grantRole([ISSUER_ROLE, employer.account.address]);
        await sbt.write.issueIdentity([referrer.account.address, "ipfs://Expert", 0n]);
        await token.write.transfer([employer.account.address, parseEther("500")]);
    });

    it("应该完成:发布赏金 -> 推荐好友 -> 入职结算的闭环", async function () {
        const bountyAmount = parseEther("100");

        // 1. 企业发布赏金
        await token.write.approve([bountyContract.address, bountyAmount], { account: employer.account });
        await bountyContract.write.postJobBounty([1n, bountyAmount], { account: employer.account });

        // 2. 推荐人推荐候选人
        await bountyContract.write.referCandidate([candidate.account.address], { account: referrer.account });

        // 3. 企业确认入职并拨付
        const initialBalance = await token.read.balanceOf([referrer.account.address]);
        await bountyContract.write.fulfillBounty([1n, candidate.account.address], { account: employer.account });

        // 4. 验证推荐人收到赏金
        const finalBalance = await token.read.balanceOf([referrer.account.address]);
        assert.equal(finalBalance - initialBalance, bountyAmount, "推荐人应收到 100 枚代币赏金");
    });
});

部署脚本

// 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 TokenArtifact = await artifacts.readArtifact("contracts/DAO.sol:MyToken");
  const SoulboundIdentityArtifact = await artifacts.readArtifact("contracts/SoulboundIdentity.sol:SoulboundIdentity");
  const DeLinkedInArtifact = await artifacts.readArtifact("contracts/DeLinkedIn.sol:DeLinkedIn");
  const BountyLinkedInArtifact = await artifacts.readArtifact("contracts/BountyLinkedIn.sol:BountyLinkedIn");
 const TokenHash = await deployer.deployContract({
    abi: TokenArtifact.abi,//获取abi
    bytecode: TokenArtifact.bytecode,//硬编码
    args: [],
  });
  const TokenReceipt = await publicClient.waitForTransactionReceipt({ hash: TokenHash });
  console.log("Token合约地址:", TokenReceipt.contractAddress);
  // 部署
  const SoulboundIdentityHash = await deployer.deployContract({
    abi: SoulboundIdentityArtifact.abi,//获取abi
    bytecode: SoulboundIdentityArtifact.bytecode,//硬编码
    args: [],
  });
   const SoulboundIdentityReceipt = await publicClient.waitForTransactionReceipt({ hash: SoulboundIdentityHash });
   console.log("SoulboundIdentity合约地址:", SoulboundIdentityReceipt.contractAddress);
   const DeLinkedInHash = await deployer.deployContract({
    abi: DeLinkedInArtifact.abi,//获取abi
    bytecode: DeLinkedInArtifact.bytecode,//硬编码
    args: [TokenReceipt.contractAddress, SoulboundIdentityReceipt.contractAddress],
  });
   const DeLinkedInReceipt = await publicClient.waitForTransactionReceipt({ hash: DeLinkedInHash });
   console.log("DeLinkedIn合约地址:", DeLinkedInReceipt.contractAddress);
   const BountyLinkedInHash = await deployer.deployContract({
    abi: BountyLinkedInArtifact.abi,//获取abi
    bytecode: BountyLinkedInArtifact.bytecode,//硬编码
    args: [TokenReceipt.contractAddress, SoulboundIdentityReceipt.contractAddress],
  });
   const BountyLinkedInReceipt = await publicClient.waitForTransactionReceipt({ hash: BountyLinkedInHash });
   console.log("BountyLinkedIn合约地址:", BountyLinkedInReceipt.contractAddress);
}
main().catch(console.error);

结语

至此,基于DAO、SSI身份、社区所有权社交三大核心技术的综合案例——2026去中心化领英(DeLinkedIn)已全部完成。本文延续了“理论+代码”的呈现风格,从项目架构设计、核心业务逻辑拆解,到智能合约开发、测试脚本编写、部署脚本实现,完整呈现了去中心化社区的落地过程。

❌
❌