Chapter 12 / 15
UUPS Upgradeable
upgradeable_by, version, migrate — safe contract upgrades.
The Universal Upgradeable Proxy Standard (EIP-1822) separates logic from storage.
Covenant's upgradeable_by and version keywords generate
a compliant UUPS implementation. The migrate from N to M block
handles storage layout migrations with a re-initialization guard so migration
logic can only run once.
counter_v1.cov
record CounterV1 {
value: u256;
owner: address;
version: 1; // compile-time version constant
upgradeable_by: self.owner; // who can authorize an upgrade
action set(v: u256) only(self.owner) {
self.value = v;
}
action increment() only(self.owner) {
self.value += 1;
}
// Migration from version 0 (unversioned) to version 1
// Runs once at upgrade time, never again
migrate from 0 to 1 {
// Initialize new fields added in V1
// (owner was always present; nothing new here)
}
}counter_v2.cov — adding a step field
record CounterV2 {
value: u256;
owner: address;
step: u256; // new in V2
version: 2;
upgradeable_by: self.owner;
action set(v: u256) only(self.owner) {
self.value = v;
}
action set_step(s: u256) only(self.owner) {
self.step = s;
}
action increment() only(self.owner) {
self.value += self.step;
}
// Storage migration: initialize step when upgrading from V1
migrate from 1 to 2 {
self.step = 1; // sensible default
}
// Guard against re-initialization: compiler enforces this runs once
}Deploying and upgrading
# Deploy V1 through a UUPS proxy covenant deploy counter_v1.cov --proxy uups --network sepolia # After logic change, deploy V2 and upgrade the proxy covenant deploy counter_v2.cov --network sepolia covenant upgrade--network sepolia
Annotations
version: N | is a compile-time constant embedded in the bytecode. The upgrade mechanism verifies version monotonicity. |
upgradeable_by: expr | generates an _authorizeUpgrade override that requires expr to be true. |
migrate from N to M { ... } | generates an initializer guarded by a storage flag — it cannot be called twice (re-initialization guard). |
| Storage layout | The compiler validates that V2 does not change the slot positions of V1 fields — only appending new fields at the end is allowed. |
Key takeaways
upgradeable_byandversiongive you UUPS in two declarations — no boilerplate proxy code to maintain.- The
migrateblock is the safest place for post-upgrade initialization — the compiler ensures it runs exactly once. - Always audit storage layout compatibility between versions. The compiler warns on slot conflicts.