Skip to content

Simplicity

A typed, combinator-based, smart contract language for Bitcoin-like blockchains.

Live on Liquid mainnet since July 2025

Get Started Read the Docs

AI / LLM agents: For a machine-readable summary of this site, see /llms.txt or /llms-full.txt.

Simplicity is a low-level smart contract language for Bitcoin-like blockchains, built on a small set of functional combinators rather than a growing opcode set. Programs are statically analyzable: every contract has a resource cost that's known before you fund it, and the language's formal semantics support machine-checked proofs of contract behavior.

You write contracts in SimplicityHL, a higher-level language with Rust-like syntax that compiles down to Simplicity.

The tutorials on this site currently target Liquid testnet for learning purposes; production deployments run on Liquid mainnet.

Write in a language you already know

You can use SimplicityHL, a high-level language with a clean, Rust-like syntax. This abstracts away low-level complexity, making it straightforward to write clear and reliable financial contracts with minimal code.

Allowance Covenant
// An allowance covenant.
//
// A beneficiary may withdraw up to ALLOWANCE_AMOUNT, no more often
// than once every MIN_DISTANCE blocks, returning the remainder to a
// fresh copy of this same contract. Once the remaining balance is
// ALLOWANCE_AMOUNT or less, the beneficiary may take all of it and the
// covenant ends.
//
// Funding note: each UTXO at this contract's address is an independent
// allowance with its own timelock. Every spend of this contract uses
// exactly one input (see enforce_single_input), so the balance of an
// existing instance can only decrease; no transaction adds funds to
// one. Paying the address again creates a second allowance rather than
// extending the first, and the two can never be combined. Merging would
// require a separate deposit path, which this contract deliberately
// omits for readability.
//
// Warning: fund this address only with explicit (unblinded) outputs.
// same_asset and full_withdrawal panic on a confidential asset or
// amount, so a confidential output sent here can never be spent and its
// funds are stuck permanently.

fn checksig(pk: Pubkey, sig: Signature) {
    let msg: u256 = jet::sig_all_hash();
    jet::bip_0340_verify((pk, msg), sig);
}

fn not(bit: bool) -> bool {
    <u1>::into(jet::complement_1(<bool>::into(bit)))
}

fn same_asset(x: (Asset1, Amount1), y: (Asset1, Amount1)) -> (u64, u64) {
    // Confirm that two (asset, amount) pairs are denominated in the same asset,
    // and return their explicit amounts.
    //
    // Comparing amounts without comparing assets is unsafe: anyone can issue an
    // Elements asset carrying any amount at no cost, so an amount check on its own
    // can be satisfied with worthless tokens. Returning the amounts from the same
    // function that checks the assets means the amounts cannot be read without the
    // check happening.
    //
    // Panics if either asset or either amount is confidential.
    //
    // This functionality is scheduled to be added to the SimplicityHL standard
    // library.
    let (x_asset, x_amount): (Asset1, Amount1) = x;
    let (y_asset, y_amount): (Asset1, Amount1) = y;
    let x_asset_id: u256 = unwrap_right::<(u1, u256)>(x_asset);// (1)!
    let y_asset_id: u256 = unwrap_right::<(u1, u256)>(y_asset);
    assert!(jet::eq_256(x_asset_id, y_asset_id));
    (unwrap_right::<(u1, u256)>(x_amount), unwrap_right::<(u1, u256)>(y_amount))
}

fn recursive_covenant() {
    // Enforce the covenant to repeat in the first output.
    //
    // Output 0: the covenant
    // Output 1: the beneficiary's withdrawal
    // Output 2: the fee (Elements has explicit fee outputs)
    // Disallow further outputs.
    assert!(jet::eq_32(jet::num_outputs(), 3));
    let this_script_hash: u256 = jet::current_script_hash();// (2)!
    let output_script_hash: u256 = unwrap(jet::output_script_hash(0));
    assert!(jet::eq_256(this_script_hash, output_script_hash));
    assert!(unwrap(jet::output_is_fee(2)));// (3)!
}

fn full_withdrawal() -> bool {
    // Is the beneficiary currently allowed to take all of the
    // remaining funds? (return true or false)
    let (_, available_amount): (Asset1, Amount1) = jet::current_amount();
    let explicit_amount: u64 = unwrap_right::<(u1, u256)>(available_amount);
    jet::le_64(explicit_amount, param::ALLOWANCE_AMOUNT)// (4)!
}

fn partial_withdrawal() {
    // Is the beneficiary validly taking an appropriate amount of
    // the funds and returning the rest to a new instance of this
    // same contract? (panic if not)

    // Ensure some funds are being sent back to the same contract on
    // output 0.
    recursive_covenant();

    // The covenant's current balance, and the amount retained on output 0.
    // Both must be denominated in the same asset.
    let (remaining_amount, retained_amount): (u64, u64) =
        same_asset(jet::current_amount(), unwrap(jet::output_amount(0)));

    // Subtract
    let (borrow, difference): (bool, u64) = jet::subtract_64(remaining_amount, param::ALLOWANCE_AMOUNT);// (5)!
    // Ensure calculated amount did not go negative.
    assert!(not(borrow));
    // Ensure retained amount is large enough.
    assert!(jet::le_64(difference, retained_amount));
}

fn enforce_signature(sig: Signature) {
    checksig(param::BENEFICIARY_KEY, sig);
}

fn enforce_single_input() {
    // Only one copy of this covenant may run in a transaction. Otherwise
    // several inputs would each check their own balance against the same
    // output 0, only the largest of those checks would bind, and the rest of
    // the co-spent balances could be taken.
    //
    // This is enforced here rather than inside partial_withdrawal so that it
    // holds on every path, including a full withdrawal, and so that it keeps
    // holding if further paths are added later.
    //
    // As a result, instances can never be merged (see the funding
    // note at the top of this file), and fees must come out of the
    // covenant's own balance, since an outside input cannot help pay
    // them. Fees therefore come out of the allowance itself.
    //
    // The fee output checked in recursive_covenant must be denominated in
    // the network's policy asset (e.g. LBTC for Liquid), and the single-input
    // restriction leaves no other input available to supply the fee. Funding
    // this covenant with any other asset limits every partial withdrawal to a
    // zero-value fee output, which nodes will generally not relay.
    assert!(jet::eq_32(jet::num_inputs(), 1));
}

fn enforce_relative_distance(min_distance: Distance) {
    // Assert that the current input is spent in a transaction that can
    // only appear a distance of at least min_distance blocks after the
    // block containing the input UTXO.
    // Panic otherwise.

    // This is a replacement for the deprecated jet::check_lock_distance.

    // Transaction version must be at least 2.
    assert!(jet::le_32(2, jet::version()));

    // Fetch and parse sequence
    let actual_data: Either<Distance, Duration> = unwrap(jet::parse_sequence(jet::current_sequence()));// (6)!
    let actual_distance: Distance = unwrap_left::<Duration>(actual_data);

    assert!(jet::le_16(min_distance, actual_distance));
}

fn enforce_time_delay() {
    enforce_relative_distance(param::MIN_DISTANCE);
}

fn main(){
    // Transactions must be signed by the beneficiary.
    enforce_signature(witness::SIGNATURE);

    // Transactions must not be too frequent.
    enforce_time_delay();

    // Only one instance of this covenant may be spent per transaction.
    enforce_single_input();

    // If the amount remaining is no more than the allowance amount,
    // the beneficiary is allowed to take all of it without sending
    // anything back to the covenant.
    match full_withdrawal() {
        true => (),
        false => partial_withdrawal(),
    }
}
  1. Elements assets and amounts can be confidential (encrypted) or explicit; unwrap_right assumes explicit and panics on a confidential value, since this contract only knows how to check plaintext amounts.

  2. A covenant restricts future spends by requiring the same script to reappear in an output; current_script_hash is how the contract refers to its own code to check for that.

  3. Elements (Liquid's underlying protocol) pays fees through an explicit fee output rather than Bitcoin's implicit input/output-value difference.

  4. param:: values aren't defined in this file: they're compile-time parameters bound when the contract is instantiated, and they become part of the resulting address.

  5. Simplicity has no exceptions, so arithmetic jets like subtract_64 return an explicit borrow flag instead of panicking or silently wrapping on underflow.

  6. Bitcoin's nSequence field is overloaded to encode either a block-count or a time-duration relative timelock; parse_sequence decodes which one a transaction is using.