-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SRC-15; Offchain Metadata Standard (#159)
* Create SRC-15 specifications * Create SRC-15 standard * Create SRC-15 examples * Add SRC-15 examples to CI * Update CHANGELOG * Run formatter * Fix markdown * Update custom words spellcheck * Remove sender and add nonce to SRC15 event * Update standards and examples with nonce * Resolve review comments * Add comment on restricting who may emit * Remove nonce from SRC-15 log
- Loading branch information
Showing
13 changed files
with
444 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -268,3 +268,6 @@ SetDecimalsEvent | |
UpdateTotalSupplyEvent | ||
Onchain | ||
onchain | ||
Offchain | ||
offchain | ||
MetadataEvent |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# SRC-15: Off-Chain Native Asset Metadata | ||
|
||
The following standard attempts to define arbitrary metadata for any [Native Asset](https://docs.fuel.network/docs/sway/blockchain-development/native_assets) that is not required by other contracts onchain, in a stateless manner. Any contract that implements the SRC-15 standard MUST implement the [SRC-20](./src-20-native-asset.md) standard. | ||
|
||
## Motivation | ||
|
||
The SRC-15 standard seeks to enable data-rich assets on the Fuel Network while maintaining a stateless solution. All metadata queries are done off-chain using the indexer. | ||
|
||
## Prior Art | ||
|
||
The SRC-7 standard exists prior to the SRC-15 standard and is a stateful solution. The SRC-15 builds off the SRC-7 standard by using the `Metadata` enum however provides a stateless solution. | ||
|
||
The use of generic metadata was originally found in the Sway-Lib's [NFT Library](https://github.com/FuelLabs/sway-libs/tree/v0.12.0/libs/nft) which did not use Fuel's [Native Assets](https://docs.fuel.network/docs/sway/blockchain-development/native_assets). This library has since been deprecated. | ||
|
||
A previous definition for a metadata standard was written in the original edit of the now defunct [SRC-721](https://github.com/FuelLabs/sway-standards/issues/2). This has since been replaced with the [SRC-20](./src-20-native-asset.md) standard as `SubId` was introduced to enable multiple assets to be minted from a single contract. | ||
|
||
## Specification | ||
|
||
### Metadata Type | ||
|
||
The `Metadata` enum from the SRC-7 standard is also used to represent the metadata in the SRC-15 standard. | ||
|
||
### Logging | ||
|
||
The following logs MUST be implemented and emitted to follow the SRC-15 standard. Logging MUST be emitted from the contract which minted the asset. | ||
|
||
#### SRC15MetadataEvent | ||
|
||
The `SRC15MetadataEvent` MUST be emitted at least once for each distinct piece of metadata. The latest emitted `SRC15MetadataEvent` is determined to be the current metadata. | ||
|
||
There SHALL be the following fields in the `SRC15MetadataEvent` struct: | ||
|
||
* `asset`: The `asset` field SHALL be used for the corresponding `AssetId` for the metadata. | ||
* `metadata`: The `metadata` field SHALL be used for the corresponding `Metadata` which represents the metadata of the asset. | ||
|
||
Example: | ||
|
||
```sway | ||
pub struct SRC15MetadataEvent { | ||
pub asset: AssetId, | ||
pub metadata: Metadata, | ||
} | ||
``` | ||
|
||
## Rationale | ||
|
||
The SRC-15 standard allows for data-rich assets in a stateless manner by associating an asset with some metadata that may later be fetched by the indexer. | ||
|
||
## Backwards Compatibility | ||
|
||
This standard is compatible with Fuel's [Native Assets](https://docs.fuel.network/docs/sway/blockchain-development/native_assets) and the [SRC-20](./src-20-native-asset.md) standard. This standard is also compatible with the SRC-7 standard which defines a stateful solution. It also maintains compatibility with existing standards in other ecosystems. | ||
|
||
## Security Considerations | ||
|
||
When indexing for SRC-15 metadata, developers should confirm that the contract that emitted the `SRC15MetadataEvent` is also the contract that minted the asset that the metadata associates with. Additionally, restrictions via access control on who may emit the Metadata should be considered. | ||
|
||
## Example Implementation | ||
|
||
### Single Native Asset | ||
|
||
Example of the SRC-15 implementation where metadata exists for only a single asset with one `SubId`. | ||
|
||
```sway | ||
{{#include ../examples/src15-offchain-metadata/single_asset/src/single_asset.sw}} | ||
``` | ||
|
||
### Multi Native Asset | ||
|
||
Example of the SRC-15 implementation where metadata exists for multiple assets with differing `SubId` values. | ||
|
||
```sway | ||
{{#include ../examples/src15-offchain-metadata/multi_asset/src/multi_asset.sw}} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[workspace] | ||
members = ["single_asset", "multi_asset"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
[project] | ||
authors = ["Fuel Labs <contact@fuel.sh>"] | ||
entry = "multi_asset.sw" | ||
license = "Apache-2.0" | ||
name = "multi_src15_asset" | ||
|
||
[dependencies] | ||
standards = { path = "../../../standards" } |
123 changes: 123 additions & 0 deletions
123
examples/src15-offchain-metadata/multi_asset/src/multi_asset.sw
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
contract; | ||
|
||
use standards::{ | ||
src15::{ | ||
SRC15MetadataEvent, | ||
}, | ||
src20::{ | ||
SetDecimalsEvent, | ||
SetNameEvent, | ||
SetSymbolEvent, | ||
SRC20, | ||
TotalSupplyEvent, | ||
}, | ||
src7::{ | ||
Metadata, | ||
}, | ||
}; | ||
|
||
use std::{hash::Hash, storage::storage_string::*, string::String}; | ||
|
||
// In this example, all assets minted from this contract have the same decimals, name, and symbol | ||
configurable { | ||
/// The decimals of every asset minted by this contract. | ||
DECIMALS: u8 = 0u8, | ||
/// The name of every asset minted by this contract. | ||
NAME: str[7] = __to_str_array("MyAsset"), | ||
/// The symbol of every asset minted by this contract. | ||
SYMBOL: str[5] = __to_str_array("MYAST"), | ||
/// The metadata for the "social:x" key. | ||
SOCIAL_X: str[12] = __to_str_array("fuel_network"), | ||
/// The metadata for the "site:forum" key. | ||
SITE_FORUM: str[27] = __to_str_array("https://forum.fuel.network/"), | ||
} | ||
|
||
storage { | ||
/// The total number of distinguishable assets this contract has minted. | ||
total_assets: u64 = 0, | ||
/// The total supply of a particular asset. | ||
total_supply: StorageMap<AssetId, u64> = StorageMap {}, | ||
} | ||
|
||
abi EmitSRC15Events { | ||
#[storage(read)] | ||
fn emit_src15_events(asset: AssetId, svg_image: String, health_attribute: u64); | ||
} | ||
|
||
impl EmitSRC15Events for Contract { | ||
#[storage(read)] | ||
fn emit_src15_events(asset: AssetId, svg_image: String, health_attribute: u64) { | ||
// NOTE: There are no checks for if the caller has permissions to emit the metadata | ||
// NOTE: Nothing is stored in storage and there is no method to retrieve the configurables. | ||
|
||
// If this asset does not exist, revert | ||
if storage.total_supply.get(asset).try_read().is_none() { | ||
revert(0); | ||
} | ||
|
||
let metadata_1 = Metadata::String(String::from_ascii_str(from_str_array(SOCIAL_X))); | ||
let metadata_2 = Metadata::String(String::from_ascii_str(from_str_array(SITE_FORUM))); | ||
let metadata_3 = Metadata::String(svg_image); | ||
let metadata_4 = Metadata::Int(health_attribute); | ||
|
||
SRC15MetadataEvent::new(asset, metadata_1).log(); | ||
SRC15MetadataEvent::new(asset, metadata_2).log(); | ||
SRC15MetadataEvent::new(asset, metadata_3).log(); | ||
SRC15MetadataEvent::new(asset, metadata_4).log(); | ||
} | ||
} | ||
|
||
// SRC15 extends SRC20, so this must be included | ||
impl SRC20 for Contract { | ||
#[storage(read)] | ||
fn total_assets() -> u64 { | ||
storage.total_assets.read() | ||
} | ||
|
||
#[storage(read)] | ||
fn total_supply(asset: AssetId) -> Option<u64> { | ||
storage.total_supply.get(asset).try_read() | ||
} | ||
|
||
#[storage(read)] | ||
fn name(asset: AssetId) -> Option<String> { | ||
match storage.total_supply.get(asset).try_read() { | ||
Some(_) => Some(String::from_ascii_str(from_str_array(NAME))), | ||
None => None, | ||
} | ||
} | ||
|
||
#[storage(read)] | ||
fn symbol(asset: AssetId) -> Option<String> { | ||
match storage.total_supply.get(asset).try_read() { | ||
Some(_) => Some(String::from_ascii_str(from_str_array(SYMBOL))), | ||
None => None, | ||
} | ||
} | ||
|
||
#[storage(read)] | ||
fn decimals(asset: AssetId) -> Option<u8> { | ||
match storage.total_supply.get(asset).try_read() { | ||
Some(_) => Some(DECIMALS), | ||
None => None, | ||
} | ||
} | ||
} | ||
|
||
abi EmitSRC20Data { | ||
fn emit_src20_data(asset: AssetId, total_supply: u64); | ||
} | ||
|
||
impl EmitSRC20Data for Contract { | ||
fn emit_src20_data(asset: AssetId, supply: u64) { | ||
// NOTE: There are no checks for if the caller has permissions to update the metadata | ||
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(); | ||
TotalSupplyEvent::new(asset, supply, sender).log(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
[project] | ||
authors = ["Fuel Labs <contact@fuel.sh>"] | ||
entry = "single_asset.sw" | ||
license = "Apache-2.0" | ||
name = "single_src15_asset" | ||
|
||
[dependencies] | ||
standards = { path = "../../../standards" } |
118 changes: 118 additions & 0 deletions
118
examples/src15-offchain-metadata/single_asset/src/single_asset.sw
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
contract; | ||
|
||
use standards::{ | ||
src15::{ | ||
SRC15MetadataEvent, | ||
}, | ||
src20::{ | ||
SetDecimalsEvent, | ||
SetNameEvent, | ||
SetSymbolEvent, | ||
SRC20, | ||
TotalSupplyEvent, | ||
}, | ||
src7::{ | ||
Metadata, | ||
}, | ||
}; | ||
|
||
use std::string::String; | ||
|
||
configurable { | ||
/// The total supply of coins for the asset minted by this contract. | ||
TOTAL_SUPPLY: u64 = 100_000_000, | ||
/// The decimals of the asset minted by this contract. | ||
DECIMALS: u8 = 9u8, | ||
/// The name of the asset minted by this contract. | ||
NAME: str[7] = __to_str_array("MyAsset"), | ||
/// The symbol of the asset minted by this contract. | ||
SYMBOL: str[5] = __to_str_array("MYTKN"), | ||
/// The metadata for the "social:x" key. | ||
SOCIAL_X: str[12] = __to_str_array("fuel_network"), | ||
/// The metadata for the "site:forum" key. | ||
SITE_FORUM: str[27] = __to_str_array("https://forum.fuel.network/"), | ||
/// The metadata for the "attr:health" key. | ||
ATTR_HEALTH: u64 = 100, | ||
} | ||
|
||
abi EmitSRC15Events { | ||
fn emit_src15_events(); | ||
} | ||
|
||
impl EmitSRC15Events for Contract { | ||
fn emit_src15_events() { | ||
// NOTE: There are no checks for if the caller has permissions to emit the metadata. | ||
// NOTE: Nothing is stored in storage and there is no method to retrieve the configurables. | ||
let asset = AssetId::default(); | ||
let metadata_1 = Metadata::String(String::from_ascii_str(from_str_array(SOCIAL_X))); | ||
let metadata_2 = Metadata::String(String::from_ascii_str(from_str_array(SITE_FORUM))); | ||
let metadata_3 = Metadata::Int(ATTR_HEALTH); | ||
|
||
SRC15MetadataEvent::new(asset, metadata_1).log(); | ||
SRC15MetadataEvent::new(asset, metadata_2).log(); | ||
SRC15MetadataEvent::new(asset, metadata_3).log(); | ||
} | ||
} | ||
|
||
// SRC15 extends SRC20, so this must be included | ||
impl SRC20 for Contract { | ||
#[storage(read)] | ||
fn total_assets() -> u64 { | ||
1 | ||
} | ||
|
||
#[storage(read)] | ||
fn total_supply(asset: AssetId) -> Option<u64> { | ||
if asset == AssetId::default() { | ||
Some(TOTAL_SUPPLY) | ||
} else { | ||
None | ||
} | ||
} | ||
|
||
#[storage(read)] | ||
fn name(asset: AssetId) -> Option<String> { | ||
if asset == AssetId::default() { | ||
Some(String::from_ascii_str(from_str_array(NAME))) | ||
} else { | ||
None | ||
} | ||
} | ||
|
||
#[storage(read)] | ||
fn symbol(asset: AssetId) -> Option<String> { | ||
if asset == AssetId::default() { | ||
Some(String::from_ascii_str(from_str_array(SYMBOL))) | ||
} else { | ||
None | ||
} | ||
} | ||
|
||
#[storage(read)] | ||
fn decimals(asset: AssetId) -> Option<u8> { | ||
if asset == AssetId::default() { | ||
Some(DECIMALS) | ||
} else { | ||
None | ||
} | ||
} | ||
} | ||
|
||
abi EmitSRC20Events { | ||
fn emit_src20_events(); | ||
} | ||
|
||
impl EmitSRC20Events for Contract { | ||
fn emit_src20_events() { | ||
// Metadata that is stored as a configurable must 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(); | ||
TotalSupplyEvent::new(asset, TOTAL_SUPPLY, sender).log(); | ||
} | ||
} |
Oops, something went wrong.