メインコンテンツまでスキップ

Composable Vehicle NFTs Standard

Status: Draft

This CIP proposes a standard for creating composable NFTs on the Core Blockchain (XCB) that represent vehicles as holder tokens composed of individual part NFTs. Building on existing standards like CIP-721 for NFTs, CIP-150 for metadata, and CIP-151 for lifecycles, it enables features such as part attachment/detachment, modular ownership, and secure audit trails using decentralized storage and fingerprinting. This is ideal for real-world asset tokenization in automotive and supply chain applications.

Abstract

This CIP defines a standard for composable Non-Fungible Tokens (NFTs) representing vehicles and their parts on the Core Blockchain (XCB). It extends CIP-721 for basic NFT functionality, incorporates CIP-150 for on-chain key-value metadata storage, and CIP-151 for token lifecycle management. The standard introduces composability through a Composer contract that allows attaching and detaching part NFTs to a vehicle holder NFT, enabling modular ownership, upgrades, and audit trails with decentralized storage references (IPNS) and zero-trust fingerprinting (SHA256).

Motivation

Tokenizing physical assets like vehicles requires more than basic NFTs: parts must be interchangeable, lifecycles (e.g., warranty expiration) enforced, and compositions auditable. Existing standards like CIP-721 provide ownership tracking but lack native support for hierarchy, modularity, and integrated metadata for real-world assets (RWAs). This CIP addresses these gaps by standardizing interfaces for part NFTs, vehicle holder NFTs, and a composer mechanism, facilitating integration with ERP systems, supply chains, and dApps like CorePass ID. It enables use cases such as customizable vehicle configurations, provenance tracking, and loyalty programs for manufacturers.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

This standard defines three main interfaces: IPartNFT for individual components, IVehicleNFT for the holder token, and IComposer for managing compositions. Contracts implementing these MUST also conform to CIP-721 for interoperability.

IPartNFT Interface

This interface extends CIP-721 with additional metadata for parts, including lifecycle (CIP-151) and decentralized storage.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import "@core-blockchain/contracts/interfaces/ICBC721.sol"; // CIP-721 equivalent

interface IPartNFT is ICBC721 {
struct PartMetadata {
string specs; // JSON with details
string ipnsNamespace; // Decentralized storage (IPNS for brand/model)
bytes32 fingerprint; // SHA256 for zero-trust audit trail
uint256 expiration; // CIP-151 lifecycle (0 = none)
bool locked; // Prevent edits for provenance
uint256 createdAt; // Timestamp of creation
}

function partMetadata(uint256 tokenId) external view returns (PartMetadata memory);

function mintPart(
address to,
uint256 tokenId,
string calldata specs,
string calldata ipnsNamespace,
bytes32 fingerprint,
uint256 expiration
) external;

function attachToVehicle(uint256 tokenId, address composerAddress) external;

function setExpiration(uint256 tokenId, uint256 newExpiration) external;

function lockPart(uint256 tokenId) external;

function getPartMetadata(uint256 tokenId) external view returns (
string memory specs,
string memory ipnsNamespace,
bytes32 fingerprint,
uint256 expiration,
bool locked,
uint256 createdAt
);

function isPartExpired(uint256 tokenId) external view returns (bool);

function isPartLocked(uint256 tokenId) external view returns (bool);

event PartMinted(uint256 indexed tokenId, address indexed to, bytes32 fingerprint);

event PartExpirationUpdated(uint256 indexed tokenId, uint256 newExpiration);

event PartLocked(uint256 indexed tokenId);

}
  • mintPart: MUST mint a new part NFT with the provided metadata. The fingerprint ensures zero-trust auditability.
  • attachToVehicle: MUST transfer the part to the composer for attachment, checking locks and expiration.
  • setExpiration: MUST update the expiration timestamp per CIP-151, only if not locked.
  • lockPart: MUST seal the part's metadata to prevent further changes.
  • View Functions: MUST provide read access to metadata, expiration status, and lock status.
  • tokenURI: SHOULD return an IPNS-based URI for decentralized metadata access.

IVehicleNFT Interface

This interface extends CIP-721 with CIP-150 KV metadata storage, sealing, and batch updates.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import "@core-blockchain/contracts/interfaces/ICBC721.sol"; // CIP-721 equivalent

