Upgradability
Upgradability in GenVM is a native system for modifying an Intelligent Contract after deployment while preserving 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:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }
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:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }
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.