前言
在 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. 双代币模型:
- $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)和资产线性释放系统的最佳实践。