news 2026/8/28 14:39:38

Solidity实战:构建多资产代币化链上基金

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Solidity实战:构建多资产代币化链上基金

看到这样一个设想:如果一只基金同时持有代币化黄金、科技股和数字资产,是否能在链上实现统一净值、灵活申赎和透明记账?这个问题的技术答案,比金融答案更早落地。本文将从一个可运行的多资产组合基金 PoC 出发,完整拆解如何用 Solidity 编写三类 ERC-20 代币化资产、一个模拟价格预言机,以及一个负责份额发行与赎回的基金合约。无论你是想了解真实世界资产代币化(RWA)的开发路径,还是需要一份链上基金系统的最小实现,这份教程都可以直接作为起点。

我会尽可能把每个合约、每个函数、每个参数都讲清楚,并重点说明哪些代码只能用于教学演示、哪些地方在生产环境必须替换或加固。文中所有代码和数值均用于技术演示,不构成任何投资建议。

1. 背景:链上基金为什么能同时装下黄金、股票和数字资产

1.1 从“一块金条”到“一串代币”:代币化资产入门

代币化资产(Tokenized Assets)是指把现实世界中的资产,例如黄金、股票、债券、房地产等,通过区块链技术转换为一枚可在链上流通的数字代币。这个转换过程并没有把黄金变成互联网上的数据符号,而是在链下由托管机构保管真实的黄金,在链上通过合约记录“谁拥有多少份额”。

过去购买黄金的最小单位可能是一克、一盎司,但链上的代币可以拆分到小数点后 18 位,这让小额参与成为可能。更重要的是,代币化之后的资产天然支持可编程操作:可以转账、抵押、组合,也可以接入智能合约做自动化管理。这正是“一篮子资产组合”可以跑在链上的基础。

理解代币化资产时,有一个容易混淆的点:代币本身不等于黄金,它只是黄金所有权的记账凭证。只要托管、审计和合约逻辑没有做好,代币就只是一个符号。所以在学习技术的同时,也必须理解资产真实性来自链下体系。

1.2 为什么黄金、科技股和数字资产会被放进同一只基金

传统的投资组合也会同时配置黄金、股票和另类资产,但这类组合通常由中心化金融机构管理,普通用户看到的是一个净值数字,很难实时核对底层资产明细。把黄金、科技股和数字资产代币化,再放入同一个链上基金,好处是组合透明:每种资产的余额、价格、权重都在链上可知,用户可以通过合约计算实时净值。

从技术演示来看,这个组合也很有代表性:

  • 代币化黄金代表真实世界资产上链;
  • 代币化科技股代表传统金融产品的合约化表达;
  • 数字资产本身就在链上,接入成本最低。

三类资产的价格来源、波动特征和交易方式都不一样,而这正好考验基金合约的数据获取、净值计算和赎回分配逻辑。

需要再次强调,本文只讨论技术实现。真实金融市场中,这类产品涉及证券法、商品法、托管、反洗钱等复杂监管要求,不是一个智能合约就能解决的。

1.3 本文的技术目标

读完这篇文章,你会掌握以下能力:

  • 创建一个可复用的 ERC-20 资产代币合约,并通过它发行“代币化黄金”“代币化科技股”“代币化数字资产”;
  • 编写一个模拟价格预言机,为不同资产提供美元计价价格;
  • 编写一个链上基金合约,支持资产存入、份额发行、按比例赎回;
  • 使用 Hardhat 完成合约部署、脚本调用和基础结果验证;
  • 了解从教学 PoC 到真实产品之间需要补齐的安全、托管和合规能力。

2. 技术底座与开发环境

2.1 技术选型

链上基金的核心逻辑需要跑在可验证的智能合约环境中,因此本文选择 Solidity 作为合约语言,使用 Hardhat 作为开发与部署框架。

Solidity 是目前以太坊生态使用最广泛的智能合约语言,资料多、工具链成熟。OpenZeppelin 提供了 ERC-20 的标准实现和安全工具,可以直接复用。价格部分,真实项目一般接入 Chainlink Price Feeds,本文为了保持示例可离线运行,使用一个自定义的 MockPriceOracle 模拟价格读取,但接口设计尽量贴近真实使用方式。

2.2 开发环境与依赖

本文示例基于以下环境组合:

  • Node.js 16 或 18
  • Hardhat 2.x
  • Solidity 0.8.17
  • OpenZeppelin Contracts 4.x
  • ethers.js 5.x

版本需要根据你的项目实际情况调整。如果你使用的 Hardhat 3 或 ethers v6,部分 API 名称会有变化,例如ethers.utils.parseEther在 v6 中变成了ethers.parseEther。下面示例以 Hardhat 2.x + ethers v5 这一套经过大量验证的组合为准。

