前言
本文作为上一篇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) | 属性更丰富,新增套装属性、社交等级收益加成 |
技术实现上的关键点
- 增加 NFT 焚烧逻辑: 玩家需要调用一个
burnSneakerForEnergy函数。 - 动态 HP 算法: Stepn Go 的 HP 损耗通常不是线性的,而是根据等级和属性非线性变化。
- 多角色分利: 净收益(Net Reward)的一部分往往会分给"邀请人"(The Haus 房主)。
智能合于落地全流程
智能合约
- StepnGo合约
ini
// 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合约
ini
// 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 损耗结算
- 销毁运动鞋增加永久能量
typescript
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% 时应拒绝运动
ini
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");
}
);
});
});
});
部署脚本
javascript
// 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 则做了轻量化重构,剥离冗余逻辑以适配高频、轻量化的使用场景。
此次实践不仅厘清了两款产品的底层技术分野,也验证了成熟开源工具链在区块链应用开发中的核心价值:以产品定位为导向,通过精准的合约逻辑设计,让技术落地真正匹配产品的差异化诉求。