diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fd3c74e..4087010 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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", ] diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index d847721..1a698f9 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -4,8 +4,19 @@ on: pull_request: jobs: - test: + spell-check: + name: Spell Check uses: FuelLabs/github-actions/.github/workflows/mdbook-docs.yml@master with: - docs-src-path: 'docs/src' - spellcheck-config-path: 'docs/.spellcheck.yml' \ No newline at end of file + docs-src-path: "docs/src" + spellcheck-config-path: "docs/.spellcheck.yml" + + link-check: + name: Link Check + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Run Markdown Link Check + uses: gaurav-nelson/github-action-markdown-link-check@1.0.15 diff --git a/CHANGELOG.md b/CHANGELOG.md index b1d3c0d..4b1d2ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] -Description of the upcoming release here. - ### Added - Something new here 1 @@ -24,14 +22,39 @@ Description of the upcoming release here. - Some fix here 1 - Some fix here 2 -#### Breaking +### Breaking - Some breaking change here 1 - Some breaking change here 2 -## [Version 0.6.1] +## [Version 0.6.2] + +### New Standards v0.6.2 + +- [#159](https://github.com/FuelLabs/sway-standards/pull/159) Defines the SRC-15; Offchain Metadata Standard. + +### Added v0.6.2 + +- [#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 files and docs. +- [#162](https://github.com/FuelLabs/sway-standards/pull/162) Adds link checker to CI. + +### Changed v0.6.2 -Description of the upcoming release here. +- [#154](https://github.com/FuelLabs/sway-standards/pull/154) Updates the examples in the standards specififcations to use the offical abi name. +- [#157](https://github.com/FuelLabs/sway-standards/pull/157) Updates the name of the SRC-7 standard to "Onchain Native Asset Metadata Standard". +- [#163](https://github.com/FuelLabs/sway-standards/pull/163) Prepares for the v0.6.2 release. + +### Fixed v0.6.2 + +- [#153](https://github.com/FuelLabs/sway-standards/pull/153) Actually write to storage in `set_src20_data()` in the SRC-20 multi asset example. +- [#160](https://github.com/FuelLabs/sway-standards/pull/160) Fixes a typo in the SRC-7 inline docs. + +#### Breaking v0.6.2 + +- None + +## [Version 0.6.1] ### Added v0.6.1 @@ -50,6 +73,8 @@ Description of the upcoming release here. - [#137](https://github.com/FuelLabs/sway-standards/pull/137) Resolves warnings for SRC-6, SRC-14, and SRC-5 standard examples. - [#136](https://github.com/FuelLabs/sway-standards/pull/136) Fixes SRC14 to recommend namespacing all non-standardized storage variables under the SRC14 namespace, fixes typos, and improves markdown in docs and inline documentation. - [#142](https://github.com/FuelLabs/sway-standards/pull/142) Fixes errors in inline documentation for SRC-10, SRC-12, SRC-14, SRC-20, SRC-3, SRC-5, SRC-7 standards. +- [#151](https://github.com/FuelLabs/sway-standards/pull/151) Fixes SRC-6 standard examples conform to the latest SRC-20 spec of logging values after updates. +- [#151](https://github.com/FuelLabs/sway-standards/pull/151) Formats code of SRC-6 examples, and fixes some comments. ## [Version 0.6.0] diff --git a/Cargo.toml b/Cargo.toml index 28fcf2d..1aed410 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,4 @@ [package] name = "sway-standards" -version = "0.6.1" +version = "0.6.2" edition = "2021" diff --git a/README.md b/README.md index 3d91784..2624868 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@

