Upgradability
GenVM provides a native contract upgradability system that allows contracts to be modified after deployment while maintaining security guarantees and clear access controls.
The system is built around the Root Slot (gl.storage.Root), which stores:
| Field | Description |
|---|---|
code | The contract's source code |
locked_slots | Storage slots that non-upgraders cannot write to |
upgraders | Addresses authorized to modify locked slots (including code) |
How It Works
- At the start of a write transaction, GenVM reads the
upgraderslist - If the sender is in the
upgraderslist, no slot restrictions apply — the sender can modify any slot, includingcode - If the sender is not in the
upgraderslist, GenVM readslocked_slotsand prevents writes to them
During deployment (__init__), after the constructor completes, the runtime automatically calls root.lock_default(), which locks four critical slots: the root slot, the code slot, the locked_slots slot, and the upgraders slot.
Making a Contract Upgradable
To make a contract upgradable, you need to:
- Add authorized upgrader addresses in
__init__ - Expose a method that replaces the contract code
# v0.1.0
# { "Depends": "py-genlayer:latest" }
from genlayer import *
class UpgradableStorage(gl.Contract):
storage: str
def __init__(self, initial_storage: str):
self.storage = initial_storage
# Set the deployer as an upgrader
root = gl.storage.Root.get()
root.upgraders.get().append(gl.message.sender_address)
# lock_default() is called automatically after __init__
# it locks: root slot, code, locked_slots, upgraders
@gl.public.view
def get_storage(self) -> str:
return self.storage
@gl.public.write
def update_storage(self, new_storage: str) -> None:
self.storage = new_storage
@gl.public.write
def upgrade(self, new_code: bytes) -> None:
root = gl.storage.Root.get()
code = root.code.get()
# If sender is not in upgraders, this will raise a VMError
code.truncate()
code.extend(new_code)Upgrading to a New Version
The upgraded contract code must maintain the same storage layout for compatibility. Only the code changes — all storage data, the upgraders list, and locked slots persist across upgrades.
# v0.1.0
# { "Depends": "py-genlayer:latest" }
from genlayer import *
class UpgradableStorage(gl.Contract):
# Storage layout must remain compatible with v1
storage: str
def __init__(self):
pass
@gl.public.view
def get_storage(self) -> str:
return self.storage
@gl.public.write
def update_storage(self, new_storage: str) -> None:
self.storage = new_storage
# New method added in v2
@gl.public.view
def get_storage_length(self) -> int:
return len(self.storage)
# Keep the upgrade method for future upgrades
@gl.public.write
def upgrade(self, new_code: bytes) -> None:
root = gl.storage.Root.get()
code = root.code.get()
code.truncate()
code.extend(new_code)What Happens During an Upgrade
When an authorized upgrader calls the upgrade method with new code:
| Component | After upgrade |
|---|---|
code | Replaced with new code |
Contract storage data (e.g. storage: str) | Persists unchanged |
locked_slots | Persists unchanged |
upgraders | Persists unchanged |
This means:
- Upgrades are not one-shot — since the upgraders list persists, the same addresses can push another upgrade later
- Storage must be compatible — the new code must understand the existing storage layout. There is no automatic migration mechanism
- New methods can be added — but existing storage field positions must not change
- Upgraders can be modified — an upgrader can add or remove addresses from the upgraders list
Freezing a Contract
To make a contract permanently non-upgradable, either:
- Call
root.lock_default()without adding any addresses toupgraders— the code and critical slots are locked and nobody can unlock them - Remove all addresses from the
upgraderslist after locking
Once a contract is frozen (locked slots with no upgraders), it cannot be upgraded. This is irreversible.
Testing Upgrades
Using the GenLayer Testing Suite, you can test the full upgrade lifecycle:
from pathlib import Path
from gltest import get_contract_factory
from gltest.assertions import tx_execution_succeeded
CONTRACTS_DIR = Path(__file__).parent.parent / "contracts"
def test_upgradable_storage():
# Deploy v1
factory = get_contract_factory(
contract_file_path=CONTRACTS_DIR / "upgradable_storage.py"
)
contract = factory.deploy(args=["hello"])
# Use v1 methods
assert contract.get_storage(args=[]).call() == "hello"
# Read v2 code and upgrade the contract
v2_code = (CONTRACTS_DIR / "upgradable_storage_v2.py").read_bytes()
tx = contract.upgrade(args=[v2_code]).transact()
assert tx_execution_succeeded(tx)
# Rebuild the contract proxy from the v2 schema
v2_factory = get_contract_factory(
contract_file_path=CONTRACTS_DIR / "upgradable_storage_v2.py"
)
contract_v2 = v2_factory.build_contract(contract_address=contract.address)
# Storage persists across upgrades
assert contract_v2.get_storage(args=[]).call() == "hello"
# New v2 method works
assert contract_v2.get_storage_length(args=[]).call() == 5See GenVM specification (opens in a new tab) for the full technical details.