interface IVehicleNFT is ICBC721 {
function mintVehicle(address to, uint256 tokenId, string calldata overallSpecs, string calldata vin) external;
function setValue(uint256 tokenId, string calldata key, string calldata value) external;
function batchSetValues(uint256 tokenId, string[] calldata keys, string[] calldata values) external;
function seal(uint256 tokenId) external;
function getAttachedParts(uint256 tokenId) external view returns (string memory);
function kvMetadata(uint256 tokenId, string calldata key) external view returns (string memory);
function sealed(uint256 tokenId) external view returns (bool);
function createdAt(uint256 tokenId) external view returns (uint256);

event VehicleMinted(uint256 indexed tokenId, address indexed to, string vin);
event MetadataUpdated(uint256 indexed tokenId, string key, string value);
event BatchMetadataUpdated(uint256 indexed tokenId, string[] keys, string[] values);
event VehicleSealed(uint256 indexed tokenId);
}
  • mintVehicle: MUST mint the vehicle NFT with initial specs and VIN in KV metadata.
  • setValue / batchSetValues: MUST update KV metadata per CIP-150, with batching inspired by CIP-3005 for efficiency. Disallowed if sealed.
  • seal: MUST make the metadata immutable.
  • getAttachedParts: MUST return a JSON string of attached parts from KV.
  • View Functions: MUST expose KV values, seal status, and creation timestamp.

Composer Interface

This interface defines the composability logic for attaching and detaching parts.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@core-blockchain/contracts/interfaces/ICBC721Receiver.sol";

interface IComposer is ICBC721Receiver {
function attachPart(uint256 vehicleTokenId, address partContract, uint256 partTokenId) external;
function batchAttachParts(uint256 vehicleTokenId, address[] calldata partContracts, uint256[] calldata partTokenIds) external;
function detachPart(uint256 vehicleTokenId, uint256 index) external;
function transferVehicleWithParts(uint256 vehicleTokenId, address newOwner, bool bundleParts) external;
function getAttachedParts(uint256 vehicleTokenId) external view returns (address[] memory contracts, uint256[] memory tokenIds);
function getAttachedPartsCount(uint256 vehicleTokenId) external view returns (uint256);

event PartAttached(uint256 indexed vehicleTokenId, address partContract, uint256 partTokenId);
event PartDetached(uint256 indexed vehicleTokenId, address partContract, uint256 partTokenId);
}
  • attachPart / batchAttachParts: MUST transfer parts to escrow, record attachments, update vehicle metadata, and check expiration/locks.
  • detachPart: MUST remove the part from composition and transfer it back to the owner.
  • transferVehicleWithParts: MUST handle bundled or unbundled transfers of the vehicle and parts.
  • View Functions: MUST provide lists and counts of attached parts.

Metadata JSON Schema

For part and vehicle metadata (via tokenURI or KV):

{
"title": "Composable Vehicle NFT Metadata",
"type": "object",
"properties": {
"specs": { "type": "string", "description": "JSON details of the part/vehicle" },
"ipnsNamespace": { "type": "string", "description": "IPNS path for decentralized data" },
"fingerprint": { "type": "string", "description": "SHA256 hash for audit trail" },
"expiration": { "type": "integer", "description": "Unix timestamp for expiration" },
"attached_parts": { "type": "array", "description": "Array of {contract: address, id: uint256}" }
}
}

Timestamp Format

Use Unix epoch seconds. For expiration, 0 indicates no expiration.

Rationale

This design groups parts into hierarchies (10–20 major assemblies) to reduce on-chain complexity while enabling modularity. Composability via escrow in the Composer prevents unauthorized sales of attached parts. Integration with CIP-150/151 ensures durable, lifecycle-aware metadata. Batch operations optimize energy costs on XCB.

Backwards Compatibility

This standard is fully compatible with CIP-721 (all tokens are transferable NFTs), CIP-150 (KV usage), and CIP-151 (expiration logic). Existing CIP-721 contracts can be extended without breaking interoperability with wallets or marketplaces.

Test Cases

Implementations SHOULD include unit tests for:

  • Minting and metadata validation.
  • Attachment and detachment with expiration checks.
  • Sealing and immutability.
  • Batch operations and energy estimates.

Example test suite (in Ylem/Foxar):

  • Test mintPart reverts on invalid inputs.
  • Test attachPart succeeds only if not expired or locked.
  • Test transferVehicleWithParts bundles parts correctly.

Implementation

The following Ylem contracts provide a reference implementation of the interfaces defined in this CIP. They are compatible with the Core Virtual Machine (CVM) and can be compiled using the Ylem compiler. These contracts incorporate security checks, events, and optimizations for XCB.

PartNFT.sol (Implements IPartNFT)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20; // Compatible with Ylem compiler for CVM

import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; // CIP-721 equivalent; use XCB-adapted imports if available
import "@openzeppelin/contracts/access/Ownable.sol";

