UUPS Proxy Permanently Bricked by OZ v4→v5 Storage Collision — Seeking Recovery Guidance
Summary
We have a UUPS proxy on BNB Smart Chain (BSC) that is permanently deadlocked after upgrading from an OpenZeppelin v4 implementation to an OpenZeppelin v5 implementation. The ERC-7201 namespaced storage layout in v5 is incompatible with the sequential storage layout from v4, resulting in a contract that cannot be initialized, upgraded, or administered through any on-chain transaction.
We have exhaustively analyzed every possible recovery vector and believe the contract is irrecoverable without chain-level intervention. We are reaching out to ask whether the OpenZeppelin team is aware of any recovery path we may have missed.
All token data (1.44 billion tokens, all holder balances, all configuration) is fully intact in the proxy's storage. The contract logic is functional. The problem is purely a storage layout mismatch.
Contract Details
| Item |
Value |
| Proxy |
0x02634E559651Ac67B8a3c73B15C8CF79b31242Ec (BSC) |
| Current implementation (OZ v5) |
0x47de9eeAa7E9eAF07972038eA89D85181c3D20D3 |
| Previous implementation (OZ v4) |
0x3C936A5d0F55aFD352DDf1343018D68943Bc852a |
| Owner |
0x4A90d19E6E88bb7c1ab6409D40760A27Ee210362 |
| Proxy pattern |
ERC1967Proxy + UUPSUpgradeable |
| v4 framework |
@openzeppelin/contracts-upgradeable v4.x (sequential storage + __gap) |
| v5 framework |
@openzeppelin/contracts-upgradeable v5.x (ERC-7201 namespaced storage) |
| UPGRADE_INTERFACE_VERSION |
"5.0.0" (confirmed via eth_call) |
Root Cause
The proxy was originally deployed with an OZ v4 implementation using sequential storage with __gap arrays. An upgrade was performed to a new implementation compiled with OZ v5, which uses ERC-7201 namespaced storage.
The result is a complete storage misalignment:
| Variable |
OZ v4 slot (data location) |
OZ v5 slot (code reads) |
Consequence |
_initialized |
Slot 0 = 0x01 |
0xf0c57e16...00 = 0x00 |
v5 thinks not initialized |
_owner |
Slot 154 = 0x4A90d19E... |
0x9016d09d...00 = 0x00 |
v5 sees owner as address(0) |
_name |
Slot 101 = "Outter Finance" |
Slot 0 = 0x01 |
Corrupt string → Panic(0x22) |
_totalSupply |
Slot 280 = 1.44B |
v5 namespaced slot = 0x00 |
v5 sees zero supply |
The Deadlock (Two Interlocking Failures)
Failure 1: initialize() → Panic(0x22)
The v5 initialize() attempts to write _name = "Outter Finance" to storage. In v5's ERC20Upgradeable, the _name field maps to slot 0 of the ERC-7201 namespace. However, slot 0 on the proxy contains 0x01 from v4's Initializable._initialized flag.
Solidity's string storage encoding interprets 0x01 as a "long string" marker (bit 0 = 1) with length (0x01 - 1) / 2 = 0. This violates the invariant that long strings must have length ≥ 32, triggering an unrecoverable Panic(0x22) (bad storage encoding).
This panic occurs before any state is written — the owner is never set.
Note: The v5 _initialized slot (0xf0c57e16...00) is empty, so the initializer modifier itself would pass. The Panic happens inside the function body, not in the modifier.
Failure 2: upgradeToAndCall() → OwnableUnauthorizedAccount
The v5 OwnableUpgradeable reads _owner from the namespaced slot 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300. This slot was never written by v4, so it contains address(0).
Every function with onlyOwner — including upgradeToAndCall(), transferOwnership(), and all admin functions — reverts with OwnableUnauthorizedAccount(caller) for any caller.
Result: Deadlock
initialize() → Panic(0x22) → cannot set v5 owner
↓
owner = address(0)
↓
upgradeToAndCall() → OwnableUnauthorizedAccount → cannot change implementation
↓
implementation stays v5
↓
back to initialize() → Panic(0x22) → ∞
Exhaustive Analysis of All Recovery Vectors
We have systematically tested every possible path to recover this contract:
1. All 124 function selectors in the v5 bytecode
We extracted every PUSH4 from the implementation bytecode and tested each one. Complete access control map:
| Category |
Count |
Details |
| View functions (succeed) |
34 |
All return zero/empty (reading from wrong slots) |
onlyOwner blocked |
21 |
All revert with OwnableUnauthorizedAccount |
| Panic(0x22) |
2 |
initialize(), name() |
| Panic(0x12) |
1 |
getCirculatingSupply() (division by zero from empty supply) |
| Other reverts |
66 |
require(false), missing args, etc. |
Every function that writes state requires onlyOwner. The only permissionless write function is approve(), which writes to the allowance mapping — not to the owner slot.
2. address(0) simulation
Since owner() == address(0), calling any onlyOwner function with from: address(0) via eth_call succeeds:
upgradeToAndCall(v4_impl, 0x) from address(0) → SUCCESS
transferOwnership(real_owner) from address(0) → SUCCESS
This proves the upgrade mechanism is functional. The barrier is that no entity can sign a transaction from address(0).
3. initialize() with slot 0 cleared (state override simulation)
Clearing slot 0 removes the Panic(0x22), but initialize() then fails with "Pancake: PAIR_EXISTS" — the function tries to create a PancakeSwap pair that already exists. Even clearing the factory's mapping, a third internal require(false) blocks execution.
initialize() is not a viable path even if the Panic is resolved.
4. Reinitializer search
- Only one initializer function exists:
initialize() (selector 0x8129fc1c)
- No
reinitializer(2) or any other reinitializer variant found in bytecode
- The
InvalidInitialization error appears only once in bytecode
NotInitializing error does not appear at all
5. Meta-transactions / trusted forwarder (ERC-2771)
isTrustedForwarder(address): NOT in bytecode
trustedForwarder(): NOT in bytecode
- No ERC-2771 support
6. AccessControl / role-based access
hasRole(), grantRole(), DEFAULT_ADMIN_ROLE(): NOT in bytecode
- The contract uses only
OwnableUpgradeable for access control
7. Arbitrary delegatecall / execute
- No
execute(address,bytes), multicall(bytes[]), or functionDelegateCall() in bytecode
- The only delegatecall-capable function is
upgradeToAndCall(), which requires onlyOwner
8. Ownable2Step
acceptOwnership() and pendingOwner(): NOT in bytecode
- Standard single-step
OwnableUpgradeable, not Ownable2StepUpgradeable
What We Need
Since no on-chain transaction from any address can break the deadlock, the only recovery paths are:
Option A (preferred): BSC chain-level state correction — set the EIP-1967 implementation slot back to the working v4 implementation:
- Slot:
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
- Value:
0x0000000000000000000000003c936a5d0f55afd352ddf1343018d68943bc852a
Option B: Set the v5 OwnableUpgradeable _owner slot to the legitimate owner:
- Slot:
0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300
- Value:
0x0000000000000000000000004a90d19e6e88bb7c1ab6409d40760a27ee210362
Questions for OpenZeppelin
-
Are you aware of any recovery path we may have missed? We've analyzed every function selector, checked for reinitializers, meta-transactions, AccessControl, Ownable2Step, arbitrary delegatecall, and trusted forwarders. Is there any mechanism in OZ v5's upgradeable contracts that could allow state modification without passing onlyOwner?
-
Is the Panic(0x22) on slot 0 a known issue? When v4's _initialized flag (0x01) occupies the same slot as v5's first ERC-7201 string variable, the Solidity string encoding invariant is violated. Is this documented anywhere as a hazard of v4→v5 migration?
-
Has this specific v4→v5 storage collision been reported before? We'd like to know if other projects have encountered this exact deadlock and what resolution was reached.
-
Would OpenZeppelin consider issuing guidance or tooling to help projects that have accidentally performed this incompatible upgrade? The @openzeppelin/upgrades plugin warns against storage layout changes, but the v4→v5 migration is a special case because the entire storage strategy changed fundamentally.
-
Would OpenZeppelin be willing to support our request to the BNB Chain team for a chain-level state correction? A statement from the framework authors confirming that this is an unrecoverable storage collision would significantly strengthen our case.
Impact
- 1,441,103,829 OUT tokens frozen (all holder balances inaccessible)
- ~106M OUT locked in PancakeSwap LP
- All
transfer(), balanceOf(), and admin functions non-functional
- The contract cannot be upgraded through any standard mechanism
Reproducibility
All analysis scripts are available and all results can be independently verified:
// Confirm v5 owner is address(0)
await provider.call({ to: PROXY, data: "0x8da5cb5b" }); // owner() → 0x000...000
// Confirm v5 _initialized is empty
await provider.getStorage(PROXY, "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00"); // → 0x00
// Confirm v4 owner is intact
await provider.getStorage(PROXY, 154); // → 0x...4a90d19e6e88bb7c1ab6409d40760a27ee210362
// Confirm upgrade works from address(0)
await provider.call({
to: PROXY,
data: "0x4f1ef286" + /* v4 impl padded */ + /* empty bytes */,
from: ethers.ZeroAddress
}); // → SUCCESS (0x)
Contact
Eder Jhonatan
ederjhonatann22@gmail.com
Telegram: @Puttion | Discord: PuTTioN
Owner address: 0x4A90d19E6E88bb7c1ab6409D40760A27Ee210362
We can provide signed ownership proofs, full storage dumps, and any additional technical evidence.
This issue affects a live token on BNB Smart Chain with real user funds at stake. We appreciate any guidance the OpenZeppelin team can provide.
UUPS Proxy Permanently Bricked by OZ v4→v5 Storage Collision — Seeking Recovery Guidance
Summary
We have a UUPS proxy on BNB Smart Chain (BSC) that is permanently deadlocked after upgrading from an OpenZeppelin v4 implementation to an OpenZeppelin v5 implementation. The ERC-7201 namespaced storage layout in v5 is incompatible with the sequential storage layout from v4, resulting in a contract that cannot be initialized, upgraded, or administered through any on-chain transaction.
We have exhaustively analyzed every possible recovery vector and believe the contract is irrecoverable without chain-level intervention. We are reaching out to ask whether the OpenZeppelin team is aware of any recovery path we may have missed.
All token data (1.44 billion tokens, all holder balances, all configuration) is fully intact in the proxy's storage. The contract logic is functional. The problem is purely a storage layout mismatch.
Contract Details
0x02634E559651Ac67B8a3c73B15C8CF79b31242Ec(BSC)0x47de9eeAa7E9eAF07972038eA89D85181c3D20D30x3C936A5d0F55aFD352DDf1343018D68943Bc852a0x4A90d19E6E88bb7c1ab6409D40760A27Ee210362@openzeppelin/contracts-upgradeablev4.x (sequential storage +__gap)@openzeppelin/contracts-upgradeablev5.x (ERC-7201 namespaced storage)"5.0.0"(confirmed via eth_call)Root Cause
The proxy was originally deployed with an OZ v4 implementation using sequential storage with
__gaparrays. An upgrade was performed to a new implementation compiled with OZ v5, which uses ERC-7201 namespaced storage.The result is a complete storage misalignment:
_initialized0x010xf0c57e16...00=0x00_owner0x4A90d19E...0x9016d09d...00=0x00address(0)_name"Outter Finance"0x01_totalSupply0x00The Deadlock (Two Interlocking Failures)
Failure 1:
initialize()→ Panic(0x22)The v5
initialize()attempts to write_name = "Outter Finance"to storage. In v5's ERC20Upgradeable, the_namefield maps to slot 0 of the ERC-7201 namespace. However, slot 0 on the proxy contains0x01from v4'sInitializable._initializedflag.Solidity's string storage encoding interprets
0x01as a "long string" marker (bit 0 = 1) with length(0x01 - 1) / 2 = 0. This violates the invariant that long strings must have length ≥ 32, triggering an unrecoverablePanic(0x22)(bad storage encoding).This panic occurs before any state is written — the owner is never set.
Note: The v5
_initializedslot (0xf0c57e16...00) is empty, so theinitializermodifier itself would pass. The Panic happens inside the function body, not in the modifier.Failure 2:
upgradeToAndCall()→OwnableUnauthorizedAccountThe v5
OwnableUpgradeablereads_ownerfrom the namespaced slot0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300. This slot was never written by v4, so it containsaddress(0).Every function with
onlyOwner— includingupgradeToAndCall(),transferOwnership(), and all admin functions — reverts withOwnableUnauthorizedAccount(caller)for any caller.Result: Deadlock
Exhaustive Analysis of All Recovery Vectors
We have systematically tested every possible path to recover this contract:
1. All 124 function selectors in the v5 bytecode
We extracted every
PUSH4from the implementation bytecode and tested each one. Complete access control map:onlyOwnerblockedOwnableUnauthorizedAccountinitialize(),name()getCirculatingSupply()(division by zero from empty supply)require(false), missing args, etc.Every function that writes state requires
onlyOwner. The only permissionless write function isapprove(), which writes to the allowance mapping — not to the owner slot.2.
address(0)simulationSince
owner() == address(0), calling anyonlyOwnerfunction withfrom: address(0)viaeth_callsucceeds:This proves the upgrade mechanism is functional. The barrier is that no entity can sign a transaction from
address(0).3.
initialize()with slot 0 cleared (state override simulation)Clearing slot 0 removes the Panic(0x22), but
initialize()then fails with"Pancake: PAIR_EXISTS"— the function tries to create a PancakeSwap pair that already exists. Even clearing the factory's mapping, a third internalrequire(false)blocks execution.initialize()is not a viable path even if the Panic is resolved.4. Reinitializer search
initialize()(selector0x8129fc1c)reinitializer(2)or any other reinitializer variant found in bytecodeInvalidInitializationerror appears only once in bytecodeNotInitializingerror does not appear at all5. Meta-transactions / trusted forwarder (ERC-2771)
isTrustedForwarder(address): NOT in bytecodetrustedForwarder(): NOT in bytecode6. AccessControl / role-based access
hasRole(),grantRole(),DEFAULT_ADMIN_ROLE(): NOT in bytecodeOwnableUpgradeablefor access control7. Arbitrary delegatecall / execute
execute(address,bytes),multicall(bytes[]), orfunctionDelegateCall()in bytecodeupgradeToAndCall(), which requiresonlyOwner8. Ownable2Step
acceptOwnership()andpendingOwner(): NOT in bytecodeOwnableUpgradeable, notOwnable2StepUpgradeableWhat We Need
Since no on-chain transaction from any address can break the deadlock, the only recovery paths are:
Option A (preferred): BSC chain-level state correction — set the EIP-1967 implementation slot back to the working v4 implementation:
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc0x0000000000000000000000003c936a5d0f55afd352ddf1343018d68943bc852aOption B: Set the v5 OwnableUpgradeable
_ownerslot to the legitimate owner:0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993000x0000000000000000000000004a90d19e6e88bb7c1ab6409d40760a27ee210362Questions for OpenZeppelin
Are you aware of any recovery path we may have missed? We've analyzed every function selector, checked for reinitializers, meta-transactions, AccessControl, Ownable2Step, arbitrary delegatecall, and trusted forwarders. Is there any mechanism in OZ v5's upgradeable contracts that could allow state modification without passing
onlyOwner?Is the Panic(0x22) on slot 0 a known issue? When v4's
_initializedflag (0x01) occupies the same slot as v5's first ERC-7201 string variable, the Solidity string encoding invariant is violated. Is this documented anywhere as a hazard of v4→v5 migration?Has this specific v4→v5 storage collision been reported before? We'd like to know if other projects have encountered this exact deadlock and what resolution was reached.
Would OpenZeppelin consider issuing guidance or tooling to help projects that have accidentally performed this incompatible upgrade? The
@openzeppelin/upgradesplugin warns against storage layout changes, but the v4→v5 migration is a special case because the entire storage strategy changed fundamentally.Would OpenZeppelin be willing to support our request to the BNB Chain team for a chain-level state correction? A statement from the framework authors confirming that this is an unrecoverable storage collision would significantly strengthen our case.
Impact
transfer(),balanceOf(), and admin functions non-functionalReproducibility
All analysis scripts are available and all results can be independently verified:
Contact
Eder Jhonatan
ederjhonatann22@gmail.com
Telegram: @Puttion | Discord: PuTTioN
Owner address:
0x4A90d19E6E88bb7c1ab6409D40760A27Ee210362We can provide signed ownership proofs, full storage dumps, and any additional technical evidence.
This issue affects a live token on BNB Smart Chain with real user funds at stake. We appreciate any guidance the OpenZeppelin team can provide.