Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SRC-15; Offchain Metadata Standard #159

Merged
merged 13 commits into from
Dec 13, 2024
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
77 changes: 77 additions & 0 deletions docs/src/src-15-offchain-asset-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# 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 once for each distinct piece of 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.
* `nonce`: The nonce of the metadata. This SHALL begin at zero and increment by one upon every emission such that each emission has a unique nonce.

Example:

```sway
pub struct SRC15MetadataEvent {
pub asset: AssetId,
pub metadata: Metadata,
pub nonce: u64,
}
```

## 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.

This standard does not introduce any onchain security concerns, as it does not call external contracts, nor does it define any mutations of the contract state.

## 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" }
129 changes: 129 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,129 @@
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 {},
/// The nonce for the SRC15 Metadata event.
src_15_nonce: u64 = 0,
}

abi EmitSRC15Events {
#[storage(read, write)]
fn emit_src15_events(asset: AssetId, svg_image: String, health_attribute: u64);
}

impl EmitSRC15Events for Contract {
#[storage(read, write)]
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);

// Update the nonce
let nonce = storage.src_15_nonce.read();
storage.src_15_nonce.write(nonce + 1);

SRC15MetadataEvent::new(asset, metadata_1, nonce).log();
SRC15MetadataEvent::new(asset, metadata_2, nonce).log();
SRC15MetadataEvent::new(asset, metadata_3, nonce).log();
SRC15MetadataEvent::new(asset, metadata_4, nonce).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" }
129 changes: 129 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,129 @@
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,
}

storage {
/// The nonce for the SRC15 Metadata event.
src_15_nonce: u64 = 0,
bitzoic marked this conversation as resolved.
Show resolved Hide resolved
}

abi EmitSRC15Events {
#[storage(read, write)]
fn emit_src15_events();
}

impl EmitSRC15Events for Contract {
#[storage(read, write)]
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);

// Update the nonce
let nonce = storage.src_15_nonce.read();
storage.src_15_nonce.write(nonce + 1);

SRC15MetadataEvent::new(asset, metadata_1, nonce).log();
SRC15MetadataEvent::new(asset, metadata_2, nonce).log();
SRC15MetadataEvent::new(asset, metadata_3, nonce).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
Loading