Skip to content

AlejandroPanos/proxy_and_storage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

delegatecall Storage Collision Demo

A hands-on demonstration of how delegatecall works in Solidity and how storage layout mismatches lead to silent state corruption. Built as a practical exercise to understand one of the most dangerous vulnerability classes in smart contract development — the same class responsible for several high-profile DeFi exploits.


What It Demonstrates

  • How delegatecall borrows code from an implementation contract and executes it in the proxy's storage context
  • Why state is stored in the proxy, not the implementation, when using delegatecall
  • How a storage layout mismatch between the proxy and implementation silently corrupts state
  • How the corrupted proxy writes to the wrong storage slot — overwriting an address variable with an incrementing uint256

Project Structure

.
├── src/
│   ├── Implementation.sol      # Simple counter contract with an increment function
│   ├── Proxy.sol               # Proxy with matching storage layout — works correctly
│   └── CorruptedProxy.sol      # Proxy with mismatched storage layout — demonstrates corruption
├── script/
│   └── DeployImplementation.s.sol  # Deploys the Implementation contract
└── test/
    └── unit/
        └── ImplementationTest.t.sol  # Unit and fuzz tests for both scenarios

Getting Started

Prerequisites

Install dependencies and build

forge install
forge build

Run all tests

forge test

Run tests with verbose output

forge test -vvvv

Deploy the implementation contract to Anvil

In one terminal, start Anvil:

anvil

In another terminal:

forge script script/DeployImplementation.s.sol --rpc-url http://localhost:8545 --private-key $PRIVATE_KEY --broadcast

Contract Overview

Implementation

A minimal counter contract. Contains a single uint256 number variable and an increment function that adds 1 to it. This contract is never called directly in the proxy pattern — it only provides the logic that the proxy borrows via delegatecall.

Proxy

A proxy contract with a storage layout that matches the implementation exactly. Both have uint256 number in slot 0. When callIncrement is called, delegatecall runs the implementation's increment function in the proxy's context — slot 0 in the proxy is incremented correctly.

CorruptedProxy

An intentionally broken proxy that introduces a storage layout mismatch. An address myAddress variable is inserted before number, pushing number from slot 0 to slot 1. The implementation still writes to slot 0 — but in the corrupted proxy, slot 0 is myAddress. Each delegatecall increments the address variable instead of the number variable.


The Core Concept

call vs delegatecall

call delegatecall
Code executed Implementation's Implementation's
Storage modified Implementation's Proxy's
msg.sender inside Proxy's address Original caller
address(this) inside Implementation's address Proxy's address

Storage slots

Solidity assigns each state variable a storage slot based on its declaration order — slot 0, slot 1, slot 2 and so on. delegatecall does not use variable names to determine where to write — it uses slot numbers. If the proxy and implementation have variables in different slot positions, delegatecall will write to the wrong variable in the proxy.

Proxy (correct):
  slot 0 → number (uint256)    ← implementation writes here ✓

CorruptedProxy:
  slot 0 → myAddress (address) ← implementation writes here ✗
  slot 1 → number (uint256)    ← never touched

Observable symptoms

After calling callIncrement on the corrupted proxy:

  • corruptedProxy.number() remains 0 — the intended variable is never written to
  • corruptedProxy.myAddress() changes from its original value to a corrupted address — the wrong slot was written to
  • implementation.number() remains 0 in both cases — the implementation's own storage is always untouched by delegatecall

Tests

Correct storage layout tests

Test What It Checks
testNumberIncreasesOnProxy delegatecall increments the proxy's number and leaves the implementation's number at zero
testFuzz_NumberIncreasesOnProxyAfterCallingMultipleTimes The proxy's number increments correctly across a random number of delegatecall invocations

Corrupted storage layout tests

Test What It Checks
testNumberDoesNotChangeOnCorruptedProxy The number variable in the corrupted proxy is never incremented and the implementation's number stays at zero
testAddressVariableChangesOnCorruptedProxy The address in slot 0 changes after a delegatecall — proving the wrong slot was written to
testFuzz_NumberIncreasesOnCorruptedProxyAfterCallingMultipleTimes The slot 0 address value increases by exactly the number of delegatecall invocations — proving each call writes 1 to slot 0

Why This Matters for Security

Storage collision bugs have caused some of the most significant exploits in DeFi history. Any protocol that uses the proxy pattern — which includes the majority of production DeFi — is vulnerable to this class of bug if the storage layout between proxy and implementation is not carefully managed.

The ERC1967 standard exists specifically to address part of this problem by storing the implementation address in a slot far outside the normal range, preventing it from colliding with implementation variables. UUPS and Transparent Proxy patterns build further protections on top of this foundation.

Understanding storage collisions at this level is essential for auditing any upgradeable contract system.


License

MIT

About

A demonstration of how delegatecall works in Solidity and how storage layout mismatches between a proxy and implementation contract lead to silent state corruption. Includes a correct proxy, an intentionally broken proxy, and a Foundry test suite that proves the corruption with unit and fuzz tests.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors