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

ERC721 URI Storage #1031

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

gerceboss
Copy link

@gerceboss gerceboss commented Jul 1, 2024

Resolves #1019

Added extension's component implementation, mock contract, tests and updated documentation
Purpose: Adding ERC721URIstorage extension to OZ.

PR Checklist

  • Tests
  • Documentation
  • Added entry to CHANGELOG.md
  • Tried the feature on a public network

Transaction hash on Starknet sepolia: 0x044e53a70cb6cd021d2e2344a0b1921b833874cc25c676f997f8aa819302e664
Test contract deployed: 0x00f199172801f6685601944d749557d8efa121bb6f439026a9e37799c6b1fbae

@andrew-fleming
Copy link
Collaborator

the tests are not running on scarb test.

The new tests or all tests? If the latter, are you in the package root (cairo-contracts) when running the test command?

@gerceboss
Copy link
Author

gerceboss commented Jul 1, 2024

@andrew-fleming , they were running but it was not showing any errors on my local machine. When I opened the pull request three of them were failing and the tests were running. I have committed the changes and all the tests are passing right now . Let me know if more tests are needed.

@immrsd immrsd requested review from andrew-fleming, ericnordelo and immrsd and removed request for andrew-fleming July 2, 2024 09:06
// OpenZeppelin Contracts for Cairo v0.14.0 (token/erc721/extensions/erc721_uri_storage.cairo)

#[starknet::component]
pub mod ERC721URIstorageComponent {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
pub mod ERC721URIstorageComponent {
pub mod ERC721URIStorageComponent {

Copy link
Author

@gerceboss gerceboss Jul 2, 2024

Choose a reason for hiding this comment

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

Do I need to change that everywhere? ERC721URIstorage -> ERC721URIStorage.
Because I have used the earlier one in naming of files,Impl and in tests as well?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes


#[storage]
struct Storage {
token_uris: LegacyMap<u256, ByteArray>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
token_uris: LegacyMap<u256, ByteArray>,
ERC721URIStorage_token_uris: LegacyMap<u256, ByteArray>,

Storage variables should be prefixed with component name to prevent collisions with ones of other contracts or components. Here's an example from ERC721 component
image

#[event]
#[derive(Drop, PartialEq, starknet::Event)]
pub enum Event {
MetadataUpdate: MetadataUpdate,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
MetadataUpdate: MetadataUpdate,
MetadataUpdated: MetadataUpdated,

For consistency among the codebase we strive to have all (almost) events named in the same way. Following this approach the name of this event should be MetadataUpdated, although it is indeed name MetadataUpdate in the Solidity counterpart

fn symbol(self: @ComponentState<TContractState>) -> ByteArray {
self._symbol()
}
/// Returns the Uniform Resource Identifier (URI) for the `token_id` token.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// Returns the Uniform Resource Identifier (URI) for the `token_id` token.
/// Returns the Uniform Resource Identifier (URI) for the `token_id` token.

fn _name(self: @ComponentState<TContractState>) -> ByteArray {
let erc721_component = get_dep_component!(self, ERC721);
let name = erc721_component.ERC721_name.read();
return name;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return name;
name

fn _symbol(self: @ComponentState<TContractState>) -> ByteArray {
let erc721_component = get_dep_component!(self, ERC721);
let symbol = erc721_component.ERC721_symbol.read();
return symbol;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return symbol;
symbol

return "";
} else {
return format!("{}{}", base_uri, token_id);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. Double if-else statement
  2. Consider avoiding early returns if possible

Copy link
Author

@gerceboss gerceboss Jul 2, 2024

Choose a reason for hiding this comment

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

return format!("{}{}", base_uri, token_id);

@immrsd, should I replace the second if-else with this?

@immrsd immrsd changed the title Resolves #1019 ERC721 URI Storage Jul 2, 2024
@gerceboss
Copy link
Author

@immrsd , @andrew-fleming , I wanted to contribute more to the repository and am waiting for any suggestions to add to this PR to get merged, to take up a new issue. Can you please review again?

Copy link
Collaborator

@andrew-fleming andrew-fleming left a comment

Choose a reason for hiding this comment

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

Hey, thank you for opening this PR!

I wanted to contribute more to the repository and am waiting for any suggestions to add to this PR to get merged, to take up a new issue. Can you please review again?

We appreciate your eagerness to contribute! We review contributions as soon as we can, but please understand that reviews take time. That said, I finished my initial review. I left a lot of feedback (sorry!). In addition to addressing the review, could you kindly finish the PR checklist:

  • add this entry to CHANGELOG
  • deploy/interact with this feature on sepolia and provide an address and/or tx hash to document that the feature behaves as expected on a public network?

src/token/erc721/extensions/erc721_uri_storage.cairo Outdated Show resolved Hide resolved
src/token/erc721/extensions/erc721_uri_storage.cairo Outdated Show resolved Hide resolved
src/token/erc721/extensions/erc721_uri_storage.cairo Outdated Show resolved Hide resolved
src/token/erc721/extensions/erc721_uri_storage.cairo Outdated Show resolved Hide resolved
src/token/erc721/extensions/erc721_uri_storage.cairo Outdated Show resolved Hide resolved
src/token/erc721/extensions/erc721_uri_storage.cairo Outdated Show resolved Hide resolved
src/token/erc721/extensions/erc721_uri_storage.cairo Outdated Show resolved Hide resolved
docs/modules/ROOT/pages/api/erc721.adoc Outdated Show resolved Hide resolved
docs/modules/ROOT/pages/api/erc721.adoc Outdated Show resolved Hide resolved
docs/modules/ROOT/pages/api/erc721.adoc Outdated Show resolved Hide resolved
Copy link
Collaborator

@andrew-fleming andrew-fleming left a comment

Choose a reason for hiding this comment

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

Looking much better! I left some feedback. Would you mind also updating your branch and fixing conflicts?

CHANGELOG.md Outdated Show resolved Hide resolved
src/tests/token/erc721/test_erc721_uri_storage.cairo Outdated Show resolved Hide resolved
src/tests/token/erc721/test_erc721_uri_storage.cairo Outdated Show resolved Hide resolved
src/token/erc721/extensions/erc721_uri_storage.cairo Outdated Show resolved Hide resolved
src/token/erc721/extensions/erc721_uri_storage.cairo Outdated Show resolved Hide resolved
docs/modules/ROOT/pages/api/erc721.adoc Outdated Show resolved Hide resolved
docs/modules/ROOT/pages/api/erc721.adoc Outdated Show resolved Hide resolved
src/tests/token/erc721/test_erc721_uri_storage.cairo Outdated Show resolved Hide resolved
src/token/erc721/extensions/erc721_uri_storage.cairo Outdated Show resolved Hide resolved
docs/modules/ROOT/pages/api/erc721.adoc Outdated Show resolved Hide resolved
@gerceboss gerceboss force-pushed the feature/ERC721URIstorage-component-#1019 branch from e605ffe to eacbd42 Compare July 15, 2024 11:40
Copy link
Collaborator

@andrew-fleming andrew-fleming left a comment

Choose a reason for hiding this comment

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

Looking good! Left a few comments mostly from the cairo bump

CHANGELOG.md Outdated Show resolved Hide resolved
src/token/erc721/extensions/erc721_uri_storage.cairo Outdated Show resolved Hide resolved
docs/modules/ROOT/pages/api/erc721.adoc Outdated Show resolved Hide resolved
docs/modules/ROOT/pages/api/erc721.adoc Outdated Show resolved Hide resolved
src/token/erc721/extensions/erc721_uri_storage.cairo Outdated Show resolved Hide resolved
@gerceboss gerceboss force-pushed the feature/ERC721URIstorage-component-#1019 branch from 507eadb to e4e7368 Compare July 15, 2024 20:17
Copy link
Collaborator

@andrew-fleming andrew-fleming left a comment

Choose a reason for hiding this comment

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

Great work on the changes! LGTM! Let's wait on a second pair of eyes before merging

@ericnordelo
Copy link
Member

Hey @gerceboss, sorry for being this late to the conversation. I reviewed the PR and the implementation, tests, and docs look great. With this said, I think that for this particular case we don't need an extra component, because we can add just an extra embeddable implementation (ERC721MetadataURIStorage) to the existing ERC721 component (besides the storage member, the event, and the internal set_token_uri function). This would make the usage of it less verbose, since users would only need to change an embedded implementation on the final contract, instead of adding the extra component verbosity on it. What do you think @andrew-fleming?

@andrew-fleming
Copy link
Collaborator

andrew-fleming commented Jul 31, 2024

@ericnordelo I lean toward keeping the component light and not introduce non-core impls directly. The additions are small, but it forces all contracts to add extra storage for a feature that many contracts likely won't use.

On the contrary, if it seems worth it to include this in the component itself, why don't we make the URIStorage token_uri the default impl? token_uri is really for off-chain queries; therefore, gas shouldn't be a concern. If there's no storage URI for a given token id, it'll behave like the current impl with base_uri + id

@ericnordelo
Copy link
Member

ericnordelo commented Aug 1, 2024

I lean toward keeping the component light and not introduce non-core impls directly. The additions are small, but it forces all contracts to add extra storage for a feature that many contracts likely won't use.

Note that the extra storage is completely transparent to the final contract, and it doesn't require any overhead gas-wise. We also have the two-step ownable setup as an extra embeddable implementation to avoid adding an extra component, so this would be consistent, and even smaller and easier to use than that.

On the contrary, if it seems worth it to include this in the component itself, why don't we make the URIStorage token_uri the default impl? token_uri is really for off-chain queries; therefore, gas shouldn't be a concern. If there's no storage URI for a given token id, it'll behave like the current impl with base_uri + id

We can make it the default implementation, but I'm leaning toward keeping consistency with our Solidity counterpart on this, for the sake of consistency but also because even when it may be negligible gas-wise, developers often want the option to avoid an extra check on storage, that could be useful for a use case that we currently don't see.

@andrew-fleming
Copy link
Collaborator

@ericnordelo yeah, those are fair points. A separate impl in the core component sounds good to me. One last thing to note is that the Solidity implementation also supports erc4906. If we include support for that or a SN equivalent, I think we'll need to have a separate initializer for URI storage to declare support. We can address that later though

@gerceboss
Copy link
Author

gerceboss commented Aug 1, 2024

@ericnordelo @andrew-fleming ,so , how do we need to change the implementation as your suggestion is to not have it as an extension? And about erc4906, I am planning to make a PR but am waiting to complete this one first.

@andrew-fleming
Copy link
Collaborator

how do we need to change the implementation as your suggestion is to not have it as an extension?

We can move the embeddable URI storage impl from the extension into the erc721 component itself. Be aware, we're now using SN Foundry for testing so the tests will need to be refactored accordingly and we're also in the process of migrating to multiple packages (#1065)

@gerceboss
Copy link
Author

Will check that tomorrow 👍

///
/// - `token_id` exists.
fn token_uri(self: @ComponentState<TContractState>, token_id: u256) -> ByteArray {
let mut erc721_component = get_dep_component!(self, ERC721);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does it have to be mutable?

Comment on lines 66 to 77
// If there is no base_uri, return the token_uri
if base_uri.len() == 0 {
return token_uri;
}

// If both are set, concatenate the base_uri and token_uri
if token_uri.len() > 0 {
return format!("{}{}", base_uri, token_uri);
}

// Implementation from ERC721Metadata::token_uri
return format!("{}{}", base_uri, token_id);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// If there is no base_uri, return the token_uri
if base_uri.len() == 0 {
return token_uri;
}
// If both are set, concatenate the base_uri and token_uri
if token_uri.len() > 0 {
return format!("{}{}", base_uri, token_uri);
}
// Implementation from ERC721Metadata::token_uri
return format!("{}{}", base_uri, token_id);
if base_uri.len() == 0 {
// If there is no base_uri, return the token_uri
token_uri
} else if token_uri.len() == 0 {
// Implementation from ERC721Metadata::token_uri
format!("{}{}", base_uri, token_id)
} else {
// If both are set, concatenate the base_uri and token_uri
format!("{}{}", base_uri, token_uri)
}

ref self: ComponentState<TContractState>, token_id: u256, token_uri: ByteArray
) {
self.ERC721URIStorage_token_uris.write(token_id, token_uri);
self.emit(MetadataUpdated { token_id: token_id });
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
self.emit(MetadataUpdated { token_id: token_id });
self.emit(MetadataUpdated { token_id });

@gerceboss gerceboss force-pushed the feature/ERC721URIstorage-component-#1019 branch from 0fc0b3b to 5f2a7de Compare August 5, 2024 11:10
@gerceboss
Copy link
Author

@andrew-fleming @ericnordelo , wanted to confirm the steps for the change:

  1. Add an internal implementation in the packages/token/src/erc721/erc721.cairo for the _set_token_uri
  2. Add a storage member for the URIs in the same file itself

the second option is to add a new interface like ERC721Metadata interface, can you confirm what to do?

@andrew-fleming
Copy link
Collaborator

  1. Add an internal implementation in the packages/token/src/erc721/erc721.cairo for the _set_token_uri

Do we need another implementation just for _set_token_uri? Can't we just add the function to ERC721Component::InternalImpl?

  1. Add a storage member for the URIs in the same file itself

In the same Storage struct, yes

the second option is to add a new interface like ERC721Metadata interface, can you confirm what to do?

Not sure what you mean by second option. It's the same interface so we don't need to define a new interface. The idea is to have an additional implementation of the same IERC721Metadata interface within the ERC721Component module like this:

#[starknet::component]
pub mod ERC721Component {
    ...

    #[embeddable_as(ERC721MetadataImpl)]
    impl ERC721Metadata<...> of interface::IERC721Metadata<...> {
        ...
    }

    #[embeddable_as(ERC721URIStorageImpl)]
    impl ERC721URIStorage<...> of interface::IERC721Metadata<...> {
        ...
    }

    ...
}

@gerceboss
Copy link
Author

@andrew-fleming, can you also mention how do I change the testing ?

@andrew-fleming
Copy link
Collaborator

can you also mention how do I change the testing ?

I think the only thing to change in these tests are the event assertions. Instead of using utils::pop_log, we should be using snforge_std's spy_events in conjunction with our EventSpyExt

Here's an implementation for reference: https://github.com/OpenZeppelin/cairo-contracts/blob/main/packages/security/src/tests/test_pausable.cairo#L125-L154

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ERC721 URIstorage extension implementation
4 participants