Web3 中级实战:基于智能合约与钱包登录构建去中心化会员积分系统
很多团队一提到“会员积分”,第一反应还是传统 Web2 方案:用户账号体系、中心化数据库、后台发积分、活动规则写在服务端。它当然能用,但一旦业务开始强调“资产归属”“跨平台通用”“可验证”“用户自主控制身份”,这套方案就会显得不够灵活。
这篇文章我换一个更偏架构落地的角度,带你做一套 基于智能合约 + 钱包登录 的去中心化会员积分系统。重点不是炫技,而是回答几个中级开发者最关心的问题:
- 为什么会员积分适合链上化,哪些部分适合,哪些不适合?
- 钱包登录到底怎么和会员体系打通?
- 积分要不要做成 ERC20?还是自己写积分账本?
- 后端在去中心化系统里还扮演什么角色?
- 如何兼顾可运营、可扩展、安全和成本?
我会给出一套可运行的最小实现:
钱包签名登录 + 积分智能合约 + 后端鉴权 + 前端调用流程。
背景与问题
为什么要把会员积分搬到 Web3
传统会员积分系统的核心痛点通常有这几类:
-
积分归属不透明
用户只能相信平台数据库,无法独立验证“我到底有多少积分”。 -
系统间不互通
A 产品积分无法自然流转到 B 产品,合作方对账复杂。 -
账号体系割裂
用户在多个站点重复注册,身份碎片化。 -
平台单点控制过强
后台想改规则、回滚数据、封号清零,技术上通常都能做到,用户缺少可验证性保障。
而 Web3 提供了两块很实用的能力:
- 钱包即身份:用户用钱包地址作为统一账户标识;
- 合约即账本:积分发放、消费、冻结、查询都可上链验证。
但别急着“All in on-chain”。真正可落地的方案,通常是:
- 身份认证链下完成:通过钱包签名登录;
- 积分状态链上存证:核心积分账本放合约;
- 业务规则链下协同:风控、活动引擎、订单系统、BI 仍由后端处理。
也就是说,这不是“不要后端”,而是后端从唯一真相源,变成协调者和服务层。
方案目标与边界
在开始写代码前,先把目标说清楚。我们这次做的是一个去中心化会员积分系统 MVP,满足:
- 用户使用 MetaMask 登录;
- 后端生成 nonce,用户签名,服务端验签并发 JWT;
- 管理员可给用户发积分;
- 用户可消费积分;
- 前端可实时读取链上积分;
- 后端监听链上事件同步业务库。
不做的内容
为了控制复杂度,这篇文章不展开:
- 多链部署
- Account Abstraction
- 零知识隐私积分
- DAO 治理积分规则
- 积分商城完整订单系统
这些可以在后续架构迭代中逐步补。
架构总览
先看整体架构。一个比较稳妥的实现方式如下:
flowchart LR
U[用户钱包] --> F[前端 DApp]
F --> B[业务后端 API]
F --> C[积分智能合约]
B --> DB[(业务数据库)]
B --> I[链上事件监听器]
I --> C
I --> DB
这套设计的关键点:
- 前端 + 钱包:完成登录签名、发起交易、读取积分;
- 后端 API:处理 nonce、验签、JWT、活动规则、管理端鉴权;
- 智能合约:保存积分余额与变更记录;
- 事件监听器:把链上事件同步到数据库,支持报表与运营后台。
方案对比:ERC20 vs 自定义积分合约
很多人做积分系统时第一反应是:“那直接发 ERC20 不就行了?”
这不一定错,但在会员积分场景里,往往不是最优。
方案 A:直接使用 ERC20
优点:
- 标准化程度高;
- 钱包和区块浏览器天然支持;
- 转账逻辑现成。
缺点:
- 积分通常不希望自由交易;
- 运营规则常常包含“不可转让、可过期、仅平台核销”等限制;
- 标准 ERC20 容易被外部 DEX、钱包误认为通证资产。
方案 B:自定义积分账本合约
优点:
- 能精准控制业务语义;
- 可限制仅管理员发放;
- 可选择是否允许用户间转移;
- 可加入积分过期、冻结、核销等规则。
缺点:
- 需要自己写接口和事件;
- 钱包展示体验不如 ERC20 标准资产友好。
我的建议
如果你做的是会员成长值 / 平台积分 / 活动积分,更推荐 自定义积分合约。
如果你做的是可流通奖励代币,再考虑 ERC20。
本文就采用 自定义积分合约。
核心原理
这套系统有两个核心链路:
- 钱包登录链路
- 积分记账链路
1. 钱包登录原理
用户不是输入密码,而是:
- 前端向后端请求一个
nonce - 后端返回一段待签名消息
- 用户用钱包签名
- 后端验签,确认该地址确实控制私钥
- 验签通过后签发 JWT
这个过程里,钱包相当于“身份凭证”。
sequenceDiagram
participant U as 用户
participant F as 前端
participant B as 后端
participant W as 钱包
F->>B: 请求 nonce(address)
B-->>F: 返回 nonce/message
F->>W: 发起 personal_sign
W-->>F: 返回 signature
F->>B: 提交 address + signature
B->>B: 验签并校验 nonce
B-->>F: 返回 JWT
2. 积分记账原理
积分系统链上部分,本质是一个“受控账本”:
earn(address, amount):发积分spend(address, amount):扣积分balanceOf(address):查余额
如果你把“谁能发积分”也开放给任意人,那系统就废了。所以这里必须有权限控制。
classDiagram
class LoyaltyPoint {
+balanceOf(address) uint256
+earn(address,uint256)
+spend(address,uint256)
+setOperator(address,bool)
}
class Admin {
+DEFAULT_ADMIN_ROLE
}
class Operator {
+OPERATOR_ROLE
}
Admin --> LoyaltyPoint : 管理权限
Operator --> LoyaltyPoint : 发放/扣减权限
数据与职责划分
中级开发里最常见的问题不是“功能不会写”,而是“边界没划清”。下面这张表很关键:
| 模块 | 放链上 | 放链下 |
|---|---|---|
| 用户身份地址 | 是 | 可缓存 |
| 登录态 JWT | 否 | 是 |
| nonce | 否 | 是 |
| 积分余额 | 是 | 可同步 |
| 积分变更流水 | 是(事件) | 是(索引后查询) |
| 活动规则 | 否 | 是 |
| 风控策略 | 否 | 是 |
| 运营报表 | 否 | 是 |
为什么 nonce 不上链
因为登录是一种会话行为,不是资产状态。
nonce 只用来防重放,放数据库或 Redis 就足够了,没必要增加链上成本。
为什么积分余额上链
因为余额是整个系统最核心、最需要可验证的状态。
只要余额和关键变更上链,用户就能独立核验。
实战代码(可运行)
下面我给出一个最小可运行版本。技术栈如下:
- 合约:Solidity + OpenZeppelin
- 开发框架:Hardhat
- 后端:Node.js + Express + ethers
- 前端:原生 HTML + ethers.js 示例
一、智能合约:积分账本
1. 安装项目
mkdir web3-loyalty && cd web3-loyalty
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npm install @openzeppelin/contracts
npx hardhat
选择一个 JavaScript 项目。
2. 编写合约
创建 contracts/LoyaltyPoint.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract LoyaltyPoint is AccessControl {
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
mapping(address => uint256) private _balances;
event PointsEarned(address indexed user, uint256 amount, string reason);
event PointsSpent(address indexed user, uint256 amount, string reason);
event OperatorUpdated(address indexed operator, bool enabled);
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(OPERATOR_ROLE, admin);
}
function balanceOf(address user) external view returns (uint256) {
return _balances[user];
}
function earn(address user, uint256 amount, string calldata reason) external onlyRole(OPERATOR_ROLE) {
require(user != address(0), "invalid user");
require(amount > 0, "amount must > 0");
_balances[user] += amount;
emit PointsEarned(user, amount, reason);
}
function spend(address user, uint256 amount, string calldata reason) external onlyRole(OPERATOR_ROLE) {
require(user != address(0), "invalid user");
require(amount > 0, "amount must > 0");
require(_balances[user] >= amount, "insufficient points");
_balances[user] -= amount;
emit PointsSpent(user, amount, reason);
}
function setOperator(address operator, bool enabled) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(operator != address(0), "invalid operator");
if (enabled) {
_grantRole(OPERATOR_ROLE, operator);
} else {
_revokeRole(OPERATOR_ROLE, operator);
}
emit OperatorUpdated(operator, enabled);
}
}
这个版本故意保持简洁,但已经满足最核心场景。
3. 部署脚本
创建 scripts/deploy.js:
const hre = require("hardhat");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Deploying with:", deployer.address);
const LoyaltyPoint = await hre.ethers.getContractFactory("LoyaltyPoint");
const contract = await LoyaltyPoint.deploy(deployer.address);
await contract.waitForDeployment();
const address = await contract.getAddress();
console.log("LoyaltyPoint deployed to:", address);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
4. 编译与部署
本地链运行:
npx hardhat node
另开终端部署:
npx hardhat run scripts/deploy.js --network localhost
5. Hardhat 配置
编辑 hardhat.config.js:
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: "0.8.20",
networks: {
localhost: {
url: "http://127.0.0.1:8545"
}
}
};
二、后端:钱包登录与验签
1. 安装依赖
mkdir server && cd server
npm init -y
npm install express cors jsonwebtoken ethers
2. 后端代码
创建 server/index.js:
const express = require("express");
const cors = require("cors");
const jwt = require("jsonwebtoken");
const { ethers } = require("ethers");
const app = express();
app.use(cors());
app.use(express.json());
const JWT_SECRET = "replace-with-your-secret";
const nonces = new Map();
app.post("/auth/nonce", (req, res) => {
const { address } = req.body;
if (!address || !ethers.isAddress(address)) {
return res.status(400).json({ error: "invalid address" });
}
const nonce = Math.floor(Math.random() * 1_000_000).toString();
nonces.set(address.toLowerCase(), nonce);
const message = `欢迎登录去中心化会员积分系统\n地址: ${address}\nNonce: ${nonce}`;
res.json({ message });
});
app.post("/auth/verify", async (req, res) => {
try {
const { address, signature } = req.body;
if (!address || !signature) {
return res.status(400).json({ error: "missing params" });
}
const nonce = nonces.get(address.toLowerCase());
if (!nonce) {
return res.status(400).json({ error: "nonce not found" });
}
const message = `欢迎登录去中心化会员积分系统\n地址: ${address}\nNonce: ${nonce}`;
const recovered = ethers.verifyMessage(message, signature);
if (recovered.toLowerCase() !== address.toLowerCase()) {
return res.status(401).json({ error: "signature invalid" });
}
nonces.delete(address.toLowerCase());
const token = jwt.sign(
{ sub: address.toLowerCase() },
JWT_SECRET,
{ expiresIn: "2h" }
);
res.json({ token });
} catch (err) {
console.error(err);
res.status(500).json({ error: "verify failed" });
}
});
function authMiddleware(req, res, next) {
const auth = req.headers.authorization || "";
const token = auth.replace("Bearer ", "");
if (!token) {
return res.status(401).json({ error: "missing token" });
}
try {
const payload = jwt.verify(token, JWT_SECRET);
req.user = payload.sub;
next();
} catch (e) {
return res.status(401).json({ error: "invalid token" });
}
}
app.get("/me", authMiddleware, (req, res) => {
res.json({ address: req.user });
});
app.listen(3001, () => {
console.log("API server running at http://localhost:3001");
});
3. 启动后端
node index.js
三、前端:连接钱包并登录
这里用一个最小 HTML 示例,方便你快速跑通流程。
创建 client/index.html:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Web3 会员积分系统</title>
</head>
<body>
<h2>Web3 会员积分系统</h2>
<button id="connectBtn">连接钱包并登录</button>
<pre id="output"></pre>
<script type="module">
import { ethers } from "https://cdn.jsdelivr.net/npm/ethers@6.13.1/+esm";
const output = document.getElementById("output");
const log = (...args) => output.textContent += args.join(" ") + "\n";
document.getElementById("connectBtn").onclick = async () => {
if (!window.ethereum) {
log("请先安装 MetaMask");
return;
}
const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const signer = await provider.getSigner();
const address = await signer.getAddress();
log("当前地址:", address);
const nonceResp = await fetch("http://localhost:3001/auth/nonce", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address })
});
const nonceData = await nonceResp.json();
const signature = await signer.signMessage(nonceData.message);
log("签名完成");
const verifyResp = await fetch("http://localhost:3001/auth/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address, signature })
});
const verifyData = await verifyResp.json();
log("JWT:", verifyData.token || JSON.stringify(verifyData));
};
</script>
</body>
</html>
直接用本地静态服务器打开即可,比如:
npx serve .
四、后端操作合约:发积分与扣积分
接下来让后端作为运营服务,调用合约发积分。
1. 安装 ethers
如果你后端目录还没安装:
npm install ethers
2. 添加合约调用逻辑
在后端新增 server/contract.js:
const { ethers } = require("ethers");
const RPC_URL = "http://127.0.0.1:8545";
const PRIVATE_KEY = "替换成部署账户私钥";
const CONTRACT_ADDRESS = "替换成部署后的合约地址";
const ABI = [
"function earn(address user, uint256 amount, string reason) external",
"function spend(address user, uint256 amount, string reason) external",
"function balanceOf(address user) external view returns (uint256)"
];
const provider = new ethers.JsonRpcProvider(RPC_URL);
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, wallet);
module.exports = { contract };
3. 增加积分接口
在 server/index.js 中增加:
const { contract } = require("./contract");
app.post("/points/earn", authMiddleware, async (req, res) => {
try {
const { user, amount, reason } = req.body;
const tx = await contract.earn(user, amount, reason || "manual earn");
const receipt = await tx.wait();
res.json({
success: true,
txHash: receipt.hash
});
} catch (err) {
console.error(err);
res.status(500).json({ error: "earn failed" });
}
});
app.post("/points/spend", authMiddleware, async (req, res) => {
try {
const { user, amount, reason } = req.body;
const tx = await contract.spend(user, amount, reason || "manual spend");
const receipt = await tx.wait();
res.json({
success: true,
txHash: receipt.hash
});
} catch (err) {
console.error(err);
res.status(500).json({ error: "spend failed" });
}
});
app.get("/points/:address", async (req, res) => {
try {
const balance = await contract.balanceOf(req.params.address);
res.json({ address: req.params.address, balance: balance.toString() });
} catch (err) {
console.error(err);
res.status(500).json({ error: "query failed" });
}
});
生产环境里这里不能让任意登录用户都调用发积分接口,必须再加管理角色鉴权。
本文为了演示流程,先保持最小实现。
五、链上事件同步:给运营后台可读数据
如果你只把数据放链上、不做索引,运营同学大概率会先崩溃。
所以一个可用架构一定要做事件监听。
创建 server/listener.js:
const { ethers } = require("ethers");
const RPC_URL = "http://127.0.0.1:8545";
const CONTRACT_ADDRESS = "替换成部署后的合约地址";
const ABI = [
"event PointsEarned(address indexed user, uint256 amount, string reason)",
"event PointsSpent(address indexed user, uint256 amount, string reason)"
];
const provider = new ethers.JsonRpcProvider(RPC_URL);
const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, provider);
contract.on("PointsEarned", (user, amount, reason, event) => {
console.log("[Earned]", {
user,
amount: amount.toString(),
reason,
txHash: event.log.transactionHash
});
// 这里可以写入数据库
});
contract.on("PointsSpent", (user, amount, reason, event) => {
console.log("[Spent]", {
user,
amount: amount.toString(),
reason,
txHash: event.log.transactionHash
});
// 这里可以写入数据库
});
console.log("Listening contract events...");
运行:
node listener.js
容量估算与架构取舍
很多文章讲到这里就停了,但真正做架构,必须考虑“系统在业务增长后会不会炸”。
1. 写链成本
如果每次积分变动都上链,那吞吐量和 Gas 成本会直接影响业务。
适合直接上链的场景
- 日活不高但强调资产可信;
- 发积分频率低,比如签到、活动奖励、兑换核销;
- B 端联盟积分,需要跨平台共识账本。
不适合每笔都实时上链的场景
- 高频埋点奖励;
- 游戏式实时积分更新;
- 秒级大量写入。
2. 可选优化策略
策略 A:链下累计,链上批量结算
比如用户一天内完成多个动作,后端先累计,定时批量上链。
优点: 成本低
缺点: 实时性下降
策略 B:Layer2 部署
把积分系统部署在成本更低的 L2 网络。
优点: 更接近实时链上
缺点: 增加链选择、桥接、生态兼容成本
策略 C:链上只存最终余额,流水放索引层
这是本文的思路之一。
即:余额和关键事件可验证,复杂分析在链下完成。
常见坑与排查
这部分我尽量写得实战一点,因为我自己做这类系统时,问题大多不是出在“不会写代码”,而是“系统间认知不一致”。
坑 1:签名能成功,但后端验签失败
常见原因
- 前端签名的 message 和后端验签 message 不完全一致;
- 地址大小写处理不统一;
- nonce 被重复使用或过期;
- 使用了
eth_sign、personal_sign、EIP-712,但后端验签方式不匹配。
排查方法
- 直接把前端实际签名字符串打印出来;
- 后端重新拼接 message 时逐字比对;
- 确认
ethers.verifyMessage()对应的是普通消息签名; - 确认 nonce 验证后立即失效。
坑 2:合约调用一直报权限不足
报错通常类似:
AccessControl: account xxx is missing role xxx
原因
- 后端使用的钱包私钥不是部署账户;
- 部署后没有给运营钱包授予
OPERATOR_ROLE; - 你切换了本地链,合约地址变了。
排查建议
- 打印后端钱包地址;
- 确认部署脚本输出的地址和后端配置一致;
- 用脚本调用
setOperator()显式授权。
坑 3:本地链重启后,合约地址失效
这是 Hardhat 新手非常常见的问题。
本地链一重启,状态就清空了,之前部署的地址自然失效。
解决办法
- 重启本地链后重新部署;
- 把最新合约地址同步给前后端;
- 用
.env或配置中心统一管理地址。
坑 4:前端能查余额,运营后台却查不到流水
原因一般不是链上没数据,而是监听器没做断点续扫。
如果监听器掉线,直接只靠 contract.on(),中间那段事件就丢了。
更稳妥的做法
- 记录上次同步到的 block height;
- 重启后用
queryFilter()补扫历史事件; - 再切换到实时订阅。
坑 5:Gas 估算失败
如果 earn/spend 在模拟执行阶段就会 revert,钱包或 ethers 会提示 Gas estimation failed。
常见触发条件
- 扣积分时余额不足;
- 操作者没有权限;
- 地址传成了空地址;
- amount 为 0。
建议
对业务参数先做链下校验,减少无意义交易。
安全/性能最佳实践
这一部分非常重要。会员积分虽然不像大额资金池那样危险,但一旦涉及兑换权益、折扣、等级晋升,同样具备真实经济价值。
1. 钱包登录必须防重放
最基本的防重放要求:
- nonce 一次一用;
- nonce 设置过期时间;
- 登录 message 带上域名、时间、用途说明;
- 最好采用 SIWE(Sign-In with Ethereum) 标准格式。
如果你只让用户签“登录系统”四个字,那被截获后极易重放。
2. 合约最小权限原则
不要让业务后端直接持有 admin 权限做所有事。
比较合理的拆分:
DEFAULT_ADMIN_ROLE:仅用于配置与授权;OPERATOR_ROLE:只负责 earn/spend;- 多签钱包持有 admin;
- 热钱包只持有 operator。
我个人非常建议:管理员权限上多签,运营写入用受限热钱包。
3. 后端接口也要做 RBAC
很多团队犯的一个错误是:
“反正链上有权限控制,后端无所谓。”
这不对。
后端仍然应该做:
- 管理员角色校验;
- 审计日志;
- 请求幂等;
- 风控限流。
否则你只是把“链上最终失败”当成安全策略,体验和成本都很差。
4. 事件驱动而不是轮询余额
如果你有运营报表、等级计算、用户画像,不要每次都上链扫余额。
更好的办法:
- 监听
PointsEarned/PointsSpent事件; - 增量更新数据库;
- 查询优先走索引库;
- 对账时再和链上余额抽样核验。
5. 为积分系统设计幂等键
比如订单支付成功发积分时,必须防止消息重复投递导致重复发放。
推荐做法:
- 每个业务动作生成唯一
bizId; - 后端落库去重;
- 合约层如果需要更强一致性,也可扩展
processedBizIds。
本文示例里没写这个字段,但在真实生产环境中很常见。
6. 预留升级路径,但别滥用可升级
如果你一开始就上 UUPS / Transparent Proxy,也可以,但要考虑:
- 升级治理复杂度;
- 审计成本;
- 管理员权限更敏感。
如果系统还在早期验证期,我建议:
- 先用简单不可升级版本验证业务;
- 合约地址通过注册表或配置可替换;
- 真正确认模型后再上升级代理。
7. 积分是否允许转让,要尽早定死
这是业务语义的核心分叉点:
- 不可转让:更像会员权益;
- 可转让:更像资产代币。
一旦上线再改,用户预期会彻底变掉。
如果你的业务不是明确要做“可流通激励”,我建议默认不可转让。
一个更稳的生产化演进路线
如果你准备把这套 MVP 逐步推到生产,我建议按下面顺序演进:
stateDiagram-v2
[*] --> MVP
MVP --> Indexing
Indexing --> RBAC
RBAC --> BatchSettlement
BatchSettlement --> MultiSigAdmin
MultiSigAdmin --> Monitoring
Monitoring --> Production
MVP: 钱包登录 + 基础积分合约
Indexing: 事件索引与运营查询
RBAC: 管理后台角色细分
BatchSettlement: 批量结算降成本
MultiSigAdmin: 多签管理高权限
Monitoring: 告警、对账、审计
Production: 生产可用
我建议的里程碑
第一阶段:先打通主流程
- 钱包登录
- 发积分
- 扣积分
- 查余额
第二阶段:补上运营能力
- 事件同步
- 流水查询
- 管理后台
第三阶段:补上工程化能力
- 幂等
- 告警
- 权限治理
- 批量上链优化
可执行验证清单
如果你准备自己跑一遍,可以按下面顺序验证:
登录验证
- 钱包可正常连接
- 可获取 nonce
- 签名成功
- 后端验签成功并返回 JWT
- nonce 复用会失败
合约验证
- 合约部署成功
- 可查询初始余额为 0
- earn 后余额增加
- spend 后余额减少
- 超额 spend 会 revert
同步验证
- 事件监听器可收到发积分事件
- 事件监听器可收到扣积分事件
- 重启监听器后可补扫历史区块
总结
去中心化会员积分系统并不是“把传统积分搬到链上”这么简单,它本质上是一种身份、账本和业务服务重新分层的架构设计:
- 钱包负责身份证明
- 智能合约负责核心积分状态
- 后端负责规则编排、风控、索引和运营支持
如果你问我,中级开发者最应该抓住什么,我会给三个非常具体的建议:
-
先明确哪些状态必须上链,别什么都往链上塞
积分余额和关键流水适合上链,nonce、报表、活动规则不适合。 -
先做自定义积分合约,不要急着 ERC20 化
会员积分的重点是可控、可核销、可验证,不一定是可交易。 -
把钱包登录和合约权限当成两个系统来设计
登录证明“你是谁”,权限决定“你能做什么”,不要混为一谈。
最后再给一个边界判断:
如果你的业务是高频、低价值、强实时积分,那完全链上未必划算;
如果你的业务是强调可信归属、跨平台协同、可验证权益,那这套架构就很值得做。
这也是 Web3 在会员体系里最有现实意义的地方:
不是为了“上链而上链”,而是让用户真正拥有一部分可验证的数字权益。