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

NEP: SBT Metadata Standard #504

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
249 changes: 249 additions & 0 deletions neps/nep-504.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
---
NEP: 504
Title: SBT Metadata Interface
Authors: Robert Zaremba (@robert-zaremba)
Status: New
DiscussionsTo: https://github.com/nearprotocol/neps/pull/393
Type: Contract Standard
Version: 1.0.0
Created: 2023-08-21
LastUpdated: 2023-09-29
---

## Summary

A standard interface for [SBT](https://github.com/near/NEPs/pull/393) Metadata: tokens, classes and issuers.
This allows an SBT registry and issuers to be interrogated for its name and for details an SBT represent.

The proposal addresses potential challenges related to metadata reconstruction, off-chain storage security, owner/operator dependence, and data privacy.

## Motivation

[NEP-393](https://github.com/near/NEPs/pull/393) defines a standard for Soul Bound Tokens, which are a foundational for Decentralized Society. SBTs can represent certificates, badges and anything that should be bound to a "soul".
The contract issuers often need more details than what NEP-393 defines. Such details are meant to be set in the `reference` field, which has an open, non standardized format.

NEAR doesn't have a well structured Metadata standard. The [NFT Metadata](https://github.com/near/NEPs/blob/master/neps/nep-0177.md) doesn't clearly define the `reference` extension. In fact, the NEP-393 Metadata is defined based on NFT Metadata. The schema defined here can be used by other token variations as well.

Issuers, contract implementers and users demand a guideline and a minimum schema to make better integration with tools and projects. This document defines a minimum common schema for reference fields of `ContractMetadata` and `TokenMetadata`. Moreover, while working with various issuers, we found that issuers and minters often duplicate lot of data in the `TokenMetadata`. We found that it is unnecessary and it would be useful to create a new type: `ClassMetadata`, that defines common data for all tokens of a given (issuer, class) pair.

## Specification

NEP-393 `TokenMetadata` is minimalistic. It only requires data which a contract can reason about, and all user facing details (such as title, description) are meant to be part of the reference.
Similarly, `ContractMetadata` only defines issuer basic info.

### Class Metadata
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The big question is: where the class metadata is actually used? Any API (view/call methods)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should represent information about the whole class. For example see the "SBT" cards https://i-am-human.app/community-sbts - it will use information from the class metadata (currently this is hardcoded in the UI).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robert-zaremba I think I still don't get whether it's returned via API, or is posted as a log string in the L1 block (?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassMetadata is stored in the Issuer, and returned via an issuer call. Will add this information

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added in the paragraph below


We start by defining a `ClassMetadata` which will contain all common information about tokens of the same class. The `ClassMeetadata` object must be stored in the SBT issuer contract and accessible via `class_metadata(class: ClassId)` query.
This will allow to avoid duplicating such information in all `TokenMetadata` and potentially allow to leave `TokenMetadata.reference` empty, if the tokens only differentiate by the class, issued_at and expires_at attributes.

```rust
pub struct ClassMetadata {
/// Issuer class name. Required.
pub name: String,
/// If defined, should be used instead of `contract_metadata.symbol`.
pub symbol: Option<String>,
/// An URL to an Icon. To protect fellow developers from unintentionally triggering any
/// SSRF vulnerabilities with URL parsers, we don't allow to set an image bytes here.
/// If it doesn't start with a scheme (eg: https://)
/// then `contract_metadata.base_uri` should be prepended.
pub icon: Option<String>,
/// JSON or an URL to a JSON file with more info. If it doesn't start with a scheme
/// (eg: https://) then base_uri should be prepended.
pub reference: Option<String>,
/// Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included.
pub reference_hash: Option<Base64VecU8>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robert-zaremba Let me try to deliver my point once again:

NEPs/neps/nep-0177.md

Lines 45 to 46 in 6eee9dc

reference: string|null, // URL to a JSON file with more info
reference_hash: string|null, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included.

reference field is designed to be a link and nothing else.

If you want to have more fields stored as part of ClassMetadata, just add those fields on the same level as name, symbol, ... fields - this is not a breaking change and won't affect consumers of the API as they will just ignore unsupported fields. There is no need in string-encoding JSON data into the reference field.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. For link vs having reference directly. For small content, I think it makes sense to store it directly, rather than querying another resource.

Copy link
Contributor Author

@robert-zaremba robert-zaremba Oct 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue about adding more records to the ClassMetadata vs requiring them in reference

ClassMetadata is not open ended. If we would like to add it, then we would need to add bunch of optional maps: social_media, contact, icon....

My reasoning is to have a strict minimum amount of keys in ClassMetadata, and add more structure for other common requirements, so tools and websites can display more information about tokens.

So, I'm not against the @frol suggestion (adding all fields from the ReferenceBase to the ClassMetadata and potentially to the IssuerMetadata). Maybe this is a solution? In such case, we would need to extend the ClassMetadata as more requirements will emerge from the ecosystem. So far, for the SBTs we were working with, we need different pictures (size / format), website address, social_media links ...

I would like to hear more opinions from the tool designers and other projects dealing with tokens.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reasoning is to have a strict minimum amount of keys in ClassMetadata, and add more structure for other common requirements, so tools and websites can display more information about tokens.

There is no reason to overcomplicate things. If you want to standardize fields, add them to ClassMetadata optional or not. reference is needed when the metadata is too big to be stored inline. There is no reason to store JSON as a string inside JSON value.

In such case, we would need to extend the ClassMetadata as more requirements will emerge from the ecosystem. So far, for the SBTs we were working with, we need different pictures (size / format), website address, social_media links ...

If you will want to standardize on those, you will still need to extend the NEP and some structure, be it ClassMetadata or some other struct, it does not really change things. Also, you don't always need to standardize every single field as that may limit the usability of the whole spec if those fields will be used sparsely.

I would like to hear more opinions from the tool designers and other projects dealing with tokens.

Sure, make a call in community channels.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you will still need to extend the NEP and some structure

This is what an appendix is for.

We need to have a structure to make sure tools will have a common way to handle the data we describe in this PR. Can be in reference (as originally proposed), or by adding more optional attributes to the top level Metadata. Either way we need to have that structure.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to have a structure to make sure tools will have a common way to handle the data we describe in this PR. Can be in reference (as originally proposed), or by adding more optional attributes to the top level Metadata. Either way we need to have that structure.

I don't argue against having the defined structure, I just suggest moving it to where it belongs (new optional fields on ClassMetadata), instead of abusing reference field.

Copy link
Contributor Author

@robert-zaremba robert-zaremba Dec 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'm OK with that, see the second part of the comment above.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassMetadata is not open ended.

It is, actually, open ended by JSON spec, and that should be encouraged to be used whenever it makes sense.

Sure, I'm OK with that, see the second part of the #504 (comment).

Alright, so you are waiting for inputs from someone else besides me, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, so you are waiting for inputs from someone else besides me, right?

Yes, with @codingshot we left countless messages to get the input.

}
```

The Class Metadata should be defined by the Issuer and stored by the issuer contract. Note, that Contract Metadata is stored in the issuer contract and the Token Metadata is stored in the registry.
Having Class Metadata in issuer contract allows more granular storage control, shard the data, and facilitate easier migration patterns.

We recommend to store in the Class Metadata as much common information as possible, rather than pushing it down to the Token Metadata. For example if all tokens of give (issuer, class) pair have the same `icon`, then the `icon` should be defined in the `ClassMetadata` rather than `TokenMetadata.reference`.

The API to set `ClassMetadata` is not part of the standard, and can be custom for every implementation.

### Reference Field

The `reference` is attribute is part of all Metadata structures (Token, Class and Contract). It must be a valid JSON string or an URL link to a valid JSON file. In the latter case we recommend to set `reference_hash` to facilitate secure off-chain metadata storage and a mechanism for consistency checks.
It's up to the issuer if he wants to store `reference` fully on chain (as a JSON string) or off-chain. The latter case won't guarantee data availability, but may provide faster data access.

We define a `ReferenceBase` type. All entries are optional, and don't have to be set. If an entry is set, then it must have type compatible with `ReferenceBase`. The Reference object can extend the `ReferenceBase` and provide more fields.

```rust
struct ReferenceBase {
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
// Human readable description.
description: Option<String>,
// URL to a website.
website: Option<String>,
// Information how to get a token.
minting_info: Option<String>,
// Time (as Unixt Time in miliseconds), when the class or issuer was created.
created_at: u64,
// Map of social media kind to the profile link.
social_media: Map<String, String>,
// keys are contact types
contact: Map<String, Vec<String>>,
// keys are image format
icon: Map<String, Vec<String>>,
// keys are video format
video: Map<String, Vec<String>>,
// keys are audio format
audio: Map<String, Vec<String>>,
}
```

Example JS value:

```js
{
"description": "Detailed description about the issuer and the contract",
// resource address. For token, it should point to a token website, or an project webiste. In
// the latter case it's recommended to set such information in the Class or Contract Metadata.
"website": "your.address.com/resource",
"minting_info": "additional information about minting or a link to a resource",
// Unix time in ms, when the project / issuer was created.
"created_at": 1689330000000,
// Sub object defining social media references. Keys should represent the social media domain, not a name.
"social_media": {
"near.social": "https://nearefi.org/bos,"
"x.com": "https://nearefi.org/twitter", // twitter is X now
"facebook.com": "https://facebook.com/...",
"soundcloud.com": "https://soundcloud.com/..."
},
// Sub object defining contact resources. Each value must be a list.
"contact": {
"email": ["contact@example.com", "contact2@example.com"], // must be a list
"telegram": ["telegram_handle_1", "telegram_handle2"], // must be a list
},
// Default token icon
"icon": {
"png": "https://genadrop.mypinata.cloud/ipfs/...",
"svg": "https://genadrop.mypinata.cloud/ipfs/..."
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should have one more nesting level, to indicate the picture size? This will allow to set multiple sizes. Example:

"png": {"200x100": "link1", "4000x2000": "link2"}

Similarly we can do with vide and audio: video.mp4.full_hd = "link..."

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robert-zaremba ...or it may be just a part of the URL: "https://image.com/?w=1024&h=748"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if a http server doesn't support that?

},
// additional video content. Sub object, where key is a video format.
"video": {
"mp4": "https://youtube.com/...",
"vp9": "ipfs://..."
},
// additional audio content. Sub object, where key is an audio format.
"audio": {
"ogg": "ipfs://..."
}
```

All reference fields (`TokenMetadata.reference`, `ClassMetadata.reference`, `ContractMetadata.reference`) share the same scheme. Common information should be pushed up in the category. For example project information should be stored in Class Metadata or Contract Metadata, rather than the Token Metadata. Issuer information and issuer website should be stored in the `ContractMetadata`.

### Information flow

Projects and dapps should use all stack of information to correctly render token information. Let's consider the following examples, where each token is part of the same class (`class1`) and same issuer (`issuer1`)

1. All `token1metadata.reference.icon`, `class1metadata.reference.icon` and `issuer1metadata.reference.icon` are defined. To render token, the `token1metadata` icon field should be used. Moreover, each application can choose which format (png, jpg, svg...) to use.
1. All `token1metadata.reference.icon` is not defined and `class1metadata.reference.icon` is defined. The `class1metadata` icon should be used to render token.
1. Both `token1metadata.reference.webiste` and `class1metadata.reference.webiste` are defined. A dapp can use both token and class level metadata to provide detailed information.

### Events

Whenever a metadata is updated, an event should be emitted. We propose to follow the same event principles as defined in NEP-393:

- Events don't need to repeat all function arguments - these are easy to retrieve by indexer (events are consumed by indexers anyway).
- Events must include fields necessary to identify subject matters related to use case.
- When possible, events should contain aggregated data, with respect to the standard function related to the event.

The Contract and Class Metadata are stored and managed by the Issuer contract. This NEP covers only the Issuer contract. `TokenMetadata` is stored in the SBT registry, consequently Token Metadata updates and events are defined in NEP-393.

```typescript
type Nep504Event {
standard: "nep504";
version: "1.0.0";
event: "class" | "contract";
data: Class | Contract;
}

/// An event emitted by an SBT Issuer when Class Metadata are updated.
type ClassEvent {
classes: ClassId[]; // list of class IDs whose metadata has been updated
}

/// An event emitted by an SBT Issuer when Contract Metadata is updated.
/// Note: it's an empty object
type ContractEvent { }
```

## Reference Implementation

The concept was implemented and iterated in the [I Am Human](https://i-am-human.app/) project and used in the [Community SBT](https://github.com/near-ndc/i-am-human/tree/master/contracts/community-sbt) contracts.

## Security Implications

The proposal doesn't introduce new cross contract calls, nor extends the API of SBT reference. The implementation is encapsulated in the issuer contract, and should not impact any other contract.

## Alternatives

NEP-177 was used as a base for the Token and Contract Metadata in NEP-393. This proposal builds on top of that.

## Future possibilities

We can introduce Metadata versioning by wrapping the `ReferenceBase` object in an enum:

```json
{
"v1": reference
}
```

or adding new field: `"ver": 1` to the ReferenceBase.

## Consequences

### Positive

- Guideline for Issuers to correctly describe their tokens.
- `ReferenceBase` is a standarized and extendible type.
- `ReferenceBase` can be adopted by NFT Metadata.
- Better dapp integration.
- Added missing events to notify about metadata change.

### Neutral

### Negative

No negative consequences have been indicated.

### Backwards Compatibility

The standard is compatible with NEP-393 (SBT) and can be used by NEP-177 (NFT Metadata).

## Unresolved Issues (Optional)

[Explain any issues that warrant further discussion. Considerations

- What parts of the design do you expect to resolve through the NEP process before this gets merged?
- What parts of the design do you expect to resolve through the implementation of this feature before stabilization?
- What related issues do you consider out of scope for this NEP that could be addressed in the future independently of the solution that comes out of this NEP?]

## Changelog

### 1.0.0 - Initial Version

> Placeholder for the context about when and who approved this NEP version.

#### Benefits

> List of benefits filled by the Subject Matter Experts while reviewing this version:

- Benefit 1
- Benefit 2

#### Concerns

> Template for Subject Matter Experts review for this version:
> Status: New | Ongoing | Resolved

| # | Concern | Resolution | Status |
| --: | :------ | :--------- | -----: |
| 1 | | | |
| 2 | | | |

## Copyright

Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
Loading