Expandable Onchain SVG Images Storage Structure

Hey, Ethereum Magicians :sparkles::unicorn:

How are you all doing? Spring is slowly approaching in South Korea.


Anyway, as you can see from the title and the content of the Abstract below, I have envisioned an Expandable Onchain NFT Images Storage, and based on this, I’m preparing an EIP.

And I’d like to know what you guys think about this.


Finally, I am always happy to get to your feedback, such as the possibility of adopting the EIP and comments on further improvements.

So everyone, be careful of COVID-19, and have a good day today!


Simple Summary

It is a Expandable Onchain SVG Images Storage Structure Model on the Ethereum.


Abstract

This standard proposal is a Expandable Onchain SVG Images Storage Structure Model on the Ethereum that permanently preserves images and prevents tampering, and can store larger-capacity images furthermore.

It is a structure designed to store SVG images with a larger capacity by distributed SVG images in units of tags on Ethereum.


The structure presented by this EIP consists of a total of three layers as shown below.

  1. Storage Layer ─ A contract layer that stores distributed SVG images by tags.
  2. Assemble Layer ─ A contract layer that creates SVG images by combining tags stored in the Storage Layer’s contract.
  3. Property Layer ─ A contract layer that stores the attribute values for which SVG tag to use.

It is designed to flexibly store and utilize larger capacity SVG images by interacting with the above three layer-by-layer contracts each other.


Also, you can configure the Onchain NFT Images Storage by adjusting the Assemble Layer’s contract like below.

  • A storage with expandability by allowing additional deployment on Storage Layer’s contracts
  • A storage with immutability after initial deployment

Additionally, this standard proposal focuses on, but is not limited to, compatibility with the EIP-721 standard.


Motivation

Most NFT projects store their NFT metadata on a centralized server rather than on the Ethereum. Although this method is the cheapest and easiest way to store and display the content of the NFT, there is a risk of corruption or loss of the NFT’s metadata.


To solve this problem, most NFT metadata is stored on Ethereum. However, it can only be expressed as a simple shape such as a circle or a rectangle, since one contract can be distributed 24KB size for maximum.


We propose this model ─ a more secure way to store NFT metadata ─ to create and own high-quality of NFT metadata.


Reference Implementation

Proxy Layer

pragma solidity ^0.8.0;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {IAssembleContract} from "./IAssembleContract.sol";

/**
 * @title PropertyContract
 * @author 
 * @notice A contract that stores property values.
 */
contract PropertyContract is ERC721 {
    /**
     * @notice A variable that stores the object of `AssembleContract`.
     */
    IAssembleContract public assembleContract;

    // Storing property values corresponding to each number of storage. (tokenId -> attr[])
    mapping(uint256 => uint256[]) private _attrs;

    /**
     * @dev `name_` and `symbol_` are passed to ERC-721, and in case of `assembleContractAddr_`, the `setAssembleContract` function is used.
     */
    constructor(string memory name_, string memory symbol_, address assembleContractAddr_) ERC721(name_, symbol_) {
        setAssembleContract(assembleContractAddr_);
    }

    /**
     * @dev See {IAssembleContract-getImage}
     */
    function getImage(uint256 tokenId_) public view virtual returns (string memory) {
        return assembleContract.getImage(_attrs[tokenId_]);
    }

    /**
     * @param newAssembleContractAddr_ Address value of `AssembleContract` to be changed.
     * @dev If later changes or extensions are unnecessary, write directly to `constructor` without implementing the function.
     */
    function setAssembleContract(address newAssembleContractAddr_) public virtual {
        assembleContract = IAssembleContract(newAssembleContractAddr_);
    }

    /**
     * @param tokenId_ The token ID for which you want to set the attribute value.
     * @dev Set the attribute value of the corresponding `tokenId_` sequentially according to the number of asset storage.
     */
    function _setAttr(uint256 tokenId_) internal virtual {
        for (uint256 idx=0; idx < assembleContract.getStorageCount(); idx++) {
            uint256 newValue = 0;

            /// @dev Implement the property value setting logic.

            _attrs[tokenId_].push(newValue);  
        }
    }
}

Assemble Layer

pragma solidity ^0.8.0;

/** 
 * @title IAssembleContract
 * @author 
 */
