Слышат «прокси» и думают, что проблема неизменяемости исчезла. Она переехала. Прокси хранит состояние и пересылает вызовы через delegatecall, так что код реализации выполняется поверх хранилища прокси. Каждая следующая реализация должна совпадать со всеми предыдущими в том, какой слот что содержит.
Переменные состояния раскладываются в порядке объявления, упаковываются в 32-байтовые слоты, где помещаются, и унаследованные идут первыми. Вставьте поле в середину, переставьте базовые контракты или расширьте упакованный uint128 до uint256, и новый код прочитает старые байты с новым смыслом. Компилятор понятия не имеет, что старая раскладка существует. Он компилирует новый исходник так, будто это первое развёртывание.
Конструкторы не помогают. Конструктор выполняется при развёртывании самой реализации и пишет в её собственное хранилище, поэтому прокси опираются на функцию инициализации, которую нужно защитить от повторного вызова.
Моё правило простое, только добавлять. На ревью сначала сравниваются раскладки хранилища старой и новой версии, и лишь потом кто-то читает дифф логики. Дифф логики это лёгкая часть.