Documentation Index
9 min readChapter 29

29. Contract Upgradability Patterns

Smart contracts on the Kortana blockchain are immutable once compiled and deployed to the Kortana Virtual Machine (KVM). When a Quorlin contract source file (.ql) is compiled, it generates a KVM module with fixed bytecode instruction streams, rigid constant pools, and precise entry points. Once stored in Kortana's unified state trie, the code residing at a given contract address cannot be directly overwritten or modified.

However, software systems evolve. Bugs must be patched, financial formulas updated, and new features introduced. To adapt to changing requirements while maintaining continuous operating state and address stability, developers use Upgradability Patterns.

This chapter details the primary architectural patterns for upgrading Quorlin smart contracts on the KVM, focusing on:

  1. Data-Logic Separation (Eternal Storage)
  2. Interface-Based Router Delegation
  3. State Migration and Deprecation

1. Immutability in the KVM Architecture

When a Quorlin contract is deployed, the compiler outputs a binary KVM module starting with the magic bytes "KVM\0", followed by constant declarations and 32-bit instructions (such as LoadK, Add, MStore, and state-access opcodes). The Kortana state engine indexes this module by its deployment address.

Because KVM bytecode execution is deterministic and immutable:

  • Storage (map, number, address, record state variables) is tied directly to the contract account address in the global state trie.
  • Code Execution is strictly bound to the instructions stored at that address.

To upgrade contract functionality without losing accumulated state (such as user balances or protocol parameters), you must decouple state storage from execution logic.


2. Pattern 1: Data-Logic Separation (Eternal Storage)

The Eternal Storage pattern separates storage maintenance from business logic. A main storage contract acts as an immutable database (the "Vault"), while secondary contracts implement execution logic.

Architecture Overview

  1. Storage Vault Contract: Holds all state mapping data. It exposes reads and writes endpoints that are guarded so that only the authorized logic contract (caller == currentLogic) can modify storage.
  2. Logic Contract: Contains core calculations, state transitions, and business rules. It reads from and writes to the Vault via inter-contract interface calls.
  3. Upgrade Path: To upgrade, deploy a new Logic Contract (V2) and update the Vault's authorized logic pointer.
       +-------------------+
       |   User / Client   |
       +---------+---------+
                 |
                 v
       +-------------------+
       | Logic Contract V2 |
       +---------+---------+
                 | (External Calls)
                 v
       +-------------------+
       |   Data Vault      |
       | (Eternal Storage) |
       +-------------------+

Quorlin Implementation

First, define the interface for the Data Vault:

interface IDataVault { reads number getNumber(address account); writes truth setNumber(address account, number amount); }

Next, implement the immutable DataVault contract:

contract DataVault { address admin; address currentLogic; map<address, number> userBalances; event LogicUpdated(address indexed oldLogic, address indexed newLogic); event BalanceSet(address indexed account, number amount); constructor { admin = caller; currentLogic = caller; } writes truth setLogic(address newLogic) { require caller == admin, "Vault: caller is not admin"; require newLogic != 0x0000000000000000000000000000000000000000, "Vault: invalid address"; emit LogicUpdated(currentLogic, newLogic); currentLogic = newLogic; return yes; } reads number getNumber(address account) { return userBalances[account]; } writes truth setNumber(address account, number amount) { require caller == currentLogic, "Vault: caller is not authorized logic"; userBalances[account] = amount; emit BalanceSet(account, amount); return yes; } }

Now implement the execution logic contract TokenLogicV1:

contract TokenLogicV1 { address vaultAddress; address admin; constructor { admin = caller; } writes truth setVault(address vault) { require caller == admin, "Logic: caller is not admin"; vaultAddress = vault; return yes; } reads number getBalance(address account) { IDataVault vault = IDataVault(vaultAddress); return vault.getNumber(account); } writes truth deposit(number amount) { require amount > 0, "Logic: deposit must be positive"; IDataVault vault = IDataVault(vaultAddress); number currentBalance = vault.getNumber(caller); number newBalance = currentBalance + amount; vault.setNumber(caller, newBalance); return yes; } }

Upgrading to Version 2

Suppose TokenLogicV2 is introduced to add a 1% fee on deposits. The state in DataVault remains untouched:

