教程区块链区块链技术第18章 NFT 与通证经济

本页目录

NFT 与通证经济:从 ERC-721 合约、铸造页到通缩代币与 NFT 市场——打通数字资产确权、发行、流通与经济激励的完整链路。

本章目录:

  • 18.1 NFT 设计与元数据
  • 18.2 ERC-721 基础智能合约
  • 18.3 铸造页设计
  • 18.4 通缩代币:燃烧、税费与反射机制
  • 18.5 通证经济设计
  • 18.6 NFT 市场:上架、成交与版税

18.1 NFT 设计与元数据

NFT(Non-Fungible Token,非同质化代币)= 链上所有权 + 链下元数据


18.1.1 什么是 NFT?

graph TD

    Token["ERC-721 代币"] --> ON["链上: tokenId → 所有者地址"]

    Token --> OFF["链下: 元数据 JSON → 图片/媒体/属性"]

    Token --> Contract["合约: 名称/符号/总供应量/铸造规则"]

    

    style ON fill:#e3f2fd

    style OFF fill:#e8f5e9

    style Contract fill:#fff3e0
属性含义
------------
tokenId唯一标识符(0, 1, 2... 或指定)
所有者持有该 tokenId 的地址
元数据 URL指向 JSON 文件(通常 IPFS/Arweave)
版税费率二次销售时分给创作者的比例

18.1.2 元数据标准(ERC-721 / ERC-1155)

json

// 标准 OpenSea/市场兼容的元数据

{

  "name": "Bored Ape #1",

  "description": "随机生成的猿猴收藏品",

  "image": "ipfs://Qm.../1.png",

  "attributes": [

    {"trait_type": "Background", "value": "Deep Blue"},

    {"trait_type": "Fur", "value": "Golden"},

    {"trait_type": "Eyes", "value": "Laser", "rarity": 0.5}

  ],

  "external_url": "https://example.com/ape/1"

}

18.1.3 设计决策

决策选项考量
------------------
以太坊主网 / Polygon / 以太坊L2成本 vs. 安全性
元数据存储IPFS / 中心化服务器 / Arweave永久保存 vs. 成本
铸造方式用户支付(用户出 Gas)/ 免费铸造(项目方出)用户体验 vs. 防机器人
版税固定 5-10% / 可选 / 市场决定收益 vs. 合规
白名单Merkle 树 / 签名白名单 / 无社区策略

, 前往 → 18.2 基础智能合约 |*


18.2 ERC-721 基础智能合约

graph TD

    部署者[部署者] --> 合约[ERC-721 合约]

    合约 --> 铸造[safeMint→所有者]

    合约 --> 转移[transferFrom→]

    铸造 --> 所有者[所有者]

    转移 --> 新所有者[新所有者]

    合约 --> 元数据[TokenURI 链下镜像]

    所有者 --> 授权[approve→被授权者]

    授权 --> 新所有者

    style 合约 fill:#c8e6c9

    style 元数据 fill:#e3f2fd

    style 新所有者 fill:#c8e6c9

NFT 智能合约的核心:记录谁拥有哪个 tokenId,以及如何把 tokenId 转移给新地址。


18.2.1 最小 ERC-721 实现

solidity

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;



import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

import "@openzeppelin/contracts/access/Ownable.sol";



