Skip to content

Token Metadata extension to form standard#2439

Open
afa7789 wants to merge 98 commits into0xMiden:nextfrom
afa7789:oz/metadata_extension
Open

Token Metadata extension to form standard#2439
afa7789 wants to merge 98 commits into0xMiden:nextfrom
afa7789:oz/metadata_extension

Conversation

@afa7789
Copy link
Copy Markdown
Contributor

@afa7789 afa7789 commented Feb 12, 2026

Unified metadata: One place for account/faucet metadata: token (symbol, decimals, max_supply), owner, name, and content URI. Slot names live under miden::standards::metadata::* (and ownable for owner).

Layout: Token metadata and owner in slots 0–1; name in 2 words (name_0, name_1); content URI in 6 words (content_uri_0..5). Same layout in Rust and MASM.

Faucets: Basic and network fungible faucets support optional name and content URI; both re-export metadata getters (get_name, get_content_uri, get_token_metadata, get_max_supply, get_decimals, get_token_symbol; network also get_owner).

Standalone Info: Non-faucet accounts can use the metadata Info component (name + content URI) for future use (e.g. NFTs).

Testing: Unit tests in miden-standards (metadata storage, recovery); integration tests in miden-testing (MASM getters, faucet + metadata).

@afa7789 afa7789 marked this pull request as ready for review February 12, 2026 20:12
@afa7789 afa7789 force-pushed the oz/metadata_extension branch 2 times, most recently from 55282e1 to b1dba96 Compare February 14, 2026 12:48
@onurinanc
Copy link
Copy Markdown
Collaborator

@afa7789 In the context of token metadata discussion #2423 : Let's keep the contractURI as optional.

Basically, instead of:

 name: Option<TokenName>,
 contract_uri: Option<TokenContractURI>

We need to have:

 name: TokenName,
 contract_uri: Option<TokenContractURI>

@onurinanc
Copy link
Copy Markdown
Collaborator

We should also consider adding the corresponding metadata and the new constructor to the network fungible faucets: https://github.com/afa7789/miden-base/blob/f7426116833b1f76da3195738ccb838a52880f80/crates/miden-standards/src/account/faucets/network_fungible.rs#L93-L101

@onurinanc
Copy link
Copy Markdown
Collaborator

@afa7789 we should also add a flag and procedure to change max_supply. It's basically similar to have we have done in optional_set_contract_uri

@afa7789
Copy link
Copy Markdown
Contributor Author

afa7789 commented Feb 17, 2026

@afa7789 we should also add a flag and procedure to change max_supply. It's basically similar to have we have done in optional_set_contract_uri

In this branch ? pr ?

@onurinanc
Copy link
Copy Markdown
Collaborator

@afa7789 we should also add a flag and procedure to change max_supply. It's basically similar to have we have done in optional_set_contract_uri

In this branch ? pr ?

Yes, it would be better if you can have this in this PR.

@afa7789 afa7789 changed the title Oz/metadata extension Token Metadata extension to form standard. Feb 19, 2026
@afa7789
Copy link
Copy Markdown
Contributor Author

afa7789 commented Feb 19, 2026

@bobbinth @mmagician this is ready for review :)

@mmagician mmagician added the pr-from-maintainers PRs that come from internal contributors or integration partners. They should be given priority label Feb 19, 2026
Copy link
Copy Markdown
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Not a review - but I left a couple of comments inline. The main one is about how we can handle returning large amounts of data from account interface procedures.

@onurinanc
Copy link
Copy Markdown
Collaborator

@afa7789 Additionally, as this discussion #2423 (comment) has been concluded, you can update the PR with the following:

  • name (mandatory): up to 2 words, ~64 bytes (UTF-8)
  • description (optional): up to 6 words, ~192 bytes (UTF-8)
  • logo_uri (optional): up to 6 words, ~192 bytes (UTF-8)
  • external_link (optional): up to 6 words, ~192 bytes (UTF-8)

For description, logo_uri, and external_link:

  • configuration flags will be introduced to indicate whether the account was initialized with these fields.
  • mutable/immutable flags will also be added for these fields. (similar to how you did in content_uri)

@afa7789 afa7789 force-pushed the oz/metadata_extension branch 2 times, most recently from 5548bd5 to 609b355 Compare February 26, 2026 14:43
/// - Slot 12–17: logo_uri (6 Words)
/// - Slot 18–23: external_link (6 Words)
#[derive(Debug, Clone, Default)]
pub struct Info {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this not part of TokenMetadata directly?

It feels very closely related, so I would consider including it. Making it a separate component means users always need to remember to include it in their account next to TokenMetadata and need to decode both TokenMetadata and Info to get all the related data, so this pushes some complexity up the stack.

It would also be nice if we could set mutability flags directly via the mentioned TokenMetadataBuilder, e.g. TokenMetadataBuilder::new(...).description("abc").mutable_description().build().

Related, I think Info does not be an AccountComponent, since it does not have any code. This suggests it is a set of standard storage slots but not a full account component (a combination of functionality / code and storage). So in the same way as TokenMetadata is not an account component (but more like a standardized storage slot), we could make Info a reusable set of storage slots. I would then include it in TokenMetadata, which in turn is included in BasicFungibleFaucet (a proper account component). Notably, this does not prevent reusing Info for other purposes in the future (such as for NFTs).

Naming: I think this is more aptly described as TokenMetadata. This is more generic metadata than what is currently called TokenMetadata which is specific to fungible assets. So maybe it is better to rename the current TokenMetadata to FungibleTokenMetadata to free up that name for this.

@afa7789
Copy link
Copy Markdown
Contributor Author

afa7789 commented Mar 2, 2026

@PhilippGackstatter I did most of the mentioned things.
The exceptions are the Builder pattern and Generalized encoding, which will be left for the future.

@afa7789 afa7789 force-pushed the oz/metadata_extension branch 3 times, most recently from ebcbd2e to 671a9dc Compare March 5, 2026 04:05
@afa7789 afa7789 force-pushed the oz/metadata_extension branch from ecf267b to 2e50884 Compare March 5, 2026 14:17
Copy link
Copy Markdown
Contributor

@PhilippGackstatter PhilippGackstatter left a comment

Choose a reason for hiding this comment

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

Looks good. I think the overall structure is fine for now, and we can reconsider it a bit more with #2613.

Comment on lines +431 to +438
if max_supply.as_canonical_u64() > FungibleAsset::MAX_AMOUNT {
return Err(FungibleFaucetError::MaxSupplyTooLarge {
actual: max_supply.as_canonical_u64(),
max: FungibleAsset::MAX_AMOUNT,
});
}
if token_supply.as_canonical_u64() > max_supply.as_canonical_u64() {
return Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It would be great if we could implement validation (e.g. of max supply, token supply, ...) once and then reuse it, and also avoid instantiating the struct directly (Self { ... }) in multiple places.

Ideally, we end up with a single constructor that validates the basics and instantiates the struct, all other constructors are wrappers around this. This results in the least amount of duplication and the least chance to introduce differences in how validation works.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We now have new_validated, could we now call it here instead of reimplementing the checks?

Self::new_validated(
    symbol,
    decimals,
    max_supply,
    token_supply,
    metadata.name.ok_or_else(todo!()),
    metadata.description,
    metadata.logo_uri,
    metadata.external_link,
);

Comment on lines +505 to +509
token_metadata = token_metadata
.with_description_mutable(mutability_word[0] != Felt::ZERO)
.with_logo_uri_mutable(mutability_word[1] != Felt::ZERO)
.with_external_link_mutable(mutability_word[2] != Felt::ZERO)
.with_max_supply_mutable(mutability_word[3] != Felt::ZERO);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It would be good to avoid using magic numbers here, especially because if we update the layout in TokenMetadata, these changes are less likely to propagate here since this is a different module. TokenMetadata should be the only place that defines the exact layout of this mutability config word and should provide helpers for constructing and reading from those instead.

So basically, I think we should change TokenMetadata::read_metadata_from_storage to return TokenMetadata (unless this doesn't work for some reason), and maybe rename it to try_from_storage.

@afa7789
Copy link
Copy Markdown
Contributor Author

afa7789 commented Mar 20, 2026

Add a temporary setback here on my machine, but plan on finishing this this weeknd.

@afa7789 afa7789 force-pushed the oz/metadata_extension branch from 8774394 to 2bb8371 Compare March 20, 2026 21:06
@afa7789
Copy link
Copy Markdown
Contributor Author

afa7789 commented Mar 20, 2026

The erros in CI onward last commit are regarding crates/miden-agglayer, cause I revoked/reverted the changes as requested!

@bobbinth
Copy link
Copy Markdown
Contributor

The erros in CI onward last commit are regarding crates/miden-agglayer, cause I revoked/reverted the changes as requested!

Ah - sorry. I assumed that it would still work correctly as we are not really changing the existing TokenMetadata. But if that's not the case, I'd make the minimal changes needed to make it work correctly.

@afa7789
Copy link
Copy Markdown
Contributor Author

afa7789 commented Mar 21, 2026

I rolled back the agglayer code and left a residue of TokenMetadata as before to try and touch it as little as possible. We can them in a future PR adapt it.

To make easier reviewing this, since it's too big, we can close this or rebase this if needed.
If closing it I would of course open a new branch with the files.

Copy link
Copy Markdown
Contributor

@PhilippGackstatter PhilippGackstatter left a comment

Choose a reason for hiding this comment

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

Thanks for the updates!

I left a few more comments, but am still in the process of getting through everything.

Comment on lines +36 to +38
const METADATA_SLOT=word("miden::standards::fungible_faucets::metadata")
# Note: this slot name should match TOKEN_METADATA_SLOT in metadata/fungible_faucet.masm.
const METADATA_SLOT=word("miden::standards::metadata::fungible_faucet::token_metadata")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should be able to remove this, make the slot in crates/miden-standards/asm/standards/metadata/fungible_faucet.masm public and then import it here.

Comment on lines +431 to +438
if max_supply.as_canonical_u64() > FungibleAsset::MAX_AMOUNT {
return Err(FungibleFaucetError::MaxSupplyTooLarge {
actual: max_supply.as_canonical_u64(),
max: FungibleAsset::MAX_AMOUNT,
});
}
if token_supply.as_canonical_u64() > max_supply.as_canonical_u64() {
return Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We now have new_validated, could we now call it here instead of reimplementing the checks?

Self::new_validated(
    symbol,
    decimals,
    max_supply,
    token_supply,
    metadata.name.ok_or_else(todo!()),
    metadata.description,
    metadata.logo_uri,
    metadata.external_link,
);

Copy link
Copy Markdown
Contributor

@PhilippGackstatter PhilippGackstatter left a comment

Choose a reason for hiding this comment

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

I think the last merge commit broke a few things like removing some comments in faucets/mod.masm and deleting most of the changelog. Might be best to remove this commit and re-do the merge.

I left a few more comments to simplify the encoding/decoding logic and some structural comments, but overall I think we can soon merge.

I have yet to take a look at most the tests, though.

Copy link
Copy Markdown
Contributor

@PhilippGackstatter PhilippGackstatter left a comment

Choose a reason for hiding this comment

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

I left a few comments to remove duplicate tests or using more concise setups. Tests look good overall, thank you for setting them up in a well-structured way!

The size of the PR and the number of reviews made it tricky to keep track, so I'm not convinced I reviewed every single detail of the PR, but I think it can be merged, and so I'll approve it.

Comment on lines -126 to -127
if self.max_supply > FungibleAsset::MAX_AMOUNT {
return Err(FungibleFaucetError::MaxSupplyTooLarge {
actual: self.max_supply,
max: FungibleAsset::MAX_AMOUNT,
});
}
// SAFETY: max_supply is validated above to be <= MAX_AMOUNT (2^63 - 1),
// which is well below the Goldilocks prime, so Felt::new will not wrap.
let max_supply = Felt::new(self.max_supply);
let mut meta = FungibleTokenMetadata::new_validated(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can avoid this code if we change new_validated to:

pub(crate) fn new_validated(
    symbol: TokenSymbol,
    decimals: u8,
    max_supply: u64,
    token_supply: u64,
    metadata: TokenMetadata,
) -> Result<Self, FungibleFaucetError> {

Then we can convert the u64 in that constructor to a Felt if it's valid and we only implement the max supply check exactly once.

I'd also change FungibleTokenMetadataBuilder::token_supply to take u64 for consistency with max_supply.

Comment on lines +65 to +66
let name = TokenName::new(token_symbol)
.unwrap_or_else(|_| TokenName::new("").expect("empty string is a valid token name"));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd still suggest returning an error instead of silently falling back to an empty string which could hide issues in tests.

Comment on lines +147 to +150
FungibleTokenMetadataBuilder::new(
TokenName::new("POL").unwrap(),
TokenSymbol::new("POL").unwrap(),
8,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: I'd use a longer name, one that is encoded into two words (where both words are non-empty) as a more interesting test case.

Comment on lines +381 to +388
call.::miden::standards::metadata::fungible_faucet::get_decimals
push.{expected_decimals} assert_eq.err="decimals does not match"
push.0
call.::miden::standards::metadata::fungible_faucet::get_token_symbol
push.{expected_symbol} assert_eq.err="token_symbol does not match"
push.0
call.::miden::standards::metadata::fungible_faucet::get_max_supply
push.{expected_max_supply} assert_eq.err="max_supply does not match"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need the push.0 for anything here or can we remove them?

Comment on lines +443 to +463
// (metadata_builder, proc_name, expected_value)
let cases: Vec<(FungibleTokenMetadata, &str, u8)> = vec![
(
build_faucet_metadata().with_max_supply_mutable(true),
"is_max_supply_mutable",
1,
),
(
FungibleTokenMetadataBuilder::new(
TokenName::new("T").unwrap(),
"TST".try_into().unwrap(),
2,
1_000u64,
)
.description(desc.clone())
.is_description_mutable(true)
.build()
.unwrap(),
"is_description_mutable",
1,
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  • Can we use rstest to encode these cases?
  • Can we use the more concise build_faucet_metadata() for the second and following cases?


let tx_context = mock_chain
.build_tx_context(faucet.id(), &[], &[note])?
.add_note_script(note_script)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
.add_note_script(note_script)

Nit: We don't need this line, same in test_field_setter_owner_succeeds.

Comment on lines +986 to +990
begin
dropw push.{e3} push.{e2} push.{e1} push.{e0}
call.::miden::standards::metadata::fungible_faucet::{proc_name}
dropw
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: For the non-owner test, we don't need to pass any data in, so we can simplify the test setup.

Suggested change
begin
dropw push.{e3} push.{e2} push.{e1} push.{e0}
call.::miden::standards::metadata::fungible_faucet::{proc_name}
dropw
end
begin
call.::miden::standards::metadata::fungible_faucet::{proc_name}
dropw
end


let tx_context = mock_chain
.build_tx_context(faucet.id(), &[], &[note])?
.add_note_script(note_script)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
.add_note_script(note_script)

Nit: We probably don't need this here either.

Comment on lines +1192 to +1195
let metadata_word = updated_faucet.storage().get_item(FungibleTokenMetadata::metadata_slot())?;
assert_eq!(metadata_word[1], Felt::new(new_max_supply), "max_supply should be updated");
assert_eq!(metadata_word[0], Felt::new(0), "token_supply should remain unchanged");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

let metadata = FungibleTokenMetadata::try_from(updated_faucet.storage())?;
assert_eq!(
    metadata.max_supply().as_canonical_u64(),
    new_max_supply,
    "max_supply should be updated"
);
assert_eq!(
    metadata.token_supply().as_canonical_u64(),
    0,
    "token_supply should remain unchanged"
);

Nit: Avoid using Felt::new. This is a nice opportunity to also test the try from storage impl.


let tx_context = mock_chain
.build_tx_context(faucet.id(), &[], &[note])?
.add_note_script(note_script)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
.add_note_script(note_script)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-from-maintainers PRs that come from internal contributors or integration partners. They should be given priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.