Security·9 min का पठन·Solingo द्वारा

Delegatecall Vulnerabilities — Storage Collision Explained

Delegatecall एक powerful feature है लेकिन misuse होने पर devastating हो सकता है। Storage collisions, context preservation, और proxy patterns को समझें।

# 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.sender preserved रहता है (original caller)
  • msg.value preserved रहता है
  • 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:

  • Attacker calls setLibValue on Vulnerable contract
  • Fallback delegates to Library
  • Library writes to slot 0 (someValue)
  • लेकिन Vulnerable contract के context में, slot 0 = owner
  • Owner overwritten with attacker's address!
  • Storage 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:

  • Attacker calls initWallet on deployed Wallet (via fallback)
  • Delegatecall executes Library's initWallet
  • Library writes to owners (slot 0)
  • Wallet के context में, owners overwrite हो जाता है
  • Attacker becomes sole owner!
  • Attacker drains wallet
  • 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:

  • Library contract uninitialized था (owner = address(0))
  • Attacker calls initWallet directly on library (not via proxy)
  • Attacker becomes library owner
  • Attacker calls kill
  • Library contract destroyed!
  • सभी wallets जो library use कर रहे थे, permanently frozen
  • Result: $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 delegatecall to users

    Conclusion

    delegatecall एक powerful feature है जो upgradeable contracts enable करती है, लेकिन यह भी सबसे dangerous vulnerabilities में से कुछ को introduce करती है।

    Key takeaways:

    • delegatecall caller के 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

    Practice में लगाने के लिए तैयार हैं?

    Solingo पर interactive exercises के साथ इन concepts को apply करें।

    मुफ्त में शुरू करें