toll Qlorix
arrow_back Back to Blog
Tutorial May 24, 2026 · 10 min read

Building Your First Smart Contract on Qlorix with Photon

If you have written Solidity, you already understand about 60% of what Photon does. The remaining 40% - linear types, EffectManifest declarations, and quantum-safe cryptographic primitives baked into the standard library - is where the interesting differences live. This tutorial walks you from a blank editor to a deployed token contract on the Qlorix testnet, explaining the design choices along the way.

Why Not Just Use Solidity?

The honest answer is that Solidity was designed for the EVM, and the EVM was designed in 2014 with assumptions that no longer hold. Reentrancy vulnerabilities, unchecked arithmetic, implicit integer coercions, and the absence of ownership semantics in the type system have collectively cost the industry billions of dollars across hundreds of exploits. Solidity has improved - SafeMath is now default, Solidity 0.8 added overflow checks - but each fix is a patch on an architecture that was not built to prevent the problem in the first place.

Photon takes a different starting point. It draws on ideas from Rust (ownership and move semantics), Haskell (algebraic types, explicit effects), and the academic linear type literature to create a language where whole classes of bugs are prevented at compile time rather than caught by auditors at review time. It also integrates with the Qlorix runtime's CRYSTALS-Dilithium3 signature verification and Kyber-768 key derivation primitives as first-class standard library functions, rather than requiring you to import an unaudited third-party cryptography library.

Photon compiles to Qlorix bytecode, not EVM bytecode. If you are looking for EVM compatibility, the Qlorix network also supports a compatibility layer for Solidity contracts - but for anything you are writing fresh, Photon is the right choice.

Setting Up the Development Environment

The Photon toolchain ships as a single binary called photon that bundles the compiler, a local test node, a deployment CLI, and an interactive REPL. Install it with the Qlorix installer script:

# macOS / Linux curl -sSL https://get.qlorix.com/photon | sh # Verify the installation photon --version # photon 0.9.4 (qlorix-testnet-2)

The toolchain requires no additional dependencies on macOS or modern Linux distributions. On Windows, use WSL2 - the native Windows build is in progress but not yet stable for testnet work.

Create a new project with photon init:

photon init my-token cd my-token ls # photon.toml src/ tests/ .gitignore

The photon.toml manifest specifies the contract name, the target network, and dependencies. Open it to confirm the testnet endpoint is set correctly:

[package] name = "my-token" version = "0.1.0" edition = "2026" [network] rpc = "https://testnet-rpc.qlorix.com" chain = "qlorix-testnet-2" [dependencies] qlorix-std = "0.9"

Photon Concepts You Need Before Writing Code

Linear Types and Move Semantics

In Solidity, token balances are just integers in a mapping. You can copy them, accidentally double-count them, or forget to subtract from one side of a transfer. The ERC-20 standard works around this through convention and auditing, but the language itself provides no guarantee that value is conserved.

Photon's type system enforces conservation directly. A value of a linear type can be used exactly once - it must be either consumed (transferred, burned) or explicitly stored, never silently dropped. The compiler rejects code that loses track of a linear value. For token balances, this means an entire category of double-spend and accounting bugs become compile-time errors rather than runtime vulnerabilities.

EffectManifest

Every Photon contract function carries an EffectManifest annotation that declares which side effects the function is permitted to perform. The compiler enforces that the function's body matches its manifest. A function annotated @pure cannot read storage. A function annotated @reads(balances) can read the balances storage slot but cannot write to it or emit events. A function annotated @mutates(balances) @emits(Transfer) can do both of those things but nothing else.

This is the Photon equivalent of Solidity's view and pure modifiers, but far more granular and enforced at the type level rather than as an optimizer hint. It means that when you read a Photon contract, every function's signature tells you exactly what it can touch. You do not need to read the entire body to understand the blast radius of a given call.

Quantum-Safe Primitives

