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

  • 木西
  • 发布于 8小时前
  • 阅读 31

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

前言

本文作为上一篇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 则做了轻量化重构,剥离冗余逻辑以适配高频、轻量化的使用场景。

此次实践不仅厘清了两款产品的底层技术分野,也验证了成熟开源工具链在区块链应用开发中的核心价值:以产品定位为导向,通过精准的合约逻辑设计,让技术落地真正匹配产品的差异化诉求。
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。