Templates

Every template ships as a complete, compiling contract rather than a snippet. The Quorlin ones are written against the language specification and are checked against the grammar in CI, so none of them teaches syntax the compiler would reject.

Counter

QUORLIN · KVM

The smallest contract that shows reads against writes.

The smallest contract that shows the difference between reads and writes: only increment produces a transaction hash.

contracts/Counter.ql
// Counter — the smallest contract that shows `reads` against `writes`.
//
// Only `increment` is a `writes`, so only `increment` produces a transaction
// hash. `value` compiles to a static call, which the VM refuses to let write.

contract Counter {

    number count;
    address owner;

    event Incremented(address indexed by, number newValue);

    constructor {
        owner = caller;
        count = 0;
    }

    reads number value() {
        return count;
    }

    writes number increment() {
        // Checked arithmetic: this stops the call on overflow rather than wrapping.
        count = count + 1;
        emit Incremented(caller, count);
        return count;
    }

    writes reset() {
        require caller == owner, "only the owner";
        count = 0;
    }
}

Fungible Token

QUORLIN · KVM

A complete EIP-20 token. The allowance is spent, not merely checked.

The allowance is spent rather than merely checked, and transfers to the zero address are refused — a burn the contract never accounted for is the most common way tokens are lost.

contracts/Token.ql
// A complete EIP-20 fungible token.
//
// The allowance is spent, not merely checked — an allowance that is verified
// and left in place can be replayed for the whole approved amount.

contract Token {

    text tokenName;
    text tokenSymbol;
    number totalTokens;
    address owner;

    map<address, number> balances;
    map<address, map<address, number>> allowances;

    event Transfer(address indexed from, address indexed to, number amount);
    event Approval(address indexed owner, address indexed spender, number amount);

    constructor {
        owner = caller;
        tokenName = "Kortana Example";
        tokenSymbol = "KEX";
        totalTokens = 1000000;
        balances[caller] = 1000000;
        emit Transfer(nobody, caller, 1000000);
    }

    reads text name() {
        return tokenName;
    }

    reads text symbol() {
        return tokenSymbol;
    }

    reads number decimals() {
        return 18;
    }

    reads number totalSupply() {
        return totalTokens;
    }

    reads number balanceOf(address account) {
        return balances[account];
    }

    reads number allowance(address holder, address spender) {
        return allowances[holder][spender];
    }

    writes truth transfer(address recipient, number amount) {
        // A transfer to the zero address is indistinguishable from a burn the
        // contract never accounted for. Refuse it.
        require recipient != nobody, "transfer to the zero address";

        number held = balances[caller];
        require held >= amount, "transfer amount exceeds balance";

        balances[caller] = held - amount;
        balances[recipient] = balances[recipient] + amount;

        emit Transfer(caller, recipient, amount);
        return yes;
    }

    writes truth approve(address spender, number amount) {
        require spender != nobody, "approve to the zero address";

        allowances[caller][spender] = amount;
        emit Approval(caller, spender, amount);
        return yes;
    }

    writes truth transferFrom(address holder, address recipient, number amount) {
        require recipient != nobody, "transfer to the zero address";

        number allowed = allowances[holder][caller];
        require allowed >= amount, "insufficient allowance";

        number held = balances[holder];
        require held >= amount, "transfer amount exceeds balance";

        // Spend the allowance before moving the balance.
        allowances[holder][caller] = allowed - amount;
        balances[holder] = held - amount;
        balances[recipient] = balances[recipient] + amount;

        emit Transfer(holder, recipient, amount);
        return yes;
    }
}

Escrow

QUORLIN · KVM

A record-based agreement between a buyer and a seller, with an arbiter.

State transitions are guarded on the current state, so a released agreement cannot be refunded and vice versa.

contracts/Escrow.ql
// Escrow — a record-based agreement between a buyer and a seller.
//
// Records are one level and hold scalars only, which keeps the storage layout
// readable straight off the source.

