Hawk High

First Flight #39
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

Incorrect use of UUPS — no call to `upgradeTo`

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:

function test_upgradeIsNotApplied() public {
// implementación original
bytes32 slot = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
address implBefore = address(uint160(uint256(vm.load(proxyAddress, slot))));
// desplegamos LevelTwo y “upgrademos”
LevelTwo implV2 = new LevelTwo();
vm.prank(principal);
levelOneProxy.graduateAndUpgrade(address(implV2), bytes(""));
// la implementación debería seguir siendo la misma
address implAfter = address(uint160(uint256(vm.load(proxyAddress, slot))));
assertEq(implAfter, implBefore, "Implementation unexpectedly changed");
}

Output

❯ forge test --mt test_upgradeIsNotApplied -vvvv
Warning: This is a nightly build of Foundry. It is recommended to use the latest stable version. Visit https://book.getfoundry.sh/announcements for more information.
To mute this warning set `FOUNDRY_DISABLE_NIGHTLY_WARNING` in your environment.
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/LeveOnelAndGraduateTest.t.sol:LevelOneAndGraduateTest
[PASS] test_upgradeIsNotApplied() (gas: 516578)
Traces:
[516578] LevelOneAndGraduateTest::test_upgradeIsNotApplied()
├─ [0] VM::load(ERC1967Proxy: [0x90193C961A926261B756D1E5bb255e67ff9498A1], 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc) [staticcall]
│ └─ ← [Return] 0x00000000000000000000000034a1d3fff3958843c43ad80f30b94c510645c316
├─ [445078] → new LevelTwo@0x2e234DAe75C793f67A35089C9d99245E1C58470b
│ └─ ← [Return] 2223 bytes of code
├─ [0] VM::prank(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca])
│ └─ ← [Return]
├─ [25207] ERC1967Proxy::fallback(LevelTwo: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 0x)
│ ├─ [22224] LevelOne::graduateAndUpgrade(LevelTwo: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 0x) [delegatecall]
│ │ ├─ [7850] MockUSDC::transfer(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca], 0)
│ │ │ ├─ emit Transfer(from: ERC1967Proxy: [0x90193C961A926261B756D1E5bb255e67ff9498A1], to: principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca], value: 0)
│ │ │ └─ ← [Return] true
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [0] VM::load(ERC1967Proxy: [0x90193C961A926261B756D1E5bb255e67ff9498A1], 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc) [staticcall]
│ └─ ← [Return] 0x00000000000000000000000034a1d3fff3958843c43ad80f30b94c510645c316
├─ [0] VM::assertEq(LevelOne: [0x34A1D3fff3958843C43aD80F30b94c510645C316], LevelOne: [0x34A1D3fff3958843C43aD80F30b94c510645C316], "Implementation unexpectedly changed") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.33ms (208.63µs CPU time)
Ran 1 test suite in 637.79ms (2.33ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation:

  1. Perform the real upgrade step after passing the authorization hook:

function graduateAndUpgrade(address newImpl, bytes calldata data) external onlyPrincipal {
_authorizeUpgrade(newImpl); // access-control
_upgradeToAndCallUUPS(newImpl, data, false); // <-- actual switch
}

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.

Updates

Lead Judging Commences

yeahchibyke Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

failed upgrade

The system doesn't implement UUPS properly.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.