Repository: mediolano-app/mediolano-app
Pull request #119: feat: IP Identity
Author: login: anonfedora
Created: 2025-04-14T12:37:00Z | Merged: 2025-04-17T23:46:53Z

# Implement IP Identity Cairo Smart Contract for Mediolano IP ID Onchain (#75)

Closes #75

## Description

This PR implements the Mediolano IP ID Onchain Cairo Smart Contract as specified in issue #75. The contract serves as an identity protocol for Programmable IP on Starknet, enabling permissionless minting and purchasing of NFTs, ownership validation, and access to exclusive features within the Mediolano ecosystem. The implementation includes comprehensive testing and detailed documentation to ensure robustness and usability.

## Changes Made

### Smart Contract Development:
- Created `IPIdentity.cairo`, a Cairo smart contract that implements the `IIPIdentity` interface with the following functionalities:
  - `register_ip_id`: Allows users to register an IP ID with metadata, IP type, and license terms, minting an ERC721 NFT.
  - `update_ip_id_metadata`: Enables owners to update the metadata URI of their IP ID.
  - `get_ip_id_data`, `get_ip_id_by_owner`, `get_owner_by_ip_id`: Retrieve IP ID details and ownership information.
  - `verify_ip_id`: Allows the contract owner to verify an IP ID.
  - `set_payment_token`, `set_paused`: Administrative functions for managing payment tokens and contract pausing.
- Integrated OpenZeppelin Cairo contracts (v0.17.0) for ERC721, Ownable, Pausable, and SRC5 functionality.
- Implemented payment processing with a configurable ERC20 token (default mint price: 0.001 ETH).
- Added security mechanisms to prevent unauthorized access (e.g., ownership checks, pausable state).
- Optimized storage with mappings for IP ID data and ownership tracking.

### Testing:
- Added `test_contract.cairo` with comprehensive unit tests covering:
  - Successful and failed scenarios for metadata updates, IP ID verification, and administrative functions.
  - Edge cases like invalid IP IDs, non-owner access, and paused contract states.
- Used `snforge_std` for testing with mocked caller addresses and timestamps.
- Tests validate minting, ownership, and exclusive access mechanisms.

### Configuration and Build:
- Added `Scarb.toml` with dependencies: starknet (2.8.4), openzeppelin (v0.17.0), snforge_std (0.36.0), and assert_macros (2.8.4).
- Included `Scarb.lock` for reproducible builds.
- Configured `snfoundry.toml` for testing and deployment settings.
- Added `.gitignore` to exclude build artifacts (target, .snfoundry_cache/).

### Directory Structure:
```
onchain/ip_id/
├── .gitignore
├── Scarb.lock
├── Scarb.toml
├── snfoundry.toml
├── src/
│   ├── IPIdentity.cairo
│   └── lib.cairo
├── tests/
│   └── test_contract.cairo
```

## Criteria Fulfillment

- **Develop the Cairo Smart Contract:** Implemented `IPIdentity.cairo` with all required functionalities.
- **Support permissionless minting and purchasing of NFTs:** `register_ip_id` allows anyone to mint an IP ID NFT with payment.
- **Validate ownership and enable access to exclusive features:** Ownership checks in `update_ip_id_metadata` and `verify_ip_id` ensure only authorized users can modify or verify IP IDs.
- **Provide thorough testing and detailed documentation:**
  - Comprehensive tests in `test_contract.cairo`.
  - Documentation included in code comments and external README.md (to be added in a follow-up commit if required).

## Technical Details

- **Platform:** Starknet Blockchain
- **Language:** Cairo
- **Dependencies:** Integrated with OpenZeppelin for ERC721 and Mediolano dApp compatibility.
- **Security:** 
  - Ownership validation using OpenZeppelin's OwnableComponent.
  - Pausable functionality to mitigate risks.
  - Input validation to prevent invalid IP ID registration or updates.
- **Scalability:** 
  - Efficient storage using Map for IP ID data and ownership.
  - Optimized event emissions for tracking (IPIDRegistered, IPIDUpdated, PaymentTokenUpdated).

## Testing Instructions

1. Navigate to `onchain/ip_id/`.
2. Run tests using:
```bash
scarb test
```
3. Verify test coverage for minting, ownership validation, and error handling.

## Documentation

- Inline comments in `IPIdentity.cairo` explain key functions and logic.
- Tests in `test_contract.cairo` serve as usage examples.
- A detailed README.md will be added in a follow-up commit to include user guides and developer instructions, as per the issue requirements.

## Additional Notes

- The contract assumes a dummy payment token for testing; integration with the actual Mediolano payment token should be configured post-deployment.
- Suggestions for additional features (e.g., batch minting, metadata versioning) can be discussed for future iterations.

## Commits

- **feat: IP Identity** (Apr 14, 2025, by @anonfedora)
  - Added 651 lines across 7 files: .gitignore, Scarb.lock, Scarb.toml, snfoundry.toml, IPIdentity.cairo, lib.cairo, test_contract.cairo.

