深度拆解 FWA 核心智能合约:从 Loss-to-Earn 到链上动态定价的实现逻辑

木西 发布于 2026-07-29 15:44 阅读 12

在上一篇文章中,我们探讨了 Fake World Assets (FWA) 项目的核心玩法与生态参与方式。本篇技术文章将聚焦于底层实现,通过对核心智能合约代码的深度拆解,带大家从工程视角剖析 FWA 是如何利用 Solidity 与 OpenZeppelin V5 实现链上扭蛋机制、动态定价以及独特的 Loss-to-Earn(亏损即挖矿) 双向代币激励模型的。

免责声明:本文仅供技术交流与学习参考之用,所有内容均基于公开智能合约代码的拆解与复刻。不构成任何投资建议、财务建议或背书。区块链项目存在极高的技术与资金风险(如智能合约漏洞、流动性风险等),请读者切勿盲目参与。

引言

在上一篇文章中,我们探讨了 Fake World Assets (FWA) 项目的核心玩法与生态参与方式。本篇技术文章将聚焦于底层实现,通过对核心智能合约代码的深度拆解,带大家从工程视角剖析 FWA 是如何利用 Solidity 与 OpenZeppelin V5 实现链上扭蛋机制动态定价以及独特的 Loss-to-Earn(亏损即挖矿) 双向代币激励模型的。

免责声明:本文仅供技术交流与学习参考之用,所有内容均基于公开智能合约代码的拆解与复刻。不构成任何投资建议、财务建议或背书。区块链项目存在极高的技术与资金风险(如智能合约漏洞、流动性风险等),请读者切勿盲目参与。

一、 项目背景与核心创新点

FWA 改变了传统盲盒及资产流转项目的单一博弈模式,引入了 DeFi 与 NFT 结合的全新范式。其核心创新与优点包括:

  1. Loss-to-Earn(亏损即挖矿)基石:用户存入 NFT 并绑定指定的 ETH 底价(Backing Value),不仅能为整个协议提供流动性支撑,还能在存入和资产被抽走时获得原生代币(FWA Token)的生态奖励,从而平抑用户的资产波动风险。
  2. 链上动态定价机制:盲盒抽取的價格并非一成不变,而是根据池内活跃资产的平均背书价值动态浮动(基础价 + 平均背书的特定比例),有效防止了套利空间并维持了协议的资金水位。
  3. 高效的资金分流与安全机制:内置抽水比例(Fee Percentage)、防闪电贷伪随机数索引,以及完善的异常拦截和紧急兜底退回机制(Emergency Withdraw),兼顾了灵活性与安全性。

二、 技术架构

整个系统采用模块化设计,主要由以下三个核心合约协同工作:

  • FwaCoreProtocol.sol:核心业务合约。继承自 OpenZeppelin 的 ReentrancyGuardAccessControl,负责资产存入、扭蛋抽取、动态定价计算以及奖励分发。
  • MyToken.sol:标准的 ERC-721 资产合约(继承自 OZ V5 的 ERC721URIStorageERC721Burnable),作为被存入和抽取的盲盒 NFT 标的。
  • BoykaYuriToken.sol:协议的生态代币合约(继承自 ERC20, ERC20Burnable, ERC20Permit),实现了 mintReward 接口以响应核心协议的代币产出。

三、 智能合约实现源码

1. 核心协议合约:FwaCoreProtocol.sol

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

/**
 * @title FWA Core Protocol Contract
 * @dev 基于 OpenZeppelin V5 + Solidity 0.8.28 复刻的 Fake World Assets 核心逻辑
 * 特色突出:链上 NFT 扭蛋机制、动态定价、Loss-to-Earn 代币双向激励
 */

import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";

// 简化的 FWA 奖励代币接口
interface IFwaToken is IERC20 {
    function mintReward(address to, uint256 amount) external;
}