2.3 项目结构

项目目录规划如下:

portfolio-fund-poc/ ├── contracts/ │ ├── AssetToken.sol │ ├── MockPriceOracle.sol │ └── PortfolioFund.sol ├── scripts/ │ ├── deploy.js │ └── demo.js ├── hardhat.config.js └── package.json

contracts目录保存三个核心合约,scripts目录保存部署和演示脚本。整体结构非常简单,适合作为学习模板继续扩展。

3. 核心机制拆解

3.1 ERC-20 资产代币怎么表达“黄金份额”

ERC-20 是 Ethereum 上最常见的代币标准。它定义了balanceOftransferapprovetransferFrom等接口,让每个地址都能查询余额并进行转账。

如果要表达“代币化黄金”,可以创建一个名为AssetToken的合约,传入不同名称和符号,分别部署为黄金、股票和数字资产。为了简化演示,合约中加入了公开的mintburn方法。这里必须特别注意:公开mint意味着任何人都能凭空铸造代币,这是非常危险的设计,仅适用于教学演示。生产环境必须把mint权限限制给经过授权的托管人合约,并且结合 KYC 流程。

教学合约的价值在于展示 ERC-20 的最小实现与使用方式,但绝不能直接用来发行真实资产。

3.2 价格预言机:基金怎么知道金价、股价、币价

基金计算净值时,需要知道每种资产的美元价格。如果由基金合约自己保存价格,就会存在两个问题:管理员可以随意修改价格操纵净值;价格数据无法自证来源。

真实产品通常使用去中心化预言机,例如 Chainlink。Chainlink 通过多个独立节点从交易所聚合价格,再上链供合约读取。本文为了本地演示,使用MockPriceOracle合约:

  • 管理员设置每个资产代币对应的美元价格;
  • 基金合约调用getPriceUSD(asset)获取价格;
  • 价格同样使用 18 位小数精度,与资产代币保持一致。

这个设计让基金合约不关心价格从哪来,只负责读取。后续要接入真实预言机,只需要替换oracle地址,基金主逻辑不需要大幅改动。

3.3 基金净值与份额:NAV / pricePerShare / deposit / redeem

基金的核心是净值计算。

基金总资产totalAssetsUSD等于三种资产数量分别乘以对应价格后的总和。每个份额对应的价值pricePerShare等于总资产除以总份额:

pricePerShare = totalAssetsUSD / totalShares

用户存入资产时,合约先根据存入资产的美元价值计算可发行份额:

sharesToMint = valueUSD / pricePerShare

用户赎回时,合约先计算用户份额对应的美元价值,再按当前三种资产在组合中的权重,分别转出对应数量的资产。这种方式避免了“用户只赎回美元,基金却缺少美元流动性”的问题,保持了底层资产池的完整性。

4. 完整实战:搭建一个多资产代币化基金 PoC

4.1 初始化项目

先在本地创建项目目录并初始化 npm:

mkdir portfolio-fund-poc && cd portfolio-fund-poc npm init -y

安装 Hardhat 和 OpenZeppelin 依赖:

npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts

安装完成后,创建hardhat.config.js

require("@nomicfoundation/hardhat-toolbox"); module.exports = { solidity: "0.8.17", };

4.2 编写资产代币合约

资产代币使用统一的AssetToken合约,通过构造参数区分名称和符号。

// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract AssetToken is ERC20 { constructor( string memory name_, string memory symbol_ ) ERC20(name_, symbol_) {} function mint(address to, uint256 amount) external { _mint(to, amount); } function burn(address from, uint256 amount) external { _burn(from, amount); } }

部署时,我们会创建三个AssetToken实例:

  • Tokenized Mock Gold,符号MGLD,模拟代币化黄金;
  • Tokenized Tech Stock,符号MTECH,模拟代币化科技股;
  • Tokenized Digital Asset,符号MDIG,模拟数字资产。

mintburn方法没有做权限控制,这是为了让演示脚本可以随时铸币。真实场景下,建议将mint修改为:

function mint(address to, uint256 amount) external onlyMinter { _mint(to, amount); }

并通过minter角色管理铸造权限。

4.3 编写 MockPriceOracle

价格预言机保存每个资产代币对应的美元价格。为了方便计算,价格同样使用 18 位小数,60000 * 1e18表示一个 MGLD 价值 60000 美元。

// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "@openzeppelin/contracts/access/Ownable.sol"; contract MockPriceOracle is Ownable { mapping(address => uint256) public pricesUSD; function setPrice(address asset, uint256 priceUSD) external onlyOwner { require(priceUSD > 0, "invalid price"); pricesUSD[asset] = priceUSD; } function getPriceUSD(address asset) public view returns (uint256) { uint256 price = pricesUSD[asset]; require(price > 0, "price not set"); return price; } }

这个合约使用Ownable,只有合约所有者才能修改价格。真实项目中,价格应该由 Chainlink 数据源维护,而不是管理员直接写入。这里的setPrice只用于模拟行情变化。

4.4 编写 PortfolioFund 基金合约

这是整个 PoC 的核心。合约在构造函数中接收预言机地址,以及三个资产代币地址:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "./MockPriceOracle.sol"; contract PortfolioFund is Ownable { MockPriceOracle public oracle; IERC20 public goldToken; IERC20 public stockToken; IERC20 public digitalToken; uint256 public totalShares; mapping(address => uint256) public shares; bool public initialized; event Deposit(address indexed user, uint256 sharesMinted, uint256 valueUSD); event Redeem(address indexed user, uint256 sharesBurned, uint256 valueUSD); constructor( address _oracle, address _gold, address _stock, address _digital ) { oracle = MockPriceOracle(_oracle); goldToken = IERC20(_gold); stockToken = IERC20(_stock); digitalToken = IERC20(_digital); } function seedFund(address asset, uint256 amount) external onlyOwner { IERC20(asset).transferFrom(msg.sender, address(this), amount); } function initializeFund(uint256 initialShares) external onlyOwner { require(!initialized, "already initialized"); require(initialShares > 0, "invalid initial shares"); require(totalAssetsUSD() > 0, "fund is empty"); initialized = true; shares[msg.sender] = initialShares; totalShares = initialShares; } function totalAssetsUSD() public view returns (uint256) { uint256 goldValue = _assetValue(address(goldToken)); uint256 stockValue = _assetValue(address(stockToken)); uint256 digitalValue = _assetValue(address(digitalToken)); return goldValue + stockValue + digitalValue; } function _assetValue(address asset) internal view returns (uint256) { uint256 balance = IERC20(asset).balanceOf(address(this)); uint256 price = oracle.getPriceUSD(asset); return (balance * price) / 1e18; } function pricePerShare() public view returns (uint256) { require(initialized, "not initialized"); require(totalShares > 0, "no shares"); return (totalAssetsUSD() * 1e18) / totalShares; } function deposit(address asset, uint256 amount) external { require(initialized, "not initialized"); require(amount > 0, "amount is zero"); require(_isSupported(asset), "asset not supported"); uint256 valueUSD = (amount * oracle.getPriceUSD(asset)) / 1e18; uint256 sharesToMint = (valueUSD * 1e18) / pricePerShare(); require(sharesToMint > 0, "shares too small"); IERC20(asset).transferFrom(msg.sender, address(this), amount); shares[msg.sender] += sharesToMint; totalShares += sharesToMint; emit Deposit(msg.sender, sharesToMint, valueUSD); } function redeem(uint256 shareAmount) external { require(initialized, "not initialized"); require(shares[msg.sender] >= shareAmount, "insufficient shares"); uint256 valueUSD = (shareAmount * pricePerShare()) / 1e18; uint256 totalValue = totalAssetsUSD(); require(totalValue > 0, "fund is empty"); shares[msg.sender] -= shareAmount; totalShares -= shareAmount; _payOut(address(goldToken), valueUSD, totalValue); _payOut(address(stockToken), valueUSD, totalValue); _payOut(address(digitalToken), valueUSD, totalValue); emit Redeem(msg.sender, shareAmount, valueUSD); } function _payOut( address asset, uint256 userValueUSD, uint256 totalValue ) internal { uint256 balance = IERC20(asset).balanceOf(address(this)); uint256 assetValue = (balance * oracle.getPriceUSD(asset)) / 1e18; if (assetValue == 0) return; uint256 payValue = (userValueUSD * assetValue) / totalValue; uint256 payAmount = (payValue * 1e18) / oracle.getPriceUSD(asset); if (payAmount > 0) { IERC20(asset).transfer(msg.sender, payAmount); } } function _isSupported(address asset) internal view returns (bool) { return asset == address(goldToken) || asset == address(stockToken) || asset == address(digitalToken); } }

合约整体流程如下:

  1. 管理员通过seedFund把三种资产转入基金,模拟初始建仓;
  2. 管理员通过initializeFund设定初始份额,相当于为基金“定价”;
  3. 普通用户调用deposit存入任意一种受支持资产,获得相应份额;
  4. 用户调用redeem销毁份额,按当前权重获得三种资产。