## Checklist
![Screenshot_20250414_133207](https://github.com/user-attachments/assets/207ad064-2d29-430e-8594-29f3161a3189)

- [x] Code follows Starknet and Cairo best practices.
- [x] Tests cover critical functionalities and edge cases.
- [x] Dependencies are correctly configured.
- [x] Contract is secure and scalable.

## Reviewer Instructions

- Verify contract functionality using the test suite.
- Check security mechanisms (e.g., ownership checks, pausable state).
- Review test coverage and suggest additional test cases if needed.
- Confirm compatibility with Mediolano dApp integration requirements.

Please let me know if additional changes or clarifications are needed!

Linked issue #75: Mediolano IP ID Onchain - Cairo Smart Contract
IP ID serves as a identity protocol for Programmable IP in Starknet, facilitating storage and sharing of specific data within the IP ID certified page and Mediolano ecosystem. This IP identity protocol allows various Starknet app to access and utilize user IP information effortlessly, enhancing integrations across the blockchain.

When a user registers a IP ID, this information is accessible across the Starknet Ecosystem. This means that every app will can share and use the IP ID info and Programmable licensing.

Criteria:

[ ] Develop the Cairo Smart Contract.
[ ] Support permissionless minting and purchasing of NFTs for community access.
[ ] Validate ownership and enable access to exclusive features.
[ ] Provide thorough testing and detailed documentation for the smart contract.

Technical Details:
Platform: Starknet Blockchain
Language: Cairo
Dependencies: Integration with Mediolano dApp and Starknet 
Security: Implement mechanisms to prevent unauthorized access and ensure data integrity.
Scalability: Optimize the contract to handle high volumes.

Testing:
Conduct comprehensive testing to validate all functionalities, including community creation, minting, and exclusive access mechanisms.

Documentation:
Create detailed technical documentation and user guides for developers and community creators.

Linked issue comments: user: login: caxtonacollins
created_at: 2025-03-20T14:50:58Z
body: I would love to be the given the opportunity to solve this issue please. Iam a fullstack dev proficient in JavaScript, TypeScript, solidity and cairo.

please assign this issue to me.
author_association: CONTRIBUTOR
user: login: ekumamatthew
created_at: 2025-03-21T03:10:40Z
body: I'd love to give this a go.
author_association: NONE
user: login: Akanimorex
created_at: 2025-03-22T02:14:44Z
body: I would love to take this on. I'm proficient with integrating dapps with cairo smart contracts so this is very familiar to me. If assigned i can begin immediately 

ETA: 27 hours
author_association: NONE
user: login: soyaya
created_at: 2025-03-22T14:14:29Z
body: I'm David, a full stack blockchain developer with experience in Cairo smart contracts and frontend development. I'm writing to express my interest in the Mediolano dApp crowdfunding integration issue.

I've reviewed the requirements for implementing permissionless crowdfunding campaigns for Programmable IP, and I'm confident in my ability to deliver this solution. My expertise includes:

Cairo smart contract development
Frontend integration with Starknet
Tokenized contribution systems
Real-time status tracking
Security implementation
I'm ready to contribute to this project and implement the crowdfunding campaign functionality. Please let me know if you'd like to discuss any specific aspects of the implementation.
author_association: NONE
user: login: michojekunle
created_at: 2025-03-22T18:45:11Z
body: I’d like to resolve this.
author_association: NONE
user: login: salvadorcamino
created_at: 2025-03-25T14:59:11Z
body: Devs: reach our group at telegram https://t.me/mediolanoapp ++ our account on x https://x.com/MediolanoApp
author_association: CONTRIBUTOR
user: login: josephchimebuka
created_at: 2025-03-26T20:55:50Z
body: The description has been clearly laid out I will follow it to ensure the smart contract is integrated
author_association: CONTRIBUTOR
user: login: anonfedora
created_at: 2025-03-27T10:26:32Z
body: Hi, I'm a fullstack Blockchain developer.
I'm proficient in  Cairo, Rust (Soroban SDK), Nestjs, TypeScript.

I can implement the required functions and tests.

ETA: 72 hours
author_association: CONTRIBUTOR

Diff:
diff --git a/onchain/ip_id/.gitignore b/onchain/ip_id/.gitignore
new file mode 100644
index 00000000..73aa31e6
--- /dev/null
+++ b/onchain/ip_id/.gitignore
@@ -0,0 +1,2 @@
+target
+.snfoundry_cache/
diff --git a/onchain/ip_id/Scarb.lock b/onchain/ip_id/Scarb.lock
new file mode 100644
index 00000000..3d2dcf4b
--- /dev/null
+++ b/onchain/ip_id/Scarb.lock
@@ -0,0 +1,127 @@
+# Code generated by scarb DO NOT EDIT.
+version = 1
+
+[[package]]
+name = "ip_id"
+version = "0.1.0"
+dependencies = [
+ "openzeppelin",
+ "snforge_std",
+]
+
+[[package]]
+name = "openzeppelin"
+version = "0.17.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289"
+dependencies = [
+ "openzeppelin_access",
+ "openzeppelin_account",
+ "openzeppelin_finance",
+ "openzeppelin_governance",
+ "openzeppelin_introspection",
+ "openzeppelin_merkle_tree",
+ "openzeppelin_presets",
+ "openzeppelin_security",
+ "openzeppelin_token",
+ "openzeppelin_upgrades",
+ "openzeppelin_utils",
+]
+
+[[package]]
+name = "openzeppelin_access"
+version = "0.17.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289"
+dependencies = [
+ "openzeppelin_introspection",
+ "openzeppelin_utils",
+]
+
+[[package]]
+name = "openzeppelin_account"
+version = "0.17.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289"
+dependencies = [
+ "openzeppelin_introspection",
+ "openzeppelin_utils",
+]
+
+[[package]]
+name = "openzeppelin_finance"
+version = "0.17.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289"
+dependencies = [
+ "openzeppelin_access",
+ "openzeppelin_token",
+]
+
+[[package]]
+name = "openzeppelin_governance"
+version = "0.17.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289"
+dependencies = [
+ "openzeppelin_access",
+ "openzeppelin_introspection",
+]
+
+[[package]]
+name = "openzeppelin_introspection"
+version = "0.17.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289"
+
+[[package]]
+name = "openzeppelin_merkle_tree"
+version = "0.17.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289"
+
+[[package]]
+name = "openzeppelin_presets"
+version = "0.17.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289"
+dependencies = [
+ "openzeppelin_access",
+ "openzeppelin_account",
+ "openzeppelin_finance",
+ "openzeppelin_introspection",
+ "openzeppelin_token",
+ "openzeppelin_upgrades",
+]
+
+[[package]]
+name = "openzeppelin_security"
+version = "0.17.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289"
+
+[[package]]
+name = "openzeppelin_token"
+version = "0.17.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289"
+dependencies = [
+ "openzeppelin_account",
+ "openzeppelin_governance",
+ "openzeppelin_introspection",
+]
+
+[[package]]
+name = "openzeppelin_upgrades"
+version = "0.17.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289"
+
+[[package]]
+name = "openzeppelin_utils"
+version = "0.17.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289"
+
+[[package]]
+name = "snforge_scarb_plugin"
+version = "0.36.0"
+source = "registry+https://scarbs.xyz/"
+checksum = "sha256:b9550f5c921502be3e240e83aa9e2dc6ee63ae76bfecee082b7a15bead0460c0"
+
+[[package]]
+name = "snforge_std"
+version = "0.36.0"
+source = "registry+https://scarbs.xyz/"
+checksum = "sha256:9f38c6c376447cfe225c5869905122f6a64bc301f5872c7855d2abe6e9c0a1da"
+dependencies = [
+ "snforge_scarb_plugin",
+]
diff --git a/onchain/ip_id/Scarb.toml b/onchain/ip_id/Scarb.toml
new file mode 100644
index 00000000..fe59fa15
--- /dev/null
+++ b/onchain/ip_id/Scarb.toml
@@ -0,0 +1,50 @@
+[package]
+name = "ip_id"
+version = "0.1.0"
+edition = "2024_07"
+
+# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html
+
+[dependencies]
+starknet = "2.8.4"
+openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.17.0" }
+
+[dev-dependencies]
+snforge_std = "0.36.0"
+assert_macros = "2.8.4"
+
+[[target.starknet-contract]]
+sierra = true
+
+[scripts]
+test = "snforge test"
+
+# Visit https://foundry-rs.github.io/starknet-foundry/appendix/scarb-toml.html for more information
+
+# [tool.snforge]                                             # Define `snforge` tool section
+# exit_first = true                                          # Stop tests execution immediately upon the first failure
+# fuzzer_runs = 1234                                         # Number of runs of the random fuzzer
+# fuzzer_seed = 1111                                         # Seed for the random fuzzer
+
+# [[tool.snforge.fork]]                                      # Used for fork testing
+# name = "SOME_NAME"                                         # Fork name
+# url = "http://your.rpc.url"                                # Url of the RPC provider
+# block_id.tag = "latest"                                    # Block to fork from (block tag)
+
+# [[tool.snforge.fork]]
+# name = "SOME_SECOND_NAME"
+# url = "http://your.second.rpc.url"                         
+# block_id.number = "123"                                    # Block to fork from (block number)
+
+# [[tool.snforge.fork]]
+# name = "SOME_THIRD_NAME"
+# url = "http://your.third.rpc.url"
+# block_id.hash = "0x123"                                    # Block to fork from (block hash)
+
+# [profile.dev.cairo]                                        # Configure Cairo compiler
+# unstable-add-statements-code-locations-debug-info = true   # Should be used if you want to use coverage
+# unstable-add-statements-functions-debug-info = true        # Should be used if you want to use coverage/profiler
+# inlining-strategy = "avoid"                                # Should be used if you want to use coverage
+
+# [features]                                                 # Used for conditional compilation
+# enable_for_tests = []                                      # Feature name and list of other features that should be enabled with it
diff --git a/onchain/ip_id/snfoundry.toml b/onchain/ip_id/snfoundry.toml
new file mode 100644
index 00000000..306a0971
--- /dev/null
+++ b/onchain/ip_id/snfoundry.toml
@@ -0,0 +1,11 @@
+# Visit https://foundry-rs.github.io/starknet-foundry/appendix/snfoundry-toml.html
+# and https://foundry-rs.github.io/starknet-foundry/projects/configuration.html for more information
+
+# [sncast.default]                                         # Define a profile name
+# url = "https://free-rpc.nethermind.io/sepolia-juno/v0_7" # Url of the RPC provider
+# accounts-file = "../account-file"                        # Path to the file with the account data
+# account = "mainuser"                                     # Account from `accounts_file` or default account file that will be used for the transactions
+# keystore = "~/keystore"                                  # Path to the keystore file
+# wait-params = { timeout = 300, retry-interval = 10 }     # Wait for submitted transaction parameters
+# block-explorer = "StarkScan"                             # Block explorer service used to display links to transaction details
+# show-explorer-links = true                               # Print links pointing to pages with transaction details in the chosen block explorer
diff --git a/onchain/ip_id/src/IPIdentity.cairo b/onchain/ip_id/src/IPIdentity.cairo
new file mode 100644
index 00000000..7982b04d
--- /dev/null
+++ b/onchain/ip_id/src/IPIdentity.cairo
@@ -0,0 +1,212 @@
+#[starknet::interface]
+pub trait IIPIdentity<TContractState> {
+    fn register_ip_id(
+        ref self: TContractState,
+        ip_id: felt252,
+        metadata_uri: ByteArray,
+        ip_type: ByteArray,
+        license_terms: ByteArray,
+    ) -> u256;
+
+    fn update_ip_id_metadata(
+        ref self: TContractState, ip_id: felt252, new_metadata_uri: ByteArray,
+    );
+
+    fn get_ip_id_data(self: @TContractState, ip_id: felt252) -> IPIDData;
+
+    fn verify_ip_id(ref self: TContractState, ip_id: felt252);
+}
+
+#[derive(Drop, Serde, starknet::Store, Clone)]
+pub struct IPIDData {
+    pub metadata_uri: ByteArray,
+    pub ip_type: ByteArray,
+    pub license_terms: ByteArray,
+    pub is_verified: bool,
+    pub created_at: u64,
+    pub updated_at: u64,
+}
+
+#[starknet::contract]
+pub mod IPIdentity {
+    use core::{
+        array::ArrayTrait, traits::{Into,}, num::traits::Zero,
+        starknet::{
+            ContractAddress,
+            storage::{
+                StoragePointerWriteAccess, StoragePointerReadAccess, StorageMapReadAccess,
+                StorageMapWriteAccess, Map
+            },
+            get_caller_address, get_block_timestamp
+        },
+    };
+    use openzeppelin::access::ownable::OwnableComponent;
+    use openzeppelin::token::erc721::ERC721Component;
+    use openzeppelin::introspection::src5::SRC5Component;
+    use super::{IIPIdentity, IPIDData};
+
+    // Components
+    component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
+    component!(path: ERC721Component, storage: erc721, event: ERC721Event);
+    component!(path: SRC5Component, storage: src5, event: SRC5Event);
+
+    // Impls
+    #[abi(embed_v0)]
+    impl OwnableImpl = OwnableComponent::OwnableImpl<ContractState>;
+    impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;
+    #[abi(embed_v0)]
+    impl ERC721Impl = ERC721Component::ERC721Impl<ContractState>;
+    impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;
+    #[abi(embed_v0)]
+    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
+
+    // Implement ERC721HooksTrait
+    impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait<ContractState> {
+        fn before_update(
+            ref self: ERC721Component::ComponentState<ContractState>,
+            to: ContractAddress,
+            token_id: u256,
+            auth: ContractAddress
+        ) {}
+
+        fn after_update(
+            ref self: ERC721Component::ComponentState<ContractState>,
+            to: ContractAddress,
+            token_id: u256,
+            auth: ContractAddress
+        ) {}
+    }
+
+    // Constants
+    const ERROR_ALREADY_REGISTERED: felt252 = 'IP ID already registered';
+    const ERROR_NOT_OWNER: felt252 = 'Caller is not the owner';
+    const ERROR_INVALID_IP_ID: felt252 = 'Invalid IP ID';
+
+    #[storage]
+    struct Storage {
+        #[substorage(v0)]
+        erc721: ERC721Component::Storage,
+        #[substorage(v0)]
+        ownable: OwnableComponent::Storage,
+        #[substorage(v0)]
+        src5: SRC5Component::Storage,
+        ip_id_to_token_id: Map<felt252, u256>,
+        ip_id_data: Map<felt252, IPIDData>,
+        token_counter: u256,
+    }
+
+    #[event]
+    #[derive(Drop, starknet::Event)]
+    enum Event {
+        #[flat]
+        OwnableEvent: OwnableComponent::Event,
+        #[flat]
+        ERC721Event: ERC721Component::Event,
+        #[flat]
+        SRC5Event: SRC5Component::Event,
+        IPIDRegistered: IPIDRegistered,
+        IPIDUpdated: IPIDUpdated,
+    }
+
+    #[derive(Drop, starknet::Event)]
+    pub struct IPIDRegistered {
+        pub ip_id: felt252,
+        pub owner: ContractAddress,
+        pub token_id: u256,
+    }
+
+    #[derive(Drop, starknet::Event)]
+    struct IPIDUpdated {
+        ip_id: felt252,
+        owner: ContractAddress,
+    }
+
+    #[constructor]
+    fn constructor(
+        ref self: ContractState,
+        owner: ContractAddress,
+        name: ByteArray,
+        symbol: ByteArray,
+        base_uri: ByteArray,
+    ) {
+        self.ownable.initializer(owner);
+        self.erc721.initializer(name, symbol, base_uri);
+    }
+
+    #[abi(embed_v0)]
+    impl IPIdentityImpl of IIPIdentity<ContractState> {
+        fn register_ip_id(
+            ref self: ContractState,
+            ip_id: felt252,
+            metadata_uri: ByteArray,
+            ip_type: ByteArray,
+            license_terms: ByteArray,
+        ) -> u256 {
+            // Check if IP ID is already registered
+            assert(self.ip_id_to_token_id.read(ip_id).is_zero(), ERROR_ALREADY_REGISTERED);
+
+            let caller = get_caller_address();
+
+            // Mint NFT
+            let token_id = self.token_counter.read() + 1;
+            self.token_counter.write(token_id);
+            self.erc721.mint(caller, token_id);
+
+            // Store IP ID data
+            let ip_data = IPIDData {
+                metadata_uri,
+                ip_type,
+                license_terms,
+                is_verified: false,
+                created_at: get_block_timestamp(),
+                updated_at: get_block_timestamp(),
+            };
+
+            self.ip_id_data.write(ip_id, ip_data);
+            self.ip_id_to_token_id.write(ip_id, token_id);
+
+            self.emit(IPIDRegistered { ip_id, owner: caller, token_id });
+
+            token_id
+        }
+
+        fn update_ip_id_metadata(
+            ref self: ContractState, ip_id: felt252, new_metadata_uri: ByteArray,
+        ) {
+            let token_id = self.ip_id_to_token_id.read(ip_id);
+            assert(token_id.is_non_zero(), ERROR_INVALID_IP_ID);
+
+            let caller = get_caller_address();
+            let owner = self.erc721.owner_of(token_id);
+            assert(caller == owner, ERROR_NOT_OWNER);
+
+            // Update data
+            let mut ip_data = self.ip_id_data.read(ip_id);
+            ip_data.metadata_uri = new_metadata_uri;
+            ip_data.updated_at = get_block_timestamp();
+            self.ip_id_data.write(ip_id, ip_data);
+
+            self.emit(IPIDUpdated { ip_id, owner: caller });
+        }
+
+        fn get_ip_id_data(self: @ContractState, ip_id: felt252) -> IPIDData {
+            assert(self.ip_id_to_token_id.read(ip_id).is_non_zero(), ERROR_INVALID_IP_ID);
+            self.ip_id_data.read(ip_id)
+        }
+
+        fn verify_ip_id(ref self: ContractState, ip_id: felt252) {
+            self.ownable.assert_only_owner();
+
+            let token_id = self.ip_id_to_token_id.read(ip_id);
+            assert(token_id.is_non_zero(), ERROR_INVALID_IP_ID);
+
+            let owner = self.erc721.owner_of(token_id);
+            let mut ip_data = self.ip_id_data.read(ip_id);
+            ip_data.is_verified = true;
+            ip_data.updated_at = get_block_timestamp();
+            self.ip_id_data.write(ip_id, ip_data);
+
+            self.emit(IPIDUpdated { ip_id, owner });
+        }
+    }
+}
diff --git a/onchain/ip_id/src/lib.cairo b/onchain/ip_id/src/lib.cairo
new file mode 100644
index 00000000..6179a57a
--- /dev/null
+++ b/onchain/ip_id/src/lib.cairo
@@ -0,0 +1 @@
+pub mod IPIdentity;
diff --git a/onchain/ip_id/tests/test_contract.cairo b/onchain/ip_id/tests/test_contract.cairo
new file mode 100644
index 00000000..dcfb4413
--- /dev/null
+++ b/onchain/ip_id/tests/test_contract.cairo
@@ -0,0 +1,188 @@
+use ip_id::IPIdentity::{IIPIdentityDispatcher, IIPIdentityDispatcherTrait,};
+use snforge_std::{
+    declare, ContractClassTrait, DeclareResultTrait, cheat_caller_address,
+    start_cheat_block_timestamp, stop_cheat_block_timestamp, CheatSpan,
+};
+use starknet::{ContractAddress, contract_address_const};
+use core::serde::Serde;
+
+// Helper functions to get test addresses
+fn owner() -> ContractAddress {
+    contract_address_const::<'owner'>()
+}
+
+fn non_owner() -> ContractAddress {
+    contract_address_const::<'non_owner'>()
+}
+
+fn user() -> ContractAddress {
+    contract_address_const::<'user'>()
+}
+
+// Helper function to deploy the contract
+fn deploy_ip_identity() -> (IIPIdentityDispatcher, ContractAddress) {
+    // Declare IPIdentity contract
+    let contract_class = declare("IPIdentity").unwrap().contract_class();
+
+    // Prepare constructor calldata
+    let owner_addr = owner();
+    let name: ByteArray = "IPIdentity";
+    let symbol: ByteArray = "IPID";
+    let base_uri: ByteArray = "https://ipfs.io/ipfs/";
+
+    let mut calldata = array![];
+    owner_addr.serialize(ref calldata);
+    name.serialize(ref calldata);
+    symbol.serialize(ref calldata);
+    base_uri.serialize(ref calldata);
+
+    // Deploy contract
+    let (contract_address, _) = contract_class.deploy(@calldata).unwrap();
+    let ip_identity = IIPIdentityDispatcher { contract_address };
+
+    (ip_identity, owner_addr)
+}
+
+#[test]
+#[should_panic(expected: ('IP ID already registered',))]
+fn test_register_ip_id_already_registered() {
+    let (ip_identity, _) = deploy_ip_identity();
+    let caller = user();
+    let ip_id = 123;
+    let metadata_uri: ByteArray = "ipfs://metadata";
+    let ip_type: ByteArray = "image";
+    let license_terms: ByteArray = "MIT";
+
+    // Register IP ID first time
+    cheat_caller_address(ip_identity.contract_address, caller, CheatSpan::TargetCalls(1));
+    ip_identity.register_ip_id(ip_id, metadata_uri.clone(), ip_type.clone(), license_terms.clone());
+
+    // Attempt to register same IP ID again
+    cheat_caller_address(ip_identity.contract_address, caller, CheatSpan::TargetCalls(1));
+    ip_identity.register_ip_id(ip_id, metadata_uri, ip_type, license_terms);
+}
+
+#[test]
+fn test_update_ip_id_metadata_success() {
+    let (ip_identity, _) = deploy_ip_identity();
+    let caller = user();
+    let ip_id = 123;
+    let metadata_uri: ByteArray = "ipfs://metadata";
+    let new_metadata_uri: ByteArray = "ipfs://new_metadata";
+    let ip_type: ByteArray = "image";
+    let license_terms: ByteArray = "MIT";
+
+    // Register IP ID
+    cheat_caller_address(ip_identity.contract_address, caller, CheatSpan::TargetCalls(1));
+    ip_identity.register_ip_id(ip_id, metadata_uri, ip_type, license_terms);
+
+    // Update metadata
+    cheat_caller_address(ip_identity.contract_address, caller, CheatSpan::TargetCalls(1));
+    start_cheat_block_timestamp(ip_identity.contract_address, 2000);
+    ip_identity.update_ip_id_metadata(ip_id, new_metadata_uri.clone());
+    stop_cheat_block_timestamp(ip_identity.contract_address);
+
+    // Verify updated data
+    let ip_data = ip_identity.get_ip_id_data(ip_id);
+    assert(ip_data.metadata_uri == new_metadata_uri, 'Invalid new metadata URI');
+    assert(ip_data.updated_at == 2000, 'Invalid updated_at');
+}
+
+#[test]
+#[should_panic(expected: ('Caller is not the owner',))]
+fn test_update_ip_id_metadata_not_owner() {
+    let (ip_identity, _) = deploy_ip_identity();
+    let owner = user();
+    let non_owner_addr = non_owner();
+    let ip_id = 123;
+    let metadata_uri: ByteArray = "ipfs://metadata";
+    let new_metadata_uri: ByteArray = "ipfs://new_metadata";
+    let ip_type: ByteArray = "image";
+    let license_terms: ByteArray = "MIT";
+
+    // Register IP ID
+    cheat_caller_address(ip_identity.contract_address, owner, CheatSpan::TargetCalls(1));
+    ip_identity.register_ip_id(ip_id, metadata_uri, ip_type, license_terms);
+
+    // Attempt to update metadata as non-owner
+    cheat_caller_address(ip_identity.contract_address, non_owner_addr, CheatSpan::TargetCalls(1));
+    ip_identity.update_ip_id_metadata(ip_id, new_metadata_uri);
+}
+
+#[test]
+#[should_panic(expected: ('Invalid IP ID',))]
+fn test_update_ip_id_metadata_invalid_id() {
+    let (ip_identity, _) = deploy_ip_identity();
+    let caller = user();
+    let ip_id = 123;
+    let new_metadata_uri: ByteArray = "ipfs://new_metadata";
+
+    // Attempt to update metadata for non-existent IP ID
+    cheat_caller_address(ip_identity.contract_address, caller, CheatSpan::TargetCalls(1));
+    ip_identity.update_ip_id_metadata(ip_id, new_metadata_uri);
+}
+
+#[test]
+fn test_verify_ip_id_success() {
+    let (ip_identity, owner_addr) = deploy_ip_identity();
+    let user_addr = user();
+    let ip_id = 123;
+    let metadata_uri: ByteArray = "ipfs://metadata";
+    let ip_type: ByteArray = "image";
+    let license_terms: ByteArray = "MIT";
+
+    // Register IP ID
+    cheat_caller_address(ip_identity.contract_address, user_addr, CheatSpan::TargetCalls(1));
+    ip_identity.register_ip_id(ip_id, metadata_uri, ip_type, license_terms);
+
+    // Verify IP ID
+    cheat_caller_address(ip_identity.contract_address, owner_addr, CheatSpan::TargetCalls(1));
+    start_cheat_block_timestamp(ip_identity.contract_address, 2000);
+    ip_identity.verify_ip_id(ip_id);
+    stop_cheat_block_timestamp(ip_identity.contract_address);
+
+    // Verify updated data
+    let ip_data = ip_identity.get_ip_id_data(ip_id);
+    assert(ip_data.is_verified, 'Should be verified');
+    assert(ip_data.updated_at == 2000, 'Invalid updated_at');
+}
+
+#[test]
+#[should_panic(expected: ('Caller is not the owner',))]
+fn test_verify_ip_id_not_owner() {
+    let (ip_identity, _) = deploy_ip_identity();
+    let caller = user();
+    let ip_id = 123;
+    let metadata_uri: ByteArray = "ipfs://metadata";
+    let ip_type: ByteArray = "image";
+    let license_terms: ByteArray = "MIT";
+
+    // Register IP ID
+    cheat_caller_address(ip_identity.contract_address, caller, CheatSpan::TargetCalls(1));
+    ip_identity.register_ip_id(ip_id, metadata_uri, ip_type, license_terms);
+
+    // Attempt to verify as non-owner
+    cheat_caller_address(ip_identity.contract_address, caller, CheatSpan::TargetCalls(1));
+    ip_identity.verify_ip_id(ip_id);
+}
+
+#[test]
+#[should_panic(expected: ('Invalid IP ID',))]
+fn test_verify_ip_id_invalid_id() {
+    let (ip_identity, owner_addr) = deploy_ip_identity();
+    let ip_id = 123;
+
+    // Attempt to verify non-existent IP ID
+    cheat_caller_address(ip_identity.contract_address, owner_addr, CheatSpan::TargetCalls(1));
+    ip_identity.verify_ip_id(ip_id);
+}
+
+#[test]
+#[should_panic(expected: ('Invalid IP ID',))]
+fn test_get_ip_id_data_invalid_id() {
+    let (ip_identity, _) = deploy_ip_identity();
+    let ip_id = 999;
+
+    // Attempt to get data for non-existent IP ID
+    ip_identity.get_ip_id_data(ip_id);
+}


Reviews:
user: login: PedroRosalba
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T21:57:46Z

Review comments:
diff_hunk: @@ -0,0 +1,285 @@
+use starknet::ContractAddress;
+
+#[starknet::interface]
+pub trait IIPIdentity<TContractState> {
+    fn register_ip_id(
+        ref self: TContractState,
+        ip_id: felt252,
+        metadata_uri: ByteArray,
+        ip_type: ByteArray,
+        license_terms: ByteArray,
+    ) -> u256;
+
+    fn update_ip_id_metadata(
+        ref self: TContractState, ip_id: felt252, new_metadata_uri: ByteArray,
+    );
+
+    fn get_ip_id_data(self: @TContractState, ip_id: felt252) -> IPIDData;
+
+    fn get_ip_id_by_owner(self: @TContractState, owner: ContractAddress) -> felt252;
+
+    fn get_owner_by_ip_id(self: @TContractState, ip_id: felt252) -> ContractAddress;
+
+    fn verify_ip_id(ref self: TContractState, ip_id: felt252);
+
+    fn set_payment_token(ref self: TContractState, token_address: ContractAddress);
+
+    fn set_paused(ref self: TContractState, paused: bool);
+}
+
+#[derive(Drop, Serde, starknet::Store, Clone)]
+pub struct IPIDData {
+    pub metadata_uri: ByteArray,
+    pub ip_type: ByteArray,
+    pub license_terms: ByteArray,
+    pub is_verified: bool,
+    pub created_at: u64,
+    pub updated_at: u64,
+}
+
+#[starknet::contract]
+pub mod IPIdentity {
+    use core::{
+        array::ArrayTrait, traits::{Into,}, num::traits::Zero,
+        starknet::{
+            ContractAddress,
+            storage::{
+                StoragePointerWriteAccess, StoragePointerReadAccess, StorageMapReadAccess,
+                StorageMapWriteAccess, Map
+            },
+            get_caller_address, get_contract_address, get_block_timestamp
+        },
+    };
+    use openzeppelin::access::ownable::OwnableComponent;
+    use openzeppelin::security::pausable::PausableComponent;
+    use openzeppelin::token::erc721::ERC721Component;
+    use openzeppelin::introspection::src5::SRC5Component;
+    use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
+    use super::{IIPIdentity, IPIDData};
+
+    // Components
+    component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
+    component!(path: PausableComponent, storage: pausable, event: PausableEvent);
+    component!(path: ERC721Component, storage: erc721, event: ERC721Event);
+    component!(path: SRC5Component, storage: src5, event: SRC5Event);
+
+    // Impls
+    #[abi(embed_v0)]
+    impl OwnableImpl = OwnableComponent::OwnableImpl<ContractState>;
+    impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;
+    #[abi(embed_v0)]
+    impl PausableImpl = PausableComponent::PausableImpl<ContractState>;
+    impl PausableInternalImpl = PausableComponent::InternalImpl<ContractState>;
+    #[abi(embed_v0)]
+    impl ERC721Impl = ERC721Component::ERC721Impl<ContractState>;
+    impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;
+    #[abi(embed_v0)]
+    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
+
+    // Implement ERC721HooksTrait
+    impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait<ContractState> {
+        fn before_update(
+            ref self: ERC721Component::ComponentState<ContractState>,
+            to: ContractAddress,
+            token_id: u256,
+            auth: ContractAddress
+        ) {}
+
+        fn after_update(
+            ref self: ERC721Component::ComponentState<ContractState>,
+            to: ContractAddress,
+            token_id: u256,
+            auth: ContractAddress
+        ) {}
+    }
+
+    // Constants
+    const MINT_PRICE: u256 = 1000000000000000; // 0.001 ETH
+    const ERROR_INVALID_PAYMENT: felt252 = 'Invalid payment amount';
+    const ERROR_ALREADY_REGISTERED: felt252 = 'IP ID already registered';
+    const ERROR_NOT_OWNER: felt252 = 'Caller is not the owner';
+    const ERROR_INVALID_IP_ID: felt252 = 'Invalid IP ID';
+
+    #[storage]
+    struct Storage {
+        #[substorage(v0)]
+        erc721: ERC721Component::Storage,
+        #[substorage(v0)]
+        ownable: OwnableComponent::Storage,
+        #[substorage(v0)]
+        pausable: PausableComponent::Storage,
+        #[substorage(v0)]
+        src5: SRC5Component::Storage,
+        ip_id_to_owner: Map<felt252, ContractAddress>,
+        owner_to_ip_id: Map<ContractAddress, felt252>,
+        ip_id_data: Map<felt252, IPIDData>,
+        token_counter: u256,
+        mint_payment_token: ContractAddress,
+    }
+
+    #[event]
+    #[derive(Drop, starknet::Event)]
+    enum Event {
+        #[flat]
+        OwnableEvent: OwnableComponent::Event,
+        #[flat]
+        PausableEvent: PausableComponent::Event,
+        #[flat]
+        ERC721Event: ERC721Component::Event,
+        #[flat]
+        SRC5Event: SRC5Component::Event,
+        IPIDRegistered: IPIDRegistered,
+        IPIDUpdated: IPIDUpdated,
+        PaymentTokenUpdated: PaymentTokenUpdated,
+    }
+
+    #[derive(Drop, starknet::Event)]
+    struct IPIDRegistered {
+        ip_id: felt252,
+        owner: ContractAddress,
+        token_id: u256,
+    }
+
+    #[derive(Drop, starknet::Event)]
+    struct IPIDUpdated {
+        ip_id: felt252,
+        owner: ContractAddress,
+    }
+
+    #[derive(Drop, starknet::Event)]
+    struct PaymentTokenUpdated {
+        new_token: ContractAddress,
+    }
+
+    #[constructor]
+    fn constructor(
+        ref self: ContractState,
+        owner: ContractAddress,
+        name: ByteArray,
+        symbol: ByteArray,
+        base_uri: ByteArray,
+        payment_token: ContractAddress,
+    ) {
+        self.ownable.initializer(owner);
+        self.erc721.initializer(name, symbol, base_uri);
+        self.mint_payment_token.write(payment_token);
+    }
+
+    #[abi(embed_v0)]
+    impl IPIdentityImpl of IIPIdentity<ContractState> {
+        fn register_ip_id(
+            ref self: ContractState,
+            ip_id: felt252,
+            metadata_uri: ByteArray,
+            ip_type: ByteArray,
+            license_terms: ByteArray,
+        ) -> u256 {
+            self.pausable.assert_not_paused();
+
+            // Check if IP ID is already registered
+            assert(self.ip_id_to_owner.read(ip_id).is_zero(), ERROR_ALREADY_REGISTERED);
+
+            let caller = get_caller_address();
+
+            // Process payment
+            self._process_payment(caller, MINT_PRICE);
path: onchain/ip_id/src/IPIdentity.cairo
user: login: PedroRosalba
body: this process_payment does not make much sense since the user's already going to have to pay to mint the NFT automatically (it's a blockchain event, the user's wallet is connected)
created_at: 2025-04-15T21:54:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,285 @@
+use starknet::ContractAddress;
+
+#[starknet::interface]
+pub trait IIPIdentity<TContractState> {
+    fn register_ip_id(
+        ref self: TContractState,
+        ip_id: felt252,
+        metadata_uri: ByteArray,
+        ip_type: ByteArray,
+        license_terms: ByteArray,
+    ) -> u256;
+
+    fn update_ip_id_metadata(
+        ref self: TContractState, ip_id: felt252, new_metadata_uri: ByteArray,
+    );
+
+    fn get_ip_id_data(self: @TContractState, ip_id: felt252) -> IPIDData;
+
+    fn get_ip_id_by_owner(self: @TContractState, owner: ContractAddress) -> felt252;
+
+    fn get_owner_by_ip_id(self: @TContractState, ip_id: felt252) -> ContractAddress;
+
+    fn verify_ip_id(ref self: TContractState, ip_id: felt252);
+
+    fn set_payment_token(ref self: TContractState, token_address: ContractAddress);
+
+    fn set_paused(ref self: TContractState, paused: bool);
+}
+
+#[derive(Drop, Serde, starknet::Store, Clone)]
+pub struct IPIDData {
+    pub metadata_uri: ByteArray,
+    pub ip_type: ByteArray,
+    pub license_terms: ByteArray,
+    pub is_verified: bool,
+    pub created_at: u64,
+    pub updated_at: u64,
+}
+
+#[starknet::contract]
+pub mod IPIdentity {
+    use core::{
+        array::ArrayTrait, traits::{Into,}, num::traits::Zero,
+        starknet::{
+            ContractAddress,
+            storage::{
+                StoragePointerWriteAccess, StoragePointerReadAccess, StorageMapReadAccess,
+                StorageMapWriteAccess, Map
+            },
+            get_caller_address, get_contract_address, get_block_timestamp
+        },
+    };
+    use openzeppelin::access::ownable::OwnableComponent;
+    use openzeppelin::security::pausable::PausableComponent;
+    use openzeppelin::token::erc721::ERC721Component;
+    use openzeppelin::introspection::src5::SRC5Component;
+    use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
+    use super::{IIPIdentity, IPIDData};
+
+    // Components
+    component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
+    component!(path: PausableComponent, storage: pausable, event: PausableEvent);
+    component!(path: ERC721Component, storage: erc721, event: ERC721Event);
+    component!(path: SRC5Component, storage: src5, event: SRC5Event);
+
+    // Impls
+    #[abi(embed_v0)]
+    impl OwnableImpl = OwnableComponent::OwnableImpl<ContractState>;
+    impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;
+    #[abi(embed_v0)]
+    impl PausableImpl = PausableComponent::PausableImpl<ContractState>;
+    impl PausableInternalImpl = PausableComponent::InternalImpl<ContractState>;
+    #[abi(embed_v0)]
+    impl ERC721Impl = ERC721Component::ERC721Impl<ContractState>;
+    impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;
+    #[abi(embed_v0)]
+    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
+
+    // Implement ERC721HooksTrait
+    impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait<ContractState> {
+        fn before_update(
+            ref self: ERC721Component::ComponentState<ContractState>,
+            to: ContractAddress,
+            token_id: u256,
+            auth: ContractAddress
+        ) {}
+
+        fn after_update(
+            ref self: ERC721Component::ComponentState<ContractState>,
+            to: ContractAddress,
+            token_id: u256,
+            auth: ContractAddress
+        ) {}
+    }
+
+    // Constants
+    const MINT_PRICE: u256 = 1000000000000000; // 0.001 ETH
+    const ERROR_INVALID_PAYMENT: felt252 = 'Invalid payment amount';
+    const ERROR_ALREADY_REGISTERED: felt252 = 'IP ID already registered';
+    const ERROR_NOT_OWNER: felt252 = 'Caller is not the owner';
+    const ERROR_INVALID_IP_ID: felt252 = 'Invalid IP ID';
+
+    #[storage]
+    struct Storage {
+        #[substorage(v0)]
+        erc721: ERC721Component::Storage,
+        #[substorage(v0)]
+        ownable: OwnableComponent::Storage,
+        #[substorage(v0)]
+        pausable: PausableComponent::Storage,
+        #[substorage(v0)]
+        src5: SRC5Component::Storage,
+        ip_id_to_owner: Map<felt252, ContractAddress>,
+        owner_to_ip_id: Map<ContractAddress, felt252>,
path: onchain/ip_id/src/IPIdentity.cairo
user: login: PedroRosalba
body: do you need both storages ? in my mind that's like "redundant" (they're not exactly the same but serve similar functionalities, so consider brushing this to a simpler resolution
created_at: 2025-04-15T21:56:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,285 @@
+use starknet::ContractAddress;
+
+#[starknet::interface]
+pub trait IIPIdentity<TContractState> {
+    fn register_ip_id(
+        ref self: TContractState,
+        ip_id: felt252,
+        metadata_uri: ByteArray,
+        ip_type: ByteArray,
+        license_terms: ByteArray,
+    ) -> u256;
+
+    fn update_ip_id_metadata(
+        ref self: TContractState, ip_id: felt252, new_metadata_uri: ByteArray,
+    );
+
+    fn get_ip_id_data(self: @TContractState, ip_id: felt252) -> IPIDData;
+
+    fn get_ip_id_by_owner(self: @TContractState, owner: ContractAddress) -> felt252;
+
+    fn get_owner_by_ip_id(self: @TContractState, ip_id: felt252) -> ContractAddress;
+
+    fn verify_ip_id(ref self: TContractState, ip_id: felt252);
+
+    fn set_payment_token(ref self: TContractState, token_address: ContractAddress);
+
+    fn set_paused(ref self: TContractState, paused: bool);
+}
+
+#[derive(Drop, Serde, starknet::Store, Clone)]
+pub struct IPIDData {
+    pub metadata_uri: ByteArray,
+    pub ip_type: ByteArray,
+    pub license_terms: ByteArray,
+    pub is_verified: bool,
+    pub created_at: u64,
+    pub updated_at: u64,
+}
+
+#[starknet::contract]
+pub mod IPIdentity {
+    use core::{
+        array::ArrayTrait, traits::{Into,}, num::traits::Zero,
+        starknet::{
+            ContractAddress,
+            storage::{
+                StoragePointerWriteAccess, StoragePointerReadAccess, StorageMapReadAccess,
+                StorageMapWriteAccess, Map
+            },
+            get_caller_address, get_contract_address, get_block_timestamp
+        },
+    };
+    use openzeppelin::access::ownable::OwnableComponent;
+    use openzeppelin::security::pausable::PausableComponent;
+    use openzeppelin::token::erc721::ERC721Component;
+    use openzeppelin::introspection::src5::SRC5Component;
+    use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
+    use super::{IIPIdentity, IPIDData};
+
+    // Components
+    component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
+    component!(path: PausableComponent, storage: pausable, event: PausableEvent);
+    component!(path: ERC721Component, storage: erc721, event: ERC721Event);
+    component!(path: SRC5Component, storage: src5, event: SRC5Event);
+
+    // Impls
+    #[abi(embed_v0)]
+    impl OwnableImpl = OwnableComponent::OwnableImpl<ContractState>;
+    impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;
+    #[abi(embed_v0)]
+    impl PausableImpl = PausableComponent::PausableImpl<ContractState>;
+    impl PausableInternalImpl = PausableComponent::InternalImpl<ContractState>;
+    #[abi(embed_v0)]
+    impl ERC721Impl = ERC721Component::ERC721Impl<ContractState>;
+    impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;
+    #[abi(embed_v0)]
+    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
+
+    // Implement ERC721HooksTrait
+    impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait<ContractState> {
+        fn before_update(
+            ref self: ERC721Component::ComponentState<ContractState>,
+            to: ContractAddress,
+            token_id: u256,
+            auth: ContractAddress
+        ) {}
+
+        fn after_update(
+            ref self: ERC721Component::ComponentState<ContractState>,
+            to: ContractAddress,
+            token_id: u256,
+            auth: ContractAddress
+        ) {}
+    }
+
+    // Constants
+    const MINT_PRICE: u256 = 1000000000000000; // 0.001 ETH
+    const ERROR_INVALID_PAYMENT: felt252 = 'Invalid payment amount';
+    const ERROR_ALREADY_REGISTERED: felt252 = 'IP ID already registered';
+    const ERROR_NOT_OWNER: felt252 = 'Caller is not the owner';
+    const ERROR_INVALID_IP_ID: felt252 = 'Invalid IP ID';
+
+    #[storage]
+    struct Storage {
+        #[substorage(v0)]
+        erc721: ERC721Component::Storage,
+        #[substorage(v0)]
+        ownable: OwnableComponent::Storage,
+        #[substorage(v0)]
+        pausable: PausableComponent::Storage,
+        #[substorage(v0)]
+        src5: SRC5Component::Storage,
+        ip_id_to_owner: Map<felt252, ContractAddress>,
+        owner_to_ip_id: Map<ContractAddress, felt252>,
+        ip_id_data: Map<felt252, IPIDData>,
+        token_counter: u256,
+        mint_payment_token: ContractAddress,
+    }
+
+    #[event]
+    #[derive(Drop, starknet::Event)]
+    enum Event {
+        #[flat]
+        OwnableEvent: OwnableComponent::Event,
+        #[flat]
+        PausableEvent: PausableComponent::Event,
+        #[flat]
+        ERC721Event: ERC721Component::Event,
+        #[flat]
+        SRC5Event: SRC5Component::Event,
+        IPIDRegistered: IPIDRegistered,
+        IPIDUpdated: IPIDUpdated,
+        PaymentTokenUpdated: PaymentTokenUpdated,
+    }
+
+    #[derive(Drop, starknet::Event)]
+    struct IPIDRegistered {
+        ip_id: felt252,
+        owner: ContractAddress,
+        token_id: u256,
+    }
+
+    #[derive(Drop, starknet::Event)]
+    struct IPIDUpdated {
+        ip_id: felt252,
+        owner: ContractAddress,
+    }
+
+    #[derive(Drop, starknet::Event)]
+    struct PaymentTokenUpdated {
+        new_token: ContractAddress,
+    }
+
+    #[constructor]
+    fn constructor(
+        ref self: ContractState,
+        owner: ContractAddress,
+        name: ByteArray,
+        symbol: ByteArray,
+        base_uri: ByteArray,
+        payment_token: ContractAddress,
+    ) {
+        self.ownable.initializer(owner);
+        self.erc721.initializer(name, symbol, base_uri);
+        self.mint_payment_token.write(payment_token);
+    }
+
+    #[abi(embed_v0)]
+    impl IPIdentityImpl of IIPIdentity<ContractState> {
+        fn register_ip_id(
+            ref self: ContractState,
+            ip_id: felt252,
+            metadata_uri: ByteArray,
+            ip_type: ByteArray,
+            license_terms: ByteArray,
+        ) -> u256 {
+            self.pausable.assert_not_paused();
+
+            // Check if IP ID is already registered
+            assert(self.ip_id_to_owner.read(ip_id).is_zero(), ERROR_ALREADY_REGISTERED);
+
+            let caller = get_caller_address();
+
+            // Process payment
+            self._process_payment(caller, MINT_PRICE);
+
+            // Mint NFT
+            let token_id = self.token_counter.read() + 1;
+            self.token_counter.write(token_id);
+            self.erc721.mint(caller, token_id); // Changed from _mint to mint
+
+            // Store IP ID data
+            let ip_data = IPIDData {
+                metadata_uri,
+                ip_type,
+                license_terms,
+                is_verified: false,
+                created_at: get_block_timestamp(),
+                updated_at: get_block_timestamp(),
+            };
+
+            self.ip_id_data.write(ip_id, ip_data);
+            self.ip_id_to_owner.write(ip_id, caller);
+            self.owner_to_ip_id.write(caller, ip_id);
+
+            self.emit(IPIDRegistered { ip_id, owner: caller, token_id });
+
+            token_id
+        }
+
+        fn update_ip_id_metadata(
+            ref self: ContractState, ip_id: felt252, new_metadata_uri: ByteArray,
+        ) {
+            self.pausable.assert_not_paused();
+
+            let caller = get_caller_address();
+            let owner = self.ip_id_to_owner.read(ip_id);
+            assert(owner.is_non_zero(), ERROR_INVALID_IP_ID);
+            assert(caller == owner, ERROR_NOT_OWNER);
+
+            // Update data
+            let mut ip_data = self.ip_id_data.read(ip_id);
+            ip_data.metadata_uri = new_metadata_uri;
+            ip_data.updated_at = get_block_timestamp();
+            self.ip_id_data.write(ip_id, ip_data);
+
+            self.emit(IPIDUpdated { ip_id, owner: caller });
+        }
+
+        fn get_ip_id_data(self: @ContractState, ip_id: felt252) -> IPIDData {
+            assert(self.ip_id_to_owner.read(ip_id).is_non_zero(), ERROR_INVALID_IP_ID);
+            self.ip_id_data.read(ip_id)
+        }
+
+        fn get_ip_id_by_owner(self: @ContractState, owner: ContractAddress) -> felt252 {
+            let ip_id = self.owner_to_ip_id.read(owner);
+            assert(ip_id != 0, ERROR_INVALID_IP_ID);
+            ip_id
+        }
+
+        fn get_owner_by_ip_id(self: @ContractState, ip_id: felt252) -> ContractAddress {
+            let owner = self.ip_id_to_owner.read(ip_id);
+            assert(owner.is_non_zero(), ERROR_INVALID_IP_ID);
+            owner
+        }
+
+        fn verify_ip_id(ref self: ContractState, ip_id: felt252) {
+            self.ownable.assert_only_owner();
+
+            let owner = self.ip_id_to_owner.read(ip_id);
+            assert(owner.is_non_zero(), ERROR_INVALID_IP_ID);
+
+            let mut ip_data = self.ip_id_data.read(ip_id);
+            ip_data.is_verified = true;
+            ip_data.updated_at = get_block_timestamp();
+            self.ip_id_data.write(ip_id, ip_data);
+
+            self.emit(IPIDUpdated { ip_id, owner });
+        }
+
+        fn set_payment_token(ref self: ContractState, token_address: ContractAddress) {
+            self.ownable.assert_only_owner();
+            self.mint_payment_token.write(token_address);
+            self.emit(PaymentTokenUpdated { new_token: token_address });
+        }
+
+        fn set_paused(ref self: ContractState, paused: bool) {
path: onchain/ip_id/src/IPIdentity.cairo
user: login: PedroRosalba
body: do you need the pausable component?
created_at: 2025-04-15T21:57:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-14T12:37:04Z
body: @anonfedora is attempting to deploy a commit to the **mediolanocombr-gmailcom's projects** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=mediolanocombr-gmailcom's%20projects&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%22d9e6529e803a5f3bf6e1d124e65fbd6ee2441e37%22%7D%2C%22id%22%3A%22QmZ6CA4N2wUzs7p1rVe4UxDVMD8ocriZhPYWUAeZMNTcbe%22%2C%22org%22%3A%22mediolano-app%22%2C%22prId%22%3A119%2C%22repo%22%3A%22mediolano-app%22%7D).


author_association: NONE
user: login: anonfedora
created_at: 2025-04-17T12:59:41Z
body: Comments resolved.
![Screenshot_20250417_135852](https://github.com/user-attachments/assets/447ee60f-4209-4060-a0cd-34a94f64fa6b)

author_association: CONTRIBUTOR