How do I mint large svg strings in Solidity?

Hi, I have just discovered that I cannot mint SVG's that have really strong strings. The maximum limit is 24576. Here is the code below:

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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/interfaces/IERC2981.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@limitbreak/creator-token-contracts/contracts/erc721c/ERC721C.sol";
import "./SSTORE2.sol";
import "./Base64.sol";
import "./.deps/DefaultOperatorFilterer.sol";
import "./RoyaltySplitter.sol";

contract SVGEditorNFTV4 is ERC721C, IERC2981, Ownable, DefaultOperatorFilterer {
    using Strings for uint256;
    struct TokenData {
        uint256 royalties;
        address royaltyRecipient;
        address attributesPointer;

    mapping(uint256 => TokenData) private _tokenData;

    struct Config {
        uint256 payment;
        uint96 platformRoyalty;
        uint256 currentTokenId;
        bool paused;

    Config public config = Config({
        payment: 96000000000000000,
        platformRoyalty: 69,
        currentTokenId: 1,
        paused: false

    constructor() ERC721OpenZeppelin("NFT Cubes", "NFT Cubes") {}

    modifier whenNotPaused() {
        require(!config.paused, "Contract is paused.");

    // Implement the _requireCallerIsContractOwner function
    function _requireCallerIsContractOwner() internal view override {
        require(owner() == _msgSender(), "Caller is not the contract owner");

    function setPayment(uint256 x) external onlyOwner whenNotPaused {
        config.payment = x;

    function returnPayment() external view returns (uint256) {
        return config.payment;

    function setPlatformRoyalty(uint96 x) external onlyOwner whenNotPaused {
        config.platformRoyalty = x;

    function platformRoyalty() external view returns (uint96) {
        return config.platformRoyalty;

    function division(uint256 decimalPlaces, uint256 numerator, uint256 denominator) pure internal returns(string memory result) {
        uint256 factor = 10**decimalPlaces;
        uint256 quotient  = numerator / denominator;
        bool rounding = 2 * ((numerator * factor) % denominator) >= denominator;
        uint256 remainder = (numerator * factor / denominator) % factor;
        if (rounding) {
            remainder += 1;
        // directly incorporate the logic of numToFixedLengthStr here
        bytes memory byteString;
        for (uint256 i = 0; i < decimalPlaces; i++) {
            uint256 rem = remainder % 10;
            byteString = abi.encodePacked(rem.toString(), byteString);
            remainder = remainder / 10;
        result = string(abi.encodePacked(quotient.toString(), '.', string(byteString)));

   function mint(
        string calldata svg,
        string calldata name,
        uint96 percent
    ) external whenNotPaused {
        // Instantiate the RoyaltySplitter contract with two recipients
        RoyaltySplitter splitter = new RoyaltySplitter(payable(msg.sender), payable(0xbe94F301cDb6eD1aFae186D79631B112129de041), percent, config.platformRoyalty);
        // Store the contract's address
        _tokenData[config.currentTokenId].royalties = 100;
        _tokenData[config.currentTokenId].royaltyRecipient = address(splitter);
        _safeMint(msg.sender, config.currentTokenId);
        // Split the SVG string into chunks and store each chunk separately
        uint256 svgLength = bytes(svg).length;
        uint256 chunkSize = 24576;  // Set the chunk size to the maximum contract storage limit
        for (uint256 i = 0; i < svgLength; i += chunkSize) {
            uint256 end = i + chunkSize < svgLength ? i + chunkSize : svgLength;
            string memory chunk = substring(svg, i, end);
        string memory percentString = division(2,percent,100);
        _tokenData[config.currentTokenId].attributesPointer = SSTORE2.write(abi.encode(name, percentString, msg.sender));

     * @dev See {IERC721-transferFrom}.
     *      In this example the added modifier ensures that the operator is allowed by the OperatorFilterRegistry.
    function transferFrom(address from, address to, uint256 tokenId) public override onlyAllowedOperator(from) {
        super.transferFrom(from, to, tokenId);

     * @dev See {IERC721-safeTransferFrom}.
     *      In this example the added modifier ensures that the operator is allowed by the OperatorFilterRegistry.
    function safeTransferFrom(address from, address to, uint256 tokenId) public override onlyAllowedOperator(from) {
        super.safeTransferFrom(from, to, tokenId);

     * @dev See {IERC721-safeTransferFrom}.
     *      In this example the added modifier ensures that the operator is allowed by the OperatorFilterRegistry.
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
        super.safeTransferFrom(from, to, tokenId, data);

    function tokenURI(uint256 tokenId) public whenNotPaused view virtual override returns (string memory) {
        (string memory svg, string memory name, string memory percent, address minter) = abi.decode([tokenId].attributesPointer), (string, string, string, address));

        // Convert minter address to string
        string memory minterString = _toString(minter);

        string memory json = Base64.encode(
                '{"name":"', name, '",',
                '"image_data":"', svg, '",',
                '"attributes":[{"trait_type":"Minter Fees (%)", "value": "', percent, '"},',  // Notice that I've added double quotes around the value.
                '{"trait_type":"Minter","value":"', minterString, '"}]',
        return string(abi.encodePacked("data:application/json;base64,", json));

    // Function to convert address to string
    function _toString(address _addr) private pure returns(string memory) {
        bytes32 value = bytes32(uint256(uint160(_addr)));
        bytes memory alphabet = "0123456789abcdef";

        bytes memory str = new bytes(42);
        str[0] = '0';
        str[1] = 'x';
        for (uint256 i = 0; i < 20; i++) {
            str[2+i*2] = alphabet[uint8(value[i + 12] >> 4)];
            str[3+i*2] = alphabet[uint8(value[i + 12] & 0x0f)];
        return string(str);

    function royaltyInfo(uint256 tokenId, uint256 value) external view virtual override returns (address receiver, uint256 royaltyAmount) {
        receiver = _tokenData[tokenId].royaltyRecipient;
        royaltyAmount = value * _tokenData[tokenId].royalties / 10000;

    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721C, IERC165) returns (bool) {
        return super.supportsInterface(interfaceId) || interfaceId == type(IERC2981).interfaceId;

    function pause() external onlyOwner {
        config.paused = !config.paused;


I am currently looking at an on-chain method and not off-chain. The current method that I am using is splitting the svg strings into chunks but the mint function still does not work. I get the following error

"Gas estimation errored with the following message (see below). The transaction execution will likely fail. Do you want to force sending?
Returned error: gas required exceeds allowance (30000000)".

I am using remix IDE and tried to set the gas as high as possible like 999999999 and optimisations runs up to 1000. But no matter what I do. It still gives me the error above. I am currently aiming to match my code to be like the one in this link but for svg strings. Is there anything that you can do to help me with the mint and tokenURI functions of the code please? My expectation is that the svg is split into chunks when minting and then put them back together in the tokenURI regardless of the size of the string. Thank you.

First of all, trying to reproduce this claim of yours using the following MVP:

string[] public svgChunks;
function mint(string calldata svg) external {

I am able to pass an input string of up to 42176 characters before the transaction runs out of gas.

So the stated number of 24576 (bytes?) seems wrong.

TBH, I've never heard of such limit when it comes to string-length; this number happens to be the maximum size of a contract's byte-code, but I am not aware of it also being the maximum length of a string.

Second, iterating a data structure of arbitrary (unlimited) size is generally wrong, due to the exact reason which you've experienced, i.e., transaction exceeding block gas limit.

The general approach is allowing the user to execute the desired action using several transactions, typically by specifying start index + end index or start index + length.

Having said that, I am not so sure how you could actually follow this approach and reconstruct your function in this specific case...