Proxy Upgrades, Multiple Inheritance, and Storage Slots

Foundation for questions: scenario where multiple parent (base) contracts are inherited:

contract Base1 {
       // do something
       uint256[1000] private __gap;
}

contract Base2 {
       // do something
       uint256[1000] private __gap;
}

contract Child is Base1, Base2 {
       uint256 testVariable = 123;
}

Questions:

  1. If a large storage gap is left, you could theoretically replace Base2 with two new contracts (ex: Base2A and Base2B), so long as the storage used, including gaps, for both sums to the total gap left by the original Base2, right?

  2. On implementation upgrades, since storage variables from previous implementations can't be removed, but byte code can nevertheless be reduced (ex: nuking Base2's code), each proxy upgrade is still ultimately constrained by the max contract size. To be clear, the available bytecode space would be 24576 bytes - contract_to_replace_size. Given that the legacy storage variables from Base2 are still taking up storage slots, they would eat into the available bytecode space for implementation upgrades, correct? In other words, even if, like in #1, large gaps were left, you cant continue replacing parent contracts forever, because each replacement leaves behind non-removable storage variables that eats into contract size?

  1. Yes, this is right.

  2. Storage variables are not placed in bytecode space, so they do not consume bytecode space!

There is a small caveat to point 2 that I'll mention for completeness. Gaps in storage are not completely unrelated to bytecode size efficiency. Because the state variable location in storage has to be PUSHed into the stack, and there exist different PUSH* instructions depending on the size of the argument, having many large gaps will result in larger numbers for state variable locations and will slightly increase bytecode size. However, note that if a variable is unused it will never show up in the bytecode and in that case would have no effect.