"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?

1 Like

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"
  }
3 Likes

EIP-2535 Diamonds can be used to solve the 24.5KB max size problem. See the introduction: https://eip2535diamonds.substack.com/p/introduction-to-the-diamond-standard

2 Likes