Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
Upgradeable proxies and storage collisions (2018)#
Deployed code cannot change, so upgradeable contracts split in two. A
proxy keeps the address, the ether and the storage, and forwards every
call with DELEGATECALL to an implementation that holds the code.
Upgrading points the proxy at new code, and the state stays.
Both contracts now read one storage through two layouts. Field \(i\) of a layout lives in slot \(i\), so if the proxy keeps its implementation address in slot 0 and the implementation keeps its owner there, setting the owner overwrites the implementation pointer. EIP-1967 moves the proxy’s own fields to slots chosen by a hash,
far from any compiled layout. Upgrades must keep the old fields in order and only append: a version that reorders them silently reinterprets the stored values.
import matplotlib.pyplot as plt
import blockchainkit as bk
from blockchainkit.contracts.systems.proxies import ADMIN_SLOT, IMPLEMENTATION_SLOT
from blockchainkit.contracts.visualizers import plot_storage
A naive proxy: the owner lands on the implementation pointer#
world = bk.contracts.World()
box = world.deploy("dev", bk.contracts.BoxV1, name="BoxV1")
naive = world.deploy("dev", bk.contracts.NaiveProxy, box, name="naive proxy")
print("owner before initialize:", world.name(world.view(naive, "owner")))
world.transact("alice", naive, "initialize")
stored = world.transact("alice", naive, "store", 42)
print("store succeeded:", stored.success, "-> retrieve:", world.view(naive, "retrieve"))
assert world.read(naive, "implementation") == "alice" # Overwritten by the owner.
assert stored.success and world.view(naive, "retrieve") is None # Calls now reach nothing.
naive_storage = world.storage(naive)
owner before initialize: BoxV1
store succeeded: True -> retrieve: None
An EIP-1967 proxy, a compatible upgrade, and a reordered one#
proxy = world.deploy("dev", bk.contracts.EIP1967Proxy, box, name="EIP-1967 proxy")
world.transact("alice", proxy, "initialize")
world.transact("alice", proxy, "store", 42)
world.transact("dev", proxy, "upgrade_to", world.deploy("dev", bk.contracts.BoxV2))
assert (world.view(proxy, "retrieve"), world.view(proxy, "owner")) == (42, "alice")
world.transact("dev", proxy, "upgrade_to", world.deploy("dev", bk.contracts.BoxV2Reordered))
swapped = (world.view(proxy, "retrieve"), world.view(proxy, "owner"))
print("after a reordered upgrade: value", swapped[0], "owner", swapped[1])
assert swapped == ("alice", 42)
fig, (left, right) = plt.subplots(1, 2, figsize=(11, 3))
plot_storage(
naive_storage,
fields={0: "implementation | owner", 1: "admin | value", 2: "initialized"},
title="Naive proxy: collisions",
ax=left,
)
plot_storage(
world.storage(proxy),
fields={
IMPLEMENTATION_SLOT: "implementation (EIP-1967)",
ADMIN_SLOT: "admin (EIP-1967)",
0: "owner",
1: "value",
2: "initialized",
3: "changes",
},
title="EIP-1967 proxy: separate slots",
ax=right,
)
fig.tight_layout()
plt.show()

after a reordered upgrade: value alice owner 42
Exercise#
In the naive proxy, slot 1 is both the proxy’s admin and the
implementation’s second field. Write an implementation whose layout is
("unused", "value") and whose store(value) anyone may call. What can
Mallory do with it, and why is that worse than a lost implementation
pointer? A worked solution is in Exercises: contracts.
Total running time of the script: (0 minutes 0.068 seconds)