# Delegatecall Vulnerabilities — Storage Collision Explained
delegatecall Solidity की सबसे powerful और dangerous features में से एक है। यह upgradeable contracts और proxy patterns को enable करती है, लेकिन improper use से complete contract takeover हो सकता है।
इस deep dive में, हम explore करेंगे कि delegatecall कैसे काम करती है, common vulnerabilities, और safe patterns।
delegatecall क्या है?
delegatecall एक low-level function है जो दूसरे contract का code execute करती है caller के context में।
call vs delegatecall
contract Target {
uint256 public value;
function setValue(uint256 _value) external {
value = _value;
}
}
contract Caller {
uint256 public value;
Target public target;
constructor(address _target) {
target = Target(_target);
}
// Using call: modifies Target's storage
function regularCall(uint256 _value) external {
(bool success, ) = address(target).call(
abi.encodeWithSignature("setValue(uint256)", _value)
);
require(success);
}
// Using delegatecall: modifies Caller's storage
function delegateCall(uint256 _value) external {
(bool success, ) = address(target).delegatecall(
abi.encodeWithSignature("setValue(uint256)", _value)
);
require(success);
}
}
Key difference:
call: Target contract का storage modify होता है
delegatecall: Caller contract का storage modify होता है (Target के code use करके)
Context Preservation
delegatecall के साथ:
msg.senderpreserved रहता है (original caller)
msg.valuepreserved रहता है
- Storage caller contract का use होता है
- Code called contract का execute होता है
Storage Collision Vulnerability
सबसे common और dangerous delegatecall vulnerability।
Vulnerable Example
// VULNERABLE CONTRACT
contract Vulnerable {
address public owner;
Library public lib;
constructor(address _lib) {
owner = msg.sender;
lib = Library(_lib);
}
fallback() external {
// Delegate all calls to library
(bool success, ) = address(lib).delegatecall(msg.data);
require(success);
}
}
contract Library {
uint256 public someValue; // Slot 0
function setLibValue(uint256 _value) external {
someValue = _value; // Writes to slot 0
}
}
Attack Scenario
contract Attacker {
function attack(address vulnerable) external {
// Call setLibValue via fallback
// This will write to slot 0 of Vulnerable contract
// Slot 0 = owner!
Vulnerable(vulnerable).call(
abi.encodeWithSignature("setLibValue(uint256)", uint256(uint160(msg.sender)))
);
// Attacker is now the owner!
}
}
What happened:
setLibValue on Vulnerable contractsomeValue)ownerStorage Layout में Storage Slots को समझना
Solidity में, state variables sequential storage slots में store होते हैं।
contract StorageExample {
uint256 public a; // Slot 0
address public b; // Slot 1
uint128 public c; // Slot 2 (lower 128 bits)
uint128 public d; // Slot 2 (upper 128 bits) — packed!
mapping(address => uint256) public e; // Slot 3 (mapping)
}
delegatecall rules:
- Called contract के storage variables caller contract के slots में map होते हैं
- अगर layouts match नहीं करते, तो storage collision होता है
Safe Storage Layout
// SAFE: Matching storage layout
contract SafeProxy {
address public owner; // Slot 0
address public lib; // Slot 1
uint256 public value; // Slot 2
fallback() external {
(bool success, ) = lib.delegatecall(msg.data);
require(success);
}
}
contract SafeLibrary {
address public owner; // Slot 0 (same as proxy)
address public lib; // Slot 1 (same as proxy)
uint256 public value; // Slot 2 (same as proxy)
function setValue(uint256 _value) external {
value = _value; // Correctly writes to slot 2
}
}
Real-World Example: Parity Wallet Hack #1 (July 2017)
Parity Multi-Sig Wallet में एक delegatecall vulnerability थी।
// Simplified Parity Wallet (vulnerable version)
contract Wallet {
address[] public owners;
address public library;
function initWallet(address[] _owners) external {
// Supposed to be called once during deployment
owners = _owners;
}
fallback() external payable {
// Delegate to library
library.delegatecall(msg.data);
}
}
contract Library {
address[] public owners;
function initWallet(address[] _owners) external {
owners = _owners;
}
}
Attack:
initWallet on deployed Wallet (via fallback)initWalletowners (slot 0)owners overwrite हो जाता हैResult: $30+ million stolen from multiple wallets.
Parity Wallet Hack #2 (November 2017)
दूसरी Parity hack में library contract खुद destroy हुआ था।
contract WalletLibrary {
address public owner;
function initWallet() external {
owner = msg.sender;
}
function kill() external {
require(msg.sender == owner);
selfdestruct(payable(owner));
}
}
Attack:
initWallet directly on library (not via proxy)killResult: $150+ million permanently locked (funds still frozen today).
Safe Delegatecall Patterns
1. Proxy Pattern with Storage Separation
// SAFE: Proxy storage never overlaps with logic
contract Proxy {
// Admin-only storage
address private immutable _admin;
address private _implementation;
constructor(address admin, address implementation) {
_admin = admin;
_implementation = implementation;
}
function upgradeTo(address newImplementation) external {
require(msg.sender == _admin, "Not admin");
_implementation = newImplementation;
}
fallback() external payable {
address impl = _implementation;
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()) }
}
}
}
contract Logic {
// Logic contract NEVER touches proxy's admin storage
uint256 public value;
function setValue(uint256 _value) external {
value = _value;
}
}
2. OpenZeppelin Transparent Proxy
OpenZeppelin का pattern admin और user calls को separate करता है।
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
// Deployment:
// 1. Deploy logic contract
// 2. Deploy TransparentUpgradeableProxy(logic, admin, initData)
// 3. Interact via proxy address
Key feature: Admin calls proxy directly (for upgrades), users' calls delegate to logic.
3. UUPS (Universal Upgradeable Proxy Standard)
Logic contract खुद upgradeable है।
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract LogicV1 is UUPSUpgradeable {
uint256 public value;
function _authorizeUpgrade(address newImplementation) internal override {
// Only owner can upgrade
require(msg.sender == owner, "Not authorized");
}
function setValue(uint256 _value) external {
value = _value;
}
}
Storage Gaps for Upgradeability
Future upgrades के लिए storage में space reserve करें।
contract BaseV1 {
uint256 public value;
// Reserve 50 slots for future variables
uint256[49] private __gap;
}
contract BaseV2 is BaseV1 {
uint256 public newValue; // Uses first slot of __gap
// Update gap to maintain 50 reserved slots
uint256[48] private __gap;
}
Why: अगर आप बाद में base contract में variable add करते हैं, तो derived contracts का storage shift नहीं होगा।
Testing Delegatecall
Foundry Test Example
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
contract DelegatecallTest is Test {
Vulnerable vulnerable;
Library lib;
Attacker attacker;
function setUp() public {
lib = new Library();
vulnerable = new Vulnerable(address(lib));
attacker = new Attacker();
}
function testStorageCollision() public {
address originalOwner = vulnerable.owner();
// Execute attack
attacker.attack(address(vulnerable));
address newOwner = vulnerable.owner();
// Owner has been changed!
assertTrue(newOwner != originalOwner);
assertEq(newOwner, address(attacker));
}
function testSafeProxy() public {
SafeProxy proxy = new SafeProxy(address(new SafeLibrary()));
// This should work safely
(bool success, ) = address(proxy).call(
abi.encodeWithSignature("setValue(uint256)", 42)
);
assertTrue(success);
assertEq(proxy.value(), 42);
// Owner should be unchanged
assertEq(proxy.owner(), address(this));
}
}
Best Practices Checklist
- ✅ OpenZeppelin के battle-tested proxy patterns use करें
- ✅ Storage layouts को carefully design करें
- ✅ हमेशा storage gaps reserve करें upgradeable contracts में
- ✅ Proxy और logic contracts के storage variables को match करें
- ✅
delegatecallको restrict करें trusted contracts के लिए
- ✅ Initialize functions को protect करें (one-time only)
- ✅ Library contracts को initialize करें deployment के दौरान
- ✅ Storage layout changes को extensively test करें
- ✅ Upgrades को multi-sig governance के पीछे रखें
- ✅ Never expose raw
delegatecallto users
Conclusion
delegatecall एक powerful feature है जो upgradeable contracts enable करती है, लेकिन यह भी सबसे dangerous vulnerabilities में से कुछ को introduce करती है।
Key takeaways:
delegatecallcaller के storage में code execute करती है
- Storage collisions से complete contract takeover हो सकता है
- Parity hacks ने $180+ million loss की (frozen + stolen)
- OpenZeppelin proxy patterns follow करें
- Storage layouts को meticulously plan करें
- Library contracts को initialize करना न भूलें
- हमेशा professional audits करवाएं upgradeable contracts के लिए
Solingo पर, आप delegatecall vulnerabilities को hands-on challenges के साथ explore करेंगे। Proxy patterns implement करें, storage collisions debug करें, और upgradeable contract security master करें।
अभी शुरू करें: solingo.io/security/delegatecall