Delegatecall Remediation
How to safely use delegatecall in proxy patterns by validating the target address and using EIP-1967 storage slots to prevent storage collisions.
Delegatecall Remediation
Overview
Related Detector: Delegatecall
delegatecall executes code from another contract in the caller’s storage context. The primary risks are: (1) user-controlled call targets enabling arbitrary code execution, and (2) storage slot collisions between proxy administrative variables and implementation state variables. Both must be addressed.
Recommended Fix
Before (Vulnerable)
contract VulnerableProxy {
address public implementation; // slot 0
// VULNERABLE: target comes from user input — attacker passes malicious contract
function execute(address target, bytes calldata data) external {
(bool success, ) = target.delegatecall(data);
require(success);
}
}
After (Fixed)
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts/access/OwnableUpgradeable.sol";
// FIXED: use battle-tested proxy pattern with EIP-1967 storage slots
// and governance-controlled upgrade authorization
contract SafeImplementation is UUPSUpgradeable, OwnableUpgradeable {
// State variables start at slot 0 — no collision because proxy uses
// EIP-1967 pseudo-random high slots for implementation and admin
uint256 public totalSupply;
mapping(address => uint256) public balances;
function initialize(address initialOwner) public initializer {
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
}
// Only owner can upgrade — all other callers are rejected
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
// Deploy with:
// ERC1967Proxy proxy = new ERC1967Proxy(
// address(implementation),
// abi.encodeCall(SafeImplementation.initialize, (owner))
// );
Alternative Mitigations
1. Allowlist Valid Implementation Addresses
contract AllowlistedProxy {
// Implementation slot: keccak256("eip1967.proxy.implementation") - 1
bytes32 private constant _IMPL_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
mapping(address => bool) public allowedImplementations;
address public proxyAdmin;
modifier onlyAdmin() {
require(msg.sender == proxyAdmin, "Not admin");
_;
}
function allowImplementation(address impl) external onlyAdmin {
allowedImplementations[impl] = true;
}
function upgradeTo(address newImpl) external onlyAdmin {
require(allowedImplementations[newImpl], "Implementation not allowlisted");
assembly {
sstore(_IMPL_SLOT, newImpl)
}
}
fallback() external payable {
address impl;
assembly { impl := sload(_IMPL_SLOT) }
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
2. Transparent Proxy Pattern
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
// ProxyAdmin handles upgrade authorization — admin calls go to proxy,
// user calls go to implementation (no function selector clash)
ProxyAdmin admin = new ProxyAdmin(owner);
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(implementation),
address(admin),
initData
);
3. Storage Gap Pattern for Future-Safe Upgrades
// Reserve storage gap to prevent collision when adding new variables in upgrades
abstract contract StorageV1 {
uint256 public value;
address public owner;
// Reserve 48 slots for future V1 variables — total contract uses ≤50 slots
uint256[48] private __gap;
}
// V2 adds new variable AFTER the gap — no collision with V1 layout
abstract contract StorageV2 is StorageV1 {
uint256 public newVariable; // Goes into the next available slot (50)
}
Common Mistakes
Mistake 1: Uninitialized Implementation Contract
// WRONG: leaving the implementation contract uninitialized allows anyone
// to call initialize() and take ownership
contract MyImplementation is UUPSUpgradeable {
address public owner;
function initialize(address _owner) public initializer {
owner = _owner;
}
}
// WRONG: deploying implementation without calling initialize
// Anyone can call initialize() on the bare implementation contract
MyImplementation implementation = new MyImplementation();
// Should immediately call: implementation.initialize(owner) or disable initializers
Fix: Call _disableInitializers() in the implementation constructor:
constructor() {
_disableInitializers(); // Prevents anyone from calling initialize() directly
}
Mistake 2: Selfdestruct in Implementation
// WRONG: if the implementation selfdestructs, the proxy is permanently bricked
function destroy() external onlyOwner {
selfdestruct(payable(owner)); // Destroys implementation — proxy now points to dead code
}
Never put selfdestruct in an upgradeable implementation contract.
Mistake 3: Storage Collision from Inheritance Ordering
// WRONG: multiple inheritance where both parents use slot 0
contract A { uint256 public value; } // slot 0
contract B { uint256 public counter; } // slot 0
contract C is A, B {
// A.value and B.counter both map to slot 0 — collision!
}