引言
在上一篇文章中,我们探讨了 Fake World Assets (FWA) 项目的核心玩法与生态参与方式。本篇技术文章将聚焦于底层实现,通过对核心智能合约代码的深度拆解,带大家从工程视角剖析 FWA 是如何利用 Solidity 与 OpenZeppelin V5 实现链上扭蛋机制 、动态定价 以及独特的 Loss-to-Earn(亏损即挖矿) 双向代币激励模型的。
免责声明:本文仅供技术交流与学习参考之用,所有内容均基于公开智能合约代码的拆解与复刻。不构成任何投资建议、财务建议或背书。区块链项目存在极高的技术与资金风险(如智能合约漏洞、流动性风险等),请读者切勿盲目参与。
一、 项目背景与核心创新点
FWA 改变了传统盲盒及资产流转项目的单一博弈模式,引入了 DeFi 与 NFT 结合的全新范式。其核心创新与优点包括:
- Loss-to-Earn(亏损即挖矿)基石:用户存入 NFT 并绑定指定的 ETH 底价(Backing Value),不仅能为整个协议提供流动性支撑,还能在存入和资产被抽走时获得原生代币(FWA Token)的生态奖励,从而平抑用户的资产波动风险。
- 链上动态定价机制:盲盒抽取的價格并非一成不变,而是根据池内活跃资产的平均背书价值动态浮动(基础价 + 平均背书的特定比例),有效防止了套利空间并维持了协议的资金水位。
- 高效的资金分流与安全机制:内置抽水比例(Fee Percentage)、防闪电贷伪随机数索引,以及完善的异常拦截和紧急兜底退回机制(Emergency Withdraw),兼顾了灵活性与安全性。
二、 技术架构
整个系统采用模块化设计,主要由以下三个核心合约协同工作:
FwaCoreProtocol.sol:核心业务合约。继承自 OpenZeppelin 的ReentrancyGuard和AccessControl,负责资产存入、扭蛋抽取、动态定价计算以及奖励分发。MyToken.sol:标准的 ERC-721 资产合约(继承自 OZ V5 的ERC721URIStorage和ERC721Burnable),作为被存入和抽取的盲盒 NFT 标的。BoykaYuriToken.sol:协议的生态代币合约(继承自ERC20,ERC20Burnable,ERC20Permit),实现了mintReward接口以响应核心协议的代币产出。
三、 智能合约实现源码
1. 核心协议合约:FwaCoreProtocol.sol
js
// 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
js
// 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
js
// 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 应返回基于平均背书的动态价格
- 扭蛋抽取流程:买家支付动态价格抽取盲盒,资产正确交割并分配奖励与资金
- 异常拦截:余额不足或无资产时抽取应回滚
js
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):
js
// 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 安全库,该架构为去中心化盲盒与资产托管项目提供了优秀的参考范本。