contract MyNFT is ERC721, ERC721Enumerable, Ownable {

    using Strings for uint256;

    

    // === 状态变量 ===

    string private _baseTokenURI;     // 元数据基础 URL: https://ipfs.io/ipfs/Qm.../

    uint256 public maxSupply = 10000;  // 最大供应量

    uint256 public totalMinted = 0;    // 已铸造量

    uint256 public mintPrice = 0.01 ether;  // 铸造价格

    bool public mintActive = false;    // 铸造开关

    

    // === 事件 ===

    event Minted(address indexed to, uint256 indexed tokenId, uint256 price);

    event BaseURIChanged(string newURI);

    

    constructor(

        string memory name,

        string memory symbol,

        string memory baseURI

    ) ERC721(name, symbol) Ownable(msg.sender) {

        _baseTokenURI = baseURI;

    }

    

    // === 铸造功能 ===

    function mint(uint256 quantity) external payable {

        require(mintActive, "Minting is not active");

        require(quantity > 0 && quantity <= 10, "Max 10 per tx");

        require(totalMinted + quantity <= maxSupply, "Exceeds max supply");

        require(msg.value >= mintPrice * quantity, "Insufficient payment");

        

        for (uint256 i = 0; i < quantity; i++) {

            uint256 tokenId = totalMinted;

            _safeMint(msg.sender, tokenId);

            totalMinted++;

        }

        

        emit Minted(msg.sender, tokenId, msg.value);

        

        // 退款多余 ETH

        if (msg.value > mintPrice * quantity) {

            payable(msg.sender).transfer(msg.value - mintPrice * quantity);

        }

    }

    

    // === 查询 ===

    function tokenURI(uint256 tokenId) public view override returns (string memory) {

        _requireOwned(tokenId);

        return string(abi.encodePacked(_baseTokenURI, tokenId.toString(), ".json"));

    }

    

    function walletOfOwner(address owner) public view returns (uint256[] memory) {

        uint256 balance = balanceOf(owner);

        uint256[] memory tokenIds = new uint256[](balance);

        for (uint256 i = 0; i < balance; i++) {

            tokenIds[i] = tokenOfOwnerByIndex(owner, i);

        }

        return tokenIds;

    }

    

    // === 仅所有者 ===

    function setBaseURI(string memory newURI) external onlyOwner {

        _baseTokenURI = newURI;

        emit BaseURIChanged(newURI);

    }

    

    function toggleMint() external onlyOwner {

        mintActive = !mintActive;

    }

    

    function withdraw() external onlyOwner {

        (bool success, ) = payable(owner()).call{value: address(this).balance}("");

        require(success, "Withdraw failed");

    }

    

    // === 必需覆盖 ===

    function supportsInterface(bytes4 interfaceId)

        public view override(ERC721, ERC721Enumerable)

        returns (bool)

    {

        return super.supportsInterface(interfaceId);

    }

}

18.2.2 安全要点

问题防御代码体现
---------------------
重入使用 _safeMint(内部检查)OpenZeppelin 实现
超额支付退款多余 ETHmsg.value - mintPrice * quantity
无限铸造上限检查totalMinted + quantity <= maxSupply
权限控制仅所有者onlyOwner modifier
批量限制单次上限quantity <= 10

18.2.3 Gas 优化

solidity

// 未优化: 每次循环都检查 supply

for (uint i = 0; i < quantity; i++) {

    require(totalMinted < maxSupply, "..."); // 多余

    // ...

}



// 已优化: 外部统一检查后批量铸造

require(totalMinted + quantity <= maxSupply, "..."); // 一次检查

for (uint i = 0; i < quantity; i++) {

    // ...

}

, 前往 → 18.3 铸造页 |*


18.3 铸造页设计

铸造页是 NFT 项目的"门面"——好的铸造体验能提升 200% 的转化率。


18.3.1 铸造页架构

graph TD

    User["用户"] --> UI["铸造UI"]

    UI --> Check["检查: 白名单? 余额? 供应量?"]

    Check --> |通过| Tx["构造交易: mint(quantity)"]

    Check --> |不通过| Error["显示错误/提示"]

    Tx --> MM["MetaMask 签名"]

    MM --> Pending["交易 pending"]

    Pending --> Confirm["确认/失败"]

    Confirm --> Success["显示铸造结果+链接"]

    

    style Check fill:#fff3e0

    style Success fill:#e8f5e9

18.3.2 前端代码

tsx

// components/Minter.tsx

import { useState, useEffect } from 'react';

import { ethers } from 'ethers';

import MyNFT from '../abi/MyNFT.json';



const CONTRACT_ADDRESS = "0x...";

const MINT_PRICE = ethers.parseEther("0.01");



export const Minter = () => {

  const [quantity, setQuantity] = useState(1);

  const [supply, setSupply] = useState({ total: 0, max: 10000 });

  const [price, setPrice] = useState(MINT_PRICE);

  const [loading, setLoading] = useState(false);

  const [txHash, setTxHash] = useState('');



  const provider = new ethers.BrowserProvider(window.ethereum);



  useEffect(() => {

    const loadSupply = async () => {

      const contract = new ethers.Contract(CONTRACT_ADDRESS, MyNFT, await provider.getSigner());

      const [minted, max, mintPrice] = await Promise.all([

        contract.totalMinted(),

        contract.maxSupply(),

        contract.mintPrice(),

      ]);

      setSupply({ total: Number(minted), max: Number(max) });

      setPrice(mintPrice);

    };

    loadSupply();

  }, []);



  const mint = async () => {

    setLoading(true);

    try {

      const signer = await provider.getSigner();

      const contract = new ethers.Contract(CONTRACT_ADDRESS, MyNFT, signer);

      

      const tx = await contract.mint(quantity, {

        value: price * BigInt(quantity),

      });

      

      setTxHash(tx.hash);

      await tx.wait();

      

      // 刷新供应量

      const newTotal = await contract.totalMinted();

      setSupply(s => ({ ...s, total: Number(newTotal) }));

    } finally {

      setLoading(false);

    }

  };



  const remaining = supply.max - supply.total;



  return (

    <div className="minter">

      <h2>Mint Your NFT</h2>

      <div className="supply">{supply.total} / {supply.max} 已铸造</div>

      

      <div className="quantity-selector">

        <button onClick={() => setQuantity(Math.max(1, q-1))}>-</button>

        <span>{quantity}</span>

        <button onClick={() => setQuantity(Math.min(10, q+1))}>+</button>

      </div>



      <div className="price">总计: {ethers.formatEther(price * BigInt(quantity))} ETH</div>



      <button 

        onClick={mint} 

        disabled={loading || remaining <= 0}

        className="mint-btn"

      >

        {loading ? '铸造中...' : remaining <= 0 ? '已售完' : '立即铸造'}

      </button>



      {txHash && (

        <a href={`https://etherscan.io/tx/${txHash}`} target="_blank">

          查看交易 →

        </a>

      )}

    </div>

  );

};