contract PartNFT is ERC721, Ownable {
struct PartMetadata {
string specs; // JSON with details
string ipnsNamespace; // Decentralized storage (IPNS for brand/model)
bytes32 fingerprint; // SHA256 for zero-trust audit trail
uint256 expiration; // CIP-151 lifecycle (0 = none)
bool locked; // Prevent edits for provenance
uint256 createdAt; // Core Blockchain timestamp
}

mapping(uint256 => PartMetadata) public partMetadata;

// Core Blockchain events
event PartMinted(uint256 indexed tokenId, address indexed to, bytes32 fingerprint);
event PartExpirationUpdated(uint256 indexed tokenId, uint256 newExpiration);
event PartLocked(uint256 indexed tokenId);

constructor() ERC721("VehiclePartNFT", "VPART") Ownable(msg.sender) {}

function mintPart(
address to,
uint256 tokenId,
string memory specs,
string memory ipnsNamespace,
bytes32 fingerprint,
uint256 expiration
) external onlyOwner {
require(to != address(0), "Invalid recipient address");
require(bytes(specs).length > 0, "Specs cannot be empty");
require(bytes(ipnsNamespace).length > 0, "IPNS namespace cannot be empty");
require(fingerprint != bytes32(0), "Invalid fingerprint");

_safeMint(to, tokenId);
partMetadata[tokenId] = PartMetadata(
specs,
ipnsNamespace,
fingerprint,
expiration,
false,
block.timestamp
);

emit PartMinted(tokenId, to, fingerprint);
}

function attachToVehicle(uint256 tokenId, address composerAddress) external {
require(ownerOf(tokenId) == msg.sender, "Not owner");
require(composerAddress != address(0), "Invalid composer address");
require(composerAddress.code.length > 0, "Composer contract does not exist");
require(!partMetadata[tokenId].locked, "Part locked");
require(partMetadata[tokenId].expiration == 0 || partMetadata[tokenId].expiration > block.timestamp, "Expired");

safeTransferFrom(msg.sender, composerAddress, tokenId);
}

// CIP-151: Update expiration
function setExpiration(uint256 tokenId, uint256 newExpiration) external {
require(ownerOf(tokenId) == msg.sender, "Not owner");
require(!partMetadata[tokenId].locked, "Part locked");
require(newExpiration == 0 || newExpiration > block.timestamp, "Invalid expiration time");

partMetadata[tokenId].expiration = newExpiration;
emit PartExpirationUpdated(tokenId, newExpiration);
}

function lockPart(uint256 tokenId) external {
require(ownerOf(tokenId) == msg.sender, "Not owner");
require(!partMetadata[tokenId].locked, "Part already locked");

partMetadata[tokenId].locked = true;
emit PartLocked(tokenId);
}

// Core Blockchain compatible view functions
function getPartMetadata(uint256 tokenId) external view returns (
string memory specs,
string memory ipnsNamespace,
bytes32 fingerprint,
uint256 expiration,
bool locked,
uint256 createdAt
) {
PartMetadata memory metadata = partMetadata[tokenId];
return (
metadata.specs,
metadata.ipnsNamespace,
metadata.fingerprint,
metadata.expiration,
metadata.locked,
metadata.createdAt
);
}

function isPartExpired(uint256 tokenId) external view returns (bool) {
return partMetadata[tokenId].expiration != 0 && partMetadata[tokenId].expiration <= block.timestamp;
}

function isPartLocked(uint256 tokenId) external view returns (bool) {
return partMetadata[tokenId].locked;
}

// tokenURI with IPNS for decentralized data
function tokenURI(uint256 tokenId) public view override returns (string memory) {
require(_exists(tokenId), "Token does not exist");
return string(abi.encodePacked("ipns://", partMetadata[tokenId].ipnsNamespace, "/metadata.json"));
}
}

