资讯动态

Sway 原生资产(Native Assets)完全指南:从 AssetId 到 SRC-20 标准合约实战

发布时间:2026/9/11 22:07:42 来源:尧图企业网站定制
Sway 原生资产Native Assets完全指南从 AssetId 到 SRC-20 标准合约实战【免费下载链接】sway Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway本指南以 Sway 语言与 FuelVM 的原生资产模型为核心系统讲解 Fuel 网络「一切资产皆原生」的设计理念以及如何在智能合约中通过std::asset、std::context等标准库完成资产的铸造Mint、销毁Burn、转账Transfer与余额查询。读完本文你将掌握AssetId/SubId的正确构造方式、与 ERC-20/ERC-721/ERC-1155 的差异、#[payable]接收资产的规则并能独立编写出实现 SRC-20 标准的单资产与多资产合约。FuelVM 的原生资产模型与 EVM 的本质区别FuelVM 对多资产multiple assets提供了内建built-in支持这与 EVM 有根本性的架构差异。理解这些差异是编写 Sway 资产合约的前提。ERC-20 与原生资产Native Asset在 EVM 上Ether 是唯一的原生资产向某个地址或合约发送 ETH 是 EVM 的内建操作不需要依赖任何智能合约来维护余额账本。而 ERC-20 代币则恰恰相反——它必须借助一个记录余额的合约才能实现所有权追踪。在 FuelVM 上所有资产都是原生资产发送任意原生资产的过程完全一致。你依然需要一个智能合约来处理资产的铸造与销毁minting and burning但资产的发送与接收可以完全独立于该资产合约之外进行。注意Fuel 网络没有「token」这一概念只有资产。ERC-721 与原生资产在 EVM 上ERC-721NFT是一个包含多个互不相同non-fungibletoken 的合约。在 FuelVM 上ERC-721 的等价物是每个资产供应量为 1的原生资产这一规定定义在 SRC-20 标准Native Asset Standard的「非同质化资产限制」Non-Fungible Asset Restrictions一节中。这意味着在实践中Fuel 上的所有 NFT 与其他原生资产的处理方式完全相同。编写 Sway 代码时无需为同质化资产与非同质化资产编写额外的处理分支——同一套转账、查询、存储代码即可覆盖两类场景。没有 Token Approvals授权机制原生资产带来的一个显著优势是不存在 token approval代币授权这与 EVM 上 Ether 的行为一致。由于每年都有大量资金因误用 token approval 而被盗FuelVM 直接从协议层面消除了这一攻击向量。Asset、Coin 与 Token 的术语辨析术语含义AssetFuel 上的原生资产拥有对应的AssetId类型不同资产之间可以互相区分Coin某个 Asset 的单一单位singular unit同一 Asset 下的 Coin 彼此不可区分TokenFuel 不使用 token 概念而是采用原生资产 UTXO 设计这与 Ethereum 等生态不同AssetId 类型资产的唯一标识AssetId类型代表 Fuel 上的任意原生资产是链上交互资产时使用的核心类型。从源码看AssetId本质上是一个包装了b256字段的结构体见 sway-lib-std/src/asset_id.sw并实现了Hash、PartialEq/Eq、Fromb256、FromAssetId for b256等 trait支持哈希、比较与类型互转。Fuel 上任意原生资产的AssetId通过如下方式计算对「铸币合约的ContractId」与「SubId」取 SHA-256 哈希摘要即AssetId sha256((contract_id, sub_id))该计算公式在标准库文档注释中明确记载见 sway-lib-std/src/asset_id.swAssetId::new的实现正是通过s256汇编指令对 64 字节的(contract_id, sub_id)求哈希见 sway-lib-std/src/asset_id.sw。创建 AssetId 的三种方式1. Default默认资产当合约只会铸造单一资产时推荐使用DEFAULT_SUB_ID其值即b256::zero()见 sway-lib-std/src/constants.sw此时该资产被称为合约的默认资产default asset。在合约内部调用default()即可获得默认资产let asset_id: AssetId AssetId::default();警告来自标准库注释AssetId::default()如果在外部上下文external context中被调用将不会返回正确的 AssetId——因为此时合约 ID 字段实际使用的是 Transaction Id。它只能在合约内部安全使用。2. New新资产如果合约铸造多种资产或者资产是由外部合约铸造的则需要使用new()函数。它接受铸币合约的ContractId以及一个SubIdlet my_contract_id: ContractId ContractId::from(0x1000000000000000000000000000000000000000000000000000000000000000); let my_sub_id: SubId 0x2000000000000000000000000000000000000000000000000000000000000000; let asset_id: AssetId AssetId::new(my_contract_id, my_sub_id);3. From已知 b256 值当资产对应的b256值已经已知时可以直接通过from()函数构造let asset_id: AssetId AssetId::from(0x0000000000000000000000000000000000000000000000000000000000000000);以上三种构造方式的完整代码位于 examples/native_asset/src/main.sw该示例合约也是本章文档片段{{#include}}的直接来源。SubId 类型区分同一合约下的多资产SubId用于区分由同一个合约创建的不同资产。从标准库看SubId就是b256的类型别名见 sway-lib-std/src/alias.swpub type SubId b256;在 Fuel 上创建单个新资产时推荐使用DEFAULT_SUB_ID或SubId::zero()。基础资产Base AssetEther在 Fuel 网络上基础资产是Ether它是 Fuel 网络上唯一没有SubId的资产。随时可以通过AssetId类型的base()函数获得基础资产let base_asset: AssetId AssetId::base();从实现看AssetId::base()通过gm r1 i6汇编指令读取全局元数据寄存器获得基础资产 ID见 sway-lib-std/src/asset_id.sw。原生资产的基础操作本节围绕 examples/native_asset/src/main.sw 示例合约展开覆盖铸造、销毁、转账与查询等完整生命周期操作。标准库函数的具体实现位于 sway-lib-std/src/asset.sw。铸造Mint原生资产铸造新资产必须在合约内部调用std::asset::mint()函数并提供SubId与铸币数量。新铸造的 Coin 由铸币合约本身所有mint(DEFAULT_SUB_ID, mint_amount);如果要铸造同一合约下的另一种资产只需把DEFAULT_SUB_ID替换为自定义的SubId即可。从源码看mint()底层是一个内联汇编mint r1 r2指令见 sway-lib-std/src/asset.sw。铸造到指定实体mint_to()还可以使用std::asset::mint_to()将新铸造的资产直接发送给指定实体需要提供一个目标Identitymint_to(target_identity, DEFAULT_SUB_ID, mint_amount);mint_to()的实现是先mint()再transfer()见 sway-lib-std/src/asset.sw。风险提示标准库在mint_to和transfer的文档注释中反复警告如果目标Identity是合约且该合约没有取回withdrawal功能资产将可能被永久锁定导致 Coins 永久损失。使用前务必确认接收方合约具备取回能力。对外提供铸币能力SRC-3 标准如果你打算允许外部用户通过你的合约铸造资产SRC-3; Mint and Burn Standard 定义了标准化的铸币 API即mint(recipient, vault_sub_id, amount)。Sway-Libs 的 Asset Library 也提供了辅助库帮助你快速实现 SRC-3 标准。销毁Burn原生资产销毁资产必须在铸造该资产的合约内部调用std::asset::burn()函数并提供铸造时使用的SubId与数量。被销毁的 Coin 必须归该合约所有。资产一旦被销毁就不复存在burn(DEFAULT_SUB_ID, burn_amount);同样burn()底层是burn r1 r2汇编指令见 sway-lib-std/src/asset.sw。要允许外部用户销毁资产同样可以参考 SRC-3 标准与 Sway-Libs Asset Library。转账Transfer原生资产在合约内部转账原生资产需要调用std::asset::transfer()函数需提供目标Identity、资产的AssetId以及数量transfer(target, asset_id, coins);transfer()会依据目标类型分派目标是Identity::Address时执行transfer_to_address通过tro指令向变量输出转账目标是Identity::ContractId时执行force_transfer_to_contract通过tr指令转账见 sway-lib-std/src/asset.sw。两种情况下当余额不足或转账数量为零时都会 revert。原生资产与交易Transactions当外部向合约发起一笔携带资产的调用时合约可以通过以下两个函数读取交易上下文。获取交易携带的资产调用std::call_frames::msg_asset_id()查询交易中发送的原生资产let amount msg_asset_id();其实现是从当前调用帧call frame读取数据addi asset_id fp i32见 sway-lib-std/src/call_frames.sw。获取交易携带的数量调用std::context::msg_amount()查询交易中发送的 Coin 数量let amount msg_amount();其实现直接读取余额寄存器balance()见 sway-lib-std/src/context.sw。原生资产与合约余额查询与资产接收查询合约余额在合约内部查询自身余额调用std::context::this_balance()并传入对应的AssetIdthis_balance(asset_id)查询外部合约的余额则调用std::context::balance_of()balance_of(target_contract, asset_id)从实现看this_balance()其实就是balance_of(ContractId::this(), asset_id)见 sway-lib-std/src/context.sw而balance_of()底层通过bal汇编指令读取指定合约指定资产的余额见 sway-lib-std/src/context.sw。注意由于 FuelVM 采用 UTXO 设计Address外部账户的余额无法在 Sway 语言中直接查询必须通过链下 SDK 完成。合约接收原生资产#[payable]默认情况下合约在收到合约调用时不能接收原生资产。如果要允许向合约转账资产需要给对应函数添加#[payable]属性#[payable] fn deposit() { assert(msg_amount() 0); }在 examples/native_asset/src/main.sw 中deposit()函数通过#[payable]声明自己可以接收资产并用msg_amount()断言确实收到了非零数量的 Coin。该示例合约对应的调用场景还可以参考 docs/book/src/blockchain-development/calling_contracts.md 中关于「FuelVM 调用可转发任意原生资产而非仅基础资产」的说明。原生资产标准Native Asset StandardsFuel 生态围绕原生资产开发了一系列标准用于扩展功能并促进跨合约协作。目前与原生资产相关的标准包括SRC-20; Native Asset Standard定义使用 Sway 语言实现原生资产的标准 API对应 ERC-20。SRC-3; Mint and Burn Standard为原生资产启用铸造与销毁功能。SRC-7; Arbitrary Asset Metadata Standard用于为原生资产存储元数据通常用于 NFT 场景见 docs/book/src/introduction/sway_standards.md。SRC-6; Vault Standard定义用 Sway 开发的资产金库vault的标准 API。原生资产库Native Asset Libraries为帮助开发者快速创建符合上述标准的 dAppSway 生态提供了额外的库支持Asset Library提供实现 SRC-20Native Asset、SRC-3Mint and Burn、SRC-7Asset Metadata三大标准所需的功能。该库的使用方法可参考 docs/book/src/reference/sway_libs.md 中关于 Asset Libraries 的说明。实战一单原生资产合约ERC-20 等价物下面的完整示例演示了一个只铸造单一资产的原生资产合约等价于 Ethereum 上的 ERC-20。注意没有任何 token approval 函数。该合约实现了 SRC-20原生资产、SRC-3铸造与销毁、SRC-5所有权标准且不依赖任何外部库// ERC20 equivalent in Sway. contract; use standards::{ src3::SRC3, src5::{ SRC5, State, AccessError, }, src20::{ SetDecimalsEvent, SetNameEvent, SetSymbolEvent, SRC20, TotalSupplyEvent, }, }; use std::{ asset::{ burn, mint_to, }, call_frames::msg_asset_id, constants::DEFAULT_SUB_ID, context::msg_amount, string::String, contract_id::ContractId }; configurable { DECIMALS: u8 9u8, NAME: str[7] __to_str_array(MyAsset), SYMBOL: str[5] __to_str_array(MYTKN), } storage { total_supply: u64 0, owner: State State::Uninitialized, } // Native Asset Standard impl SRC20 for Contract { #[storage(read)] fn total_assets() - u64 { 1 } #[storage(read)] fn total_supply(asset: AssetId) - Optionu64 { if asset AssetId::default() { Some(storage.total_supply.read()) } else { None } } #[storage(read)] fn name(asset: AssetId) - OptionString { if asset AssetId::default() { Some(String::from_ascii_str(from_str_array(NAME))) } else { None } } #[storage(read)] fn symbol(asset: AssetId) - OptionString { if asset AssetId::default() { Some(String::from_ascii_str(from_str_array(SYMBOL))) } else { None } } #[storage(read)] fn decimals(asset: AssetId) - Optionu8 { if asset AssetId::default() { Some(DECIMALS) } else { None } } } // Ownership Standard impl SRC5 for Contract { #[storage(read)] fn owner() - State { storage.owner.read() } } // Mint and Burn Standard impl SRC3 for Contract { #[storage(read, write)] fn mint(recipient: Identity, sub_id: OptionSubId, amount: u64) { require(sub_id.is_some() sub_id.unwrap() DEFAULT_SUB_ID, incorrect-sub-id); require_access_owner(); let new_supply storage.total_supply.read() amount; storage .total_supply .write(new_supply); mint_to(recipient, DEFAULT_SUB_ID, amount); TotalSupplyEvent::new( AssetId::default(), new_supply, msg_sender().unwrap() ).log(); } #[storage(read, write)] fn burn(sub_id: SubId, amount: u64) { require(sub_id DEFAULT_SUB_ID, incorrect-sub-id); require(msg_amount() amount, incorrect-amount-provided); require( msg_asset_id() AssetId::default(), incorrect-asset-provided, ); require_access_owner(); let new_supply storage.total_supply.read() - amount; storage .total_supply .write(new_supply); burn(DEFAULT_SUB_ID, amount); TotalSupplyEvent::new( AssetId::default(), new_supply, msg_sender().unwrap() ).log(); } } abi SingleAsset { #[storage(read, write)] fn constructor(owner_: Identity); } impl SingleAsset for Contract { #[storage(read, write)] fn constructor(owner_: Identity) { require(storage.owner.read() State::Uninitialized, owner-initialized); storage.owner.write(State::Initialized(owner_)); } } #[storage(read)] fn require_access_owner() { require( storage.owner.read() State::Initialized(msg_sender().unwrap()), AccessError::NotOwner, ); } abi EmitSRC20Events { fn emit_src20_events(); } impl EmitSRC20Events for Contract { fn emit_src20_events() { // Metadata that is stored as a configurable should only be emitted once. let asset AssetId::default(); let sender msg_sender().unwrap(); let name Some(String::from_ascii_str(from_str_array(NAME))); let symbol Some(String::from_ascii_str(from_str_array(SYMBOL))); SetNameEvent::new(asset, name, sender).log(); SetSymbolEvent::new(asset, symbol, sender).log(); SetDecimalsEvent::new(asset, DECIMALS, sender).log(); } }该合约的设计要点configurable 配置项DECIMALS、NAME、SYMBOL定义为可配置常量可在部署时通过编译配置覆盖无需修改源码。emit_src20_events()保证元数据事件只发出一次。默认资产约束mint/burn均校验sub_id DEFAULT_SUB_ID确保合约只操作默认资产total_supply、name、symbol、decimals查询也仅对AssetId::default()返回有效值其他资产返回None。所有权保护require_access_owner()通过 SRC-5 的State::Initialized(msg_sender().unwrap())校验调用者是否为合约 ownerconstructor只能被调用一次State::Uninitialized守卫。安全校验burn中通过msg_amount()与msg_asset_id()双重校验防止用错误资产或错误数量触发销毁。实战二多原生资产合约ERC-1155 等价物下面的完整示例演示了一个可以铸造多种资产的原生资产合约等价于 Ethereum 上的 ERC-1155。同样没有任何 token approval 函数。该合约实现了 SRC-20、SRC-3、SRC-5 标准同样不依赖任何外部库// ERC1155 equivalent in Sway. contract; use standards::{ src5::{ SRC5, State, AccessError }, src20::{ SetDecimalsEvent, SetNameEvent, SetSymbolEvent, SRC20, TotalSupplyEvent, } src3::SRC3, }; use std::{ asset::{ burn, mint_to, }, call_frames::msg_asset_id, hash::{ Hash, }, context::this_balance, storage::storage_string::*, string::String, contract_id::ContractId }; storage { total_assets: u64 0, total_supply: StorageMapAssetId, u64 StorageMap {}, name: StorageMapAssetId, StorageString StorageMap {}, symbol: StorageMapAssetId, StorageString StorageMap {}, decimals: StorageMapAssetId, u8 StorageMap {}, owner: State State::Uninitialized, } // Native Asset Standard impl SRC20 for Contract { #[storage(read)] fn total_assets() - u64 { storage.total_assets.read() } #[storage(read)] fn total_supply(asset: AssetId) - Optionu64 { storage.total_supply.get(asset).try_read() } #[storage(read)] fn name(asset: AssetId) - OptionString { storage.name.get(asset).read_slice() } #[storage(read)] fn symbol(asset: AssetId) - OptionString { storage.symbol.get(asset).read_slice() } #[storage(read)] fn decimals(asset: AssetId) - Optionu8 { storage.decimals.get(asset).try_read() } } // Mint and Burn Standard impl SRC3 for Contract { #[storage(read, write)] fn mint(recipient: Identity, sub_id: OptionSubId, amount: u64) { require(sub_id.is_some(), Error: SubId is None); require_access_owner(); let asset_id AssetId::new(ContractId::this(), sub_id.unwrap()); let supply storage.total_supply.get(asset_id).try_read(); if supply.is_none() { storage.total_assets.write(storage.total_assets.try_read().unwrap_or(0) 1); } let new_supply supply.unwrap_or(0) amount; storage.total_supply.insert(asset_id, new_supply); mint_to(recipient, sub_id, amount); TotalSupplyEvent::new( asset_id, new_supply, msg_sender().unwrap() ).log(); } #[storage(read, write)] fn burn(sub_id: SubId, amount: u64) { require_access_owner(); let asset_id AssetId::new(ContractId::this(), sub_id); require(this_balance(asset_id) amount, not-enough-coins); let supply storage.total_supply.get(asset_id).try_read(); let new_supply supply.unwrap_or(0) - amount; storage.total_supply.insert(asset_id, new_supply); burn(sub_id, amount); TotalSupplyEvent::new( asset_id, new_supply, msg_sender().unwrap() ).log(); } } abi MultiAsset { #[storage(read, write)] fn constructor(owner_: Identity); #[storage(read, write)] fn set_name(asset: AssetId, name: OptionString); #[storage(read, write)] fn set_symbol(asset: AssetId, symbol: OptionString); #[storage(read, write)] fn set_decimals(asset: AssetId, decimals: u8); } impl MultiAsset for Contract { #[storage(read, write)] fn constructor(owner_: Identity) { require(storage.owner.read() State::Uninitialized, owner-initialized); storage.owner.write(State::Initialized(owner_)); } #[storage(read, write)] fn set_name(asset: AssetId, name: OptionString) { require_access_owner(); storage.name.insert(asset, StorageString {}); storage.name.get(asset).write_slice(name); SetNameEvent::new(asset, name, msg_sender().unwrap()).log(); } #[storage(read, write)] fn set_symbol(asset: AssetId, symbol: OptionString) { require_access_owner(); storage.symbol.insert(asset, StorageString {}); storage.symbol.get(asset).write_slice(symbol); SetSymbolEvent::new(asset, symbol, msg_sender().unwrap()).log(); } #[storage(read, write)] fn set_decimals(asset: AssetId, decimals: u8) { require_access_owner(); storage.decimals.insert(asset, decimals); SetDecimalsEvent::new(asset, decimals, msg_sender().unwrap()).log(); } } #[storage(read)] fn require_access_owner() { require( storage.owner.read() State::Initialized(msg_sender().unwrap()), AccessError::NotOwner, ); }该合约的设计要点以AssetId为键的 StorageMaptotal_supply、name、symbol、decimals全部以AssetId为键存储天然支持任意数量的资产类型字符串类型使用StorageString存储读取时通过read_slice()/write_slice()操作。动态资产计数mint时若该AssetId首次出现supply.is_none()则total_assets自增维护资产总量。链上元数据管理通过set_name/set_symbol/set_decimals三个 owner 专属方法动态维护每个资产的元数据并发出SetNameEvent、SetSymbolEvent、SetDecimalsEvent事件供链下索引。销毁前余额校验burn中先用this_balance(asset_id)校验合约持有的该资产数量充足not-enough-coins再执行销毁确保不会超额销毁。小结与进一步阅读FuelVM 的原生资产模型将「资产」提升为一等公民任意资产都可像 Ether 一样被发送、接收与查询无需 approval 机制同质化与非同质化资产共用同一套处理逻辑。开发者只需掌握AssetIdsha256((contract_id, sub_id))、SubId与#[payable]三个核心概念再配合std::asset、std::context、std::call_frames标准库即可构建完整的资产合约。如需继续深入可以阅读标准库底层实现sway-lib-std/src/asset.swmint/burn/transfer、sway-lib-std/src/asset_id.swAssetId 构造、sway-lib-std/src/context.sw余额与数量查询可运行示例合约examples/native_asset/src/main.sw含全部 ANCHOR 代码片段相关章节docs/book/src/blockchain-development/calling_contracts.md合约调用中的资产转发、docs/book/src/introduction/sway_standards.mdSway 标准总览、docs/book/src/reference/sway_libs.mdAsset 库定位【免费下载链接】sway Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价