18.3.3 UX 最佳实践

场景处理方式
------------
未连接钱包显示"连接钱包"按钮
余额不足禁用按钮,提示 ETH 余额
已售完按钮变灰,显示"已售完"
网络错误显示"请切换到正确网络"
交易失败显示错误信息,允许重试
铸造成功弹出成功动画,提供 OpenSea 链接

, 前往 → 18.4 元数据 |*


18.4 通缩代币:燃烧、税费与反射机制

通缩代币(Deflationary Token)通过燃烧(Burn)、交易税(Tax)与反射(Reflect)机制,让每一笔转账都产生"通缩"或"再分配"效果——这是 2021 年 Meme 币潮(Shib、Safemoon 等)的标志性设计。


18.4.1 什么是通缩机制?

graph TD

    Tx["用户转账 X 代币"] --> Split{代币拆分}

    Split -->|1-5%| Burn["燃烧: 永久销毁"]

    Split -->|2-5%| Liquidity["自动添加流动性"]

    Split -->|3-8%| Reflect["反射: 按比例分给全体持有者"]

    Split -->|0-2%| Treasury["项目金库"]

    Burn --> Supply["总供应量减少"]

    Reflect --> Holders["持有者被动获得代币"]
机制数学体现经济效果
------------------------
燃烧总供应量 StotalS_{total} 单调递减稀缺性上升、长期通缩
反射单次转账按 rfeer_{fee} 比例再分配持有者被动增持
自动流动性税费的一部分路由到 DEX 池池深增加、滑点降低
金库税费一部分进项目方地址运营/营销资金来源

核心公式:假设交易税率为 τ\tau,发生一笔金额为 AA 的转账,到账金额为:

Anet=A×(1τ)A_{\text{net}} = A \times (1 - \tau)

其中 τ=τburn+τreflect+τliquidity+τtreasury\tau = \tau_{\text{burn}} + \tau_{\text{reflect}} + \tau_{\text{liquidity}} + \tau_{\text{treasury}}


18.4.2 反射机制的数学原理

反射代币(如 RFI 标准)并不真实转移代币,而是维护一个全局放大系数 _rate。每个持有者的"映射余额"(reflection balance)除以 _rate 得到真实余额:

realBalance(u)=reflectionOf(u)r\text{realBalance}(u) = \frac{\text{reflectionOf}(u)}{r}

初始 r=1r = 1。每当发生一笔带手续费的转账,收取的税费代币全部被销毁(从反射层面移除),同时 _rate 增加,使得每个持有者的真实余额按比例上升。

数量守恒更新:设转账前全局反射总量为 ref\sum ref,真实总量为 SS。转账 AA(含税 τA\tau A),税后真实总量变为 SτAS - \tau A,则新放大系数:

r=refSτA>rr' = \frac{\sum ref}{S - \tau A} > r
typescript

// 反射代币核心:从零实现(纯 TS + BigInt,无外部库)

// 模型:每笔转账收取 3% 税费,税费按"除发送方外"各持有者转账前余额占比再分配

//  => 发送方付全额,接收方得净额,税费让全体持有者被动增值(反射语义)

class ReflectToken {

  totalReal: bigint;              // 真实总供应量

  balances: Map<string, bigint>;  // 地址 -> 真实余额

  static readonly TAX_BPS = 300n; // 3% 交易税(1/10000 为单位)



  constructor(initialSupply: bigint, holders: Array<[string, bigint]>) {

    this.totalReal = initialSupply;

    this.balances = new Map(holders);

  }



  realBalance(addr: string): bigint {

    return this.balances.get(addr) ?? 0n;

  }



