32. DeFi Patterns in Quorlin
Decentralized Finance (DeFi) architectures on the Kortana blockchain rely on safe, legible, and gas-efficient state transitions. Quorlin is designed to make smart contract logic look like familiar object-oriented code while enforcing strict type rules and explicit state mutability at compile time.
In this chapter, we explore how to implement core DeFi primitives in Quorlin—ranging from custom fungible tokens and automated market makers (AMMs) to yield staking vaults and time-locked escrows.
32.1 Standard Fungible Tokens (IERC20 Pattern)
At the heart of DeFi is the token contract. Quorlin maps natively to the EVM ABI standard while using clean human-readable keywords. The standard number type represents a 256-bit unsigned integer (uint256), and truth represents a boolean (bool) with standard literals yes and no.
When creating a fungible token in Quorlin:
- State variables are stored at the top level of the
contract. - Mappings are declared using
map<KeyType, ValueType>. - Functions explicitly state their state mutability using
reads(view/pure) orwrites(state modifying). - System context such as the message sender is accessed via the
callerkeyword.
Implementation: Complete Token Contract
contract StandardToken { number totalSupply; 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 { totalSupply = 1000000000000000000000000; balances[caller] = totalSupply; emit Transfer(address(0), caller, totalSupply); } reads number totalTokens() { return totalSupply; } reads number balanceOf(address account) { return balances[account]; } reads number allowance(address owner, address spender) { return allowances[owner][spender]; } writes truth approve(address spender, number amount) { allowances[caller][spender] = amount; emit Approval(caller, spender, amount); return yes; } writes truth transfer(address recipient, number amount) { number senderBalance = balances[caller]; require senderBalance >= amount, "transfer amount exceeds balance"; balances[caller] = senderBalance - amount; balances[recipient] = balances[recipient] + amount; emit Transfer(caller, recipient, amount); return yes; } writes truth transferFrom(address sender, address recipient, number amount) { number currentAllowance = allowances[sender][caller]; require currentAllowance >= amount, "transfer amount exceeds allowance"; number senderBalance = balances[sender]; require senderBalance >= amount, "transfer amount exceeds balance"; allowances[sender][caller] = currentAllowance - amount; balances[sender] = senderBalance - amount; balances[recipient] = balances[recipient] + amount; emit Transfer(sender, recipient, amount); return yes; } }
32.2 Constant-Product Automated Market Maker (AMM)
An Automated Market Maker allows decentralized token exchanges based on a mathematical formula—most commonly the constant product formula ($x \cdot y = k$).
In Quorlin, we can define an interface to interact with external tokens (IERC20) and implement swap, deposit, and withdrawal logic in a liquidity pool contract.
Defining External Interfaces
Quorlin allows interface definitions via the interface keyword:
interface IERC20 { reads number balanceOf(address account); writes truth transfer(address recipient, number amount); writes truth transferFrom(address sender, address recipient, number amount); }
Liquidity Pool Implementation
The liquidity pool contract holds two assets (token0 and token1) and maintains liquidity provider (LP) shares:
contract SimpleAMM { address public token0; address public token1; number public reserve0; number public reserve1; number public totalLiquidity; map<address, number> public liquidity; event Mint(address indexed provider, number amount0, number amount1); event Burn(address indexed provider, number amount0, number amount1); event Swap(address indexed sender, number amountIn, number amountOut, address tokenIn); constructor(address _token0, address _token1) { token0 = _token0; token1 = _token1; } writes truth addLiquidity(number amount0, number amount1) { require amount0 > 0, "amount0 must be positive"; require amount1 > 0, "amount1 must be positive"; IERC20(token0).transferFrom(caller, address(this), amount0); IERC20(token1).transferFrom(caller, address(this), amount1); number mintedShares = 0; if (totalLiquidity == 0) { mintedShares = amount0 + amount1; } else { number share0 = (amount0 * totalLiquidity) / reserve0; number share1 = (amount1 * totalLiquidity) / reserve1; if (share0 < share1) { mintedShares = share0; } else { mintedShares = share1; } } require mintedShares > 0, "insufficient liquidity minted"; liquidity[caller] = liquidity[caller] + mintedShares; totalLiquidity = totalLiquidity + mintedShares; reserve0 = reserve0 + amount0; reserve1 = reserve1 + amount1; emit Mint(caller, amount0, amount1); return yes; } writes number swap(address tokenIn, number amountIn) { require tokenIn == token0 || tokenIn == token1, "invalid token"; require amountIn > 0, "amountIn must be positive"; truth isToken0 = (tokenIn == token0); number rIn = isToken0 ? reserve0 : reserve1; number rOut = isToken0 ? reserve1 : reserve0; IERC20(tokenIn).transferFrom(caller, address(this), amountIn); // Applying a 0.3% fee: multiply by 997 / 1000 number amountInWithFee = amountIn * 997; number numerator = amountInWithFee * rOut; number denominator = (rIn * 1000) + amountInWithFee; number amountOut = numerator / denominator; require amountOut > 0, "insufficient output amount"; if (isToken0) { reserve0 = reserve0 + amountIn; reserve1 = reserve1 - amountOut; IERC20(token1).transfer(caller, amountOut); } else { reserve1 = reserve1 + amountIn; reserve0 = reserve0 - amountOut; IERC20(token0).transfer(caller, amountOut); } emit Swap(caller, amountIn, amountOut, tokenIn); return amountOut; } }
32.3 Token Staking & Yield Rewards Vault
Staking protocols allow users to lock up digital assets to earn rewards over time. A common pattern in Quorlin is utilizing custom structs (record types) to cleanly bundle per-user staking metadata.
Utilizing Records for Staker State
record StakerInfo { number amount; number rewardDebt; } contract YieldVault { address public stakingToken; address public rewardToken; number public rewardRate; map<address, StakerInfo> public stakers; event Staked(address indexed user, number amount); event Withdrawn(address indexed user, number amount); event RewardPaid(address indexed user, number reward); constructor(address _stakingToken, address _rewardToken, number _rewardRate) { stakingToken = _stakingToken; rewardToken = _rewardToken; rewardRate = _rewardRate; } reads number getStakedBalance(address user) { return stakers[user].amount; } writes truth stake(number amount) { require amount > 0, "cannot stake zero"; StakerInfo userState = stakers[caller]; // Transfer funds from staker to contract IERC20(stakingToken).transferFrom(caller, address(this), amount); userState.amount = userState.amount + amount; stakers[caller] = userState; emit Staked(caller, amount); return yes; } writes truth withdraw(number amount) { StakerInfo userState = stakers[caller]; require userState.amount >= amount, "withdraw amount exceeds staked balance"; userState.amount = userState.amount - amount; stakers[caller] = userState; IERC20(stakingToken).transfer(caller, amount); emit Withdrawn(caller, amount); return yes; } }
32.4 Time-Locked Escrow Vaults
Time-locked escrows lock assets until a specific block timestamp or height condition is satisfied. They are critical for token vesting schedules, loan collateral releases, and governance locks.
contract TimeLockEscrow { address public beneficiary; address public depositToken; number public releaseTime; number public totalAmount; truth public isClaimed; event Deposited(address indexed sender, number amount, number releaseTime); event Claimed(address indexed beneficiary, number amount); constructor(address _beneficiary, address _depositToken, number _lockDuration) { beneficiary = _beneficiary; depositToken = _depositToken; releaseTime = block.timestamp + _lockDuration; isClaimed = no; } writes truth deposit(number amount) { require totalAmount == 0, "already funded"; require amount > 0, "deposit must be positive"; totalAmount = amount; IERC20(depositToken).transferFrom(caller, address(this), amount); emit Deposited(caller, amount, releaseTime); return yes; } writes truth claim() { require caller == beneficiary, "only beneficiary can claim"; require block.timestamp >= releaseTime, "escrow is still locked"; require !isClaimed, "already claimed"; require totalAmount > 0, "no funds to claim"; isClaimed = yes; IERC20(depositToken).transfer(beneficiary, totalAmount); emit Claimed(beneficiary, totalAmount); return yes; } }
32.5 Security, Gas, and EVM Interoperability
When designing complex DeFi protocols in Quorlin, developers must remain aware of how the Quorlin toolchain translates code into bytecode executed by the Kortana Virtual Machine (KVM).
1. Checks-Effects-Interactions Pattern
Always update internal storage state before invoking external contract calls (such as IERC20.transfer). External calls temporarily hand over execution control to third-party code. Reordering state updates prevents reentrancy vulnerability vectors.
// GOOD: Update state variables before transferring external assets balances[caller] = balances[caller] - amount; IERC20(token).transfer(caller, amount);
2. Standard ABI Mapping and Ethereum Toolchain Compatibility
Although Quorlin code uses clear textual types (number, truth, text, nothing), the compiler converts these directly to EVM-compatible ABI types when emitting selectors:
| Quorlin Keyword | Internal Representation | Target ABI Type | KVM Storage Format |
|---|---|---|---|
number | Type::U256 | uint256 | 32-byte Word |
truth | Type::Bool | bool | 1-byte / 32-byte Word |
address | Type::Address | address | 20-byte padded address |
text | Type::Text | string | Byte Array |
nothing | Type::Void | void | N/A |
Because function signatures are converted to standard Ethereum selector hashes (e.g., transfer(address,uint256)), Quorlin contracts can seamlessly interoperate with Solidity, Vyper, and existing Web3 libraries (ethers.js, web3.js) deployed on Kortana networks.
3. KVM Unified State Trie & Gas Optimizations
The KVM manages contract storage slots through a unified state trie (StateHost::get_storage and StateHost::set_storage). Writes to uninitialized storage slots carry higher gas overhead than overwriting warm slots. Storing protocol configurations in unified record structures keeps related state variables aligned and optimizes bytecode generation during compilation.