| name | upgrade-safety |
| description | Safe upgrade practices for upgradeable smart contracts. Use when planning or executing contract upgrades. |
Upgrade Safety Skill
Reference skill for safe upgrade practices. Detailed upgrade security checklist is in the security-audit skill.
When to Use
Use this skill when:
- Planning contract upgrades
- Executing upgrades
- Reviewing upgrade implementations
- Testing upgrade scenarios
Related Skills
For comprehensive upgrade security, see:
- security-audit:
checklists/upgrade-checklist.md- Complete upgrade security checklist - proxy-patterns: Proxy pattern selection and implementation
- contract-patterns:
patterns/upgradeable-contracts.mdandexamples/upgradeable-example.sol
Upgrade Safety Rules
Storage Layout Safety
Never:
- Delete state variables
- Change variable types
- Change variable order
- Reorder inherited contracts
Always:
- Add new variables at the end
- Use storage gaps
- Document storage layout
- Test storage compatibility
Example:
// V1
contract V1 {
uint256 public value;
address public owner;
uint256[48] private __gap; // Reserve space
}
// V2 - ✅ Safe
contract V2 {
uint256 public value;
address public owner;
uint256 public newValue; // Added at end
uint256[47] private __gap; // Reduced gap
}
Initializer Safety
Always:
- Use
_disableInitializers()in constructor - Use
initializermodifier for init function - Use
reinitializer(version)for upgrade init - Call parent initializers
Example:
contract MyContract is Initializable, UUPSUpgradeable {
constructor() {
_disableInitializers(); // ✅ Required
}
function initialize() public initializer {
__UUPSUpgradeable_init();
}
function initializeV2() public reinitializer(2) {
// V2 initialization
}
}
Authorization Safety
UUPS:
- Always keep
_authorizeUpgradefunction - Use appropriate access control
- Consider timelock for critical upgrades
function _authorizeUpgrade(address) internal override onlyOwner {}
Never remove this function in UUPS upgrades!
Upgrade Process
Pre-Upgrade Checklist
- Storage layout verified compatible
- New implementation tested
- Initializers properly protected
- Authorization functions present
- Tests passing
- Storage verification tool run
- Deployment addresses verified
- Backup plan prepared
Verification Tools
Foundry:
# Check storage layout
forge inspect MyContract storage-layout
# Compare layouts
forge inspect MyContractV1 storage-layout > v1.json
forge inspect MyContractV2 storage-layout > v2.json
diff v1.json v2.json
Hardhat:
# Validate upgrade
npx hardhat verify-upgrade <PROXY> <NEW_IMPL>
Slither:
# Check upgradeability
slither-check-upgradeability . MyContract
During Upgrade
- Pause if applicable
- Deploy new implementation
- Verify implementation
- Execute upgrade
- Verify proxy points to new implementation
- Test critical functions
- Unpause if paused
Post-Upgrade
- Verify implementation address
- Test all critical functions
- Monitor for issues
- Document changes
- Update documentation
Testing Upgrades
Storage Preservation Test
function test_UpgradePreservesStorage() public {
// Deploy V1
MyContractV1 v1 = new MyContractV1();
v1.initialize(owner);
v1.setValue(42);
// Deploy V2 implementation
MyContractV2 implementation = new MyContractV2();
// Simulate upgrade (actual upgrade depends on proxy pattern)
// ...
// Cast to V2 interface
MyContractV2 v2 = MyContractV2(address(v1));
// Verify storage preserved
assertEq(v2.getValue(), 42);
assertEq(v2.owner(), owner);
}
Fork Testing
function test_UpgradeOnFork() public {
// Fork mainnet
vm.createSelectFork(vm.envString("MAINNET_RPC_URL"));
// Get existing proxy
MyContract proxy = MyContract(PROXY_ADDRESS);
// Deploy new implementation
MyContract newImpl = new MyContract();
// Upgrade
vm.prank(OWNER);
proxy.upgradeTo(address(newImpl));
// Verify
assertEq(proxy.version(), 2);
}
Common Upgrade Mistakes
❌ 1. Storage Collision
// V1
contract V1 {
uint256 public value;
}
// V2 - ❌ Wrong!
contract V2 {
address public owner; // Overwrites value!
uint256 public value;
}
❌ 2. Uninitialized Implementation
// ❌ Missing constructor protection
contract MyContract {
function initialize() public initializer {
// Attacker can initialize implementation!
}
}
❌ 3. Removed Authorization
// V1
contract V1 is UUPSUpgradeable {
function _authorizeUpgrade(address) internal override onlyOwner {}
}
// V2 - ❌ Removed authorization!
contract V2 is UUPSUpgradeable {
// Missing _authorizeUpgrade - contract is now non-upgradeable!
}
❌ 4. Selfdestruct in Implementation
// ❌ Never use selfdestruct in upgradeable contracts
function destroy() public {
selfdestruct(payable(owner)); // Kills implementation for all proxies!
}
Security Considerations
- Multi-sig for upgrades - Never use single key in production
- Timelock delays - Give users time to exit before upgrade
- Pause before upgrade - Prevent state changes during upgrade
- Test on testnet - Always test upgrades before mainnet
- Emergency procedures - Have rollback plan ready
Best Practices
- Always use OpenZeppelin upgradeable contracts
- Test upgrades on fork before mainnet
- Use storage gaps (50 slots) in base contracts
- Document storage layout changes
- Never remove _authorizeUpgrade in UUPS
- Use reinitializer for upgrade initialization
- Verify storage compatibility with tools
- Multi-sig + timelock for production
- Monitor after upgrade
- Have rollback plan
Tools
| Tool | Purpose | Command |
|---|---|---|
| Foundry | Storage layout | forge inspect Contract storage-layout |
| Hardhat | Verify upgrade | npx hardhat verify-upgrade |
| Slither | Check upgrade safety | slither-check-upgradeability |
| OpenZeppelin Defender | Upgrade automation | Web interface |
Related Documentation
- Detailed checklist:
security-audit/checklists/upgrade-checklist.md - Proxy patterns:
proxy-patterns/SKILL.md - Implementation examples:
contract-patterns/patterns/upgradeable-contracts.md - Example contract:
contract-patterns/examples/upgradeable-example.sol
Note: This is a reference skill. For the complete upgrade security checklist, see security-audit/checklists/upgrade-checklist.md.