contract TokenLogicV2 { address vaultAddress; address admin; constructor { admin = caller; } writes truth setVault(address vault) { require caller == admin, "Logic: caller is not admin"; vaultAddress = vault; return yes; } reads number getBalance(address account) { IDataVault vault = IDataVault(vaultAddress); return vault.getNumber(account); } writes truth deposit(number amount) { require amount > 100, "Logic: deposit too small for fee"; // Calculate 1% fee using Quorlin arithmetic number fee = amount / 100; number netAmount = amount - fee; IDataVault vault = IDataVault(vaultAddress); number currentBalance = vault.getNumber(caller); number newBalance = currentBalance + netAmount; vault.setNumber(caller, newBalance); return yes; } }

Upgrade Steps:

  1. Deploy TokenLogicV2.
  2. Call TokenLogicV2.setVault(DataVaultAddress).
  3. Call DataVault.setLogic(TokenLogicV2Address).
  4. The system is now running V2 logic over the existing V1 state.

3. Pattern 2: The Interface Router Pattern

While Eternal Storage routes storage access to a static vault, the Interface Router Pattern presents a static contract address to external clients while dynamically dispatching incoming function calls to active backend implementation contracts.

Architecture Overview

  • Router Contract: Acts as the single point of entry for users and external applications. It stores protocol configuration, administrator accounts, and implementation addresses.
  • Logic Modules: External contracts implementing standardized Quorlin interface definitions.
  • Dispatching: The Router receives incoming parameters and passes them downstream to the active implementation contract via standard interface invocation.
+---------------+      1. Invokes endpoint      +-----------------+
| User / Client | ----------------------------> | Router Contract |
+---------------+                               +--------+--------+
                                                         |
                                                         | 2. Forwards call
                                                         v
                                                +-----------------+
                                                | LogicModule V1  |
                                                +-----------------+

Quorlin Implementation

First, declare the contract execution standard as an interface:

interface IExecutionModule { writes number executeTask(address account, number inputData); }

Next, implement the Router contract:

contract Router { address admin; address activeModule; event ModuleUpgraded(address indexed oldModule, address indexed newModule); constructor { admin = caller; } writes truth setModule(address newModule) { require caller == admin, "Router: caller is not admin"; require newModule != 0x0000000000000000000000000000000000000000, "Router: invalid address"; emit ModuleUpgraded(activeModule, newModule); activeModule = newModule; return yes; } reads address getActiveModule() { return activeModule; } writes number processRequest(number value) { require activeModule != 0x0000000000000000000000000000000000000000, "Router: module not set"; // Dispatch downstream call through the interface IExecutionModule module = IExecutionModule(activeModule); number result = module.executeTask(caller, value); return result; } }

Implement an initial execution module ModuleAlpha:

contract ModuleAlpha { writes number executeTask(address account, number inputData) { // Simple computation: return input doubled number result = inputData * 2; return result; } }

When upgrading system logic, deploy ModuleBeta and register it in the router:

contract ModuleBeta { writes number executeTask(address account, number inputData) { // Upgraded logic: return input tripled + baseline constant number result = (inputData * 3) + 10; return result; } }

Calling Router.setModule(ModuleBetaAddress) instantly routes all future invocations of processRequest to the upgraded ModuleBeta without breaking existing frontend client configurations.


4. Pattern 3: State Migration and Deprecation

When business models undergo radical modifications—such as transitioning state storage from basic primitives (number) to structural types (record)—neither Eternal Storage nor Router redirection is sufficient alone. You must perform a State Migration.

Architecture Overview

  1. Deprecation: The legacy contract (V1) is set into a read-only or deprecated state using safety switches.
  2. State Export: The new contract (V2) reads legacy state via standard reads functions and recreates equivalent state entries internally.
  3. Cutover: Administrative ownership and authorization rights are transferred to V2.
record UserProfile { number balance; truth isActive; number tier; } contract LegacyToken { address admin; truth isDeprecated; map<address, number> balances; constructor { admin = caller; isDeprecated = no; } writes truth deprecate() { require caller == admin, "Legacy: unauthorized"; isDeprecated = yes; return yes; } reads number balanceOf(address account) { return balances[account]; } writes truth transfer(address recipient, number amount) { require !isDeprecated, "Legacy: contract is deprecated"; number current = balances[caller]; require current >= amount, "Legacy: insufficient funds"; balances[caller] = current - amount; balances[recipient] = balances[recipient] + amount; return yes; } }

The upgraded UpgradedToken imports the legacy balances and upgrades user accounts into structured UserProfile records:

interface ILegacyToken { reads number balanceOf(address account); } contract UpgradedToken { address admin; address legacyContract; map<address, UserProfile> profiles; event AccountMigrated(address indexed account, number balance, number tier); constructor { admin = caller; } writes truth setLegacyAddress(address legacy) { require caller == admin, "Upgraded: unauthorized"; legacyContract = legacy; return yes; } writes truth migrateAccount(address account) { require caller == admin, "Upgraded: unauthorized"; require profiles[account].balance == 0, "Upgraded: account already migrated"; ILegacyToken legacy = ILegacyToken(legacyContract); number legacyBalance = legacy.balanceOf(account); require legacyBalance > 0, "Upgraded: no balance to migrate"; UserProfile profile; profile.balance = legacyBalance; profile.isActive = yes; profile.tier = 1; // Default upgraded tier classification profiles[account] = profile; emit AccountMigrated(account, legacyBalance, 1); return yes; } reads number getBalance(address account) { return profiles[account].balance; } }

5. ABI Selector Alignment and Type Security

During cross-contract upgradability dispatching, the Quorlin compiler automatically resolves type signatures to preserve Ethereum-compatible ABI specifications:

Quorlin Native TypeInternal Diagnostic KeywordABI Signature Mapping TypeKVM Storage Register Representation
numbernumberuint256256-bit Unsigned Integer Word
truthtruthbool256-bit Word (0x0 or 0x1)
addressaddressaddress160-bit Value (left-padded)
texttextstringDynamic Byte Array

When an interface definition in Quorlin is declared:

interface IToken { writes truth transfer(address recipient, number amount); }

The Quorlin ABI emitter computes the standard Keccak-256 function selector for transfer(address,uint256). This guarantees that upgraded contracts built in Quorlin remain fully compatible with external Solidity contracts, legacy Web3 tooling, and standard wallet infrastructure on the Kortana network.


6. Security Checklist for Quorlin Upgrades

When implementing contract upgrades in production, always follow these rules:

  1. Strict Access Guarding: Protect administrative upgrade calls with strict equality checks using caller:
    require caller == admin, "Unauthorized upgrade attempt";
  2. Zero-Address Validation: Ensure target addresses for logic upgrades or state vaults are non-zero:
    require newImplementation != 0x0000000000000000000000000000000000000000, "Invalid target";
  3. Explicit State Locking: Ensure deprecated contracts explicitly lock state modifications using explicit status variables (truth isDeprecated).
  4. Audit Structural Storage Layouts: When using Eternal Storage, maintain explicit register alignment across versions to ensure logic changes do not alter expected slot calculations.
  5. Emit Operational Events: Always emit indexed diagnostic events (emit Upgraded(...)) during operational upgrades to ensure off-chain indexers and block explorers track implementation state accurately.