深度拆解 Grass 模式:基于 EIP-712 与 DePIN 架构的奖励分发系统实现

  • 木西
  • 发布于 10小时前
  • 阅读 51

前言2026年的Web3赛道中,以Grass和Dawn为代表的DePIN(去中心化物理基础设施网络)项目,开创了“带宽即挖矿”的全新范式。这类项目的核心技术难点,并非数据采集本身,而是如何安全、低成本地将链下贡献转化为链上代币奖励。本文将从架构设计到智能合约实现,完整还原一套

前言

2026 年的 Web3 赛道中,以 GrassDawn 为代表的 DePIN(去中心化物理基础设施网络)项目,开创了 “带宽即挖矿” 的全新范式。这类项目的核心技术难点,并非数据采集本身,而是如何安全、低成本地将链下贡献转化为链上代币奖励。本文将从架构设计到智能合约实现,完整还原一套工业级的 Grass 奖励分发系统。特此声明:本文不构成任何项目推荐与投资建议,仅对行业主流模式与核心运行逻辑做技术拆解与原理分析。

一、Grass概述

1. 项目本质

Solana 链上 DePIN+AI 项目,核心是用户共享家庭闲置带宽,为 AI 企业提供分布式、合规的数据采集服务,用户以带宽贡献获取 $GRASS 代币奖励

2. 核心亮点

  • 商业模式清晰:98% 收入来自 AI 模型训练数据,客户群体明确

  • 资方优质:总融资 450 万美元,种子轮由 Polychain Capital、Tribe Capital 领投

  • 技术可靠:采用 zk-SNARK 技术,保障数据真实性与可追溯性

    3. 关键风险

  • 合规风险:严打多账号刷量、非住宅 IP 违规操作,违规会取消奖励资格

  • 行业依赖风险:收入高度依赖 AI 训练数据市场,行业波动影响生态收益

  • 代币与参与风险:$GRASS 价格受市场波动影响,空投、解锁规则以官方公告为准;节点需稳定在线,产生电费成本

    二、 核心架构:链下计算,链上验证

Grass 的运作并非全过程上链,其架构分为三个关键层级:

  1. 感知层 (链下) :用户运行浏览器插件,贡献闲置带宽。
  2. 验证层 (后端) :项目方服务器(或验证节点)统计用户贡献,将其转化为累计积分(Total Earned)。
  3. 结算层 (合约) :用户发起提现请求,后端生成 EIP-712 签名凭证,合约验证签名并拨付代币。

三、 工业级合约实现 (Solidity 0.8.24)

基于 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());
    }
}

GrassDistributor合约

// 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)
  };
}

五、 自动化测试 (Viem + Node:Assert)

测试用例

  1. 正常领取:验证 totalEarned 模式下余额增量是否正确。
  2. 防重放校验:确保旧签名(Nonce 0)在 Nonce 已变为 1 时无法再次被合约接受。
  3. 权限校验:普通用户伪造签名应被 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 赛道的入场券。
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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