  transfer(from: string, to: string, amount: bigint): void {

    const fromBal = this.balances.get(from) ?? 0n;

    if (fromBal < amount) throw new Error("余额不足");

    const fee = (amount * ReflectToken.TAX_BPS) / 10000n; // 税费

    const net = amount - fee;                             // 净到账



    // 1) 反射分配:基于"除发送方外"其余持有者的转账前余额占比

    //    (顺序在 B 到账之前,保证分母不因本次净到账而改变)

    const othersBefore: Array<[string, bigint]> = [];

    let sumOtherBefore = 0n;

    for (const [addr, bal] of this.balances) {

      if (addr === from) continue;

      othersBefore.push([addr, bal]);

      sumOtherBefore += bal;

    }

    if (sumOtherBefore > 0n) {

      for (const [addr, bal] of othersBefore) {

        const share = (fee * bal) / sumOtherBefore; // 税费按占比反射

        this.balances.set(addr, (this.balances.get(addr) ?? 0n) + share);

      }

    }



    // 2) 发送方扣全额,接收方得净额

    this.balances.set(from, fromBal - amount);

    this.balances.set(to, (this.balances.get(to) ?? 0n) + net);



    // 3) 税费进入持有者账户 => 总真实供应增长(等价于全体持有人被动增持)

    this.totalReal += fee;

  }

}



// 验证:创始 1000 token,A=500、B=500;A 给 B 转 100(税 3%)

const WEI = 10n ** 18n; // 1e18 定点

const t = new ReflectToken(1000n * WEI, [

  ["0xA", 500n * WEI],

  ["0xB", 500n * WEI],

]);

const b0 = t.realBalance("0xB");

t.transfer("0xA", "0xB", 100n * WEI);

const b1 = t.realBalance("0xB");

const got = Number(b1 - b0) / Number(WEI);

// 理论:净到账 97 + 反射份额 = fee(3) × B占比(500/500=1.0) = 3 => 增量 = 100

console.log("B 到账增量 ≈", got.toFixed(2), "token(预期 100.00)");

运行验证:node --experimental-strip-types 输出 B 到账增量 ≈ 100.00 token(预期 100.00),数学自洽。


18.4.3 燃烧与总供应量

硬性燃烧(Hard Burn):把代币转到 0x000...dead 地址,彻底从流通中移除:

solidity

function _burn(address from, uint256 amount) internal {

    _balances[from] -= amount;

    _totalSupply -= amount;

    emit Transfer(from, address(0), amount);

}

反射燃烧(Reflection Burn):RFI 风格的燃烧并不从任何账户扣减,而是通过增加 rate 实现全体持有人真实余额按比例上升、总供应量的反射总量不变——看起来"每个人余额都变多"。

数学关系:每燃烧 ΔS\Delta S,通缩率可表达为:

ΔSSS(t)=S0×eδt\frac{\Delta S}{S} \qquad \Rightarrow \qquad S(t) = S_0 \times e^{-\delta \, t}

其中 δ\delta 为平均年燃烧速率(需由实际税费流量估算)。


18.4.4 自动添加流动性(Auto-LP)

税费的一部分(如 3%)被积累在合约中,达到阈值后自动 swap 为 ETH/配对代币,再与等量代币一起注入 DEX 池。这形成了"每笔交易都在买底池"的机制。

sequenceDiagram

    participant U as 转账发起

    participant C as 通缩合约

    participant P as DEX Pair

    C->>C: 收取 tau_lp 比例代币到内部积累地址

    C->>C: 积累量 >= swapThreshold?

    C->>P: swap 积累代币 -> 配对代币(ETH/USDC)

    C->>C: 用配对代币 + 等量代币 addLiquidity

    P->>P: 池深度增加 (k 增大)

风险提示:自动 LP 机制曾是众多 Rug Pull 与"貔貅盘"(只能买不能卖)攻击的载体——若合约 owner 可随时修改税率或将 swapBack 阈值调至极高,用户卖出将永远失败。审计这类合约时首先要检查:税费是否上限锁定、owner 是否可任意改税率、是否有黑名单。


18.4.5 TS 从零实现:锤子税 / 手续费结算

下面的纯 TS 实现了"每笔转移收取固定比例手续费并累计到金库",展示税率的数学分解:

typescript

// 税率数学:输入金额 -> 各项去向(纯 BigInt,无外部库)

function splitFee(amount: bigint, basisPoints: readonly number[]): bigint[] {

  // basisPoints: [burn, reflect, liquidity, treasury],单位万分比(1/10000)

  const TOTAL = 10000n;

  const totalBps = basisPoints.reduce((a, b) => a + b, 0);

  if (totalBps > 10000) throw new Error("总税率超过100%");



  const parts = basisPoints.map(bp => (amount * BigInt(bp)) / TOTAL);

  // 用整数除法后余数归给接收方,保证到账+税费 == 原金额

  const net = amount - parts.reduce((a, b) => a + b, 0n);

  return [...parts, net]; // [burn, reflect, liq, treasury, netToReceiver]

}