The qlorix-std library exposes the network's CRYSTALS-Dilithium3 signature verification as a built-in function: Crypto.verifyDilithium(pubkey, message, signature). This is useful when your contract needs to verify off-chain signatures - for example, a permit-style approval flow where the user signs a message off-chain and submits the signature in a transaction. Because Qlorix uses Dilithium3 natively, you do not need to implement or import signature verification logic yourself.

Writing the Token Contract

Open src/main.photon and replace its contents with the following. Read through the code first; the explanation follows.

use qlorix_std::{Address, U256, Storage, Event, Crypto}; // Storage schema - declared separately from logic storage { balances: Map<Address, U256>, allowances: Map<Address, Map<Address, U256>>, total_supply: U256, name: Str, symbol: Str, decimals: U8, } // Events event Transfer(from: Address, to: Address, amount: U256); event Approval(owner: Address, spender: Address, amount: U256); // Contract entry point contract MyToken { // Constructor - runs once at deploy time @mutates(total_supply, balances, name, symbol, decimals) @emits(Transfer) fn init( token_name: Str, token_symbol: Str, initial_supply: U256, deployer: Address, ) { name = token_name; symbol = token_symbol; decimals = 18u8; total_supply = initial_supply; balances[deployer] = initial_supply; emit Transfer(Address::zero(), deployer, initial_supply); } // Read total supply - no storage writes, no events @reads(total_supply) fn get_total_supply() -> U256 { total_supply } // Read a balance @reads(balances) fn balance_of(account: Address) -> U256 { balances[account].unwrap_or(U256::zero()) } // Transfer tokens - linear type enforces no double-spend @mutates(balances) @emits(Transfer) fn transfer( from: Address, to: Address, amount: U256, ) -> Result<(), TransferError> { let sender_balance = balances[from].unwrap_or(U256::zero()); if sender_balance < amount { return Err(TransferError::InsufficientBalance); } // Linear: deduct must happen before credit - compiler rejects any path // where the U256 value is counted twice. balances[from] = sender_balance - amount; balances[to] = balances[to].unwrap_or(U256::zero()) + amount; emit Transfer(from, to, amount); Ok(()) } // Approve a spender @mutates(allowances) @emits(Approval) fn approve(owner: Address, spender: Address, amount: U256) { allowances[owner][spender] = amount; emit Approval(owner, spender, amount); } // Transfer on behalf of another address @mutates(balances, allowances) @emits(Transfer) fn transfer_from( spender: Address, from: Address, to: Address, amount: U256, ) -> Result<(), TransferError> { let allowed = allowances[from][spender].unwrap_or(U256::zero()); if allowed < amount { return Err(TransferError::AllowanceExceeded); } allowances[from][spender] = allowed - amount; transfer(from, to, amount) } } enum TransferError { InsufficientBalance, AllowanceExceeded, }

A few things to notice compared with a Solidity ERC-20. The storage declaration is separate from the contract body, making the full state surface explicit at a glance. Every function begins with its EffectManifest annotations - @reads, @mutates, @emits - so any reader knows immediately what the function can touch without scanning its body. The transfer function returns a typed Result rather than reverting with a string; callers can pattern-match on the error variant instead of catching a raw revert. And there is no unchecked block in sight - arithmetic overflow on U256 panics by default in Photon, so you never accidentally wrap around.

Compiling and Running Tests

Compile the contract with:

photon build # Compiling my-token v0.1.0 # Checking effect manifests... ok # Checking linearity constraints... ok # Bytecode: target/my-token.qbc (2.1 KB) # ABI: target/my-token.abi.json

The compiler runs linearity and effect-manifest checks before producing bytecode. If you violate a manifest - say, you add an emit call inside a function not annotated with @emits - the error message points to the specific line and tells you which annotation to add. Write a quick test in tests/token_test.photon to verify the transfer logic before going to testnet:

