Warning: Contract code size exceeds 24576 bytes on Upgradeable Proxy

Hello! I have an upgradeable contract (let's call it Manager) that creates instances of another contract by using a proxy (because the target contract is also upgradeable). The objective is to be able to call the state and some of the functions of the target contract from the Manager contract. The target contract compiles well but the manager one is compiling with the limit size warning and I have no clue why.

I suspect it has to do with the parents the manager inherits from, so I would like to have your perspective to work around it.

:1234: Code to reproduce

Here is the manager contract:

//SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

import "./TargetContract.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.4.1/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v4.4.1/contracts/proxy/utils/UUPSUpgradeable.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v4.4.1/contracts/access/AccessControlUpgradeable.sol";

contract Manager is Initializable, AccessControlUpgradeable, UUPSUpgradeable {
  bytes32 public constant URI_SETTER_ROLE = keccak256("URI_SETTER_ROLE");
  bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
  bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
  address public targetContractImplementation;
  event targetContractDeployed(address targetContractAddress);
  using CountersUpgradeable for CountersUpgradeable.Counter;
  CountersUpgradeable.Counter private _lomContractIds;
  address payable public companyAccount;
  TargetContract[] public targetContractArray;
  mapping(TargetContract => address) public contractsToOrganizer;
  mapping(uint256 => LomEventV3) public contractIds;

  function initialize() public initializer {
    companyAccount = payable(msg.sender);
    __AccessControl_init();
    _grantRole(DEFAULT_ADMIN_ROLE, companyAccount);
    _grantRole(PAUSER_ROLE, msg.sender);
    _grantRole(UPGRADER_ROLE, msg.sender);
  }

  function createTargetContract(uint8 _Number, string memory _Name)
    external
    payable
    onlyRole(DEFAULT_ADMIN_ROLE)
    returns (address)
  {
    TargetContract implementation = new TargetContract();
    eventImplementation = address(implementation);
    ERC1967Proxy proxy = new ERC1967Proxy(
      eventImplementation,
      abi.encodeWithSelector(
        TargetContract(address(0)).initialize.selector,
        _eventNumber,
        _seriesName
      )
    );
    contractsToOrganizer[implementation] = msg.sender;
    targetContractArray.push(implementation);
    emit targetContractDeployed(address(proxy));
    return address(proxy);
  }

  // Retrieve an array with the addresses of the contracts (TargetContract.sol) deployed by the sender
  function getTargetContracts() public view returns (TargetContract[] memory) {
    TargetContract[] memory contractsByAddress = new TargetContract[](
      targetContractArray.length
    );
    uint256 counter = 0;
    // address[] memory contractsByAddress;
    for (uint256 i = 0; i < targetContractArray.length; i++) {
      if (contractsToOrganizer[targetContractArray[i]] == msg.sender) {
        contractsByAddress[counter] = targetContractArray[i];
        counter++;
      }
    }
    return contractsByAddress;
  }

  function _authorizeUpgrade(address newImplementation)
        internal
        onlyRole(UPGRADER_ROLE)
        override
    {}

  function supportsInterface(bytes4 interfaceId)
    public
    view
    override(AccessControlUpgradeable)
    returns (bool)
  {
    return super.supportsInterface(interfaceId);
  }
}

:computer: Environment

Im running it on Remix with the optimization enabled and 200 runs because I have tried both high and low runs values without success.

Hello @nicolas.guasca

There is a maximum size contracts can have. Often, enabling optimisation allow to pass bellow the limit, but in your case that is not enough.

I believe you contract as a design inefficiency, which make your contract both way more expensive to deploy AND way more extensible to use:

TargetContract implementation = new TargetContract();

This is very expensive to use, because deploying a new contract is super expensive ... but its also expensive to deploy, because the entire bytecode of TargetContract must be part of your manager. Also, you do it twice ...

I'd encourage you consider a different approach:

  • replace the constructor in TargetContract with an initializer (like for upgradeable contracts)
  • deploy one single instance of it, and store this "template" address in the Manager (can be an initialize argument)
  • replace the "new" with Clones.

Hopefully this will fix your issue, and make your contract cheaper to use.

1 Like

Hello @Amxx and thank you very much for such and enlightening explanation. The TargetContract is already initializable (UUPS). This made my contract work:

  function initialize() public initializer {
    ...
    TargetContractTemplate = 0x...;
  }
function createTargetContract(uint8 _Number, string memory _Name)
    external
    payable
    onlyRole(DEFAULT_ADMIN_ROLE)
    returns (address)
  {
    address clone = ClonesUpgradeable.clone(TargetContractTemplate);
    ERC1967Proxy proxy = new ERC1967Proxy(
      clone,
      abi.encodeWithSelector(
        TargetContract(address(0)).initialize.selector,
        _Number,
        _Name
      )
    );
    contractsToOrganizer[proxy] = msg.sender;
    targetContractArray.push(proxy);
    emit targetContractDeployed(address(proxy));
    return address(proxy);
  }

I assume you meant to avoid the "new" only for the new TargetContract() lines as I took my corrected code from this workshop, am I right? or should I work around avoiding the new ERC1967Proxy as well? This is all very helpful for the model Im building so thanks again for the support.

Cheers!

If you reduce it to only one new operation it will likely be enough to get you below the code size limit.