Skip to content
LogoLogo

Coming from Solidity

Lithe targets the same EVM and the same ABI as Solidity, and its storage layout is slot-for-slot compatible. Most concepts carry over, with different syntax and a few deliberate omissions.

The same contract, twice

contract Vault {
    error Insufficient(uint256 available, uint256 required);
    event Withdrawn(address indexed who, uint256 amount);
 
    mapping(address => uint256) public deposits;
 
    function deposit() external payable {
        deposits[msg.sender] += msg.value;
    }
 
    function withdraw(uint256 amount) external {
        uint256 have = deposits[msg.sender];
        if (have < amount) revert Insufficient(have, amount);
        deposits[msg.sender] = have - amount;
        emit Withdrawn(msg.sender, amount);
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok);
    }
}

Both contracts have the same ABI, the same error selector, the same event topic, and the same storage slots.

Syntax

SolidityLithe
// comment, /* comment */(* comment *), nestable
x = y;x := y;
a == b, a != ba = b, a <> b
&&, ||, !and, or, not
uint256 x = 1;const x : uint := 1;
a local you reassignvar x : uint := 0;
true, falsetrue, false
if (c) { } else if (d) { }if c then { } else if d then { }
c ? a : bif c then a else b
for (uint i = 0; i < n; i++)for i : uint in 0 .. n do { }. See Loops
for (uint i = 0; i < xs.length; i++)for (i, x) : (uint, T) in xs do { }
while (cond)none. for ... until cond bound n do { }
breakuntil
f(n - 1) inside fnone. A for loop with an accumulator. See Recursion
return x;... is { ... } with x
struct S { uint a; }type s is record { a : uint; }
function f(S calldata s)a tuple record parameter. See Records in the ABI
S({a: 1}){ a = 1; }
enum E { A, B }type e is A | B, and cases may carry payloads. See Variants
e == E.Ae = A
uint[], uint[3]list(uint), list(uint, 3)
uint constant MAX = 3;pure function max() : uint is 3, folded to the literal. See Constants
bytes32 constant ROLE = keccak256("ROLE");pure function role() : bytes32 is Crypto.keccak256("ROLE")
uint[MAX]list(uint, max())
mapping(address => uint)map(address, uint)
delete x;delete x;
address(0)Address.zero
type(uint256).maxUint.max
unchecked { a + b }Uint.wrappingAdd(a, b), and wrappingSub, wrappingMul
1 ether, 1 days1eth, 1days
10 ** decimals10 ** decimals: any unsigned exponent, and the result has the base's type

Functions

SolidityLithe
function f() externalexternal function f() is { ... }
function f() external view returns (uint)external view function f() : uint is ...
function f() external pure returns (uint)external pure function f() : uint is ...
function f() external payableexternal payable function f() is { ... }
function f() internalfunction f() is { ... }
function f() internal viewview function f() : uint is ...
function f() privateprivate function f() is { ... } (private to the file)
public state variable getterwrite an external view function
modifier onlyOwnera guard function, or a dispatch group
nonReentrant (OpenZeppelin)external nonreentrant function
constructor(...)deploy(...) is { ... }. See Deployment
new Vault(a, b)defer create Vault(a, b). See create
initializer (OpenZeppelin, upgradeable)deploy, in an upgradeable contract. See Deployment
receive() external payablepayable receive is { ... }. See receive and fallback
fallback() external [payable][payable] fallback is { ... }, reading Evm.calldata()
fallback(bytes calldata) returns (bytes memory)fallback : bytes is { ... } with result, returned as it is
a proxy's fallback with delegatecall in assemblypayable fallback delegates to target;
function overloadingthe same: overloads differ in their parameter types

Globals

