The EarnVaultUpgradeable is an upgradeable version of the EarnVault that maintains all core functionality while enabling seamless upgrades through OpenZeppelin's transparent proxy pattern. This allows for future enhancements, bug fixes, and feature additions without disrupting existing user funds or state.
All features from the base EarnVault plus:
- Upgradeable Architecture: Transparent proxy pattern with ProxyAdmin control
- State Preservation: All user funds, balances, and configurations preserved across upgrades
- Version Management: Support for V1, V2, V3+ upgrade paths with initialization
- ETH Safety: Explicit ETH rejection with owner-controlled recovery via
sweepNative - Re-initialization Protection: Prevents accidental re-initialization after upgrades
- Comprehensive Testing: Extensive upgrade testing covering all scenarios
// Deployment structure
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
implementation, // Current implementation contract
admin, // ProxyAdmin owner
initData // Initialization data
);
ProxyAdmin proxyAdmin = new ProxyAdmin();The contract uses ERC7201 namespaced storage to prevent storage collisions:
// Base storage (V1)
bytes32 private constant EARN_VAULT_STORAGE_LOCATION =
0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;
// V2 storage extension
bytes32 private constant EARN_VAULT_V2_STORAGE_LOCATION =
0x234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1;
// V3 storage extension
bytes32 private constant EARN_VAULT_V3_STORAGE_LOCATION =
0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;// Deploy V2 implementation
EarnVaultV2 v2Impl = new EarnVaultV2();
// Upgrade with initialization
proxyAdmin.upgradeAndCall(
ITransparentUpgradeableProxy(address(proxy)),
address(v2Impl),
abi.encodeWithSelector(EarnVaultV2.initializeV2.selector)
);// Deploy V3 implementation
EarnVaultV3 v3Impl = new EarnVaultV3();
// Upgrade with initialization
proxyAdmin.upgradeAndCall(
ITransparentUpgradeableProxy(address(proxy)),
address(v3Impl),
abi.encodeWithSelector(EarnVaultV3.initializeV3.selector)
);- Core yield vault functionality
- Boost rewards system
- All base features from EarnVault
- Emergency Yield Multiplier: Configurable yield multiplier for emergency scenarios
- Emergency Mode: Toggle for emergency operations
- Backward Compatibility: All V1 functionality preserved
// V2 specific functions
function getEmergencyYieldMultiplier() external view returns (uint256);
function isEmergencyModeActive() external view returns (bool);
function setEmergencyYieldMultiplier(uint256 multiplier) external onlyOwner;
function setEmergencyMode(bool active) external onlyOwner;- Performance Fees: Configurable performance fee collection
- Management Fees: Ongoing management fee system
- Auto-Compounding: Automatic yield reinvestment
- Fee Collection: Automatic fee transfer to treasury
- Backward Compatibility: All V1 and V2 functionality preserved
// V3 specific functions
function getPerformanceFeeRate() external view returns (uint256);
function getManagementFeeRate() external view returns (uint256);
function isAutoCompoundEnabled() external view returns (bool);
function executeAutoCompound(address user) external;
function setPerformanceFeeRate(uint256 rate) external onlyOwner;
function setManagementFeeRate(uint256 rate) external onlyOwner;
function setAutoCompoundEnabled(bool enabled) external onlyOwner;// ProxyAdmin controls upgrades
ProxyAdmin proxyAdmin = new ProxyAdmin();
// Only ProxyAdmin owner can upgrade
proxyAdmin.upgradeAndCall(proxy, newImplementation, initData);ProxyAdmin Owner (Upgrade Control)
├── Upgrade implementation contracts
├── Transfer ProxyAdmin ownership
└── Delegate to vault owner for vault operations
Vault Owner (Vault Administration)
├── Set yield redistributor, boost reward keeper, treasury, pauser
├── Manage blacklist
├── Emergency operations (when paused)
├── Sweep native ETH via sweepNative()
└── V2/V3 specific configurations
Yield Redistributor (Yield Operations)
├── Distribute yield via onYield()
└── Typically the RewardRedistributor contract
BoostRewardKeeper (Boost Reward Operations)
├── Distribute boost rewards via onBoostReward()
└── Typically a keeper/operator address
Pauser (Emergency Response)
├── Pause contract operations
└── Unpause contract operations
The contract explicitly rejects ETH transfers to prevent accidental loss:
receive() external payable {
revert EthNotAccepted();
}
fallback() external payable {
revert EthNotAccepted();
}Owner can recover accidentally sent ETH (e.g., via selfdestruct):
function sweepNative(address payable to, uint256 amount) external onlyOwner {
if (to == address(0)) revert CanNotBeZeroAddress();
(bool success,) = to.call{value: amount}("");
if (!success) revert SweepFailed();
emit NativeSwept(to, amount);
}- Direct ETH Transfer: Reverts with
EthNotAccepted() - ETH via
receive(): Reverts withEthNotAccepted() - ETH via
fallback(): Reverts withEthNotAccepted() - ETH via
selfdestruct: ETH accumulates, recoverable viasweepNative()
// V1 initialization
function initialize(
address usdsc,
address owner,
address yieldRedistributor,
address treasury,
address pauser,
address boostRewardKeeper // Address authorized to call onBoostReward()
) public initializer {
// Initialize base contract
}
// V2 initialization (reinitializer(2))
function initializeV2() public reinitializer(2) {
// Set V2 defaults
emergencyYieldMultiplier = 10000;
emergencyMode = false;
}
// V3 initialization (reinitializer(3))
function initializeV3() public reinitializer(3) {
// Set V3 defaults
performanceFeeRate = 200;
managementFeeRate = 50;
autoCompoundEnabled = false;
}OpenZeppelin's reinitializer modifier prevents accidental re-initialization:
// This will revert with InvalidInitialization()
vaultV2.initializeV2(); // Already initialized, cannot re-initializeAll user and vault state is preserved across upgrades:
// User state
mapping(address => uint256) public principal; // User deposits
mapping(address => uint256) public accrued; // Accrued yield
mapping(address => uint256) public userIndex; // Last settlement index
mapping(address => bool) public isBlacklisted; // Blacklist status
// Vault state
uint256 public totalPrincipal; // Total deposits
uint256 public globalIndex; // Global yield index
uint256 public claimReserve; // Available for claims
address public yieldRedistributor; // Yield distributor
address public treasury; // Treasury address
address public pauser; // Pauser addressAfter each upgrade, verify state preservation:
// Verify core state preserved
assertEq(vaultV2.totalPrincipal(), totalPrincipalBefore);
assertEq(vaultV2.globalIndex(), globalIndexBefore);
assertEq(vaultV2.claimReserve(), claimReserveBefore);
// Verify user state preserved
assertEq(vaultV2.principal(alice), alicePrincipalBefore);
assertEq(vaultV2.claimable(alice), aliceClaimableBefore);
// Verify roles preserved
assertEq(vaultV2.treasury(), treasuryBefore);
assertEq(vaultV2.pauser(), pauserBefore);The upgradeable vault includes extensive testing:
- Basic Upgrade Tests: V1→V2→V3 upgrade chains
- State Preservation: All state preserved across upgrades
- Functionality Tests: All functions work before and after upgrades
- ETH Safety: ETH rejection and recovery testing
- Re-initialization Protection: Cannot re-initialize after upgrade
- Complex Scenarios: Multi-user, multi-yield, multi-upgrade testing
test/unit/EarnVaultUpgradeableSimple.t.sol- Comprehensive upgrade testingtest/unit/EarnVaultUpgrades.t.sol- Focused upgrade scenariostest/unit/EarnVaultUpgradeableUsingOz.t.sol- OpenZeppelin Foundry Upgrades integration
// Multi-version upgrade chain
test_V1ToV2ToV3ChainUpgrade()
// State preservation across upgrades
test_ComplexStatePreservationAcrossUpgrades()
// ETH safety and recovery
test_SweepNativeWorks()
test_CannotSendETHToVault()
// Re-initialization protection
test_CannotReinitializeV2()
test_CannotReinitializeV3()
// Functionality stability
test_OnYieldStabilityAcrossUpgrades()
test_OnBoostRewardStabilityAcrossUpgrades()import {Upgrades, UnsafeUpgrades} from "lib/openzeppelin-foundry-upgrades/src/Upgrades.sol";
// Deploy upgradeable proxy
vault = EarnVaultUpgradeable(payable(
UnsafeUpgrades.deployTransparentProxy(
address(implementation),
admin, // ProxyAdmin owner
abi.encodeWithSelector(
EarnVaultUpgradeable.initialize.selector,
address(usdsc),
owner,
yieldRedistributor,
treasury,
pauser,
boostRewardKeeper
)
)
));
// Upgrade to V2
UnsafeUpgrades.upgradeProxy(
address(vault),
address(v2Implementation),
abi.encodeWithSelector(EarnVaultV2.initializeV2.selector),
admin // ProxyAdmin owner
);// Deploy ProxyAdmin
ProxyAdmin proxyAdmin = new ProxyAdmin();
// Deploy proxy
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(implementation),
address(proxyAdmin),
initData
);
// Upgrade
proxyAdmin.upgradeAndCall(
ITransparentUpgradeableProxy(address(proxy)),
address(newImplementation),
upgradeInitData
);- ProxyAdmin Ownership: Only trusted multisig should own ProxyAdmin
- Implementation Verification: Verify implementation contracts before upgrade
- Storage Layout: Use ERC7201 namespaced storage to prevent collisions
- Initialization Safety: Use
reinitializermodifier for upgrade initialization
- Explicit Rejection: Contract rejects all ETH transfers
- Owner Recovery: Only owner can recover accidentally sent ETH
- Selfdestruct Handling: ETH from
selfdestructcan be recovered
- Settlement Ordering:
_settle()called before all state changes - Funding Invariants: Maintained across all upgrades
- Access Control: Roles and permissions preserved
| Feature | Non-Upgradeable | Upgradeable |
|---|---|---|
| Constructor | constructor(...) |
initialize(...) |
| Storage | Direct variables | ERC7201 namespaced |
| ETH Handling | receive() reverts |
receive() + fallback() revert |
| ETH Recovery | Not available | sweepNative() function |
| Upgrade Path | Not possible | V1→V2→V3+ supported |
- Deploy
EarnVaultUpgradeableimplementation - Deploy
TransparentUpgradeableProxywith initialization - Deploy
ProxyAdminand transfer ownership to multisig - Migrate user funds from old vault to new vault
- Update integrations to use new proxy address
- Always use
upgradeAndCallwith initialization data - Test upgrades thoroughly before mainnet deployment
- Verify state preservation after each upgrade
- Use ERC7201 storage for new storage variables
- Implement
reinitializerfor upgrade initialization
- Secure ProxyAdmin ownership with multisig
- Verify implementation contracts before upgrade
- Test upgrades on testnet first
- Monitor for ETH accumulation and sweep if needed
- Document upgrade procedures and rollback plans
- No action required - upgrades are transparent
- All funds preserved across upgrades
- Functionality unchanged unless explicitly enhanced
- ETH transfers rejected - use
sweepNative()if needed
- V4: Advanced yield strategies
- V5: Cross-chain yield distribution
- V6: Governance integration
- V7: MEV protection mechanisms
Each version maintains backward compatibility while adding new features:
- V1: Core functionality
- V2: Emergency features
- V3: Fee management and auto-compounding
- V4+: Advanced features (planned)
The EarnVaultUpgradeable provides a robust, secure, and future-proof foundation for yield vault operations. With comprehensive upgrade testing, state preservation guarantees, and clear upgrade paths, it enables continuous improvement while maintaining user fund safety and operational continuity.
The upgradeable architecture ensures that the vault can evolve with changing requirements while preserving the trust and reliability that users expect from a yield-generating protocol.