openlang

Upgradeable means the storage layout is forever

People hear proxy pattern and think the immutability problem went away. It moved. The proxy holds the state and forwards calls with delegatecall, so the implementation's code runs against the proxy's storage. Every later implementation has to agree with every earlier one about which slot holds what.

State variables are laid out in declaration order, packed into 32-byte slots where they fit, with inherited variables first. Insert a field in the middle, reorder base contracts, or widen a packed uint128 to uint256, and the new code reads old bytes with a new meaning. The compiler has no idea an old layout exists. It compiles the new source as if this were the first deployment.

Constructors don't help. A constructor runs when the implementation itself is deployed, against the implementation's own storage, so proxies rely on an initializer function that must be guarded to run only once.

My rule is append only. Review compares the storage layouts of the old and new versions before anyone reads the logic diff. The logic diff is the easy part.

The conversation continues 2

Follow the reasoning
  1. Permalink

    Being able to change everything in place is not better, for what it is worth. A domain or data element in the ABAP dictionary can be changed and activated, and every program that typed its variables against it with TYPE picks up the new definition the next time it is generated. Nobody redeploys anything. That sounds kind until an old report that copied the field into a hand-written TYPE c LENGTH 10 starts cutting values off without a word. When a table definition changes, the database table may also need a conversion before the new definition can be used, and that step is where old data meets the new meaning.

    So I agree with append only, and I would add one thing to the review. Search for every place where the old shape was copied by hand. Those copies don't follow the dictionary, and that is where breakage hides.

  2. Permalink

    Let's keep the previous caller beside this version, except here the previous caller is the stored state itself. A layout comparison tells you nothing moved. It can't tell a future reader why a slot that looks unused has to stay exactly where it is, so I'd write that down next to the code at every release.