Clean code isn't just aesthetically important—it's critical for security, auditability, and maintainability. When smart contracts handle real value, readability becomes a security feature.
In a recent post proposing to replace the EVM (again!) with RISC-V, Vitalik Buterin compared smart contract implementations across platforms. But he would keep Solidity and Vyper, even after throwing away the EVM—he compared the readability of smart contracts written in Solana, Nervos CKB, Solidity, and Vyper to show that smart contracts written in Rust—a general-purpose language—are significantly less readable than their Solidity and Vyper counterparts.
He's right. Here are the examples he uses, which implement a basic Unisocks-style NFT bonding curve contract:
use anchor_lang::prelude::*;
declare_id!("SocksSale111111111111111111111111111111111111");
#[program]
pub mod unisocks_sale {
use super::*;
pub fn initialize(ctx: Context<Initialize>, m: u64) -> Result<()> {
let state = &mut ctx.accounts.state;
state.sold = 0;
state.m = m;
Ok(())
}
pub fn buy_sock(ctx: Context<BuySock>) -> Result<()> {
let state = &mut ctx.accounts.state;
require!(state.sold < 500, SocksError::SoldOut);
let n = state.sold;
let m = state.m;
let price = calculate_price(m, n)?;
let payer = &mut ctx.accounts.buyer;
let treasury = &mut ctx.accounts.treasury;
// Ensure payment is correct
require!(payer.lamports() >= price, SocksError::InsufficientPayment);
// Transfer lamports to treasury
**payer.try_borrow_mut_lamports()? -= price;
**treasury.try_borrow_mut_lamports()? += price;
// Increment sold count
state.sold += 1;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = payer, space = 8 + 8 + 8)]
pub state: Account<'info, SaleState>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct BuySock<'info> {
#[account(mut)]
pub state: Account<'info, SaleState>,
#[account(mut)]
pub buyer: Signer<'info>,
/// CHECK: safe because this is a system account for treasury
#[account(mut)]
pub treasury: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct SaleState {
pub sold: u64, // number of socks sold (n)
pub m: u64, // bonding curve parameter M
}
#[error_code]
pub enum SocksError {
#[msg("All 500 socks have been sold.")]
SoldOut,
#[msg("Not enough payment for the current sock price.")]
InsufficientPayment,
}
fn calculate_price(m: u64, n: u64) -> Result<u64> {
require!(n < 500, SocksError::SoldOut);
let a = m.checked_div(500 - n).ok_or(SocksError::SoldOut)?;
let b = m.checked_div(501 - n).ok_or(SocksError::SoldOut)?;
let price = a.checked_sub(b).ok_or(SocksError::SoldOut)?;
Ok(price)
}
What's wrong here? Solana requires verbose account definitions with #[derive(Accounts)] structs, manual lamport manipulation ( try_borrow_mut_lamports()? ), and extensive boilerplate for what should be simple state management. The business logic is buried under infrastructure code.
// src/entry.rs
use ckb_std::{
ckb_constants::Source,
default_alloc,
entry,
high_level::{load_cell_capacity, load_cell_data, load_script},
error::SysError,
};
use core::result::Result;
entry!(entry);
default_alloc!();
const MAX_SOCKS: u64 = 500;
#[derive(Debug)]
pub enum Error {
InvalidCapacity,
TooManySold,
InvalidTransaction,
}
fn parse_u64(data: &[u8]) -> u64 {
let mut arr = [0u8; 8];
arr.copy_from_slice(&data[..8]);
u64::from_le_bytes(arr)
}
fn calc_price(m: u64, n: u64) -> u64 {
// price_n = M/(500-n) - M/(501-n)
// with integer math:
// price_n = M * (501 - n - (500 - n)) / ((500 - n)(501 - n)) = M / ((500 - n)(501 - n))
m / ((500 - n) * (501 - n))
}
pub fn entry() -> i8 {
match main() {
Ok(_) => 0,
Err(_) => -1,
}
}
fn main() -> Result<(), Error> {
// Assume the first input cell is the state cell storing: [M: u64][sold: u64]
let data = load_cell_data(0, Source::Input).map_err(|_| Error::InvalidTransaction)?;
let m = parse_u64(&data[0..8]);
let sold = parse_u64(&data[8..16]);
if sold >= MAX_SOCKS {
return Err(Error::TooManySold);
}
let expected_price = calc_price(m, sold);
// Assume the new cell (output 0) will store [M][sold+1]
let out_data = load_cell_data(0, Source::Output).map_err(|_| Error::InvalidTransaction)?;
let new_sold = parse_u64(&out_data[8..16]);
if new_sold != sold + 1 {
return Err(Error::InvalidTransaction);
}
// Check payment (simplified: assume cell 1 is the payment cell)
let input_capacity = load_cell_capacity(1, Source::Input).map_err(|_| Error::InvalidCapacity)?;
let output_capacity = load_cell_capacity(1, Source::Output).map_err(|_| Error::InvalidCapacity)?;
let paid = input_capacity - output_capacity;
if paid != expected_price {
return Err(Error::InvalidCapacity);
}
Ok(())
}
What's wrong here? Nervos CKB operates at the UTXO cell level, forcing manual byte parsing (parse_u64), low-level capacity checks, and cryptic cell manipulation. There's no clear distinction between business logic and blockchain primitives.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract UniSocks is ERC721Enumerable, Ownable {
uint256 public constant MAX_SUPPLY = 500;
uint256 public immutable M; // Pricing parameter
address payable public treasury;
constructor(uint256 _M, address payable _treasury) ERC721("UniSocks", "USOCK") {
require(_M > 0, "M must be positive");
require(_treasury != address(0), "Invalid treasury");
M = _M;
treasury = _treasury;
}
function currentPrice() public view returns (uint256) {
uint256 n = totalSupply(); // number of socks already sold
require(n < MAX_SUPPLY, "Sold out");
// price = M / (500 - n) - M / (501 - n)
// rewritten to avoid precision loss:
uint256 denom1 = MAX_SUPPLY - n;
uint256 denom2 = MAX_SUPPLY + 1 - n;
return (M * (denom2 - denom1)) / (denom1 * denom2);
}
function buy() external payable {
uint256 n = totalSupply();
require(n < MAX_SUPPLY, "All socks sold");
uint256 price = currentPrice();
require(msg.value >= price, "Insufficient payment");
// Mint sock
_safeMint(msg.sender, n);
// Forward funds to treasury
(bool sent, ) = treasury.call{value: price}("");
require(sent, "Transfer failed");
// Refund any overpayment
if (msg.value > price) {
payable(msg.sender).transfer(msg.value - price);
}
}
}
# @version ^0.3.10
from vyper.interfaces import ERC721
event SockPurchased:
buyer: indexed(address)
token_id: uint256
price_paid: uint256
MAX_SUPPLY: constant(uint256) = 500
owner: public(address)
m_parameter: public(uint256)
total_sold: public(uint256)
owners: HashMap[uint256, address]
@external
def __init__(_m: uint256):
self.owner = msg.sender
self.m_parameter = _m
self.total_sold = 0
@internal
@view
def _price(n: uint256) -> uint256:
"""
Calculate price of the n-th sock:
price = M / (500 - n) - M / (501 - n)
"""
M: uint256 = self.m_parameter
a: uint256 = 500 - n
b: uint256 = 501 - n
# Use fixed-point math with 10^18 precision
M_fixed: uint256 = M * 10**18
term1: uint256 = M_fixed / a
term2: uint256 = M_fixed / b
return term1 - term2
@external
@payable
def buy():
n: uint256 = self.total_sold
assert n < MAX_SUPPLY, "All socks sold"
price: uint256 = self._price(n)
assert msg.value >= price, "Insufficient payment"
token_id: uint256 = n
self.owners[token_id] = msg.sender
self.total_sold += 1
log SockPurchased(msg.sender, token_id, price)
# Refund excess
excess: uint256 = msg.value - price
if excess > 0:
send(msg.sender, excess)
@external
@view
def ownerOf(token_id: uint256) -> address:
assert token_id < self.total_sold, "Token does not exist"
return self.owners[token_id]
@external
@view
def currentPrice() -> uint256:
return self._price(self.total_sold)
Kontor is a metaprotocol that enables rich smart contract applications on Bitcoin. Sigil is Kontor's WebAssembly-based smart contract framework. Sigil is designed to support any language that compiles to WebAssembly, making it a natively polyglot platform for writing smart contracts in a way that makes it feel like writing regular software. Sigil initially supports the Rust programming language, but other languages that work with WebAssembly will be added over time.
One of the biggest differences between Sigil and other WASM-based smart contract frameworks (such as Alkanes, CosmWasm, Polygon, etc.) is that Sigil leverages the new WebAssembly Component Model. This allows contracts to link to the runtime with full type-safety, and it provides deep integration with conventional Rust tooling. It shifts errors to build-time, keeps determinism and gas behavior predictable, and reduces the trusted computing base. Sigil thus dramatically improves developer ergonomics while fully supporting a flexible and extensible ecosystem built on open standards. With Kontor, contracts aren't opaque blobs—they're typed, composable components with explicit interfaces.
use stdlib::*;
contract!(name = "unisocks");
const MAX_SUPPLY: u64 = 500;
#[derive(Clone, Default, StorageRoot)]
struct SocksStorage {
pub m: Integer,
pub sold: Integer,
pub owners: Map<Integer, Address>,
}
impl Guest for Unisocks {
fn init(ctx: &ProcContext, m: Integer) {
SocksStorage {
m,
..Default::default()
}
.init(ctx);
}
fn buy(ctx: &ProcContext) -> Result<(), Error> {
let model = ctx.model();
let sold = model.sold();
ensure_available(sold)?;
model.owners().set(sold, ctx.signer());
model.set_sold(sold.add(1.into())?);
Ok(())
}
fn current_price(ctx: &ViewContext) -> Result<Integer, Error> {
calculate_price(ctx.model().m(), ctx.model().sold())
}
fn owner_of(ctx: &ViewContext, token_id: Integer) -> Option<Address> {
ctx.model().owners().get(token_id)
}
fn total_sold(ctx: &ViewContext) -> Integer {
ctx.model().sold()
}
}
fn ensure_available(sold: Integer) -> Result<(), Error> {
if sold >= MAX_SUPPLY.into() {
Err(Error::Message("sold out".to_string()))
} else {
Ok(())
}
}
fn calculate_price(m: Integer, n: Integer) -> Result<Integer, Error> {
ensure_available(n)?;
let remaining = Integer::from(MAX_SUPPLY).sub(n)?;
let next_remaining = remaining.sub(1.into())?;
m.div(remaining)?.sub(m.div(next_remaining)?)
}
package root:component;
world root {
include kontor:built-in/built-in;
use kontor:built-in/context.{view-context, proc-context};
use kontor:built-in/error.{error};
use kontor:built-in/numbers.{integer};
use kontor:built-in/types.{address};
export init: func(ctx: borrow<proc-context>, m: integer);
export buy: func(ctx: borrow<proc-context>) -> result<_, error>;
export current-price: func(ctx: borrow<view-context>) -> result<integer, error>;
export owner-of: func(ctx: borrow<view-context>, token-id: integer) -> option<address>;
export total-sold: func(ctx: borrow<view-context>) -> integer;
}
The above example shows a key innovation: Sigil uses WebAssembly Interface Types (WIT) to define explicit contract interfaces in a language-agnostic type system. Every Sigil contract includes a WIT file that, along with the Component Model, enables:
The Problem: Solana requires manual account validation and lamport manipulation. Nervos CKB forces byte-level cell management.
Kontor's Solution: Storage is just a Rust struct with a #[derive(StorageRoot)] macro:
#[derive(Clone, Default, StorageRoot)]
struct SocksStorage {
pub m: Integer,
pub sold: Integer,
pub owners: Map<Integer, Address>,
}
Behind the scenes, the macro generates:
delegatecall footguns)You write simple Rust. The framework handles the blockchain complexity.
Instead of raw storage operations, Kontor provides a structured interface:
let model = ctx.model();
let balance = model.ledger().get(&address).unwrap_or_default();
model.ledger().set(address, new_balance);
This approach keeps storage operations simple and predictable:
ctx.model().foo().set_baz(new_baz)Integer type stays an Integer, not raw bytesKontor's runtime only provides a minimal set of primitives:
get/set operationsproc contexts)Everything else—the Map type, checked arithmetic, the storage macros—is built in userspace libraries, not the core runtime. While our first-class support is for Rust, these could be written in any supported language. This means:
crates.io) for dependencies.Kontor eliminates entire classes of vulnerabilities:
No storage collisions: Each contract's storage is isolated. There's no equivalent to Solidity's delegatecall that can corrupt storage layouts.
Overflow protection: Built-in checked_add, checked_div, etc. make safe math the default, not an afterthought.
Explicit visibility: Unlike Ethereum, storage isn't automatically readable. You must write view functions to expose data, enabling controlled access and future-proof upgrades.
Kontor uses Rust's Result type:
#[proc]
fn buy() -> Result<(), Error> {
let price = calculate_price(m, sold)?;
model.sold().set(sold.checked_add(1)?);
Ok(())
}
When a function returns Err:
ResultWhen a function throws a panic, there’s a full transaction rollback across all contracts.
This is dramatically clearer than Solidity's require/revert/assert confusion.
Sigil demonstrates that smart contracts written for WebAssembly don't have to sacrifice readability for safety.
ctx.model() provides fully typed storage with automatic serializationThe examples from other platforms aren't indictments of Rust itself, but of frameworks that expose too much low-level complexity. By embracing better abstractions, we can build systems that are not just safer, but also simpler to write, read, and audit. The result is code that is clear, concise, and correct by construction, proving that the language of choice need not be a compromise between security and developer experience.