record Agreement {
    address buyer;
    address seller;
    number amount;
    number state;
}

contract Escrow {

    // 0 none · 1 funded · 2 released · 3 refunded
    map<number, Agreement> agreements;
    number nextId;
    address arbiter;

    event Opened(number indexed id, address indexed buyer, address indexed seller, number amount);
    event Released(number indexed id, address indexed seller, number amount);
    event Refunded(number indexed id, address indexed buyer, number amount);

    constructor {
        arbiter = caller;
        nextId = 1;
    }

    writes number open(address seller, number amount) {
        require seller != nobody, "seller is the zero address";
        require seller != caller, "buyer and seller are the same account";
        require amount > 0, "amount must be positive";

        number id = nextId;
        agreements[id].buyer = caller;
        agreements[id].seller = seller;
        agreements[id].amount = amount;
        agreements[id].state = 1;
        nextId = id + 1;

        emit Opened(id, caller, seller, amount);
        return id;
    }

    writes release(number id) {
        require agreements[id].state == 1, "agreement is not funded";
        // Either the buyer or the arbiter may release. Quorlin spells logic
        // as words -- and, or, not -- and they short-circuit.
        require caller == agreements[id].buyer or caller == arbiter, "not authorised to release";

        agreements[id].state = 2;
        emit Released(id, agreements[id].seller, agreements[id].amount);
    }

    writes refund(number id) {
        require agreements[id].state == 1, "agreement is not funded";
        require caller == agreements[id].seller or caller == arbiter, "not authorised to refund";

        agreements[id].state = 3;
        emit Refunded(id, agreements[id].buyer, agreements[id].amount);
    }

    reads number stateOf(number id) {
        return agreements[id].state;
    }

    reads Agreement agreementOf(number id) {
        // A record may be returned, but not held in a local.
        return agreements[id];
    }
}

Access Control

QUORLIN · KVM

Two-step ownership transfer and a pause switch.

Ownership moves in two steps. A single-step transfer to a mistyped address strands the contract permanently; requiring the nominee to accept proves the key exists.

contracts/Ownable.ql
// Two-step ownership transfer.
//
// A single-step transfer to a mistyped address strands the contract for good.
// The nominee must accept, which proves the key exists and is controlled.

contract Ownable {

    address owner;
    address pendingOwner;
    truth paused;

    event OwnershipOffered(address indexed from, address indexed to);
    event OwnershipAccepted(address indexed newOwner);
    event PausedChanged(truth isPaused);

    constructor {
        owner = caller;
        paused = no;
    }

    reads address currentOwner() {
        return owner;
    }

    reads truth isPaused() {
        return paused;
    }

    writes offerOwnership(address nominee) {
        // Use caller, not sender -- a check against sender can be passed by
        // tricking a user into calling through a contract you control.
        require caller == owner, "only the owner";
        require nominee != nobody, "nominee is the zero address";

        pendingOwner = nominee;
        emit OwnershipOffered(owner, nominee);
    }

    writes acceptOwnership() {
        require caller == pendingOwner, "only the nominee";

        owner = pendingOwner;
        pendingOwner = nobody;
        emit OwnershipAccepted(owner);
    }

    writes setPaused(truth value) {
        require caller == owner, "only the owner";

        paused = value;
        emit PausedChanged(value);
    }
}

Protocol Reader

QUORLIN · KVM

Reads Kortana's staking, chain and governance system contracts.

Reads the staking, chain and governance system contracts. A contract cannot stake, delegate or vote — that bound is deliberate, because staking state lives outside the world-state trie and a reverted call could not undo a stake move.

contracts/ValidatorGated.ql
// Reading Kortana's own protocols from a contract.
//
// Staking, governance and dPOH are protocol-level rather than contracts. Three
// system contracts at reserved addresses expose them as reads. A contract
// cannot stake, delegate or vote — staking state lives outside the world-state
// trie, so a write reachable from a contract could not be rolled back by a
// revert, and two nodes would disagree about who is a validator.

