Skip to content

Commit

Permalink
SRC-15; Offchain Metadata Standard (#159)
Browse files Browse the repository at this point in the history
* 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
bitzoic authored Dec 13, 2024
1 parent ccf84fd commit e7d3617
Show file tree
Hide file tree
Showing 13 changed files with 444 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
"examples/src11-security-information",
"examples/src12-contract-factory",
"examples/src14-simple-proxy",
"examples/src15-offchain-metadata",
"examples/src20-native-asset",
]

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Description of the upcoming release here.
### Added

- [#152](https://github.com/FuelLabs/sway-standards/pull/152) Adds inline documentation examples to the SRC-6 standard.
- [#159](https://github.com/FuelLabs/sway-standards/pull/159) Adds the SRC-15 standard.

### Changed

Expand Down
3 changes: 3 additions & 0 deletions docs/spell-check-custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,6 @@ SetDecimalsEvent
UpdateTotalSupplyEvent
Onchain
onchain
Offchain
offchain
MetadataEvent
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
- [SRC-12: Contract Factory](./src-12-contract-factory.md)
- [SRC-13: Soulbound Address](./src-13-soulbound-address.md)
- [SRC-14: Simple Upgradeable Contract](./src-14-simple-upgradeable-proxies.md)
- [SRC-15: Offchain Asset Metadata](./src-15-offchain-asset-metadata.md)
- [SRC-20: Native Asset](./src-20-native-asset.md)
1 change: 1 addition & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ use standards::src20::SRC20;
- [SRC-9; Metadata Keys Standard](./src-9-metadata-keys.md) is used to store standardized metadata keys for [Native Assets](https://docs.fuel.network/docs/sway/blockchain-development/native_assets) in combination with the SRC-7 standard.
- [SRC-6; Vault Standard](./src-6-vault.md) defines the implementation of a standard API for asset vaults developed in Sway.
- [SRC-13; Soulbound Address](./src-13-soulbound-address.md) defines the implementation of a soulbound address.
- [SRC-15; Offchain Asset Metadata Standard](./src-15-offchain-asset-metadata.md) is used to associated metadata with [Native Assets](https://docs.fuel.network/docs/sway/blockchain-development/native_assets) offchain.

### Security and Access Control

Expand Down
73 changes: 73 additions & 0 deletions docs/src/src-15-offchain-asset-metadata.md
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}}
```
2 changes: 2 additions & 0 deletions examples/src15-offchain-metadata/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[workspace]
members = ["single_asset", "multi_asset"]
8 changes: 8 additions & 0 deletions examples/src15-offchain-metadata/multi_asset/Forc.toml
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 examples/src15-offchain-metadata/multi_asset/src/multi_asset.sw
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();
}
}
8 changes: 8 additions & 0 deletions examples/src15-offchain-metadata/single_asset/Forc.toml
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 examples/src15-offchain-metadata/single_asset/src/single_asset.sw
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();
}
}
Loading

0 comments on commit e7d3617

Please sign in to comment.