Intelligent Contracts
Upgradability

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:

FieldDescription
codeThe contract's source code
locked_slotsStorage slots that non-upgraders cannot write to
upgradersAddresses authorized to modify locked slots (including code)

How It Works

  1. At the start of a write transaction, GenVM reads the upgraders list
  2. If the sender is in the upgraders list, no slot restrictions apply — the sender can modify any slot, including code
  3. If the sender is not in the upgraders list, GenVM reads locked_slots and 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:

  1. Add authorized upgrader addresses in __init__
  2. 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:

ComponentAfter upgrade
codeReplaced with new code
Contract storage data (e.g. storage: str)Persists unchanged
locked_slotsPersists unchanged
upgradersPersists 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 to upgraders — the code and critical slots are locked and nobody can unlock them
  • Remove all addresses from the upgraders list 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() == 5

See GenVM specification (opens in a new tab) for the full technical details.