Critical UUPS Proxy Vulnerability in OpenZeppelin 4.1.0-4.3.1
OpenZeppelin supports two main proxy patterns:
- Transparent proxy
- UUPS proxy
The most important difference is where the upgradeTo logic lives. In a transparent proxy, the upgrade logic is implemented in the proxy contract. In a UUPS proxy, the upgrade logic lives in the implementation contract.
The Vulnerability
OpenZeppelin’s upgradeable system is usually composed of multiple contracts. Each upgradeable deployment includes an implementation contract that contains the business logic and a proxy contract that stores the state. Upgrading the proxy simply means changing the proxy’s implementation pointer to a new implementation contract.

Upgrade flow diagram:

In versions 4.1.0 through 4.3.1, the upgradeTo function on UUPSUpgradeable did not restrict who could call it. That meant anyone could call it directly on the implementation contract. An attacker could deploy a malicious contract whose upgradeTo path executes SELFDESTRUCT, then call upgradeTo on the implementation contract and point it to the malicious contract. The result is that the implementation destroys itself. One crucial detail is that calling SELFDESTRUCT here does not cause the transaction to fail. Once the implementation code is gone, the proxy loses its logic and the whole system breaks.
The older implementation of upgradeTo was:
In version 4.3.2 it became:
| |
Now upgradeTo on the implementation can only be executed through the active proxy.
How upgradeTo Works in the UUPS Pattern
The deployment process for a UUPS system is roughly:
First, deploy the implementation contract. It must inherit from ERC1967UpgradeUpgradeable.
Second, deploy the ERC1967Proxy contract and initialize it with the implementation address from the previous step.
Finally, use the proxy to call upgradeTo.
The relevant code looks like this:
| |
Here, _authorizeUpgrade is the permission check implemented by the application contract. Any contract inheriting from ERC1967UpgradeUpgradeable must provide this function, and in practice it usually restricts upgrades to the owner.
Inside _upgradeToAndCallSecure, the new implementation is subjected to a rollback test. The contract temporarily delegates into the new implementation and asks it to upgrade back to the old implementation. This verifies that the new implementation’s upgrade logic is still functional and that future upgrades will remain possible.
If that rollback test succeeds, the contract finally stores the new implementation address in _IMPLEMENTATION_SLOT.
https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680