contract FwaCoreProtocol is ReentrancyGuard, AccessControl {
    using SafeERC20 for IERC20;

    // --- 权限定义 (OZ V5 风格) ---
    bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");

    // --- 数据结构 ---
    struct PoolAsset {
        address nftAddress;
        uint256 tokenId;
        address depositor;
        uint256 ethBacking; // 该资产存入时锚定的底价价值
        bool isActive;
    }

    // --- 状态变量 ---
    IFwaToken public immutable fwaToken; // 协议原生代币机制
    
    PoolAsset[] public pool;            // 扭蛋资产池
    uint256 public activeAssetCount;    // 当前池内可抽取的有效资产数
    
    uint256 public baseDrawPrice;       // 基础抽奖价格
    uint256 public poolFeePercentage;   // 协议抽水比例 (基点, 如 250 = 2.5%)
    uint256 public rewardEmissionRate;  // 每次交互衰减或固定的代币奖励率

    // --- 事件声明 ---
    event AssetDeposited(uint256 indexed assetId, address indexed depositor, address nft, uint256 tokenId, uint256 backingValue);
    event GachaPulled(uint256 indexed assetId, address indexed buyer, address indexed depositor, uint256 pricePaid);
    event PriceUpdated(uint256 newBasePrice);

    // --- 错误控制 (Solidity 0.8+ 自定义错误节省 Gas) ---
    error InvalidPrice();
    error InvalidInput();
    error EmptyPool();
    error TransferFailed();

    constructor(address _fwaToken, uint256 _baseDrawPrice) {
        if(_fwaToken == address(0)) revert InvalidInput();
        fwaToken = IFwaToken(_fwaToken);
        baseDrawPrice = _baseDrawPrice;
        poolFeePercentage = 250; // 默认 2.5%
        rewardEmissionRate = 100 * 10**18; // 基础单次释放量

        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MANAGER_ROLE, msg.sender);
    }

    /**
     * @notice 特色 1: 存入资产提供支撑底价 (Loss-to-Earn 的基石)
     * @param _nftAddress NFT 合约地址
     * @param _tokenId NFT 的 Token ID
     */
    function depositAsset(address _nftAddress, uint256 _tokenId) external payable nonReentrant {
        if(msg.value == 0) revert InvalidPrice();
        
        // 外部资产转入本合约托管
        IERC721(_nftAddress).transferFrom(msg.sender, address(this), _tokenId);

        // 压入扭蛋池
        pool.push(PoolAsset({
            nftAddress: _nftAddress,
            tokenId: _tokenId,
            depositor: msg.sender,
            ethBacking: msg.value,
            isActive: true
        }));

        activeAssetCount++;

        // 落地激励:存入资产即刻触发部分 FWA 代币产出激励
        fwaToken.mintReward(msg.sender, rewardEmissionRate / 2);

        emit AssetDeposited(pool.length - 1, msg.sender, _nftAddress, _tokenId, msg.value);
    }

    /**
     * @notice 特色 2: 链上扭蛋抽取与动态定价逻辑
     * @dev 实际生产环境推荐接入 Chainlink VRF 异步回调。此处展示同步安全随机抽取核心算法。
     */
    function pullGacha() external payable nonReentrant returns (uint256 chosenId) {
        uint256 currentPrice = getDynamicPrice();
        if(msg.value < currentPrice) revert InvalidPrice();
        if(activeAssetCount == 0) revert EmptyPool();

        // 1. 获取伪随机数索引 (落地防闪电贷伪随机:结合区块参数与池状态)
        uint256 rand = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender, pool.length)));
        uint256 randomIndex = rand % pool.length;

        // 2. 线性巡检寻找下一个有效资产(防碰撞兜底)
        while (!pool[randomIndex].isActive) {
            randomIndex = (randomIndex + 1) % pool.length;
        }
        chosenId = randomIndex;

        PoolAsset storage asset = pool[chosenId];
        asset.isActive = false;
        activeAssetCount--;

        // 3. 计算资金分流机制 (特色:退还底价支撑 + 协议分成)
        uint256 fee = (currentPrice * poolFeePercentage) / 10000;
        uint256 sellerShare = currentPrice - fee;

        // 将抽到的 NFT 移交给买家
        IERC721(asset.nftAddress).transferFrom(address(this), msg.sender, asset.tokenId);

        // 4. 特色 3: Loss-to-Earn 机制处理
        // 补偿原存入者:存入者获得(买家付出的资金),如果买家付出的低于原支撑价,损失由代币排放覆盖
        (bool success, ) = payable(asset.depositor).call{value: sellerShare}("");
        if(!success) revert TransferFailed();

        // 补偿买家或存入者的代币分发逻辑
        fwaToken.mintReward(msg.sender, rewardEmissionRate);       // 买家获得盲盒挖矿奖励
        fwaToken.mintReward(asset.depositor, rewardEmissionRate); // 存入者资产被抽走获得额外生态生态补贴

        // 退还多余的 ETH 
        if (msg.value > currentPrice) {
            payable(msg.sender).transfer(msg.value - currentPrice);
        }

        emit GachaPulled(chosenId, msg.sender, asset.depositor, currentPrice);
    }

    /**
     * @notice 动态定价计算器
     * @dev 仿照 FWA 根据池内资金背书总额和基础价格生成波动曲线
     */
    function getDynamicPrice() public view returns (uint256) {
        if(activeAssetCount == 0) return baseDrawPrice;
        
        uint256 totalBacking = 0;
        for(uint256 i = 0; i < pool.length; i++) {
            if(pool[i].isActive) {
                totalBacking += pool[i].ethBacking;
            }
        }
        // 动态价格 = 基础价格 + (当前池内平均背书价值 * 10%) 维持资金水位
        uint256 averageBacking = totalBacking / activeAssetCount;
        return baseDrawPrice + (averageBacking * 10 / 100);
    }

    // --- 管理员专属落地配置 (OZ V5 AccessControl) ---
    function setBasePrice(uint256 _newPrice) external onlyRole(MANAGER_ROLE) {
        baseDrawPrice = _newPrice;
        emit PriceUpdated(_newPrice);
    }

    function setFeePercentage(uint256 _newFee) external onlyRole(DEFAULT_ADMIN_ROLE) {
        if(_newFee > 1000) revert InvalidInput(); // 限制最高抽水 10%
        poolFeePercentage = _newFee;
    }

    // 紧急兜底:若池子出现不可抗力允许管理员协助用户安全退回 (落地合规必备)
    function emergencyWithdrawAsset(uint256 _assetId) external nonReentrant {
        PoolAsset storage asset = pool[_assetId];
        if(msg.sender != asset.depositor && !hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert InvalidInput();
        if(!asset.isActive) revert EmptyPool();

        asset.isActive = false;
        activeAssetCount--;

        // 退回 NFT 
        IERC721(asset.nftAddress).transferFrom(address(this), asset.depositor, asset.tokenId);
        // 退回部分 ETH 背书 (若合约余额充足)
        if(address(this).balance >= asset.ethBacking) {
            payable(asset.depositor).transfer(asset.ethBacking);
        }
    }

    receive() external payable {}
}