use photon_test::{TestContext, assert_eq, assert_err}; use crate::MyToken; test transfer_moves_balance() { let ctx = TestContext::new(); let alice = ctx.address("alice"); let bob = ctx.address("bob"); MyToken::init("MyToken", "MTK", 1_000_000u256, alice); assert_eq!(MyToken::balance_of(alice), 1_000_000u256); assert_eq!(MyToken::balance_of(bob), 0u256); MyToken::transfer(alice, bob, 250_000u256).unwrap(); assert_eq!(MyToken::balance_of(alice), 750_000u256); assert_eq!(MyToken::balance_of(bob), 250_000u256); } test transfer_rejects_insufficient_balance() { let ctx = TestContext::new(); let alice = ctx.address("alice"); let bob = ctx.address("bob"); MyToken::init("MyToken", "MTK", 100u256, alice); assert_err!(MyToken::transfer(alice, bob, 500u256)); }
photon test # Running 2 tests in tests/token_test.photon # test transfer_moves_balance ... ok # test transfer_rejects_insufficient_balance ... ok # test result: ok. 2 passed; 0 failed

Deploying to Qlorix Testnet

First, generate a testnet keypair and fund it from the faucet:

# Generate a Dilithium3 keypair for deployment photon wallet new --name deployer # Address: qlx1testnet...abc (copy this) # Fund from testnet faucet curl -X POST https://faucet.testnet.qlorix.com/fund \ -d '{"address":"qlx1testnet...abc","amount":"10"}'

Note that the wallet address is derived from a Dilithium3 public key - this is a native Qlorix address, not an ECDSA address. The key material is stored in ~/.photon/wallets/deployer.key and is encrypted with your system keychain by default.

Deploy the contract by passing the constructor arguments directly to photon deploy:

photon deploy \ --wallet deployer \ --constructor-args \ name="MyToken" \ symbol="MTK" \ initial_supply=1000000000000000000000000 \ deployer=qlx1testnet...abc # Deploying my-token to qlorix-testnet-2... # Transaction: 0x4f8a...c291 # Contract address: qlx1contract...xyz # Gas used: 48,320 # Confirmed in block 1,847,203

Calling the Contract

Use photon call to read state and photon send to submit mutating transactions:

# Read total supply (no wallet needed - read-only) photon call qlx1contract...xyz get_total_supply # 1000000000000000000000000 # Transfer 1 MTK to another address photon send \ --wallet deployer \ --contract qlx1contract...xyz \ --fn transfer \ --args \ from=qlx1testnet...abc \ to=qlx1testnet...def \ amount=1000000000000000000 # Transaction: 0x9b3c...f042 # Result: Ok(()) # Gas used: 14,880

The ABI generated at compile time (target/my-token.abi.json) is the same format consumed by the Qlorix TypeScript and Rust SDKs, so wiring up a frontend or a backend service that calls your contract follows the exact same pattern. The SDK handles Dilithium3 transaction signing automatically using the wallet you configure - from a frontend developer's perspective, calling a Photon contract feels nearly identical to calling a Solidity contract via ethers.js.

What to Explore Next

This tutorial has covered the fundamentals - project setup, language concepts, a complete contract, testing, and deployment. The Photon standard library has considerably more to offer:

  • Crypto.verifyDilithium for verifying off-chain user signatures inside contracts (permit flows, meta-transactions)
  • ZkProof primitives for integrating with the Qlorix Groth16 prover to build privacy-preserving contracts
  • CrossContract for type-safe inter-contract calls with effect propagation across the call boundary
  • photon fmt and photon audit for formatting and automated vulnerability scanning before mainnet deployment
  • The @upgradeable contract modifier for proxy patterns that preserve state across bytecode upgrades

The full language reference and standard library documentation live at qlorix.com/docs. The Qlorix Grant Program is actively funding teams building DeFi protocols, tooling, and infrastructure in Photon - if you are building something interesting, the application is at qlorix.com/grants.

Read the Full Photon Language Reference

The developer docs cover linear types, effect manifests, the standard library, and advanced contract patterns in depth.

Open the Docs arrow_forward