VehicleNFT.sol (Implements IVehicleNFT)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract VehicleNFT is ERC721, Ownable {
// CIP-150: KV store for metadata
mapping(uint256 => mapping(string => string)) public kvMetadata;
mapping(uint256 => bool) public sealed;
mapping(uint256 => uint256) public createdAt; // Core Blockchain timestamp

string constant ATTACHED_PARTS_KEY = "attached_parts";

// Core Blockchain events
event VehicleMinted(uint256 indexed tokenId, address indexed to, string vin);
event MetadataUpdated(uint256 indexed tokenId, string key, string value);
event VehicleSealed(uint256 indexed tokenId);
event BatchMetadataUpdated(uint256 indexed tokenId, string[] keys, string[] values);

constructor() ERC721("VehicleNFT", "VEH") Ownable(msg.sender) {}

function mintVehicle(address to, uint256 tokenId, string memory overallSpecs, string memory vin) external onlyOwner {
require(to != address(0), "Invalid recipient address");
require(bytes(overallSpecs).length > 0, "Specs cannot be empty");
require(bytes(vin).length > 0, "VIN cannot be empty");

_safeMint(to, tokenId);
kvMetadata[tokenId]["specs"] = overallSpecs;
kvMetadata[tokenId]["vin"] = vin;
createdAt[tokenId] = block.timestamp;

emit VehicleMinted(tokenId, to, vin);
}

function setValue(uint256 tokenId, string memory key, string memory value) external {
require(ownerOf(tokenId) == msg.sender, "Not owner");
require(!sealed[tokenId], "Sealed");
require(bytes(key).length > 0, "Key cannot be empty");

kvMetadata[tokenId][key] = value;
emit MetadataUpdated(tokenId, key, value);
}

// CIP-3005-inspired batch set for efficiency
function batchSetValues(uint256 tokenId, string[] memory keys, string[] memory values) external {
require(keys.length == values.length, "Mismatch");
require(ownerOf(tokenId) == msg.sender, "Not owner");
require(!sealed[tokenId], "Sealed");
for (uint256 i = 0; i < keys.length; i++) {
kvMetadata[tokenId][keys[i]] = values[i];
}
}

function seal(uint256 tokenId) external {
require(ownerOf(tokenId) == msg.sender, "Not owner");
sealed[tokenId] = true;
}

function getAttachedParts(uint256 tokenId) public view returns (string memory) {
return kvMetadata[tokenId][ATTACHED_PARTS_KEY];
}
}

ComposerContract.sol (Implements IComposer)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@core-blockchain/contracts/interfaces/IERC721.sol";
import "@core-blockchain/contracts/interfaces/IERC721Receiver.sol";
import "./PartNFT.sol";
import "./VehicleNFT.sol";