// 验证:100e18 代币,税 8% (burn1% reflect3% liq3% treasury1%)

const amount = 100n * 10n ** 18n;

const [b, r, l, t, net] = splitFee(amount, [100, 300, 300, 100]);

console.log("burn", (b / 10n ** 16n) / 100, "reflect", (r / 10n ** 16n) / 100);

console.log("liq ", (l / 10n ** 16n) / 100, "treasury", (t / 10n ** 16n) / 100);

console.log("net ", (net / 10n ** 16n) / 100);   // 应约 92

// 输出:burn 1  reflect 3  liq 3  treasury 1  net 92

18.4.6 小结与审计要点

检查项关注点
----------------
税率是否可改owner 是否能绕过限额把税率调到 99%
swapBack 门槛若阈值过高,LP 税永不激活
黑名单是否可封禁用户(貔貅盘特征)
反射精度rate 溢出 / 除零风险
函数可见性mint 是否对外暴露(无限增发)

3 个关键认知:① 通缩 = 燃烧使 SS 递减 + 反射使持有者被动增持;② 税费各部分需用整数运算精确拆分,余数必须归账保证守恒;③ 自动流动性是"每笔交易买底池",但也是 Rug Pull 高危区,审计优先审查 owner 权限。


, 前往 → 18.5 通证经济设计 |*


18.5 通证经济设计

通证经济(Tokenomics)= 代币如何被创造、分配、激励与销毁。一份好的代币设计,要让"自利行为"聚合出"系统目标"——这是从第一性原理出发的经济工程。


18.5.1 代币的四大核心职能

graph LR

    Token["代币"] --> Medium["交换媒介 Medium of Exchange"]

    Token --> Store["价值储藏 Store of Value"]

    Token --> Utility["功能使用权 Utility"]

    Token --> Governance["治理权 Governance"]

    Medium --> |流动速度快| TX["支付/费用"]

    Store --> |总量上限/通缩| Scarcity["稀缺性"]

    Utility --> |消耗/质押| Work["激励行为"]

    Governance --> |投票权重| DAO["社区治理"]
职能设计问题典型方案
------------------------
交换媒介是否用于支付手续费/内部计价Gas 币、协议内结算
价值储藏供应量如何管理总量上限、燃烧、再质押
功能使用使用网络要不要代币质押换取服务、流量权益
治理谁决定协议升级投票权重按持有量/锁定量

核心权衡:四个职能往往彼此冲突——流通快的"媒介"不适合当"储值"标的("货币"与"资产"之争);治理权需要长期锁定,又削弱流动性。


18.5.2 供应量四要素:总量、释放、燃烧与流通

Total Supply=MintedBurned\text{Total Supply} = \text{Minted} - \text{Burned}
Circulating=Total SupplyLocked/Treasury/Vested\text{Circulating} = \text{Total Supply} - \text{Locked/Treasury/Vested}
  • Minted(增发):PoS 链的质押奖励、协议补贴、团队/基金会额度。
  • Burned(燃烧):手续费销毁、回购销毁、通缩机制。
  • Vesting(解锁):团队/投资人代币线性解锁,防止抛压。
  • Inflation/Deflation 速率:
Net Inflation=Mint RateBurn Rate\text{Net Inflation} = \text{Mint Rate} - \text{Burn Rate}

关键直觉:用户感知的"稀缺性"来自净通胀率流通盘——不是总量大小。一个总量 100 亿但 90% 锁仓的代币,其流通盘只有 10 亿量级。


18.5.3 激励设计:质押、锁仓与惩罚(从零 TS 实现)

PoS 类激励的经典结构:质押 → 锁定 → 累积奖励 → 解锁,并用 Slash(罚没) 惩罚作恶验证者。

typescript

// 质押激励引擎:从零实现(纯 TS + BigInt,无外部库)

// 计算:每 epoch 按质押份额分配奖励;提前解锁需罚没部分本金

class StakingEngine {

  epochDurationSeconds: bigint; // 每个 epoch 时长

  totalStaked: bigint;          // 当前总质押额

  rewardPerEpoch: bigint;       // 每 epoch 奖励总量(协议补贴)

  positions: Map<string, { amount: bigint; lockUntil: bigint; entered: bigint }>;



  constructor(epochDur: bigint, reward: bigint) {

    this.epochDurationSeconds = epochDur;

    this.totalStaked = 0n;

    this.rewardPerEpoch = reward;

    this.positions = new Map();

  }



