graduateAndUpgrade()
only executes _authorizeUpgrade()
; the proxy’s implementation address never changes.
Impact:
Permanent logic freeze. graduateAndUpgrade() only calls _authorizeUpgrade(); it never executes upgradeTo{,AndCall}.
All subsequently deployed versions (e.g. LevelTwo) are ignored; the proxy remains pinned to the vulnerable LevelOne bytecode.
Security-patch impossible. Any critical bug discovered after deployment is unfixable; users’ funds depend on a contract that can never be upgraded.
Governance DoS. Stakeholders may believe they have migrated and start interacting with new contracts, while the proxy silently keeps the old state machine.
Potential griefing vector. An attacker could publish a “new” implementation with malicious code and convince governance the system was upgraded, masking the fact that nothing actually changed.
Proof of Concept:
Output
Recommended Mitigation:
Perform the real upgrade step after passing the authorization hook:
If no initialization call is required, use _upgradeToAndCallUUPS(newImpl, bytes(""), false) or simply upgradeTo(newImpl) from OpenZeppelin’s UUPSUpgradeable.
Emit an explicit event confirming the implementation change (event Upgraded(address indexed newImpl);) to aid off-chain monitoring.
Unit-test the storage slot (eip1967.proxy.implementation) before and after the call, ensuring the value really changes.
Consider splitting concerns: keep “graduation” bookkeeping in one function and use the standard upgradeTo{,AndCall} pattern for upgrades, reducing the chance of future omissions.
The system doesn't implement UUPS properly.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.