Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The lending protocol

πŸ“ Draft. This chapter has not been reviewed yet β€” content may be incomplete or change.

Problem. Two strangers want to transact a collateralised loan with no escrow agent and no trusted backend. A borrower locks collateral and advertises terms; a lender supplies the principal; the loan later settles by repayment or β€” if the borrower defaults β€” by liquidation after a deadline. Every rule is enforced on-chain by Simplicity covenants.

This is the capstone of the cookbook. Everything the recipes introduced one at a time β€” covenant UTXO types, multiple spending paths, asset issuance & NFTs, formulas & derived params, hooks & tapleaf compute, and the class / instance model β€” shows up here at once, wired into a single working protocol.

The full example lives in the repository at examples/lending/: one txmanifest.json plus five .simf covenant programs. This chapter walks through it in the order you'd actually run it.

The deal, in one paragraph

Alice has collateral (say, L-BTC) and wants to borrow L-USDT against it without selling. She locks her collateral into a covenant and publishes her terms β€” amount, interest, expiry β€” as on-chain NFTs anyone can read. Bob sees the offer, likes the terms, and accepts by sending Alice the principal; in the same transaction her collateral moves into a second covenant that holds it for the life of the loan. To get her collateral back, Alice repays principal plus interest before the deadline. If she doesn't, Bob can seize the collateral once the deadline passes. At no point does either party have to trust the other or any third party β€” the covenants only permit the honest transitions.

The cast

Two roles. The protocol has a borrower and a lender. They are different people with different wallets, so when you run it you'll keep two wallet files β€” borrower.json and lender.json β€” and run each action as the appropriate party.

Five covenant programs. Each .simf file is a small Simplicity program that gates one kind of UTXO. They are deliberately tiny and composable:

ProgramRole
pre_lock.simfHolds the collateral while the offer is open. PATH::LEFT lets a lender activate the loan; PATH::RIGHT lets the borrower cancel (with a signature).
lending.simfHolds the collateral during the active loan. PATH::LEFT is repayment; PATH::RIGHT is liquidation after expiry.
script_auth.simfWraps each NFT so it can only be spent co-spent with the right collateral covenant. The glue that binds the NFTs to the deal.
asset_auth.simfGuards the lender's principal vault: release requires burning the Lender NFT.
p2pk.simfThe borrower's plain Schnorr payout address β€” the Hello World program, reused for where the principal lands.

Four NFTs. The protocol mints four single-unit Liquid assets at construction. Two are bearer auth tokens (whoever holds it can act); two encode the loan terms in their amount field so the offer is self-describing on-chain:

NFTCarriesUsed for
Borrower NFTnothing (amount = 1)proves a transaction is the borrower's; co-spent in setup and repayment
Lender NFTnothing (amount = 1)the lender's bearer token; needed to liquidate and to drain the vault
First Parameters NFTinterest rate, expiry, decimals β€” bit-packed into its amountpublishes the loan terms; checked by every covenant
Second Parameters NFTcollateral & principal base amounts β€” bit-packed into its amountpublishes the amounts; checked by every covenant

The lifecycle

The whole protocol is one lending_contract class, and the manifest closes with an optional lifecycle block that names the state machine its methods walk through:

"lifecycle": {
  "states": ["nfts_issued", "offer_open", "loan_active", "repaid", "liquidated", "cancelled"],
  "entry_actions": ["IssueUtilityNFTs"],
  "transitions": {
    "IssueUtilityNFTs":           { "to": "nfts_issued" },
    "LockCollateral":             { "from": "nfts_issued", "to": "offer_open" },
    "CancelOffer":                { "from": "offer_open",  "to": "cancelled",  "unilateral": true },
    "SetupLending":               { "from": "offer_open",  "to": "loan_active" },
    "RepayLoan":                  { "from": "loan_active", "to": "repaid",     "cooperative": true },
    "LiquidateAfterExpiry":       { "from": "loan_active", "to": "liquidated", "unilateral": true },
    "ClaimPrincipalWithInterest": { "from": "repaid",      "to": "settled" }
  }
}

