"Contract code size exceeds 24576 bytes" ERC721Upgradeable

I ran into the following limit with an ERC721Upgradeable:

Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries.
contract MyERC721 is ERC721PausableUpgradeable, AccessControlUpgradeable {

The following ERC721Upgradeable contract:

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

import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "./MyToken.sol";
import "./PriceList.sol";

contract MyERC721 is ERC721PausableUpgradeable, AccessControlUpgradeable {
  bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
  uint256 private constant DEFAULT_RATE = 100;

  // keep the order of Status
  enum Status { One, Two, Three, Four, Five, Na}
  string private constant BASE_URI_METADATA = 'https://api.mydomain.com/metadata?tokenid=';
  string private constant URI_STATUS_PARAM = '&status=';

  //EnumerableAddressToUintMapUpgradeable.AddressToUintMap private _tokenOwnersHistoric;
  mapping (address => uint256) private _tokenHistory;

  MyToken myTOken;
  PriceList priceList;

  mapping (uint256 => string) public name;
  mapping (uint256 => bool) public isSpecial;
  mapping (uint256 => uint256) public rate;
  mapping (uint256 => Status) public status;

  function initialize(address _myTokenAddress, address _priceListAddress) initializer public {
      __Context_init_unchained();
      __ERC721_init_unchained("My NFT", "MNFT");
      __AccessControl_init_unchained();
      myToken = F24(_myTokenAddress);
      priceList = PriceList(_priceListAddress);
      _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
      _setupRole(OPERATOR_ROLE, _msgSender());
  }

  function mint(address _to, uint256 _tokenId, bool _isSpecial, uint256 _rate) public {
     require(hasRole(OPERATOR_ROLE, msg.sender), "Not an operator");
     require(_mintAllowed(_to), "mint not allowed");
      _mint(_to, _tokenId);
      status[_tokenId] = Status.Invitee;
      //_tokenOwnersHistoric.set(_to, _tokenId);
      _tokenHistory[_to] = _tokenId;
      _isSpecial[_tokenId] = _isSpecial;
      if(_isSpecial) {
        name[_tokenId] = "Special";
        if(_rate == 0) {
            rate[_tokenId] = DEFAULT_RATE;
        } else {
            rate[_tokenId] = _rate;
        }
      } else {
        name[_tokenId] = "Normal";
      }
      _setTokenURI(_tokenId, string(abi.encodePacked(BASE_URI_METADATA, _tokenId.toString(), URI_STATUS_PARAM, uint256(Status.Invitee).toString())));
  }

  function mintByMyToken(uint256 _tokenId, bool _isSpecial) public {
      uint256 price = priceList.getPrice(_tokenId);
      require(price != 0, "TokenId not available");
      require(myToken.allowance(_msgSender(), address(this)) >= price);
      f24.burnFrom(_msgSender(), price);
      if(_isSpecial) {
          mint(_msgSender(), _tokenId, _isSpecial, 0);
      } else {
          mint(_msgSender(), _tokenId, _isSpecial, 0);
      }
  }

  function burn(uint256 tokenId) public {
      require(hasRole(OPERATOR_ROLE, msg.sender), "Not an operator");
      _burn(tokenId);
  }

  function transferFrom(address from, address to, uint256 tokenId) public virtual override {
      super.transferFrom(from, to, tokenId);
      _tokenHistory[to] = tokenId;
  }

  function removeHistoricOwnership(address owner) public {
      require(hasRole(OPERATOR_ROLE, msg.sender), "Not an operator");
      //_tokenOwnersHistoric.remove(owner);
      delete _tokenHistory[owner];
  }

  function setStatusActive(uint256 tokenId, string memory name) public  {
      require(hasRole(OPERATOR_ROLE, msg.sender), "Not an operator");
      require(status[tokenId] == Status.Two, "Status not Two");
      status[tokenId] = Status.One;
      name[tokenId] = name;
      _setTokenURI(tokenId, string(abi.encodePacked(BASE_URI_METADATA, tokenId.toString(), URI_STATUS_PARAM, uint256(Status.One).toString())));
  }

  function changeStatus(uint256 tokenId, Status _status) public {
      require(hasRole(OPERATOR_ROLE, msg.sender), "Not an operator");
      status[tokenId] = _status;
      _setTokenURI(tokenId, string(abi.encodePacked(BASE_URI_METADATA, tokenId.toString(), URI_STATUS_PARAM, uint256(_status).toString())));
  }

  function setRate(uint256 tokenId, uint256 _rate) public {
      require(hasRole(OPERATOR_ROLE, msg.sender), "Not an operator");
      rate[tokenId] = _rate;
  }

  function setName(uint256 tokenId, string memory name) public {
      require(_msgSender() == this.ownerOf(tokenId), "Not account owner");
      name[tokenId] = name;
  }

  function pause() public {
      require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Not an admin");
      _pause();
  }

  function unpause() public {
      require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Not an admin");
      _unpause();
  }

  function _mintAllowed(address to) internal view returns(bool){
      //return !(this.balanceOf(to) >= 1) && ! _tokenHistory(to);
      return !(this.balanceOf(to) >= 1 || _tokenHistory[to] != 0);
  }

  function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual override {
      require(!paused(), "Transfers suspended");
      if(AddressUpgradeable.isContract(to)) {
          require(this.status(tokenId) == Status.Two, "Not allowed to transfer token");
      } else {

has already token");
require(_tokenHistory[to] == 0 || _tokenHistory[to] == tokenId, “Receiver has already token”);
if(from != address(0) && to != address(0)) {
require(this.status(tokenId) == Status.One || this.status(tokenId) == Status.Two, “Transfer not allowed in this status”);
}
}
super._beforeTokenTransfer(from, to, tokenId);
}
}

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

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20BurnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";

contract MyToken is ERC20PausableUpgradeable, ERC20BurnableUpgradeable, AccessControlUpgradeable {
  bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");

  function initialize() initializer public{
      __Context_init_unchained();
      __ERC20_init_unchained("My Token", "MTK");
      __AccessControl_init_unchained();
      _setupDecimals(2);
      _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
      _setupRole(OPERATOR_ROLE, _msgSender());
  }

  function mint(address account, uint256 amount) public {
      require(hasRole(OPERATOR_ROLE, msg.sender), "Caller is not an operator");
      _mint(account, amount);
  }

  function burn(address account, uint256 amount) public {
      require(hasRole(OPERATOR_ROLE, msg.sender), "Caller is not an operator");
      _burn(account, amount);
  }

  function pause() public {
      require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Caller is not an admin");
      _pause();
  }

  function unpause() public {
      require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Caller is not an admin");
      _unpause();
  }

  function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20PausableUpgradeable) {
      super._beforeTokenTransfer(from, to, amount);
  }

}

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

