Section 5 of 5
Skills Assessment
Skills Assessment: Proxy Upgrade Patterns
This is an unguided assessment worth 100 Shields. No hints, no templates. You need to demonstrate that you can safely upgrade a proxy contract without breaking storage or losing access.
The Challenge
Safely upgrade a proxy contract from V1 to V2.
You are given a V1 contract that has been deployed behind a UUPS proxy. The contract has a critical bug: the withdraw function does not check for reentrancy, allowing an attacker to drain the contract.
V1 Contract (Deployed and Live)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract LendingVaultV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint256 public totalDeposits;
mapping(address => uint256) public balances;
mapping(address => uint256) public borrowBalances;
uint256 public interestRateBps;
bool public paused;
uint256[45] private __gap;
function initialize(address _owner, uint256 _rateBps) public initializer {
__Ownable_init(_owner);
__UUPSUpgradeable_init();
interestRateBps = _rateBps;
}
function deposit() external payable {
require(!paused, "Paused");
balances[msg.sender] += msg.value;
totalDeposits += msg.value;
}
// BUG: No reentrancy protection
function withdraw(uint256 amount) external {
require(!paused, "Paused");
require(balances[msg.sender] >= amount, "Insufficient");
balances[msg.sender] -= amount;
totalDeposits -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
function borrow(uint256 amount) external {
require(!paused, "Paused");
require(balances[msg.sender] * 75 / 100 >= borrowBalances[msg.sender] + amount, "Undercollateralized");
borrowBalances[msg.sender] += amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
function _authorizeUpgrade(address) internal override onlyOwner {}
}
The contract has been live for 3 months. Users have deposited 500 ETH. There are active borrows.
Your Task
Write a complete LendingVaultV2 contract that:
- Fixes the reentrancy bug in both
withdrawandborrow - Preserves the exact storage layout of V1 (no variable reordering, no insertions before existing variables)
- Includes
_authorizeUpgradewith proper access control - Uses
_disableInitializers()in the constructor - Adds a
reinitializerfunction for any V2-specific setup (if needed) - Maintains the storage gap (adjusted if you add new variables)
Additionally, provide:
- A Hardhat deployment script (
upgrade-to-v2.js) that upgrades the proxy from V1 to V2 - A brief explanation (100-200 words) of what would go wrong if you:
- Moved
pausedbeforeinterestRateBps - Forgot to inherit
UUPSUpgradeablein V2 - Removed the
__gaparray
- Moved
Evaluation Criteria
Storage Safety: V2 must not break any existing storage slots. All user balances, borrow positions, and configuration values must be preserved exactly. A single slot collision means total failure.
Reentrancy Fix: The fix must actually prevent reentrancy. Simply adding a nonReentrant modifier from OpenZeppelin is acceptable, but you must adjust the storage layout correctly (ReentrancyGuardUpgradeable uses a storage slot).
Upgrade Continuity: V2 must be upgradeable to V3 in the future. If V2 breaks the upgrade path, it is a critical failure.
Deliverable
Submit your complete LendingVaultV2.sol contract, the deployment script, and the explanation.
Verification
This is a skills assessment. Submit your completed code and explanation for review.