深度解析 DAWN:Solana 生态 DePIN 新星的机制实现与合约测试实践

5 阅读6分钟

前言

在 2026 年初的 Web3 领域,DePIN(去中心化物理基础设施网络) 已成为最具增长潜力的赛道之一。作为其中的领军项目,由 Andrena 团队开发的 DAWN (Decentralized Autonomous Wireless Network) 凭借其 “去中心化宽带” 的愿景和超过 4850 万美元的融资背景(Polychain 领投),吸引了全球开发者的关注。本文将从项目核心逻辑出发,只做项目核心运行逻辑拆解,不做项目投资建议,演示如何使用 Solidity 0.8.24 与 OpenZeppelin V5 实现其激励机制,并分享在使用 Viem + Hardhat 进行自动化测试时的避坑指南。

一、 DAWN 的业务本质:带宽即服务

与 Grass 等数据爬虫类 DePIN 不同,DAWN 的目标是成为去中心化 ISP(互联网服务供应商)

  • 核心逻辑:用户通过验证节点(Validator)共享闲置带宽,系统通过“带宽证明”(Proof of Bandwidth)核实贡献。
  • 激励模型:后端服务器定期统计用户贡献的累计总额(Total Earned),并生成 EIP-712 签名。用户凭此签名在链上领取(Claim)代币。

二、 核心机制实现:DawnNetworkToken 合约

基于安全性与 Gas 效率的考虑,DAWN 采用了增量领取机制。合约不记录单次奖励,而是记录用户的“生涯总领金额”,通过差额发放代币。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";

/**
 * @title DawnNetworkToken
 * @dev 模拟 DAWN DePIN 激励合约
 */
contract DawnNetworkToken is ERC20, Ownable, EIP712 {
    using ECDSA for bytes32;

    address public validator; // 官方验证者地址
    mapping(address => uint256) public totalClaimed;

    bytes32 private constant CLAIM_TYPEHASH = keccak256(
        "Claim(address user,uint256 totalAmount)"
    );

    constructor(address _validator) 
        ERC20("DAWN Network", "DAWN") 
        Ownable(msg.sender)
        EIP712("DAWN_Network", "1") 
    {
        validator = _validator;
    }

    // 设置验证节点地址
    function setValidator(address _validator) external onlyOwner {
        validator = _validator;
    }

    /**
     * @notice 领取带宽奖励
     * @param totalAmount 验证者核实的累计奖励总额
     * @param signature 验证者的加密签名
     */
    function claimRewards(uint256 totalAmount, bytes calldata signature) external {
        uint256 claimable = totalAmount - totalClaimed[msg.sender];
        require(claimable > 0, "No new rewards");

        // 验证 EIP-712 签名
        bytes32 structHash = keccak256(abi.encode(CLAIM_TYPEHASH, msg.sender, totalAmount));
        bytes32 hash = _hashTypedDataV4(structHash);
        address signer = ECDSA.recover(hash, signature);

        require(signer == validator, "Invalid validator signature");

        totalClaimed[msg.sender] = totalAmount;
        _mint(msg.sender, claimable);
    }
}

三、 工程化测试实践:Viem + Node:test 避坑指南

测试用例:DAWN DePIN 激励合约深度测试

  • 成功逻辑:验证有效签名并更新领取额度
  • 重放攻击测试:禁止使用同一签名重复领取
  • 安全边界:修改金额后原签名应失效
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { network } from "hardhat"; 
import { parseEther, type Address, getAddress } from "viem";