import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "./libraries/EnumerableUintToUintMapUpgradeable.sol";

contract Fiat24PriceList is Initializable, AccessControlUpgradeable {
    using EnumerableUintToUintMapUpgradeable for EnumerableUintToUintMapUpgradeable.UintToUintMap;

    bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");

    EnumerableUintToUintMapUpgradeable.UintToUintMap private _priceList;

    function initialize() public initializer {
        __AccessControl_init_unchained();
        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
        _setupRole(OPERATOR_ROLE, _msgSender());
        _priceList.set(1,100000000);
        _priceList.set(2, 10000000);
        _priceList.set(3, 1000000);
        _priceList.set(4, 100000);
        _priceList.set(5, 10000);
        _priceList.set(6, 1000);
        _priceList.set(7, 100);
        _priceList.set(8, 10);
    }

    function getPrice(uint256 accountNumber) external view returns(uint256) {
        uint8 digits = _numDigits(accountNumber);
        if(!_priceList.contains(digits)) {
            return 0;
        } else {
            return _priceList.get(digits);
        }
    }

    function setPrice(uint256 digits, uint256 price) external {
        require(hasRole(OPERATOR_ROLE, msg.sender), "Caller is not an operator");
        _priceList.set(digits, price);
    }

    function _numDigits(uint256 number) internal pure returns (uint8) {
      uint8 digits = 0;
      while (number != 0) {
          number /= 10;
          digits++;
      }
      return digits;
    }
}

truffle run contract size

│ MyERC721 │ 24.06 K…

I am already using sole optimisation in truffle-config.js:

   optimizer: {
     enabled: true,
     runs: 1
   }

Moreover, I’ve tried to use Factories for MyToken and PriceList.

Is there any good advice to split the contract or any other measures to shrink the contract size?

CORRECTION:
The optimiser reduces the size significantly, from around 24KB to 14.98KB.

MyERC20 │ 14.98 K…

  settings: {          // See the solidity docs for advice about optimization and evmVersion
   optimizer: {
     enabled: true,
     runs: 1
   }
  //  evmVersion: "byzantium"
  }
2 Likes