前言2026年的Web3赛道中,以Grass和Dawn为代表的DePIN(去中心化物理基础设施网络)项目,开创了“带宽即挖矿”的全新范式。这类项目的核心技术难点,并非数据采集本身,而是如何安全、低成本地将链下贡献转化为链上代币奖励。本文将从架构设计到智能合约实现,完整还原一套
2026 年的 Web3 赛道中,以 Grass 和 Dawn 为代表的 DePIN(去中心化物理基础设施网络)项目,开创了 “带宽即挖矿” 的全新范式。这类项目的核心技术难点,并非数据采集本身,而是如何安全、低成本地将链下贡献转化为链上代币奖励。本文将从架构设计到智能合约实现,完整还原一套工业级的 Grass 奖励分发系统。特此声明:本文不构成任何项目推荐与投资建议,仅对行业主流模式与核心运行逻辑做技术拆解与原理分析。
一、Grass概述
1. 项目本质
Solana 链上 DePIN+AI 项目,核心是用户共享家庭闲置带宽,为 AI 企业提供分布式、合规的数据采集服务,用户以带宽贡献获取 $GRASS 代币奖励
2. 核心亮点
商业模式清晰:98% 收入来自 AI 模型训练数据,客户群体明确
资方优质:总融资 450 万美元,种子轮由 Polychain Capital、Tribe Capital 领投
技术可靠:采用 zk-SNARK 技术,保障数据真实性与可追溯性
合规风险:严打多账号刷量、非住宅 IP 违规操作,违规会取消奖励资格
行业依赖风险:收入高度依赖 AI 训练数据市场,行业波动影响生态收益
代币与参与风险:$GRASS 价格受市场波动影响,空投、解锁规则以官方公告为准;节点需稳定在线,产生电费成本
Grass 的运作并非全过程上链,其架构分为三个关键层级:
基于 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());
}
}
// 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)
};
}
totalEarned 模式下余额增量是否正确。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 赛道的入场券。 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!