describe("DAWN DePIN 激励合约深度测试", function () {
    let token: any, dawnDistributor: any;
    let admin: any, user: any, validator: any;
    let vClient: any, pClient: any;

    // EIP-712 类型定义常量化
    const DOMAIN_NAME = "DAWN_Network";
    const VERSION = "1";

    beforeEach(async function () {
        const { viem } = await (network as any).connect();
        vClient = viem;
        [admin, user, validator] = await vClient.getWalletClients();
        pClient = await vClient.getPublicClient();

        // 部署 DAWN 代币与分配器(此处假设为统一合约)
        dawnDistributor = await vClient.deployContract("DawnNetworkToken", [
            validator.account.address
        ]);
    });

    it("成功逻辑:验证有效签名并更新领取额度", async function () {
        const totalAmount = parseEther("1000"); // 验证者核实的用户生涯总收益
        const chainId = await pClient.getChainId();

        const domain = {
            name: DOMAIN_NAME,
            version: VERSION,
            chainId,
            verifyingContract: dawnDistributor.address as Address,
        } as const;

        const types = {
            Claim: [
                { name: 'user', type: 'address' },
                { name: 'totalAmount', type: 'uint256' },
            ],
        } as const;

        // 验证者签名
        const signature = await validator.signTypedData({
            domain,
            types,
            primaryType: 'Claim',
            message: {
                user: user.account.address,
                totalAmount: totalAmount,
            },
        });

        // 执行领取
        const hash = await dawnDistributor.write.claimRewards([totalAmount, signature], { 
            account: user.account 
        });
        await pClient.waitForTransactionReceipt({ hash });

        // 验证余额
        const balance = await dawnDistributor.read.balanceOf([user.account.address]);
        assert.strictEqual(balance, totalAmount, "用户应获得全部初始奖励");

        // 验证累计领取记录
        const claimed = await dawnDistributor.read.totalClaimed([user.account.address]);
        assert.strictEqual(claimed, totalAmount, "合约应记录已领取总额");
    });

it("重放攻击测试:禁止使用同一签名重复领取", async function () {
    const totalAmount = parseEther("100");
    const chainId = await pClient.getChainId();

    const domain = { name: "DAWN_Network", version: "1", chainId, verifyingContract: dawnDistributor.address };
    const types = { Claim: [{ name: 'user', type: 'address' }, { name: 'totalAmount', type: 'uint256' }] };

    const signature = await validator.signTypedData({
        domain,
        types,
        primaryType: 'Claim',
        message: { user: user.account.address, totalAmount }
    });

    // 1. 第一次领取:应当成功
    const hash = await dawnDistributor.write.claimRewards([totalAmount, signature], { account: user.account });
    await pClient.waitForTransactionReceipt({ hash });

    // 2. 第二次领取:使用 simulateContract 捕获具体的合约错误
    await assert.rejects(
        async () => {
            // simulateContract 会执行 eth_call,比直接 write 更容易拿到 revert reason
            await pClient.simulateContract({
                address: dawnDistributor.address,
                abi: dawnDistributor.abi,
                functionName: 'claimRewards',
                args: [totalAmount, signature],
                account: user.account,
            });
        },
        (err: any) => {
            // 打印错误以防万一
            // console.log("Caught Error:", err.message);
            
            // 检查错误信息。在 simulate 模式下,viem 通常能抓到 "No new rewards"
            // 或者检查是否包含 "reverted" 关键字
            return err.message.includes("No new rewards") || err.message.includes("revert");
        },
        "应当因为没有新奖励额度而回滚"
    );
});



    it("安全边界:修改金额后原签名应失效", async function () {
        const totalAmount = parseEther("100");
        const tamperedAmount = parseEther("200"); // 尝试篡改金额
        const chainId = await pClient.getChainId();

        const signature = await validator.signTypedData({
            domain: { name: DOMAIN_NAME, version: VERSION, chainId, verifyingContract: dawnDistributor.address },
            types: { Claim: [{ name: 'user', type: 'address' }, { name: 'totalAmount', type: 'uint256' }] },
            primaryType: 'Claim',
            message: { user: user.account.address, totalAmount }
        });

        await assert.rejects(
            async () => {
                // 提交被篡改的金额 tamperedAmount,但使用 totalAmount 的签名
                await dawnDistributor.write.claimRewards([tamperedAmount, signature], { account: user.account });
            },
            /Invalid validator signature/,
            "签名恢复的地址应与验证者不符"
        );
    });
});

四、部署脚本

// scripts/deploy.js
import { network, artifacts } from "hardhat";
async function main() {
  // 连接网络
  const { viem } = await network.connect({ network: network.name });//指定网络进行链接
  
  // 获取客户端
  const [deployer] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();
 
  const deployerAddress = deployer.account.address;
   console.log("部署者的地址:", deployerAddress);
  // 加载合约
  const DawnNetworkArtifact = await artifacts.readArtifact("DawnNetworkToken");

  // 部署(构造函数参数:recipient, initialOwner)
  const DawnNetworkHash = await deployer.deployContract({
    abi: DawnNetworkArtifact.abi,//获取abi
    bytecode: DawnNetworkArtifact.bytecode,//硬编码
    args: [deployerAddress],//process.env.RECIPIENT, process.env.OWNER
  });

  // 等待确认并打印地址
  const DawnNetworkReceipt = await publicClient.waitForTransactionReceipt({ hash:DawnNetworkHash });
  console.log("合约地址:", DawnNetworkReceipt.contractAddress);
  
}

main().catch(console.error);

五、DAWN 与 Grass 的横向对比

维度Grass (小草)DAWN (黎明)
底层链SolanaSolana
激励方式带宽挖矿积分 (早期)带宽挖矿积分 (当前)
技术核心数据采集与 AI 模型对齐去中心化无线网状网络 (Mesh)
合约挑战高频、小额奖励发放大规模节点的身份校验与带宽证明

六、DAWN 项目的风险提示

  1. 隐私与安全风险:长期运行浏览器插件可能导致个人上网行为被追踪,或因插件漏洞造成数据泄露。
  2. 违约封号风险:家用宽带协议通常禁止“带宽转售”,运行该项目可能触发运营商(如电信、联通)的监测,导致宽带被限速或停机。
  3. 收益不确定性:项目融资虽高,但发币时间(TGE)未知。如果后期参与人数爆棚或币价破发,挂机累积的积分可能并不值钱。
  4. 落地难度大:DAWN 最终需要用户安装物理基站(硬件),这比纯软件挂机难得多。如果硬件普及失败,项目愿景可能无法实现。

一句话建议:用闲置设备“零成本”参与,不要为了它专门升级宽带或购买贵重硬件。

七、 结语

DAWN 不仅仅是一个挂机项目,它是对传统中心化电信运营商(ISP)的有力挑战。对于开发者而言,理解其底层的 EIP-712 签名机制 和 增量领取逻辑,是构建健壮 DePIN 应用的基础。随着 2026 年主网上线的临近,这类结合了硬件贡献与链上激励的模型将成为 Web3 落地的重要标杆。