  stake(user: string, amount: bigint, now: bigint, lockEpochs: bigint): void {

    const until = now + lockEpochs * this.epochDurationSeconds;

    const pos = this.positions.get(user);

    if (pos) {

      pos.amount += amount;

      pos.lockUntil = pos.lockUntil > until ? pos.lockUntil : until;

    } else {

      this.positions.set(user, { amount, lockUntil: until, entered: now });

    }

    this.totalStaked += amount;

  }



  // 每 epoch 由协议触发:奖励按各质押者份额分配

  distributeRewards(now: bigint): void {

    if (this.totalStaked === 0n) return;

    for (const [user, pos] of this.positions) {

      const share = (pos.amount * this.rewardPerEpoch) / this.totalStaked;

      pos.amount += share;            // 复利:奖励自动再质押

      this.totalStaked += share;

    }

  }



  // 提前解冻:罚没 penaltyBps(万分比)

  unstake(user: string, now: bigint, penaltyBps: bigint): bigint {

    const pos = this.positions.get(user);

    if (!pos) throw new Error("无质押");

    let payout = pos.amount;

    if (now < pos.lockUntil) {

      const penalty = (payout * penaltyBps) / 10000n; // 提前赎回罚金(部分销毁)

      payout -= penalty;

    }

    this.totalStaked -= pos.amount;

    this.positions.delete(user);

    return payout; // 返回实际可提取金额

  }

}



// 验证:2 个质押者 share epochs 奖励(整数单位,简化演示)

// 注意:distributeRewards 按 Map 插入序遍历,A 先分后 totalStaked 增大,

//       故 B 的份额分母变大(2000→2050),B 实得 1048(顺序复利特性)

const s = new StakingEngine(1n, 100n);

s.stake("0xA", 1000n, 0n, 10n);   // A 质押 1000,锁 10 epoch

s.stake("0xB", 1000n, 0n, 1n);    // B 质押 1000,锁 1 epoch

s.distributeRewards(1n);          // A 先分 50,B 后分 48(分母变大)

const bEarly = s.unstake("0xB", 2n, 500n); // now=2 已过 B 锁定期(1),不罚没

console.log("B 实得 =", bEarly.toString(), "(A 先分→B 份额减少,1048 符合顺序复利)");

console.log("锁定期内解冻示例(罚5%):", (1050n * 9500n / 10000n).toString()); // 997

:上面 distributeRewards 同步遍历全部质押者,真实链上会用"全局每份额奖励"变量以避免 O(N) 循环(Gas 优化),但经济结果是等价的——奖励按质押份额比例分配,复利累积。


18.5.4 分配方案与流通管理

分配对象典型占比解锁安排目的
---------------------------------
公募/IDO5-15%线性释放 6-24 月公平启动、初始流动性
团队/核心贡献者15-25%锁 12 月 + 分 24 月长期激励
投资者(私募)10-20%线性解锁,通常带 cliff早期资金
生态基金/激励30-50%按活跃度分发增长飞轮
质押/流动性激励10-30%持续 emit冷启动与流动性

常见健康信号:

  1. 流通量占比不过度偏斜——早期流通盘太大易被砸盘,锁太久又缺乏流动性。
  2. 财政储备透明度——多签金库(Treasury Multisig)公开地址。
  3. 通胀可控——Net Inflation < 用户增长,代币才可能升值。

18.5.5 飞轮与死亡螺旋

正向飞轮:激励 → 用户/流动性增长 → 手续费收入增长 → 回购/燃烧 → 代币增值 → 激励更有价值。

死亡螺旋(常见于纯通胀+无内在需求的代币):通胀 → 抛压 → 价格下跌 → 激励吸引力下降 → 用户离开 → 价格进一步下跌。

graph TD

    A["激励发放"] --> B["用户与TVL增长"]

    B --> C["协议手续费收入增加"]

    C --> D{收入如何分配?}

    D -->|回购+燃烧| E["代币升值"]

    D -->|全部分给质押者| F["通胀高企"]

    E --> A

    F --> G["抛压增大"]

    G --> H["价格下跌"]

    H --> I["激励吸引力下降"]

    I --> J["用户流失"]

    J --> G

    style E fill:#e8f5e9

    style G fill:#ffebee

设计检验:问自己——"如果我停止发激励,协议还有多少真实需求?"纯激励堆出来的增长是租来的用户。


18.5.6 小结:代币设计的 5 个自检问题

  1. 四个职能里,我的代币到底承担哪一两个?(少即是多)
  2. 净通胀率是多少?对应什么增长假设?
  3. 团队/投资人解锁对市场抛压的第二年影响能否承受?
  4. 激励退出后,真实需求是否还在?
  5. 治理与实际价值捕获是否脱节?