_payOut的逻辑需要重点理解。假设用户赎回份额价值 100 美元,基金总资产 1000 美元,其中黄金资产价值 200 美元,那么用户应该从黄金池中分到:

payValue = 100 * 200 / 1000 = 20 美元

再根据黄金当前价格换算成黄金代币数量转给用户。这个按比例分配的方法,保证了赎回后剩余用户的组合比例不会被破坏。

不过这个合约只是 PoC,仍有几个简化点:

  • 未做滑点控制,用户在价格波动期间赎回可能产生组合偏移;
  • 未使用 ReentrancyGuard,虽然代码先更新状态再转账,但生产环境建议显式加保护;
  • 未处理价格精度不一致问题,假设所有资产和价格都是 18 位小数;
  • mint/burn无权限控制,不能用于真实资产。

4.5 编写部署与演示脚本

部署脚本负责创建资产代币、预言机和基金合约,并设置初始模拟价格:

// scripts/deploy.js const hre = require("hardhat"); async function main() { const { ethers } = hre; const AssetToken = await ethers.getContractFactory("AssetToken"); const gold = await AssetToken.deploy("Tokenized Mock Gold", "MGLD"); await gold.deployed(); const stock = await AssetToken.deploy("Tokenized Tech Stock", "MTECH"); await stock.deployed(); const digital = await AssetToken.deploy("Tokenized Digital Asset", "MDIG"); await digital.deployed(); const MockPriceOracle = await ethers.getContractFactory("MockPriceOracle"); const oracle = await MockPriceOracle.deploy(); await oracle.deployed(); // 模拟价格,仅用于演示 await oracle.setPrice(gold.address, ethers.utils.parseEther("60000")); await oracle.setPrice(stock.address, ethers.utils.parseEther("180")); await oracle.setPrice(digital.address, ethers.utils.parseEther("3000")); const PortfolioFund = await ethers.getContractFactory("PortfolioFund"); const fund = await PortfolioFund.deploy( oracle.address, gold.address, stock.address, digital.address ); await fund.deployed(); console.log("MGLD:", gold.address); console.log("MTECH:", stock.address); console.log("MDIG:", digital.address); console.log("Oracle:", oracle.address); console.log("Fund:", fund.address); } main().catch((error) => { console.error(error); process.exitCode = 1; });

演示脚本会完成建仓、初始化、申购和赎回的完整链路:

// scripts/demo.js const hre = require("hardhat"); async function main() { const { ethers } = hre; const [admin, user] = await ethers.getSigners(); const AssetToken = await ethers.getContractFactory("AssetToken"); const gold = await AssetToken.deploy("Tokenized Mock Gold", "MGLD"); const stock = await AssetToken.deploy("Tokenized Tech Stock", "MTECH"); const digital = await AssetToken.deploy("Tokenized Digital Asset", "MDIG"); await gold.deployed(); await stock.deployed(); await digital.deployed(); const MockPriceOracle = await ethers.getContractFactory("MockPriceOracle"); const oracle = await MockPriceOracle.deploy(); await oracle.deployed(); await oracle.setPrice(gold.address, ethers.utils.parseEther("60000")); await oracle.setPrice(stock.address, ethers.utils.parseEther("180")); await oracle.setPrice(digital.address, ethers.utils.parseEther("3000")); const PortfolioFund = await ethers.getContractFactory("PortfolioFund"); const fund = await PortfolioFund.deploy( oracle.address, gold.address, stock.address, digital.address ); await fund.deployed(); // 给 admin 和 user 铸造测试代币 await gold.mint(admin.address, ethers.utils.parseEther("1000")); await gold.mint(user.address, ethers.utils.parseEther("100")); await stock.mint(admin.address, ethers.utils.parseEther("1000")); await digital.mint(admin.address, ethers.utils.parseEther("1000")); // 管理员建仓:100 MGLD、200 MTECH、100 MDIG await gold.approve(fund.address, ethers.utils.parseEther("100")); await stock.approve(fund.address, ethers.utils.parseEther("200")); await digital.approve(fund.address, ethers.utils.parseEther("100")); await fund.seedFund(gold.address, ethers.utils.parseEther("100")); await fund.seedFund(stock.address, ethers.utils.parseEther("200")); await fund.seedFund(digital.address, ethers.utils.parseEther("100")); await fund.initializeFund(ethers.utils.parseEther("10000")); const ppsBefore = await fund.pricePerShare(); console.log("pricePerShare before:", ethers.utils.formatEther(ppsBefore)); // 用户存入 1 MGLD await gold.connect(user).approve(fund.address, ethers.utils.parseEther("1")); await fund.connect(user).deposit(gold.address, ethers.utils.parseEther("1")); const userShares = await fund.shares(user.address); console.log("user shares:", ethers.utils.formatEther(userShares)); const totalAssets = await fund.totalAssetsUSD(); console.log("totalAssets after deposit:", ethers.utils.formatEther(totalAssets)); // 用户赎回全部份额 await fund.connect(user).redeem(userShares); const goldBalance = await gold.balanceOf(user.address); console.log("user MGLD balance after redeem:", ethers.utils.formatEther(goldBalance)); } main().catch((error) => { console.error(error); process.exitCode = 1; });