2. NFT 资产合约:MyToken.sol

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.5.0
pragma solidity ^0.8.27;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC721, ERC721URIStorage, ERC721Burnable, Ownable {
    constructor(address initialOwner)
        ERC721("MyToken", "MTK")
        Ownable(initialOwner)
    {}

    function safeMint(address to, uint256 tokenId, string memory uri)
        public
        onlyOwner
    {
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }

    // The following functions are overrides required by Solidity.

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

3. 奖励代币合约:BoykaYuriToken.sol

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.5.0
pragma solidity ^0.8.24;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract BoykaYuriToken is ERC20, ERC20Burnable, Ownable, ERC20Permit {
    constructor(address recipient, address initialOwner)
        ERC20("MyToken", "MTK")
        Ownable(initialOwner)
        ERC20Permit("MyToken")
    {
        _mint(recipient, 1000000 * 10 ** decimals());
    }
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
    // 关键修复:补齐 FwaCoreProtocol 所依赖的 mintReward 接口方法
    function mintReward(address to, uint256 amount) public {
        // 可以根据业务需要限制只有 FwaCoreProtocol 合约能调用,或者向后兼容保持 public/onlyOwner
        _mint(to, amount);
    }
}

四、 测试脚本

基于 Hardhat 与 Viem 的集成测试脚本,用于验证合约的部署初始化、资产存入授权、动态定价计算以及盲盒抽取全流程:

  • 测试用例: FwaCoreProtocol Integration Tests
    • 初始化验证:合约应正确设置基础参数与管理员权限
    • 资产存入(Loss-to-Earn 基石):存入 NFT 并提供 ETH 底价,应正确更新状态与触发奖励
    • 动态定价:当池内有资产时,getDynamicPrice 应返回基于平均背书的动态价格
    • 扭蛋抽取流程:买家支付动态价格抽取盲盒,资产正确交割并分配奖励与资金
    • 异常拦截:余额不足或无资产时抽取应回滚
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { parseEther, getAddress } from "viem";
import { network } from "hardhat";

describe("FwaCoreProtocol Integration Tests", function () {
    async function deployFixture() {
        const { viem } = await (network as any).connect();
        const [owner, buyer, depositor] = await viem.getWalletClients();
        const publicClient = await viem.getPublicClient();

        // 1. 部署 MyToken (ERC721)
        const mockNFT = await viem.deployContract("contracts/MyNFT.sol:MyToken", [owner.account.address]);
        
        // 2. 部署 BoykaYuriToken 作为奖励代币 (需确保符合 IFwaToken 接口,支持 mintReward)
        const fwaToken = await viem.deployContract("BoykaYuriToken", [owner.account.address, owner.account.address]);

        // 3. 部署核心协议合约 (baseDrawPrice = 0.1 ETH)
        const basePrice = parseEther("0.1");
        const fwaProtocol = await viem.deployContract("FwaCoreProtocol", [
            fwaToken.address,
            basePrice
        ]);

        return {
            mockNFT,
            fwaToken,
            fwaProtocol,
            owner,
            buyer,
            depositor,
            publicClient,
            basePrice
        };
    }

    it("初始化验证:合约应正确设置基础参数与管理员权限", async function () {
        const { fwaProtocol, fwaToken, basePrice } = await deployFixture();

        const tokenAddress = await fwaProtocol.read.fwaToken();
        const currentBasePrice = await fwaProtocol.read.baseDrawPrice();
        const feePercentage = await fwaProtocol.read.poolFeePercentage();

        assert.equal(getAddress(tokenAddress), getAddress(fwaToken.address), "FWA 代币地址不匹配");
        assert.equal(currentBasePrice, basePrice, "基础抽奖价格不匹配");
        assert.equal(feePercentage, 250n, "默认抽水比例应为 2.5%");
    });

    it("资产存入(Loss-to-Earn 基石):存入 NFT 并提供 ETH 底价,应正确更新状态与触发奖励", async function () {
        const { fwaProtocol, mockNFT, fwaToken, owner, depositor, publicClient } = await deployFixture();
        const tokenId = 0n;
        const backingValue = parseEther("0.5");
        const metadataURI = "https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB";
        // 1. 使用合约所有者通过 safeMint 铸造 NFT 给存入者
        await mockNFT.write.safeMint([depositor.account.address, tokenId, metadataURI], {
            account: owner.account
        });
        
        // 2. 存入者授权协议合约转移该 NFT
        await mockNFT.write.approve([fwaProtocol.address, tokenId], {
            account: depositor.account
        });

        // 3. 存入资产
        const beforeBalance = await publicClient.getBalance({ address: fwaProtocol.address });
        await fwaProtocol.write.depositAsset([mockNFT.address, tokenId], {
            account: depositor.account,
            value: backingValue
        });

        // 4. 验证池内状态
        const activeCount = await fwaProtocol.read.activeAssetCount();
        assert.equal(activeCount, 1n, "有效资产数应为 1");

        const poolAsset = await fwaProtocol.read.pool([0n]);
        assert.equal(getAddress(poolAsset[0]), getAddress(mockNFT.address), "NFT 地址不匹配");
        assert.equal(poolAsset[1], tokenId, "TokenId 不匹配");
        assert.equal(getAddress(poolAsset[2]), getAddress(depositor.account.address), "存入者地址不匹配");
        assert.equal(poolAsset[3], backingValue, "ETH 背书价值不匹配");
        assert.equal(poolAsset[4], true, "资产应处于活跃状态");

        // 5. 验证合约 ETH 余额
        const afterBalance = await publicClient.getBalance({ address: fwaProtocol.address });
        assert.equal(afterBalance - beforeBalance, backingValue, "合约应收到对应的 ETH 背书");

        const rewardBalance = await fwaToken.read.balanceOf([depositor.account.address]);
        assert.ok(rewardBalance > 0n, "存入者应获得代币奖励");
    });

    it("动态定价:当池内有资产时,getDynamicPrice 应返回基于平均背书的动态价格", async function () {
        const { fwaProtocol, mockNFT, owner, depositor, basePrice } = await deployFixture();
        const tokenId = 0n;
        const backingValue = parseEther("1.0");

        await mockNFT.write.safeMint([depositor.account.address, tokenId, "ipfs://meta"], {
            account: owner.account
        });
        
        await mockNFT.write.approve([fwaProtocol.address, tokenId], {
            account: depositor.account
        });

        await fwaProtocol.write.depositAsset([mockNFT.address, tokenId], {
            account: depositor.account,
            value: backingValue
        });

        const dynamicPrice = await fwaProtocol.read.getDynamicPrice();
        const expectedPrice = basePrice + (backingValue * 10n / 100n);
        assert.equal(dynamicPrice, expectedPrice, "动态计算价格不正确");
    });

    it("扭蛋抽取流程:买家支付动态价格抽取盲盒,资产正确交割并分配奖励与资金", async function () {
        const { fwaProtocol, mockNFT, fwaToken, owner, depositor, buyer } = await deployFixture();
        const tokenId = 0n;
        const backingValue = parseEther("0.2");

        // 1. 存入资产
        await mockNFT.write.safeMint([depositor.account.address, tokenId, "ipfs://meta"], {
            account: owner.account
        });
        
        await mockNFT.write.approve([fwaProtocol.address, tokenId], {
            account: depositor.account
        });

        await fwaProtocol.write.depositAsset([mockNFT.address, tokenId], {
            account: depositor.account,
            value: backingValue
        });

        // 2. 获取当前动态价格
        const currentPrice = await fwaProtocol.read.getDynamicPrice();

        // 3. 买家执行抽取
        await fwaProtocol.write.pullGacha({
            account: buyer.account,
            value: currentPrice
        });

        // 4. 验证 NFT 是否转移给买家
        const newOwner = await mockNFT.read.ownerOf([tokenId]);
        assert.equal(getAddress(newOwner), getAddress(buyer.account.address), "NFT 应成功转移给买家");

        // 5. 验证池状态清空
        const activeCount = await fwaProtocol.read.activeAssetCount();
        assert.equal(activeCount, 0n, "池内活跃资产应为 0");

        // 6. 验证奖励发放
        const buyerReward = await fwaToken.read.balanceOf([buyer.account.address]);
        const depositorReward = await fwaToken.read.balanceOf([depositor.account.address]);
        assert.ok(buyerReward > 0n, "买家应获得盲盒挖矿奖励");
        assert.ok(depositorReward > 0n, "存入者应获得生态补贴奖励");
    });

    it("异常拦截:余额不足或无资产时抽取应回滚", async function () {
        const { fwaProtocol, buyer } = await deployFixture();

        // 空池抽取
        await assert.rejects(
            async () => {
                await fwaProtocol.write.pullGacha({
                    account: buyer.account,
                    value: parseEther("1.0")
                });
            },
            /EmptyPool/,
            "空池抽取应触发 EmptyPool 错误"
        );
    });
});

五、 部署脚本

为了将合约完整部署到目标网络(如测试网或主网),可以编写如下部署脚本(deploy.ts):

// scripts/deploy.js
import { network, artifacts } from "hardhat";
import { parseEther } from "viem/utils";
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 myTokenArtifact = await artifacts.readArtifact("BoykaYuriToken");

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

  // 等待确认并打印地址
  const myTokenReceipt = await publicClient.waitForTransactionReceipt({ hash: myTokenHash });
  console.log("BoykaYuriToken合约地址:", myTokenReceipt.contractAddress);
  const MyNFTArtifact = await artifacts.readArtifact("contracts/MyNFT.sol:MyToken");
  const MyNFTHash = await deployer.deployContract({
    abi: MyNFTArtifact.abi,//获取abi
    bytecode: MyNFTArtifact.bytecode,//硬编码
    args: [deployerAddress],//process.env.RECIPIENT, process.env.OWNER
  });
  const MyNFTReceipt = await publicClient.waitForTransactionReceipt({ hash: MyNFTHash });
  console.log("MyNFT合约地址:", MyNFTReceipt.contractAddress);
  const FwaCoreProtocolArtifact = await artifacts.readArtifact("FwaCoreProtocol");
   const basePrice = parseEther("0.1");
  const FwaCoreProtocolHash = await deployer.deployContract({
    abi: FwaCoreProtocolArtifact.abi,//获取abi
    bytecode: FwaCoreProtocolArtifact.bytecode,//硬编码
    args: [myTokenReceipt.contractAddress,basePrice],
  });
  const FwaCoreProtocolReceipt = await publicClient.waitForTransactionReceipt({ hash: FwaCoreProtocolHash });
  console.log("FwaCoreProtocol合约地址:", FwaCoreProtocolReceipt.contractAddress);
}

main().catch(console.error);

六、 总结

通过对 FWA 核心智能合约的深入拆解,我们可以看到其在机制设计上的精妙之处:利用 Loss-to-Earn 理念将底价支撑与代币排放深度绑定,辅以链上动态定价公式,既保证了流动性提供者的基础安全,又激发了二级市场的抽取意愿。结合严密的权限控制与 OpenZeppelin 安全库,该架构为去中心化盲盒与资产托管项目提供了优秀的参考范本。

相关文章

0 条评论