SolidityLithe
msg.senderEvm.sender()
msg.valueEvm.value() (only in payable functions)
msg.data, msg.sigEvm.calldata(), Evm.selector()
I.f.selector, type(I).interfaceIdAbi.selector(I.f), Abi.interfaceId(I)
tx.origin, tx.gaspriceEvm.txOrigin(), Evm.txGasPrice()
block.timestamp, block.numberEvm.blockTimestamp(), Evm.blockNumber()
block.chainid, block.basefeeEvm.chainId(), Evm.blockBaseFee()
address(this)Evm.self()
addr.balanceEvm.balance(addr)
gasleft()Evm.gasLeft()
keccak256(x)Crypto.keccak256(x)
ecrecover(h, v, r, s)Crypto.ecrecover(h, v, r, s)
abi.encode(...)Abi.encode(...)
abi.decode(data, (uint))Abi.decode(data, uint)
b[i], msg.data[4:], bytes4(b)the same: b[i], Evm.calldata()[4:], bytes4(b). See Bytes and String
abi.decode(data, (address, uint))Abi.decode(data, (address, uint))
function f() returns (uint a, string b)a record result, : result, whose fields are the results
function f() returns (S memory)a tuple result, : (uint, string)
uint[][], S[] with strings insidelist(list(uint)), list(s): lists nest to any depth
precompiles by address, in assemblyCrypto.bn254Pairing, Crypto.p256Verify, and the rest. See Precompiles

The full list is in Built-ins.

Errors

SolidityLithe
error NotOwner();error NotOwner;
error TooSmall(uint256);error TooSmall of uint;
error Low(uint256 have, uint256 want);error Low of record { have : uint; want : uint; }
error Missing(string key, uint256[] ids);error Missing of record { key : string; ids : list(uint); }
if (!c) revert NotOwner();assert(c, NotOwner);
revert NotOwner();fail NotOwner;
require(c, "message")not allowed. Errors are typed
(nothing)raises NotOwner | TooSmall, checked by the compiler

A Lithe error's ABI name is its declared name, so NotOwner is NotOwner(), with the selector Solidity would compute.

External calls

SolidityLithe
interface IERC20 { ... }interface IERC20 is { query ...; call ...; }
token.balanceOf(a) (view)query token.balanceOf(a)
token.transfer(to, n)defer token.transfer(to, n)
pool.deposit{value: v}(a)defer pool.deposit(a) with value = v
c.f{gas: g}(a), to.staticcall{gas: g}(data)with gas = g on the defer or query. See Limiting a call's gas
payable(to).transfer(v), .call{value: v}("")defer Evm.transfer(to, v)
uint r = pool.swap(x); use(r);defer pool.swap(x) then (r : uint) { ... }
try c.f() returns (r) { } catch { }defer c.f() then { | Ok(r : T) -> { } | Error(e : bytes) -> { } }
try on a view callcase query c.f() of { Ok(r) -> ... | Error(e) -> ... }
new C{salt: s}(...)defer create C(...) with salt = s. See create
create(v, p, n), create2(v, p, n, s) in assemblydefer create(code) with { value = v; salt = s; }. See Creation code as bytes
type(C).creationCodeEvm.creationCode(C)
a.code, Clones.clone(impl), SSTORE2.write(data)Evm.code(a), defer create(Clones.code(impl)), defer create(SSTORE2.code(data)). See Utilities
token.call(...) accepting empty return dataa result of option(T): None when nothing comes back. See Optional results
to.call{value: v}(data)defer Evm.call(to, data) with value = v then (result : bytes) { ... }. See Raw calls
to.staticcall(data)query Evm.staticCall(to, data)
to.delegatecall(data)defer Evm.delegateCall(to, data). For a contract's own upgrades, use upgradeable

Inheritance and libraries

SolidityLithe
contract A is B, Ccontract A is { use B; use C; ... }
inherited public functionsa module's externals, exposed with use B exposing I, { ... }
ERC165 and supportsInterface overridessupports I, J;, and the compiler writes supportsInterface. See ERC-165
an abstract _canSetX() hook the contract overridesa module guard, which each contract that exposes the guarded externals writes. See Guards
a virtual function the contract overrides, such as _beforeTokenTransfer, _update, or tokenURIa module hook, which the module's code calls and each contract that reaches it implements, or skips with is skip. Only the contract implements hooks, so it composes extensions in the hook's body. See Hooks, and the standard library's tokens' beforeTransfer and afterTransfer, where an _update override's code before and after super._update goes: Transfer hooks
super.f()call the module function directly: B.f()
library La module, or plain functions in an imported file
using L for Tnone. Call L.f(x)
ERC-7201 @custom:storage-locationstorage name at "id" is record { ... }
OpenZeppelin UUPSupgradeable plus an upgrade guard
initializer / reinitializermigrate functions, run by upgradeToAndCall