interface IAssembleContract {
    /**
     * @notice For each `StorageContract`, get the corresponding SVG image tag and combine it and return it.
     * @param attrs_ Array of corresponding property values sequentially for each connected contract.
     * @return A complete SVG image in the form of a String.
     * @dev It runs the connected `StorageContract` in the registered order, gets the SVG tag value and combines it into one image.
     * It should be noted that the order in which the asset storage contract is registered must be carefully observed.
     */
    function getImage(uint256[] memory attrs_) external view returns (string memory);

    /**
     * @notice Returns the count of connected Asset Storages.
     * @return Count of Asset Storage.
     * @dev Instead of storing the count of storage separately in `PropertyContract`, get the value through this function and use it.
     */
    function getStorageCount() external view returns (uint256);
}

pragma solidity ^0.8.0;

import {IAssembleContract} from "./IAssembleContract.sol";
import {IStorageContract} from "./IStorageContract.sol";

/**
 * @title AssembleContract
 * @author 
 * @notice A contract that assembles SVG images.
 */
contract AssembleContract is IAssembleContract {

    /**
     * @dev Asset storage structure. Stores the contract address value and the corresponding object.
     */
    struct AssetStorage {
        address addr;
        IStorageContract stock;
    }

    AssetStorage[] private _assets;

    /**
     * @dev Register address values of `StorageContract`. Pay attention to the order when registering.
     */
    constructor (address[] memory assetStorageAddrList_) {
        for (uint256 i=0; i < assetStorageAddrList_.length; i++) {
            addStorage(assetStorageAddrList_[i]);
        }
    }

    /**
     * @dev See {IAssembleContract-getImage}
     */
    function getImage(uint256[] memory attrs_) external view virtual override returns (string memory) {
        string memory imageString = "";

        imageString = string(abi.encodePacked(imageString, "<svg version='1.1' xmlns='http://www.w3.org/2000/svg'>"));

        for (uint256 i=0; i < attrs_.length; i++) {
            imageString = string(
                abi.encodePacked(
                    imageString,
                    _assets[i].stock.getAsset(attrs_[i])
                )
            );
        }

        imageString = string(abi.encodePacked(imageString, '</svg>'));

        return imageString;
    }

    /**
     * See {IAssembleContract-getStorageCount}
     */
    function getStorageCount() external view virtual override returns (uint256) {
        return _assets.length;
    }

    /**
     * @param storageAddr_ Address of `StorageContract`.
     * @dev If later changes or extensions are unnecessary, write directly to `constructor` without implementing the function.
     */
    function addStorage(address storageAddr_) public virtual returns (uint256) {
        _assets.push(AssetStorage({
            addr: storageAddr_,
            stock: IStorageContract(storageAddr_)
        }));
        return _assets.length-1; // index
    }
}

Storage Layer

pragma solidity ^0.8.0;

/**
 * @title IStorageContract
 * @author 
 * @dev A contract that returns stored assets (SVG image tags). `setAsset` is not implemented separately.
 * If the `setAsset` function exists, the value of the asset in the contract can be changed, and there is a possibility of data corruption.
 * Therefore, the value can be set only when the contract is created, and new contract distribution is recommended when changes are required.
 */
interface IStorageContract {
    /**
     * @notice Returns the SVG image tag corresponding to `assetId_`.
     * @param assetId_ Asset ID
     * @return A SVG image tag of type String.
     */
    function getAsset(uint256 assetId_) external view returns (string memory);
}

pragma solidity ^0.8.0;

import {IStorageContract} from "./IStorageContract.sol";

/**
 * @title StorageContract
 * @author 
 * @notice A contract that stores SVG image tags.
 * @dev See {IStorageContract}
 */
contract StorageContract is IStorageContract {

    // Asset List
    mapping(uint256 => string) private _assetList;

    /**
     * @dev Write the values of assets (SVG image tags) to be stored in this `StorageContract`.
     */
    constructor () {
        // Setting Assets such as  _assetList[1234] = "<circle ...";
    }

    /**
     * @dev See {IStorageContract-getAsset}
     */
    function getAsset(uint256 assetId_) external view override returns (string memory) {
        return _assetList[assetId_];
    }
}



Additionally, there is one thing I would like to ask you for your opinion on this EIP!

When I first conceived this EIP, it was designed so that the contract that stores assets of the storage layer can be replaced. However, while materializing the proposal, I thought that REPLACEABLE would go against the immutability of the blockchain, so I excluded it from the current version of the proposal.

I’d like to get opinions on whether it’s better to not have REPLACEABLE like the current version, or a replaceable version is better in your opinion.