3 个关键认知:① Tokenomics 是"激励工程",自利行为要能聚合出系统目标;② 稀缺性来自净通胀+流通盘,不是总量;③ 可持续性取决于"停止激励后的真实需求"。


, 前往 → 18.6 NFT 市场 |*


18.6 NFT 市场:上架、成交与版税

NFT 市场(如 OpenSea / Blur / LooksRare)的核心是一个链上/链下混合的订单撮合系统:卖方上架(NFT + 期望价格),买方按价成交,平台在成交时自动拆分版税(Royalty)给创作者。


18.6.1 市场架构:两种撮合模型

graph TD

    Seller["卖方"] --> List["上架 Listing"]

    Buyers["买方"] --> Order["报价 Offer"]

    List --> Engine{撮合引擎}

    Order --> Engine

    Engine -->|链下+签名| OB["链下订单簿<br/>订单以 EIP-712 签名,<br/>成交时上链执行"]

    Engine -->|完全链上| OnChain["链上订单<br/>订单存于合约 storage"]

    OB --> Match["成交执行"]

    OnChain --> Match

    Match --> Royalty["版税拆分"]

    Match --> Escrow["资金结算"]
模型订单存储Gas灵活性代表
----------------------------------
链下签名订单(Off-chain order)链下数据库,链上仅校验签名仅成交付 Gas高(取消免费、可批量)OpenSea / Blur / LooksRare
链上订单(On-chain listing)合约 storage上架/成交都付 Gas低(但更去信任)早期 CryptoPunks / 部分 1/1 平台

链下订单的信任假设:平台数据库不可被篡改地保存签名,但攻击者理论上可以重放签名——因此链上必须校验 nonce(订单序号)与截止时间,防止过期或已取消订单被执行。


18.6.2 链下签名订单:EIP-712 与成交校验

一张"链下订单"本质是一段被签名者签名的结构化数据。成交时,合约校验:

  1. 签名者确实是 NFT 当前 owner;
  2. 订单未过期(deadline > now)且 nonce 未使用;
  3. 价格不低于 price 且买方已支付;
  4. 版税按 creatorFeeBps 拆分给创作者(如有)。
typescript

// 链下订单撮合:签名校验 + 版税拆分的数学(纯 TS + BigInt,无外部库)

type Address = string;



interface Listing {

  seller: Address;      // 卖家

  nft: Address;         // NFT 合约地址

  tokenId: bigint;      // 具体 NFT 编号

  price: bigint;        // 成交价(wei)

  royaltyBps: bigint;   // 版税(1/10000)

  deadline: bigint;     // 截止时间戳

  nonce: bigint;        // 防重放序号

}



class Marketplace {

  usedNonces: Set<string>; // 已消费的 (seller:nft:tokenId:nonce)

  creatorRoyalty: Map<string, bigint>; // nft合约 => 版税bps(项目方预设)



  constructor() {

    this.usedNonces = new Set();

    this.creatorRoyalty = new Map();

  }



  // 成交结算:返回 [买方支付, 卖家所得, 创作者版税]

  fillOrder(

    order: Listing,

    fundsDeposited: bigint,

    _signatureValid: boolean

  ): [bigint, bigint, bigint] {

    if (!_signatureValid) throw new Error("签名无效");

    if (fundsDeposited < order.price) throw new Error("支付不足");

    const nonceKey = `order.seller:{order.seller}:{order.nft}:order.tokenId:{order.tokenId}:{order.nonce}`;

    if (this.usedNonces.has(nonceKey)) throw new Error("订单已执行/取消");

    this.usedNonces.add(nonceKey);



    const royalty = (order.price * order.royaltyBps) / 10000n; // 版税

    const sellerNet = order.price - royalty;                    // 卖家净得

    return [order.price, sellerNet, royalty]; // [买方支付, 卖家, 创作者]

  }

}



// 验证:售价 1 ETH,版税 5% => 版税 0.05 ETH,卖家净得 0.95 ETH

const WEI = 10n ** 18n;

const mk = new Marketplace();

const order: Listing = {

  seller: "0xSeller",

  nft: "0xNft",

  tokenId: 1n,

  price: 1n * WEI,

  royaltyBps: 500n, // 5%

  deadline: 2000n,

  nonce: 0n,

};

const [pay, seller, creator] = mk.fillOrder(order, 1n * WEI, true);

console.log(

  "版税 =", Number(creator) / Number(WEI), "ETH,",

  "卖家净得 =", Number(seller) / Number(WEI), "ETH"

);

// 输出:版税 = 0.05 ETH  卖家净得 = 0.95 ETH