lifecycle is documentation-only. The spec lists it among the top-level fields as "named states, transitions, execution paths" and marks it purely informative β€” nothing on-chain depends on it, and no tool is required to enforce it. It exists so a reader (or a diagram renderer) can see the intended state machine at a glance without tracing every method's inputs and outputs. It may be dropped from a future revision; treat it as a map, not machinery.

The block has three parts:

  • states β€” the named states an instance can be in. They're free-form labels; the from/to fields below refer to them. (settled appears as a to target without being listed β€” a reminder that this section is descriptive, not validated.)
  • entry_actions β€” the methods that create a fresh instance rather than advancing an existing one. Here it's the constructor, IssueUtilityNFTs.
  • transitions β€” one entry per method, each naming the state it moves from and to, plus two optional flags described below. A transition with no from (the constructor) is an entry point.

Rendered, those transitions are the protocol's flow:

                    IssueUtilityNFTs
                          β”‚   (borrower mints 4 NFTs + computes covenant hashes)
                          β–Ό
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ nfts_issuedβ”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚   LockCollateral  (borrower)
                          β–Ό
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   CancelOffer (borrower, unilateral)
                    β”‚ offer_open β”‚ ─────────────────────────────►  cancelled
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚   SetupLending  (lender accepts)
                          β–Ό
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚loan_active β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚        β”‚
        RepayLoan      β”‚        β”‚   LiquidateAfterExpiry
     (borrower,        β”‚        β”‚   (lender, unilateral,
      cooperative)     β–Ό        β–Ό    after LOAN_EXPIRATION_TIME)
                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β”‚ repaid β”‚  β”‚ liquidatedβ”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
        ClaimPrincipalWithInterest (lender drains the vault)
                       β–Ό
                    settled

The two optional flags annotate who a transition needs. "unilateral": true marks the two escape hatches β€” CancelOffer and LiquidateAfterExpiry β€” that one party can take without the other's cooperation; that's the whole point of a trustless protocol, the exits don't depend on the counterparty playing along. "cooperative": true marks RepayLoan as the happy path both sides want. The flags don't do anything on-chain β€” the covenants are what actually enforce who can spend β€” but they tell a reader at a glance which transitions are adversarial and which are mutual.

How this chapter is organised

The walkthrough follows the lifecycle across four pages, each building one phase and pulling in the recipes that introduced its pieces:

  1. Issuing the NFTs & encoding the terms β€” the IssueUtilityNFTs constructor: minting four NFTs from issuance inputs, bit-packing the loan terms into Parameter NFT amounts, and computing the web of interdependent covenant hashes that every later step relies on.
  2. Opening the offer β€” LockCollateral puts the collateral and NFTs on-chain behind the pre_lock covenant, with an op_return discovery beacon and a pre-build validations check.
  3. Accepting or cancelling the offer β€” the two spending paths of pre_lock: the lender's SetupLending (with the required_index discipline a covenant demands) versus the borrower's CancelOffer.
  4. Settling: repay, liquidate, withdraw β€” the borrower's RepayLoan versus the lender's LiquidateAfterExpiry on the lending covenant, then draining the principal vault with ClaimPrincipalWithInterest.

Before you run it

The CLI is tx-manifest-wallet, aliased throughout the book to txw. Do the one-time setup first. Because this protocol has two roles, create two wallets and fund both from the testnet faucet:

txw create-wallet --out borrower.json
txw create-wallet --out lender.json
# fund each from https://liquidtestnet.com/faucet, then:
txw sync --wallet borrower.json
txw sync --wallet lender.json

Where's the funding address? Run info on each wallet and copy the receive address it prints, then paste that into the faucet:

txw info --wallet borrower.json   # copy the receive address, fund it, repeat for lender.json

See Fund and sync for the full walk through.

Get oriented with describe and validate before building anything β€” describe prints the classes, methods, and lifecycle; validate checks the manifest is internally consistent:

txw describe examples/lending/txmanifest.json
txw validate examples/lending/txmanifest.json

Then start with Issuing the NFTs.

This is the most involved example in the book. If you haven't worked through Hello World and the Last Will covenant yet, do those first β€” they introduce the single-key and multi-path patterns this protocol composes at scale.