- - + + @@ -37,7 +37,7 @@ If you don't find what you're looking for, feel free to create an issue and prop - [SRC-20; Native Asset Standard](https://docs.fuel.network/docs/sway-standards/src-20-native-asset/) defines the implementation of a standard API for [Native Assets](https://docs.fuel.network/docs/sway/blockchain-development/native_assets) using the Sway Language. - [SRC-3; Mint and Burn](https://docs.fuel.network/docs/sway-standards/src-3-minting-and-burning/) is used to enable mint and burn functionality for fungible assets. -- [SRC-7; Arbitrary Asset Metadata Standard](https://docs.fuel.network/docs/sway-standards/src-7-asset-metadata/) is used to store metadata for [Native Assets](https://docs.fuel.network/docs/sway/blockchain-development/native_assets). +- [SRC-7; Onchain Asset Metadata Standard](https://docs.fuel.network/docs/sway-standards/src-7-asset-metadata/) is used to store metadata for [Native Assets](https://docs.fuel.network/docs/sway/blockchain-development/native_assets). - [SRC-9; Metadata Keys Standard](https://docs.fuel.network/docs/sway-standards/src-9-metadata-keys/) 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](https://docs.fuel.network/docs/sway-standards/src-6-vault/) defines the implementation of a standard API for asset vaults developed in Sway. - [SRC-13; Soulbound Address](https://docs.fuel.network/docs/sway-standards/src-13-soulbound-address/) provides a predicate interface to lock [Native Assets](https://docs.fuel.network/docs/sway/blockchain-development/native_assets) as soulbound. @@ -65,7 +65,7 @@ If you don't find what you're looking for, feel free to create an issue and prop To import a standard the following should be added to the project's `Forc.toml` file under `[dependencies]` with the most recent release: ```toml -standards = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.6.1" } +standards = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.6.2" } ``` > **NOTE:** diff --git a/docs/spell-check-custom-words.txt b/docs/spell-check-custom-words.txt index 65384a7..74620cc 100644 --- a/docs/spell-check-custom-words.txt +++ b/docs/spell-check-custom-words.txt @@ -266,3 +266,8 @@ SetNameEvent SetSymbolEvent SetDecimalsEvent UpdateTotalSupplyEvent +Onchain +onchain +Offchain +offchain +MetadataEvent diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index da0c2b7..a42d26c 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -6,7 +6,7 @@ - [SRC-3: Minting and Burning](./src-3-minting-and-burning.md) - [SRC-5: Ownership](./src-5-ownership.md) - [SRC-6: Vault](./src-6-vault.md) -- [SRC-7: Asset Metadata](./src-7-asset-metadata.md) +- [SRC-7: Onchain Asset Metadata](./src-7-asset-metadata.md) - [SRC-8: Bridged Asset](./src-8-bridged-asset.md) - [SRC-9: Metadata Keys](./src-9-metadata-keys.md) - [SRC-10: Native Bridge](./src-10-native-bridge.md) @@ -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) diff --git a/docs/src/index.md b/docs/src/index.md index e7d7b5b..742d95d 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -14,7 +14,7 @@ If you don't find what you're looking for, feel free to create an issue and prop To import a standard the following should be added to the project's `Forc.toml` file under `[dependencies]` with the most recent release: ```toml -standards = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.6.1" } +standards = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.6.2" } ``` > **NOTE:** @@ -38,10 +38,11 @@ use standards::src20::SRC20; - [SRC-20; Native Asset Standard](./src-20-native-asset.md) defines the implementation of a standard API for [Native Assets](https://docs.fuel.network/docs/sway/blockchain-development/native_assets) using the Sway Language. - [SRC-3; Mint and Burn](./src-3-minting-and-burning.md) is used to enable mint and burn functionality for fungible assets. -- [SRC-7; Arbitrary Asset Metadata Standard](./src-7-asset-metadata.md) is used to store metadata for [Native Assets](https://docs.fuel.network/docs/sway/blockchain-development/native_assets). +- [SRC-7; Onchain Asset Metadata Standard](./src-7-asset-metadata.md) is used to store metadata for [Native Assets](https://docs.fuel.network/docs/sway/blockchain-development/native_assets). - [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 diff --git a/docs/src/src-15-offchain-asset-metadata.md b/docs/src/src-15-offchain-asset-metadata.md new file mode 100644 index 0000000..068209b --- /dev/null +++ b/docs/src/src-15-offchain-asset-metadata.md @@ -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}} +``` diff --git a/docs/src/src-20-native-asset.md b/docs/src/src-20-native-asset.md index 9a89c14..78b03dd 100644 --- a/docs/src/src-20-native-asset.md +++ b/docs/src/src-20-native-asset.md @@ -151,7 +151,7 @@ This standard does not introduce any security concerns, as it does not call exte ## Example ABI ```sway -abi MyAsset { +abi SRC20 { #[storage(read)] fn total_assets() -> u64; #[storage(read)] diff --git a/docs/src/src-3-minting-and-burning.md b/docs/src/src-3-minting-and-burning.md index c36b177..f749b96 100644 --- a/docs/src/src-3-minting-and-burning.md +++ b/docs/src/src-3-minting-and-burning.md @@ -56,7 +56,7 @@ The burn function may also introduce a security consideration if the total suppl ## Example ABI ```sway -abi MySRC3Asset { +abi SRC3 { #[storage(read, write)] fn mint(recipient: Identity, sub_id: Option, amount: u64); #[payable] diff --git a/docs/src/src-7-asset-metadata.md b/docs/src/src-7-asset-metadata.md index 001749d..b19e7c5 100644 --- a/docs/src/src-7-asset-metadata.md +++ b/docs/src/src-7-asset-metadata.md @@ -1,10 +1,10 @@ -# SRC-7: Arbitrary Native Asset Metadata +# SRC-7: Onchain Native Asset Metadata -The following standard attempts to define the retrieval of on-chain arbitrary metadata for any [Native Asset](https://docs.fuel.network/docs/sway/blockchain-development/native_assets). Any contract that implements the SRC-7 standard MUST implement the [SRC-20](./src-20-native-asset.md) standard. +The following standard attempts to define the retrieval of on-chain arbitrary metadata for any [Native Asset](https://docs.fuel.network/docs/sway/blockchain-development/native_assets). This standard should be used if a stateful approach is needed. Any contract that implements the SRC-7 standard MUST implement the [SRC-20](./src-20-native-asset.md) standard. ## Motivation -The SRC-7 standard seeks to enable data-rich assets on the Fuel Network while maintaining compatibility between multiple assets minted by the same contract. The standard ensures type safety with the use of an `enum` and an `Option`. All metadata queries are done through a single function to facilitate cross-contract calls. +The SRC-7 standard seeks to enable stateful data-rich assets on the Fuel Network while maintaining compatibility between multiple assets minted by the same contract. The standard ensures type safety with the use of an `enum` and an `Option`. All metadata queries are done through a single function to facilitate cross-contract calls. ## Prior Art @@ -57,7 +57,7 @@ The `SetMetadataEvent` MUST be emitted when the metadata of an asset has updated There SHALL be the following fields in the `SetMetadataEvent` struct: -* `asset`: The `asset` field SHALL be used for the corresponding `AssetId` of the asset has been updated. +* `asset`: The `asset` field SHALL be used for the corresponding `AssetId` for the asset that has been updated. * `metadata`: The `metadata` field SHALL be used for the corresponding `Option` which represents the metadata of the asset. * `key`: The `key` field SHALL be used for the corresponding `String` which represents the key used for storing the metadata. * `sender`: The `sender` field SHALL be used for the corresponding `Identity` which made the function call that has updated the metadata of the asset. @@ -75,7 +75,7 @@ pub struct SetMetadataEvent { ## Rationale -The SRC-7 standard should allow for data-rich assets to interact with one another in a safe manner. +The SRC-7 standard should allow for stateful data-rich assets to interact with one another in a safe manner. ## Backwards Compatibility @@ -88,7 +88,7 @@ This standard does not introduce any security concerns, as it does not call exte ## Example ABI ```sway -abi SRC7Metadata { +abi SRC7 { #[storage(read)] fn metadata(asset: AssetId, key: String) -> Option; } diff --git a/examples/src15-offchain-metadata/Forc.toml b/examples/src15-offchain-metadata/Forc.toml new file mode 100644 index 0000000..c75476c --- /dev/null +++ b/examples/src15-offchain-metadata/Forc.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["single_asset", "multi_asset"] diff --git a/examples/src15-offchain-metadata/multi_asset/Forc.toml b/examples/src15-offchain-metadata/multi_asset/Forc.toml new file mode 100644 index 0000000..5eee9ec --- /dev/null +++ b/examples/src15-offchain-metadata/multi_asset/Forc.toml @@ -0,0 +1,8 @@ +[project] +authors = ["Fuel Labs "] +entry = "multi_asset.sw" +license = "Apache-2.0" +name = "multi_src15_asset" + +[dependencies] +standards = { path = "../../../standards" } diff --git a/examples/src15-offchain-metadata/multi_asset/src/multi_asset.sw b/examples/src15-offchain-metadata/multi_asset/src/multi_asset.sw new file mode 100644 index 0000000..96dac73 --- /dev/null +++ b/examples/src15-offchain-metadata/multi_asset/src/multi_asset.sw @@ -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 = 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 { + storage.total_supply.get(asset).try_read() + } + + #[storage(read)] + fn name(asset: AssetId) -> Option { + 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 { + 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 { + 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(); + } +} diff --git a/examples/src15-offchain-metadata/single_asset/Forc.toml b/examples/src15-offchain-metadata/single_asset/Forc.toml new file mode 100644 index 0000000..c3566cc --- /dev/null +++ b/examples/src15-offchain-metadata/single_asset/Forc.toml @@ -0,0 +1,8 @@ +[project] +authors = ["Fuel Labs "] +entry = "single_asset.sw" +license = "Apache-2.0" +name = "single_src15_asset" + +[dependencies] +standards = { path = "../../../standards" } diff --git a/examples/src15-offchain-metadata/single_asset/src/single_asset.sw b/examples/src15-offchain-metadata/single_asset/src/single_asset.sw new file mode 100644 index 0000000..ea9d9e8 --- /dev/null +++ b/examples/src15-offchain-metadata/single_asset/src/single_asset.sw @@ -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 { + if asset == AssetId::default() { + Some(TOTAL_SUPPLY) + } else { + None + } + } + + #[storage(read)] + fn name(asset: AssetId) -> Option { + if asset == AssetId::default() { + Some(String::from_ascii_str(from_str_array(NAME))) + } else { + None + } + } + + #[storage(read)] + fn symbol(asset: AssetId) -> Option { + if asset == AssetId::default() { + Some(String::from_ascii_str(from_str_array(SYMBOL))) + } else { + None + } + } + + #[storage(read)] + fn decimals(asset: AssetId) -> Option { + 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(); + } +} diff --git a/examples/src20-native-asset/multi_asset/src/multi_asset.sw b/examples/src20-native-asset/multi_asset/src/multi_asset.sw index d8540b7..0fec714 100644 --- a/examples/src20-native-asset/multi_asset/src/multi_asset.sw +++ b/examples/src20-native-asset/multi_asset/src/multi_asset.sw @@ -173,7 +173,7 @@ impl SRC20 for Contract { } abi SetSRC20Data { - #[storage(read)] + #[storage(read, write)] fn set_src20_data( asset: AssetId, total_supply: u64, @@ -184,7 +184,7 @@ abi SetSRC20Data { } impl SetSRC20Data for Contract { - #[storage(read)] + #[storage(read, write)] fn set_src20_data( asset: AssetId, supply: u64, @@ -199,9 +199,32 @@ impl SetSRC20Data for Contract { } let sender = msg_sender().unwrap(); - SetNameEvent::new(asset, name, sender).log(); - SetSymbolEvent::new(asset, symbol, sender).log(); + match name { + Some(unwrapped_name) => { + storage.name.get(asset).write_slice(unwrapped_name); + SetNameEvent::new(asset, name, sender).log(); + }, + None => { + let _ = storage.name.get(asset).clear(); + SetNameEvent::new(asset, name, sender).log(); + } + } + + match symbol { + Some(unwrapped_symbol) => { + storage.symbol.get(asset).write_slice(unwrapped_symbol); + SetSymbolEvent::new(asset, symbol, sender).log(); + }, + None => { + let _ = storage.symbol.get(asset).clear(); + SetSymbolEvent::new(asset, symbol, sender).log(); + } + } + + storage.decimals.get(asset).write(decimals); SetDecimalsEvent::new(asset, decimals, sender).log(); + + storage.total_supply.get(asset).write(supply); TotalSupplyEvent::new(asset, supply, sender).log(); } } diff --git a/examples/src6-vault/multi_asset_vault/src/main.sw b/examples/src6-vault/multi_asset_vault/src/main.sw index ac964e3..1b249ba 100644 --- a/examples/src6-vault/multi_asset_vault/src/main.sw +++ b/examples/src6-vault/multi_asset_vault/src/main.sw @@ -12,7 +12,20 @@ use std::{ string::String, }; -use standards::{src20::SRC20, src6::{Deposit, SRC6, Withdraw}}; +use standards::{ + src20::{ + SetDecimalsEvent, + SetNameEvent, + SetSymbolEvent, + SRC20, + TotalSupplyEvent, + }, + src6::{ + Deposit, + SRC6, + Withdraw, + }, +}; pub struct VaultInfo { /// Amount of assets currently managed by this vault @@ -170,6 +183,58 @@ impl SRC20 for Contract { } } +abi SetSRC20Data { + #[storage(read, write)] + fn set_src20_data( + asset: AssetId, + name: Option, + symbol: Option, + decimals: u8, + ); +} + +impl SetSRC20Data for Contract { + #[storage(read, write)] + fn set_src20_data( + asset: AssetId, + name: Option, + symbol: Option, + decimals: u8, + ) { + // NOTE: There are no checks for if the caller has permissions to update the metadata + // If this asset does not exist, revert + if storage.total_supply.get(asset).try_read().is_none() { + revert(0); + } + let sender = msg_sender().unwrap(); + + match name { + Some(unwrapped_name) => { + storage.name.get(asset).write_slice(unwrapped_name); + SetNameEvent::new(asset, name, sender).log(); + }, + None => { + let _ = storage.name.get(asset).clear(); + SetNameEvent::new(asset, name, sender).log(); + } + } + + match symbol { + Some(unwrapped_symbol) => { + storage.symbol.get(asset).write_slice(unwrapped_symbol); + SetSymbolEvent::new(asset, symbol, sender).log(); + }, + None => { + let _ = storage.symbol.get(asset).clear(); + SetSymbolEvent::new(asset, symbol, sender).log(); + } + } + + storage.decimals.get(asset).write(decimals); + SetDecimalsEvent::new(asset, decimals, sender).log(); + } +} + /// Returns the vault shares assetid and subid for the given assets assetid and the vaults sub id fn vault_asset_id(asset: AssetId, vault_sub_id: SubId) -> (AssetId, SubId) { let share_asset_vault_sub_id = sha256((asset, vault_sub_id)); @@ -229,10 +294,11 @@ pub fn _mint( if supply.is_none() { storage.total_assets.write(storage.total_assets.read() + 1); } - storage - .total_supply - .insert(asset_id, supply.unwrap_or(0) + amount); + let new_supply = supply.unwrap_or(0) + amount; + storage.total_supply.insert(asset_id, new_supply); mint_to(recipient, vault_sub_id, amount); + TotalSupplyEvent::new(asset_id, new_supply, msg_sender().unwrap()) + .log(); } #[storage(read, write)] @@ -245,6 +311,9 @@ pub fn _burn(asset_id: AssetId, vault_sub_id: SubId, amount: u64) { ); // If we pass the check above, we can assume it is safe to unwrap. let supply = storage.total_supply.get(asset_id).try_read().unwrap(); - storage.total_supply.insert(asset_id, supply - amount); + let new_supply = supply - amount; + storage.total_supply.insert(asset_id, new_supply); burn(vault_sub_id, amount); + TotalSupplyEvent::new(asset_id, new_supply, msg_sender().unwrap()) + .log(); } diff --git a/examples/src6-vault/single_asset_single_sub_vault/src/main.sw b/examples/src6-vault/single_asset_single_sub_vault/src/main.sw index 32a43b9..b07c572 100644 --- a/examples/src6-vault/single_asset_single_sub_vault/src/main.sw +++ b/examples/src6-vault/single_asset_single_sub_vault/src/main.sw @@ -12,7 +12,20 @@ use std::{ string::String, }; -use standards::{src20::SRC20, src6::{Deposit, SRC6, Withdraw}}; +use standards::{ + src20::{ + SetDecimalsEvent, + SetNameEvent, + SetSymbolEvent, + SRC20, + TotalSupplyEvent, + }, + src6::{ + Deposit, + SRC6, + Withdraw, + }, +}; configurable { /// The only sub vault that can be deposited and withdrawn from this vault. @@ -25,6 +38,12 @@ storage { managed_assets: u64 = 0, /// The total amount of shares minted by this vault. total_supply: u64 = 0, + /// The name of a specific asset minted by this contract. + name: StorageString = StorageString {}, + /// The symbol of a specific asset minted by this contract. + symbol: StorageString = StorageString {}, + /// The decimals of a specific asset minted by this contract. + decimals: u8 = 9, } impl SRC6 for Contract { @@ -153,7 +172,10 @@ impl SRC20 for Contract { #[storage(read)] fn name(asset: AssetId) -> Option { if asset == vault_assetid() { - Some(String::from_ascii_str("Vault Shares")) + match storage.name.read_slice() { + Some(name) => Some(name), + None => None, + } } else { None } @@ -162,7 +184,10 @@ impl SRC20 for Contract { #[storage(read)] fn symbol(asset: AssetId) -> Option { if asset == vault_assetid() { - Some(String::from_ascii_str("VLTSHR")) + match storage.symbol.read_slice() { + Some(symbol) => Some(symbol), + None => None, + } } else { None } @@ -171,13 +196,62 @@ impl SRC20 for Contract { #[storage(read)] fn decimals(asset: AssetId) -> Option { if asset == vault_assetid() { - Some(9_u8) + Some(storage.decimals.read()) } else { None } } } +abi SetSRC20Data { + #[storage(read, write)] + fn set_src20_data( + asset: AssetId, + name: Option, + symbol: Option, + decimals: u8, + ); +} + +impl SetSRC20Data for Contract { + #[storage(read, write)] + fn set_src20_data( + asset: AssetId, + name: Option, + symbol: Option, + decimals: u8, + ) { + // NOTE: There are no checks for if the caller has permissions to update the metadata + require(asset == vault_assetid(), "INVALID_ASSET_ID"); + let sender = msg_sender().unwrap(); + + match name { + Some(unwrapped_name) => { + storage.name.write_slice(unwrapped_name); + SetNameEvent::new(asset, name, sender).log(); + }, + None => { + let _ = storage.name.clear(); + SetNameEvent::new(asset, name, sender).log(); + } + } + + match symbol { + Some(unwrapped_symbol) => { + storage.symbol.write_slice(unwrapped_symbol); + SetSymbolEvent::new(asset, symbol, sender).log(); + }, + None => { + let _ = storage.symbol.clear(); + SetSymbolEvent::new(asset, symbol, sender).log(); + } + } + + storage.decimals.write(decimals); + SetDecimalsEvent::new(asset, decimals, sender).log(); + } +} + /// Returns the vault shares assetid for the given assets assetid and the vaults sub id fn vault_assetid() -> AssetId { let share_asset_id = AssetId::new(ContractId::this(), PRE_CALCULATED_SHARE_VAULT_SUB_ID); @@ -209,8 +283,11 @@ pub fn _mint(recipient: Identity, amount: u64) { use std::asset::mint_to; let supply = storage.total_supply.read(); - storage.total_supply.write(supply + amount); + let new_supply = supply + amount; + storage.total_supply.write(new_supply); mint_to(recipient, PRE_CALCULATED_SHARE_VAULT_SUB_ID, amount); + TotalSupplyEvent::new(vault_assetid(), new_supply, msg_sender().unwrap()) + .log(); } #[storage(read, write)] @@ -223,6 +300,9 @@ pub fn _burn(asset_id: AssetId, amount: u64) { ); // If we pass the check above, we can assume it is safe to unwrap. let supply = storage.total_supply.read(); - storage.total_supply.write(supply - amount); + let new_supply = supply - amount; + storage.total_supply.write(new_supply); burn(PRE_CALCULATED_SHARE_VAULT_SUB_ID, amount); + TotalSupplyEvent::new(vault_assetid(), new_supply, msg_sender().unwrap()) + .log(); } diff --git a/examples/src6-vault/single_asset_vault/src/main.sw b/examples/src6-vault/single_asset_vault/src/main.sw index 2a282b5..9ad6407 100644 --- a/examples/src6-vault/single_asset_vault/src/main.sw +++ b/examples/src6-vault/single_asset_vault/src/main.sw @@ -12,7 +12,20 @@ use std::{ string::String, }; -use standards::{src20::SRC20, src6::{Deposit, SRC6, Withdraw}}; +use standards::{ + src20::{ + SetDecimalsEvent, + SetNameEvent, + SetSymbolEvent, + SRC20, + TotalSupplyEvent, + }, + src6::{ + Deposit, + SRC6, + Withdraw, + }, +}; pub struct VaultInfo { /// Amount of assets currently managed by this vault @@ -150,7 +163,7 @@ impl SRC6 for Contract { underlying_asset == AssetId::base(), storage.vault_info.get(vault_share_asset).try_read(), ) { - // In this implementation managed_assets and max_withdrawable are the same. However in case of lending out of assets, total_assets should be greater than max_withdrawable. + // In this implementation managed_assets and max_withdrawable are the same. However in case of lending out of assets, managed_assets should be greater than max_withdrawable. (true, Some(vault_info)) => Some(vault_info.managed_assets), _ => None, } @@ -184,6 +197,58 @@ impl SRC20 for Contract { } } +abi SetSRC20Data { + #[storage(read, write)] + fn set_src20_data( + asset: AssetId, + name: Option, + symbol: Option, + decimals: u8, + ); +} + +impl SetSRC20Data for Contract { + #[storage(read, write)] + fn set_src20_data( + asset: AssetId, + name: Option, + symbol: Option, + decimals: u8, + ) { + // NOTE: There are no checks for if the caller has permissions to update the metadata + // If this asset does not exist, revert + if storage.total_supply.get(asset).try_read().is_none() { + revert(0); + } + let sender = msg_sender().unwrap(); + + match name { + Some(unwrapped_name) => { + storage.name.get(asset).write_slice(unwrapped_name); + SetNameEvent::new(asset, name, sender).log(); + }, + None => { + let _ = storage.name.get(asset).clear(); + SetNameEvent::new(asset, name, sender).log(); + } + } + + match symbol { + Some(unwrapped_symbol) => { + storage.symbol.get(asset).write_slice(unwrapped_symbol); + SetSymbolEvent::new(asset, symbol, sender).log(); + }, + None => { + let _ = storage.symbol.get(asset).clear(); + SetSymbolEvent::new(asset, symbol, sender).log(); + } + } + + storage.decimals.get(asset).write(decimals); + SetDecimalsEvent::new(asset, decimals, sender).log(); + } +} + /// Returns the vault shares assetid and subid for the given assets assetid and the vaults sub id fn vault_asset_id(underlying_asset: AssetId, vault_sub_id: SubId) -> (AssetId, SubId) { let share_asset_vault_sub_id = sha256((underlying_asset, vault_sub_id)); @@ -243,11 +308,11 @@ pub fn _mint( if supply.is_none() { storage.total_assets.write(storage.total_assets.read() + 1); } - let current_supply = supply.unwrap_or(0); - storage - .total_supply - .insert(asset_id, current_supply + amount); + let new_supply = supply.unwrap_or(0) + amount; + storage.total_supply.insert(asset_id, new_supply); mint_to(recipient, vault_sub_id, amount); + TotalSupplyEvent::new(asset_id, new_supply, msg_sender().unwrap()) + .log(); } #[storage(read, write)] @@ -260,6 +325,9 @@ pub fn _burn(asset_id: AssetId, vault_sub_id: SubId, amount: u64) { ); // If we pass the check above, we can assume it is safe to unwrap. let supply = storage.total_supply.get(asset_id).try_read().unwrap(); - storage.total_supply.insert(asset_id, supply - amount); + let new_supply = supply - amount; + storage.total_supply.insert(asset_id, new_supply); burn(vault_sub_id, amount); + TotalSupplyEvent::new(asset_id, new_supply, msg_sender().unwrap()) + .log(); } diff --git a/mlc_config.json b/mlc_config.json new file mode 100644 index 0000000..26061fe --- /dev/null +++ b/mlc_config.json @@ -0,0 +1,10 @@ +{ + "ignorePatterns": [ + { + "pattern": "^https://crates\\.io.*" + }, + { + "pattern": "https://github.com/FuelLabs/devrel-requests/issues/new/choose" + } + ] +} diff --git a/standards/src/src15.sw b/standards/src/src15.sw new file mode 100644 index 0000000..7facc3e --- /dev/null +++ b/standards/src/src15.sw @@ -0,0 +1,104 @@ +library; + +use ::src7::Metadata; + +/// The required event to be emitted for the SRC-15 standard. +pub struct SRC15MetadataEvent { + /// The asset for which metadata is associated with. + pub asset: AssetId, + /// The Metadata of the SRC-15 event. + pub metadata: Metadata, +} + +impl core::ops::Eq for SRC15MetadataEvent { + fn eq(self, other: Self) -> bool { + self.asset == other.asset && self.metadata == other.metadata + } +} + +impl SRC15MetadataEvent { + /// Returns a new `SRC15MetadataEvent` event. + /// + /// # Arguments + /// + /// * `asset`: [AssetId] - The asset for which metadata is set. + /// * `metadata`: [Option] - The Metadata that is set. + /// + /// # Returns + /// + /// * [SRC15MetadataEvent] - The new `SRC15MetadataEvent` event. + /// + /// # Examples + /// + /// ```sway + /// use standards::{src7::Metadata, src15::SRC15MetadataEvent}; + /// + /// fn foo(asset: AssetId, metadata: Metadata) { + /// let my_src15_metadata_event = SRC15MetadataEvent::new(asset, metadata); + /// assert(my_src15_metadata_event.asset == asset); + /// assert(my_src15_metadata_event.metadata == metadata); + /// } + /// ``` + pub fn new(asset: AssetId, metadata: Metadata) -> Self { + Self { + asset, + metadata, + } + } + + /// Returns the asset of the `SRC15MetadataEvent` event. + /// + /// # Returns + /// + /// * [AssetId] - The asset for the event. + /// + /// # Examples + /// + /// ```sway + /// use standards::{src7::Metadata, src15::SRC15MetadataEvent}; + /// + /// fn foo(asset: AssetId, metadata: Metadata) { + /// let my_src15_metadata_event = SRC15MetadataEvent::new(asset, metadata); + /// assert(my_src15_metadata_event.asset() == asset); + /// } + /// ``` + pub fn asset(self) -> AssetId { + self.asset + } + + /// Returns the metadata of the `SRC15MetadataEvent` event. + /// + /// # Returns + /// + /// * [Option] - The metadata for the event. + /// + /// # Examples + /// + /// ```sway + /// use standards::{src7::Metadata, src15::SRC15MetadataEvent}; + /// + /// fn foo(asset: AssetId, metadata: Metadata) { + /// let my_src15_metadata_event = SRC15MetadataEvent::new(asset, metadata); + /// assert(my_src15_metadata_event.metadata() == metadata); + /// } + /// ``` + pub fn metadata(self) -> Metadata { + self.metadata + } + + /// Logs the `SRC15MetadataEvent`. + /// + /// # Examples + /// + /// ```sway + /// use standards::{src7::Metadata, src15::SRC15MetadataEvent}; + /// + /// fn foo(asset: AssetId, metadata: Metadata) { + /// let my_event = SRC15MetadataEvent::new(asset, metadata); + /// my_event.log(); + /// } + /// ``` + pub fn log(self) { + log(self); + } +} diff --git a/standards/src/src6.sw b/standards/src/src6.sw index 5d5fe4a..c0c756e 100644 --- a/standards/src/src6.sw +++ b/standards/src/src6.sw @@ -53,6 +53,28 @@ abi SRC6 { /// * If the asset is not supported by the contract. /// * If the amount of assets forwarded to the contract is zero. /// * The user crosses any global or user specific deposit limits. + /// + /// # Examples + /// + /// ```sway + /// use standards::src6::SRC6; + /// + /// fn foo( + /// contract_id: ContractId, + /// receiver: Identity, + /// vault_sub_id: SubId, + /// amount: u64, + /// asset_id: AssetId + /// ) { + /// let contract_abi = abi(SRC6, contract_id.bits()); + /// let minted_shares: u64 = contract_abi.deposit { + /// gas: 10000, + /// coins: amount, + /// asset_id: asset_id.bits() + /// } (receiver, vault_sub_id); + /// assert(minted_shares != 0); + /// } + /// ``` #[payable] #[storage(read, write)] fn deposit(receiver: Identity, vault_sub_id: SubId) -> u64; @@ -79,6 +101,27 @@ abi SRC6 { /// * If the amount of shares is zero. /// * If the transferred shares do not corresspond to the given asset. /// * The user crosses any global or user specific withdrawal limits. + /// + /// # Examples + /// + /// ```sway + /// use standards::src6::SRC6; + /// + /// fn foo( + /// contract_id: ContractId, + /// receiver: Identity, + /// underlying_asset: AssetId, + /// vault_sub_id: SubId, + /// share_asset_id: AssetId, + /// amount: u64 + /// ) { + /// let contract_abi = abi(SRC6, contract_id.bits()); + /// let withdrawn_amount: u64 = contract_abi.withdraw { + /// gas: 10000, + /// coins: amount, + /// asset_id: share_asset_id.bits() + /// } (receiver, underlying_asset, vault_sub_id); + /// assert(withdrawn_amount != 0); #[payable] #[storage(read, write)] fn withdraw( @@ -97,6 +140,22 @@ abi SRC6 { /// # Returns /// /// * [u64] - The amount of managed assets of the given asset. + /// + /// # Examples + /// + /// ```sway + /// use standards::src6::SRC6; + /// + /// fn foo( + /// contract_id: ContractId, + /// underlying_asset: AssetId, + /// vault_sub_id: SubId + /// ) { + /// let contract_abi = abi(SRC6, contract_id.bits()); + /// let managed_assets: u64 = contract_abi.managed_assets(underlying_asset, vault_sub_id); + /// assert(managed_assets != 0); + /// } + /// ``` #[storage(read)] fn managed_assets(underlying_asset: AssetId, vault_sub_id: SubId) -> u64; @@ -116,6 +175,23 @@ abi SRC6 { /// /// * [Some(u64)] - The maximum amount of assets that can be deposited into the contract, for the given asset. /// * [None] - If the asset is not supported by the contract. + /// + /// # Examples + /// + /// ```sway + /// use standards::src6::SRC6; + /// + /// fn foo( + /// contract_id: ContractId, + /// receiver: Identity, + /// underlying_asset: AssetId, + /// vault_sub_id: SubId + /// ) { + /// let contract_abi = abi(SRC6, contract_id.bits()); + /// let max_depositable: u64 = contract_abi.max_depositable(receiver, underlying_asset, vault_sub_id).unwrap(); + /// assert(max_depositable != 0); + /// } + /// ``` #[storage(read)] fn max_depositable( receiver: Identity, @@ -138,6 +214,22 @@ abi SRC6 { /// /// * [Some(u64)] - The maximum amount of assets that can be withdrawn from the contract, for the given asset. /// * [None] - If the asset is not supported by the contract. + /// + /// # Examples + /// + /// ```sway + /// use standards::src6::SRC6; + /// + /// fn foo( + /// contract_id: ContractId, + /// underlying_asset: AssetId, + /// vault_sub_id: SubId + /// ) { + /// let contract_abi = abi(SRC6, contract_id.bits()); + /// let max_withdrawable: u64 = contract_abi.max_withdrawable(underlying_asset, vault_sub_id).unwrap(); + /// assert(max_withdrawable != 0); + /// } + /// ``` #[storage(read)] fn max_withdrawable(underlying_asset: AssetId, vault_sub_id: SubId) -> Option; } diff --git a/standards/src/src7.sw b/standards/src/src7.sw index e8a0dcc..8b0289d 100644 --- a/standards/src/src7.sw +++ b/standards/src/src7.sw @@ -88,7 +88,7 @@ impl SetMetadataEvent { /// /// * `asset`: [AssetId] - The asset for which metadata is set. /// * `metadata`: [Option] - The Metadata that is set. - /// * `ket`: [String] - The key used for the metadata. + /// * `key`: [String] - The key used for the metadata. /// * `sender`: [Identity] - The caller that set the metadata. /// /// # Returns diff --git a/standards/src/standards.sw b/standards/src/standards.sw index 0fea34b..67698b1 100644 --- a/standards/src/standards.sw +++ b/standards/src/standards.sw @@ -8,4 +8,5 @@ pub mod src10; pub mod src11; pub mod src12; pub mod src14; +pub mod src15; pub mod src20;