Intelligent Contracts
Feature List
Upgradability

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:

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: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:

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.