这段脚本特意把多个步骤放在一个演示脚本中,方便读者一次看到完整流程。真实项目建议拆分为独立的部署脚本和测试用例。

4.6 运行与验证

在项目根目录执行编译:

npx hardhat compile

编译成功后,执行部署脚本:

npx hardhat run scripts/deploy.js

部署成功后继续运行演示脚本:

npx hardhat run scripts/demo.js

如果没有报错,会在终端看到一系列日志,例如pricePerShare beforeuser sharestotalAssets after deposituser MGLD balance after redeem

如果你希望更系统地验证,可以安装 Hardhat 测试插件,使用chai编写断言,而不是仅依赖脚本输出。这里为了篇幅,不展开测试用例,但建议你在自己的项目中补上。

4.7 预期结果分析

演示脚本的核心数据如下:

初始建仓时,管理员注入了:

  • 100 MGLD,单价 60000 美元,价值 600 万美元;
  • 200 MTECH,单价 180 美元,价值 3.6 万美元;
  • 100 MDIG,单价 3000 美元,价值 30 万美元。

总资产约为 633.6 万美元。initializeFund(10000)后,每份份额价值约为 633.6 美元。

用户存入 1 MGLD,对应 60000 美元价值,可获得的份额约为:

60000 /
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/28 14:39:10

智驾安卓时刻:开源模型如何从能跑到能用

黄仁勋深夜发布开源模型,把“智驾开源”这个话题重新拉到了台前。很多人第一反应是“免费模型能白嫖了”,但我更关心另一个信号:自动驾驶是不是真的进入了类似安卓的生态阶段。所谓「安卓时刻」,不是代码免费用那么简单&#xff0…

作者头像 李华
网站建设 2026/8/28 14:36:21

高薪与闭源之外:从Claude API看开发者如何构建可迁移的AI技术栈

最近科技圈有一个话题热度很高:Anthropic 被曝出开出了 AI 行业最高薪水,内部却有声音抱怨员工是“为钱而来”。评论区很快就有人接梗:这和 Anthropic 死活不肯把 Claude 模型开源,逻辑上倒是一模一样——既要别人讲情怀&#xff…

作者头像 李华
网站建设 2026/8/28 14:35:05

高精度地图核心技术:众源更新、质量评估、编译发布与动态图层详解

1. 项目概述:从“地图”到“厘米级数字世界”的认知升级聊到高精度地图,很多刚接触自动驾驶或者智慧交通的朋友,第一反应可能就是“精度特别高的导航地图”。这个理解对,但也不全对。它确实是地图,但其核心价值早已超越…

作者头像 李华
网站建设 2026/8/28 14:34:53

Anthropic闭源争议下Claude API接入实战与开源模型替代方案

最近业内有个话题很热闹:Anthropic 被曝出在 AI 大模型公司里开出了最高级别的薪酬,但内部又有决策层公开抱怨员工是“为了钱而来”。消息传出来后,不少开发者顺着话题开始“阴阳”:一边给钱最猛,一边在开源路线上一动…

作者头像 李华
网站建设 2026/8/28 14:31:02

YOLO共享单车检测数据集:VOC格式工业级实战指南

简介:目标检测是计算机视觉的基础任务,其核心在于高质量标注数据与模型训练的深度协同。VOC格式作为经典结构化标注标准,凭借原始尺寸、绝对坐标及遮挡/截断元信息,在YOLO等主流框架中展现出强兼容性与工程可扩展性。共享单车检测…

作者头像 李华
网站建设 2026/8/28 14:30:43

智能房车技术架构:从能源调度到离线自治的关键工程

如果你长期关注嵌入式、IoT 或智能硬件方向,最近应该会注意到一条消息:一家由前安克高管创办的智能房车公司,拿到了元禾、金沙江等机构超 2 亿融资,首款产品计划 2027 年初量产。这条消息在媒体上被归为创业故事,但技术…

作者头像 李华