contract ValidatorGated {

    map<address, number> deposits;
    number totalDeposited;

    event Deposited(address indexed who, number amount);

    constructor {
        totalDeposited = 0;
    }

    writes deposit(number amount) {
        require amount > 0, "amount must be positive";
        require IKortanaStaking(kortanaStaking).isActiveValidator(caller), "validators only";

        deposits[caller] = deposits[caller] + amount;
        totalDeposited = totalDeposited + amount;
        emit Deposited(caller, amount);
    }

    reads number weightOf(address validator) {
        return IKortanaStaking(kortanaStaking).totalStakeOf(validator);
    }

    reads number networkStake() {
        return IKortanaStaking(kortanaStaking).totalActiveStake();
    }

    reads number currentEpoch() {
        return IKortanaChain(kortanaChain).epoch();
    }

    reads number minimumStake() {
        return IKortanaChain(kortanaChain).minValidatorStake();
    }

    reads truth proposalOpen(number proposalId) {
        return IKortanaGovernance(kortanaGovernance).isVotingOpen(proposalId);
    }
}

ERC-20 Token

SOLIDITY · KEVM

A self-contained ERC-20 with custom errors. Compiles with no imports.

Self-contained with no imports, so it compiles in the browser without a package manager. Uses custom errors and spends the allowance before moving the balance.

contracts/Token.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title A minimal, self-contained ERC-20 for Kortana's KEVM.
/// @notice No external imports, so it compiles in the browser without a
///         package manager. For production, prefer a reviewed library.
contract Token {
    string public name = "Kortana Example";
    string public symbol = "KEX";
    uint8 public constant decimals = 18;

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    error InsufficientBalance(uint256 available, uint256 required);
    error InsufficientAllowance(uint256 available, uint256 required);
    error ZeroAddress();

    constructor(uint256 initialSupply) {
        totalSupply = initialSupply;
        balanceOf[msg.sender] = initialSupply;
        emit Transfer(address(0), msg.sender, initialSupply);
    }

    function transfer(address to, uint256 value) external returns (bool) {
        if (to == address(0)) revert ZeroAddress();
        uint256 held = balanceOf[msg.sender];
        if (held < value) revert InsufficientBalance(held, value);

        unchecked {
            balanceOf[msg.sender] = held - value;
        }
        balanceOf[to] += value;

        emit Transfer(msg.sender, to, value);
        return true;
    }

    function approve(address spender, uint256 value) external returns (bool) {
        if (spender == address(0)) revert ZeroAddress();
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
        return true;
    }

    function transferFrom(address from, address to, uint256 value) external returns (bool) {
        if (to == address(0)) revert ZeroAddress();

        uint256 allowed = allowance[from][msg.sender];
        if (allowed < value) revert InsufficientAllowance(allowed, value);

        uint256 held = balanceOf[from];
        if (held < value) revert InsufficientBalance(held, value);

        // Spend the allowance before moving the balance.
        unchecked {
            allowance[from][msg.sender] = allowed - value;
            balanceOf[from] = held - value;
        }
        balanceOf[to] += value;

        emit Transfer(from, to, value);
        return true;
    }
}

Simple Storage

SOLIDITY · KEVM

A value, a setter and an event — the shortest path to a receipt.

A value, a setter and an event — the shortest path from source to a receipt.

contracts/SimpleStorage.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title The smallest useful contract — a value, a setter, and an event.
contract SimpleStorage {
    uint256 private _value;
    address public immutable owner;

    event ValueChanged(address indexed by, uint256 previous, uint256 current);

    constructor() {
        owner = msg.sender;
    }

    function get() external view returns (uint256) {
        return _value;
    }

    function set(uint256 newValue) external {
        uint256 previous = _value;
        _value = newValue;
        emit ValueChanged(msg.sender, previous, newValue);
    }
}

Every template is available when you create a project. Open Studio →