Proxy upgrade patterns and their bugs — uninitialized implementation, storage slot collisions, selector clashes, unprotected upgrade auth.
62
74%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./packages/decepticon/decepticon/skills/standard/contracts/upgradeable-proxy/SKILL.mdProxies separate storage (proxy) from logic (implementation). The
proxy delegatecalls into the implementation, executing logic in the
proxy's storage context. Three common patterns:
Each has distinct bug classes.
Implementations should be initialized at deployment to prevent third-party initialization:
// Vulnerable — anyone can call initialize() on the implementation
// directly (not via proxy) and become its owner
contract VulnImpl {
function initialize(address owner_) external {
owner = owner_;
}
}
// Safe — disable initializers on impl deployment
contract SafeImpl is Initializable {
constructor() { _disableInitializers(); }
function initialize(address owner_) external initializer {
owner = owner_;
}
}Attack: attacker calls initialize on the impl contract → becomes
"owner" of the impl. While this doesn't immediately drain the proxy,
it CAN matter if _authorizeUpgrade reads impl-side state, or if
selfdestruct is exposed and impl auth is on owner. Past incident:
Parity multisig 2017 — $280M frozen.
# Find unprotected impl
grep -rn 'initialize\|_disableInitializers' src/Proxy and impl share storage. New impl versions must lay storage identical to old:
// V1
contract LogicV1 {
address public owner; // slot 0
uint256 public total; // slot 1
}
// V2 — BUG: inserted new var at top
contract LogicV2 {
uint256 public newVar; // slot 0 — overwrites owner!
address public owner; // slot 1
uint256 public total; // slot 2
}After upgrade, owner is read from where total was; arbitrary value.
Check:
__gap reserved slots used in upgradeable libs# OZ provides a tool
slither-check-upgradeability V1.sol V2.sol --proxy-name MyProxyTransparent proxy: if caller is admin → proxy admin functions; else → delegatecall to impl. If impl has function with selector matching a proxy admin selector → admin sees impl, user sees impl, admin can never call admin functions. Or worse, ABI-trickery.
Common collisions:
upgradeTo(address): selector 0x3659cfe6 — what if impl defines a transferOwnership(address) that hashes to the same selector?Slither auto-detects:
slither --print human-summary src/ | grep -i selector_authorizeUpgrade empty / wrongUUPS pattern: implementation contains the upgrade authorization. Default
OZ _authorizeUpgrade is internal virtual {} — empty body that
reverts, but the function itself is empty. Devs override it:
// VULNERABLE — empty body, no actual auth check
function _authorizeUpgrade(address newImplementation) internal override {}
// SAFE
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}Attack: anyone calls upgradeTo(maliciousImpl) → all storage now
controlled by malicious code.
grep -rn '_authorizeUpgrade' src/delegatecall to attacker-controlled addressIf the impl contains a function that performs delegatecall(arbitrary_address),
attackers can pass their own contract → execute arbitrary code in
proxy storage context, including selfdestruct.
grep -rn 'delegatecall' src/Constructors run on the implementation only — proxy storage is
untouched. Storage initialization must happen in initialize(). If
critical state is set in the constructor, the proxy will read zero
values:
// VULNERABLE — admin set in constructor, proxy reads 0x0
contract Impl {
address public admin;
constructor() { admin = msg.sender; } // sets admin on IMPL only
}diamondCut access control — must be gated, often forgotten# Inspect facet selectors
slither . --print function-summary | grep -A1 'Facet'OZ upgradeable libs use uint256[N] private __gap; to reserve slots
for future vars. Custom contracts inheriting them MUST preserve this:
contract MyToken is ERC20Upgradeable {
uint256 public myVar1;
uint256 public myVar2;
uint256[48] private __gap; // reserve 48 slots for future
}Without __gap, future versions can't add fields without colliding
with parent contracts that also added fields.
function test_unauth_upgrade() public {
address attacker = address(0xBEEF);
address malicious = address(new MaliciousImpl());
vm.prank(attacker);
proxy.upgradeTo(malicious); // _authorizeUpgrade is empty
// Now any call to proxy executes malicious code
proxy.balanceOf(attacker); // returns whatever malicious wants
}
function test_impl_init() public {
address attacker = address(0xBEEF);
address impl = address(proxy.implementation());
vm.prank(attacker);
IImpl(impl).initialize(attacker);
// Attacker is "owner" of impl
// Whether that matters depends on impl logic
assertEq(IImpl(impl).owner(), attacker);
}
function test_storage_collision() public {
// Set state in V1
proxy.setOwner(address(this));
assertEq(proxy.owner(), address(this));
// Upgrade to V2 which inserted a new var at slot 0
proxy.upgradeTo(address(new LogicV2()));
// owner now reads from where total was — likely garbage
assertNotEq(proxy.owner(), address(this));
}_authorizeUpgrade: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H = 10.0 (Critical)// UUPS template
contract MyContract is UUPSUpgradeable, OwnableUpgradeable {
constructor() { _disableInitializers(); }
function initialize(address owner_) external initializer {
__Ownable_init(owner_);
__UUPSUpgradeable_init();
}
function _authorizeUpgrade(address) internal override onlyOwner {}
}
// Storage layout discipline
contract MyToken is ERC20Upgradeable, OwnableUpgradeable {
// V1 fields
uint256 public param1;
address public param2;
// Reserved gap — DECREMENT this when adding fields
uint256[48] private __gap;
}
// Verify upgrade safety in CI
forge test --match-contract Test_Upgrade
# Plus run `slither-check-upgradeability V1 V2` in CI gate0cf691e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.