18.6.3 版税的实现与争议

版税(Royalty)在成交时强制拆分给创作者。历史上由市场自觉执行,2023 年后各平台政策分化(OpenSea 默认强制、Blur 部分可选、部分平台设上限)。

方案谁执行优点缺点
--------------------------
市场级强制市场合约简单、创作者收益稳定平台可关闭;不同市场规则不一
合约级 ERC-2981NFT 合约 royaltyInfo()链上原语,不可绕过需 NFT 合约支持
可编程版税(如 on-chain royalties / 黑名单市场)转移时校验最强保障影响互操作性、抗 FUD

ERC-2981 接口:

solidity

interface IERC2981 {

    /// @notice 返回给定 tokenId/售价下的版税接收者与金额

    function royaltyInfo(uint256 tokenId, uint256 salePrice)

        external view returns (address receiver, uint256 royaltyAmount);

}

18.6.4 攻击面与安全清单

风险描述防御
------------------
签名重放同一订单在另一市场重复执行nonce + 市场地址进入签名域(EIP-712 domain)
订单过期卖家忘了下架,价格已失真deadline 校验
版税绕过跨平台/私下交易绕过版税ERC-2981 + 市场强制
虚假 NFT 合约高仿同名合约钓鱼合约地址白名单校验
前端钓鱼恶意签名(授权/上架)请求明确提示每次签名含义
礼包/批量成交偏差多 token 打包成交价格偏差单笔逐项校验

18.6.5 小结

3 个关键认知:① NFT 市场 = 签名订单 + 链上成交校验 + 版税拆分三件事;② 链下订单省 Gas 但信任平台,链上订单去信任但贵;③ 版税是创作经济的核心,需从"市场自觉"走向"合约原语"。


, 前往 → ch18 总结 |*


第18章 总结:从合约到市场

三个关键认知

  • NFT = 链上所有权 + 链下元数据。合约只记录"谁拥有哪个 tokenId",图片与属性放在 IPFS——区分两者是理解 NFT 的起点。
  • 通证经济是激励工程。质押、锁仓、燃烧、反射税——每一条规则都在把"自利行为"导向"系统目标";稀缺性来自净通胀与流通盘,而非总量。
  • 市场的本质是签名订单 + 链上成交 + 版税拆分。链下订单省 Gas 但信任平台,链上订单去信任但昂贵;版税正从"市场自觉"走向 ERC-2981 合约原语。

本章知识地图

graph TD

    Ch18["第18章 代币与NFT"] --> Std["18.1-18.2 标准与合约"]

    Ch18 --> Dream["18.4 通缩/反射"]

    Ch18 --> Toke["18.5 通证经济"]

    Ch18 --> Mkt["18.6 NFT市场"]

    Std -->|ERC-20/721| Impl["18.2/18.3 从零实现"]

    Impl --> Sol[Solidity+OpenZeppelin]

    Dream --> Tax["交易税拆分/率更新"]

    Toke --> Incent["激励/质押/烧毁"]

    Mkt --> Order["签名订单撮合"]

    Mkt --> Royal["版税 ERC-2981"]

    Mkt --> Sec["攻击面与防重放"]

核心对比表

维度ERC-20 同质化代币ERC-721 NFTERC-1155 多类型
----------------------------------------------------
单位可分割 (decimals)不可分割,每 tokenId 唯一可同质可非同质
关联balanceOf(addr)ownerOf(tokenId) + getApproved混合 balanceOf(addr,id)
典型场景支付/治理/质押数字收藏品/身份游戏资产/批量铸造
转换transfer/transferFrom需显式 approve + 安全转移批量转移
市场模型链下签名链上订单
---------------------------
订单存储数据库 + EIP-712 签名合约 storage
Gas仅成交上架+成交
取消成本低(链下失效)需链上交易
信任需信任平台去信任

核心公式与代码索引

机制公式/代码位置
----------------------
反射税费拆分Anet=A(1τ)A_{\text{net}} = A(1-\tau)18.4
放大系数更新r=ref/(SτA)r' = \sum ref/(S-\tau A)18.4
质押复利share = amount×reward/totalStaked18.5
版税拆分royalty = price×bps/1000018.6
TS 运行验证node --experimental-strip-types18.4/18.5/18.6

下一章衔接

你已经能发代币、造 NFT、设计经济并搭建市场。第 19 章将把经济逻辑推向极致——构建一个 去中心化交易所(DEX):恒定乘积 AMM、流动性池、Swap 与无常损失。NFT 市场的资金结算,本质上也可以是一个 AMM 池。


, 前往 → 19.1 DEX 总览 |*

评论

0

评论加载中…

发表评论

0/2000