contract ComposerContract is IERC721Receiver {
VehicleNFT public vehicleNFT;

// Core Blockchain compatible constant
bytes32 constant ATTACHED_PARTS_KEY = keccak256("attached_parts");

mapping(uint256 => address[]) public attachedPartContracts;
mapping(uint256 => uint256[]) public attachedPartTokenIds;

event PartAttached(uint256 indexed vehicleTokenId, address partContract, uint256 partTokenId);
event PartDetached(uint256 indexed vehicleTokenId, address partContract, uint256 partTokenId);

constructor(address _vehicleNFT) {
require(_vehicleNFT != address(0), "Invalid vehicle NFT address");
require(_vehicleNFT.code.length > 0, "Vehicle NFT contract does not exist");
vehicleNFT = VehicleNFT(_vehicleNFT);
}

function attachPart(uint256 vehicleTokenId, address partContract, uint256 partTokenId) external {
require(vehicleNFT.ownerOf(vehicleTokenId) == msg.sender, "Not vehicle owner");
require(partContract != address(0), "Invalid part contract address");
require(partContract.code.length > 0, "Part contract does not exist");

PartNFT part = PartNFT(partContract);
require(part.partMetadata(partTokenId).expiration == 0 || part.partMetadata(partTokenId).expiration > block.timestamp, "Expired");
IERC721(partContract).safeTransferFrom(msg.sender, address(this), partTokenId);
attachedPartContracts[vehicleTokenId].push(partContract);
attachedPartTokenIds[vehicleTokenId].push(partTokenId);
updateAttachedPartsMetadata(vehicleTokenId);
emit PartAttached(vehicleTokenId, partContract, partTokenId);
}

// CIP-3005-inspired batch attach
function batchAttachParts(uint256 vehicleTokenId, address[] memory partContracts, uint256[] memory partTokenIds) external {
require(partContracts.length == partTokenIds.length, "Mismatch");
require(vehicleNFT.ownerOf(vehicleTokenId) == msg.sender, "Not vehicle owner");

for (uint256 i = 0; i < partContracts.length; i++) {
address pc = partContracts[i];
uint256 pt = partTokenIds[i];
require(pc != address(0), "Invalid part contract address");
require(pc.code.length > 0, "Part contract does not exist");

PartNFT part = PartNFT(pc);
require(part.partMetadata(pt).expiration == 0 || part.partMetadata(pt).expiration > block.timestamp, "Expired");
IERC721(pc).safeTransferFrom(msg.sender, address(this), pt);
attachedPartContracts[vehicleTokenId].push(pc);
attachedPartTokenIds[vehicleTokenId].push(pt);
emit PartAttached(vehicleTokenId, pc, pt);
}
updateAttachedPartsMetadata(vehicleTokenId);
}

function detachPart(uint256 vehicleTokenId, uint256 index) external {
require(vehicleNFT.ownerOf(vehicleTokenId) == msg.sender, "Not vehicle owner");
require(index < attachedPartContracts[vehicleTokenId].length, "Invalid index");

address partContract = attachedPartContracts[vehicleTokenId][index];
uint256 partTokenId = attachedPartTokenIds[vehicleTokenId][index];
attachedPartContracts[vehicleTokenId][index] = attachedPartContracts[vehicleTokenId][attachedPartContracts[vehicleTokenId].length - 1];
attachedPartContracts[vehicleTokenId].pop();
attachedPartTokenIds[vehicleTokenId][index] = attachedPartTokenIds[vehicleTokenId][attachedPartTokenIds[vehicleTokenId].length - 1];
attachedPartTokenIds[vehicleTokenId].pop();
IERC721(partContract).safeTransferFrom(address(this), msg.sender, partTokenId);
updateAttachedPartsMetadata(vehicleTokenId);
emit PartDetached(vehicleTokenId, partContract, partTokenId);
}

function updateAttachedPartsMetadata(uint256 vehicleTokenId) internal {
string memory json = "[";
for (uint256 i = 0; i < attachedPartTokenIds[vehicleTokenId].length; i++) {
if (i > 0) json = string(abi.encodePacked(json, ","));
json = string(abi.encodePacked(json, '{"contract":"', addressToString(attachedPartContracts[vehicleTokenId][i]), '","id":', uintToString(attachedPartTokenIds[vehicleTokenId][i]), '}'));
}
json = string(abi.encodePacked(json, "]"));
vehicleNFT.setValue(vehicleTokenId, ATTACHED_PARTS_KEY, json);
}

// Core Blockchain compatible view function
function getAttachedParts(uint256 vehicleTokenId) external view returns (address[] memory contracts, uint256[] memory tokenIds) {
return (attachedPartContracts[vehicleTokenId], attachedPartTokenIds[vehicleTokenId]);
}

function getAttachedPartsCount(uint256 vehicleTokenId) external view returns (uint256) {
return attachedPartTokenIds[vehicleTokenId].length;
}

function transferVehicleWithParts(uint256 vehicleTokenId, address newOwner, bool bundleParts) external {
require(vehicleNFT.ownerOf(vehicleTokenId) == msg.sender, "Not vehicle owner");
require(newOwner != address(0), "Invalid new owner address");

if (bundleParts) {
for (uint256 i = 0; i < attachedPartTokenIds[vehicleTokenId].length; i++) {
IERC721(attachedPartContracts[vehicleTokenId][i]).safeTransferFrom(address(this), newOwner, attachedPartTokenIds[vehicleTokenId][i]);
}
delete attachedPartContracts[vehicleTokenId];
delete attachedPartTokenIds[vehicleTokenId];
vehicleNFT.setValue(vehicleTokenId, ATTACHED_PARTS_KEY, "[]");
} else {
require(attachedPartTokenIds[vehicleTokenId].length == 0, "Detach parts first");
}
vehicleNFT.safeTransferFrom(msg.sender, newOwner, vehicleTokenId);
}

function onERC721Received(address, address, uint256, bytes memory) public pure override returns (bytes4) {
return this.onERC721Received.selector;
}

// Helper functions (XCB-optimized string conversion)
function addressToString(address addr) internal pure returns (string memory) {
return uintToString(uint256(uint160(addr)));
}

function uintToString(uint256 value) internal pure returns (string memory) {
if (value == 0) return "0";
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits--;
buffer[digits] = bytes1(uint8(48 + value % 10));
value /= 10;
}
return string(buffer);
}
}

These implementations can be deployed on XCB using tools like Foxar or Remix configured for the Core network. They include security validations (e.g., address checks, existence verification via code.length) and align with CVM energy efficiency.

Security Considerations

  • Use safeTransferFrom to prevent reentrancy.
  • Seal metadata post-assembly to avoid tampering.
  • Validate contract existence (code.length > 0) to prevent invalid addresses.
  • Off-chain systems (e.g., ERP) SHOULD verify fingerprints for zero-trust.
  • Potential risks: High part counts could increase energy usage; mitigate with hierarchy and batch operations.

Copyright and related rights waived via CC0.

Tags:CBC