11. State Variables & Storage
Smart contract execution on the Kortana Virtual Machine (KVM) relies on a clear distinction between transient execution memory and persistent storage. Storage in Quorlin represents long-term, stateful data that persists across block boundaries and transaction invocations.
In Quorlin, state variables are declared at the top level of a contract. They map directly into 256-bit key-value storage slots maintained by the underlying Kortana Unified State Trie. This chapter covers state variable declarations, type system representations, function mutability contracts (reads vs writes), storage slot mechanics, cross-VM interoperability with KEVM, and the underlying gas dynamics of storage operations.
11.1 Introduction to Persistent State in Quorlin
Unlike local variables declared inside function bodies—which reside temporarily in memory or registers during instruction execution—state variables are permanently stored on the Kortana blockchain state.
When you declare a variable at the top level of a contract block, Quorlin reserves a persistent storage location for that member.
contract TokenVault { // Persistent State Variables address owner; number totalVaultBalance; truth isLocked; constructor { owner = caller; totalVaultBalance = 0; isLocked = no; } }
Every state modification is committed to the blockchain's state trie upon successful transaction execution. If a transaction encounters an error (such as a failed require check or an out-of-gas exception), all modifications made to state variables during that transaction execution are reverted.
11.2 State Variable Types & Primitive Representations
Quorlin intentionally uses intuitive, human-readable keywords for its primitive types. Behind the scenes, the Quorlin compiler (quorlin/parser.cpp and quorlin/sema.cpp) checks these primitives and maps them directly to standard Ethereum-compatible ABI types (quorlin/abi.cpp) and 256-bit KVM words (kvm/interpreter.cpp).
| Quorlin Type | Internal AST Type | Ethereum ABI Type | Description |
|---|---|---|---|
number | Type::U256 | uint256 | Unsigned 256-bit integer ($0$ to $2^{256}-1$). |
truth | Type::Bool | bool | Boolean logic value (yes or no). |
address | Type::Address | address | 160-bit (20-byte) Kortana or EVM account identifier. |
text | Type::Text | string | UTF-8 encoded text string up to maximum length bounds. |
11.2.1 Numbers (number)
The default numeric primitive in Quorlin is number. It maps to an unsigned 256-bit word (uint256_t).
number maxDepositLimit; number currentDepositCount;
Arithmetic operations on state variables of type number use two's-complement 256-bit arithmetic implemented in kvm/arith.cpp. Overflow and underflow protection can be explicitly checked or wrapped based on the expressions used.
11.2.2 Truth Values (truth)
Boolean states in Quorlin are declared using the keyword truth. The syntax accepts the explicit literals yes (representing boolean true / 1) and no (representing boolean false / 0).
truth emergencyStop; writes truth haltContract() { require caller == owner, "Unauthorized"; emergencyStop = yes; return yes; }
At the storage level, truth is encoded as a 256-bit word where 0x0 represents no and 0x1 (or any non-zero value) evaluates to yes.
11.2.3 Addresses (address)
Account identifiers on Kortana are represented by the address primitive. Addresses are 20 bytes (160 bits) in size. When loaded into a 256-bit storage slot or register, the address is zero-extended (left-padded with zeros) to form a standard 32-byte word.
address treasury; address reserveContract;
11.2.4 Text (text)
Bounded character strings are declared using text. The Quorlin compiler limits literal sizes according to kMaxTextBytes in semantic analysis (quorlin/sema.cpp). In ABI emission, text is automatically serialized as standard UTF-8 dynamic string structures.
text vaultName;
11.3 Complex Storage Data Structures
Beyond basic primitives, Quorlin natively supports dynamic storage mappings (map) and composite user-defined structure types (record).
11.3.1 Storage Mappings (map<K, V>)
Mappings act as associative hash tables that link a key type to a value type. Mappings can only reside in persistent contract storage.
map<address, number> balances; map<address, map<address, number>> allowances;
In map<K, V>, key lookup does not involve storing keys explicitly in contract storage. Instead, the storage slot for any key $K$ is computed deterministically using standard cryptographic hashing routines (keccak256 or blake3), combining the slot index of the mapping variable and the serialized key value.
Storage Mapping Usage Example:
contract Token { map<address, number> balances; writes truth deposit() { balances[caller] = balances[caller] + 100; return yes; } reads number checkBalance(address user) { return balances[user]; } }
11.3.2 Composite Types (record)
A record allows grouping multiple named fields into a single custom type. Records can be placed inside storage or passed as execution data.
record UserProfile { number depositAmount; truth isActive; number lastDepositTimestamp; } contract AccountRegistry { map<address, UserProfile> profiles; writes truth registerUser() { UserProfile profile; profile.depositAmount = 0; profile.isActive = yes; profile.lastDepositTimestamp = 0; profiles[caller] = profile; return yes; } reads truth isUserActive(address user) { UserProfile userProfile = profiles[user]; return userProfile.isActive; } }
During semantic analysis (quorlin/sema.cpp), the compiler performs field-name resolution and type checking on record access. If an invalid record field is accessed, the compiler generates descriptive error messages showing all available fields (e.g., "depositAmount, isActive and lastDepositTimestamp").
11.4 Function Mutability Qualifiers: reads vs writes
Quorlin strictly enforces state access rules at compile time through explicit function mutability qualifiers: reads and writes.
// Read-only function: cannot modify any contract state variable reads number getBalance(address account) { return balances[account]; } // State-modifying function: permitted to update storage slots writes truth updateBalance(address account, number newBalance) { balances[account] = newBalance; return yes; }
11.4.1 Semantic Enforcement of reads
During the analysis phase (quorlin/compiler.cpp step 3), the Analyzer checks every AST node within a reads function block:
- No Storage Writes: Assigning values to top-level state variables or storage mappings is prohibited inside a
readsmethod. - No Call Side-Effects: Invoking external functions marked as state-modifying (
writes) triggers a semantic compilation error. - Static Context Execution: At the KVM level,
readsfunctions execute under static call constraints, causing any underlying state write attempt to trigger an immediate execution revert.
11.5 Low-Level Storage Architecture: KVM & The Unified State Trie
To understand how Quorlin interacts with the blockchain, we must examine the underlying execution environment provided by the Kortana Virtual Machine (KVM).
+-------------------------------------------------------------------+
| Quorlin Smart Contract |
| |
| number totalCoins; map<address, number> balances;|
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| KVM Bytecode & StateHost |
| |
| StateHost::get_storage(addr, key) StateHost::set_storage(...) |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| Kortana Unified State Trie |
| |
| Key: Keccak256 / Blake3 Hash -> Value: 256-bit Word |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| KEVM (EVM Interoperability Layer) |
+-------------------------------------------------------------------+
11.5.1 The 256-Bit Key-Value Abstraction
In the KVM architecture, contract storage is modeled as an infinitely large array of 256-bit slots ($2^{256}$ addressable locations). Both key and value are uniform 32-byte primitive array types (uint256_t in C++).
// From kvm/state_host.cpp Result<uint256_t> StateHost::get_storage(const Address& address, const uint256_t& key) const { return world_.get_storage(address, key); } Result<uint256_t> StateHost::set_storage(const Address& address, const uint256_t& key, const uint256_t& value) { KORTANA_TRY_ASSIGN(const uint256_t previous, world_.get_storage(address, key)); KORTANA_TRY(world_.put_storage(address, key, value)); return previous; }
11.5.2 The Kortana Unified State Trie
Kortana utilizes a single, unified state trie architecture. State reads (get_storage) and state writes (set_storage) interact directly with world_ (state::WorldState).
This unified design ensures full state cross-interoperability:
- Quorlin contracts compiled to KVM bytecode store data in the exact same trie locations accessed by Solidity contracts compiled to KEVM bytecode.
- Quorlin can call Solidity contracts on Kortana, and Solidity contracts can read or modify Quorlin storage slots seamlessly provided selector and storage key conventions match.
11.6 Storage Gas Mechanics & Cost Optimization
Storage operations are among the most expensive instructions in blockchain execution because they permanently alter the state trie distributed across all node hardware.
11.6.1 Base vs Dynamic Storage Costs
As defined in kvm/gas.cpp, instructions in KVM are categorized by gas cost tiers:
- Zero / Low Cost Ops: Arithmetic instructions (
Add,Sub), bitwise operations (And,Or), and register moves (Mov). - Storage Load (
SLoad/get_storage): Requires disk/cache lookup in the Merkle trie, incurring a higher gas charge. - Storage Store (
SStore/set_storage): Gas cost depends heavily on whether a slot is being initialized or updated.
11.6.2 Allocation Pricing: Zero vs Non-Zero Write Differential
When modifying storage via StateHost::set_storage, the KVM interpreter inspects the previous value existing in that slot to compute the exact gas charge:
- Allocating a Fresh Slot ($0 \to \text{non-zero}$): Modifying a storage key whose current value is zero ($0x0$) requires allocating a new storage record in the global trie state. This incurs the highest gas cost tier.
- Modifying an Existing Slot ($\text{non-zero} \to \text{non-zero}$): Overwriting a slot that already contains data updates an existing key. This incurs a lower gas charge than fresh allocation.
- Clearing a Slot ($\text{non-zero} \to 0$): Resetting a stored variable back to zero frees up trie space and earns a gas refund context according to protocol parameters.
// C++ Source Context: kvm/state_host.cpp // Returning the previous slot value enables precise gas calculation: KORTANA_TRY_ASSIGN(const uint256_t previous, world_.get_storage(address, key));
11.7 Comprehensive Example: Stored Asset Vault Contract
Below is a complete Quorlin smart contract demonstrating state variable declarations, records, storage mappings, mutability safety (reads vs writes), and contextual reads (caller).
contract AssetVault { // --- State Variables --- address publicOwner; truth isVaultActive; number totalAssetsDeposited; record DepositRecord { number amount; number timestamp; truth verified; } map<address, number> balances; map<address, DepositRecord> lastDepositInfo; event Deposit(address indexed sender, number amount); event Withdraw(address indexed recipient, number amount); constructor { publicOwner = caller; isVaultActive = yes; totalAssetsDeposited = 0; } // Read-only method accessing state reads number getBalanceOf(address user) { return balances[user]; } // Read-only method inspecting complex record state reads truth checkDepositStatus(address user) { DepositRecord rec = lastDepositInfo[user]; return rec.verified; } // State-modifying function altering multiple storage slots writes truth depositAsset(number depositValue) { require isVaultActive == yes, "Vault is currently paused"; require depositValue > 0, "Deposit must be greater than zero"; // Update basic primitive mapping number currentBalance = balances[caller]; balances[caller] = currentBalance + depositValue; // Update global scalar storage state totalAssetsDeposited = totalAssetsDeposited + depositValue; // Construct and store record structure DepositRecord info; info.amount = depositValue; info.timestamp = 0; // Populated from block contextual state info.verified = yes; lastDepositInfo[caller] = info; emit Deposit(caller, depositValue); return yes; } // Administrative function modifying contract control state writes truth setVaultStatus(truth activeState) { require caller == publicOwner, "Only vault owner can perform this action"; isVaultActive = activeState; return yes; } }
11.8 Summary Checklist for Developers
- State Lifetime: Variables declared at the top level of a
contractpersist indefinitely in the Kortana Unified State Trie across block executions. - Type Mapping:
number$\rightarrow$ Unsigned 256-bit word (uint256).truth$\rightarrow$ Boolean (yes/no).address$\rightarrow$ 20-byte address (left-padded to 32 bytes in storage words).text$\rightarrow$ Bound character strings.
- Storage Collections: Use
map<Key, Value>for lookup structures andrecordfor logically grouped state objects. - Mutability Enforcement:
- Mark view methods with
readsto enforce compile-time read-only safety. - Mark state-modifying methods with
writes.
- Mark view methods with
- Gas Optimization: Be conscious of storage slot allocations. Writing data to a zero-valued slot consumes significantly more gas than updating existing values.