Docs Técnicas
Simple Loan
A minimal loan lifecycle contract: originate, repay in instalments, declare default. Composes Roles for a separate admin address.
A minimal loan lifecycle contract: originate, repay in instalments, declare default. Composes Roles for a separate admin address.
Use this as the on-chain record of a bilateral loan. Interest calculation is intentionally off-chain — the admin instructs repayments with the correct amount (principal + accrued interest) already computed.
Source
import std.access.Roles
contract SimpleLoan impl Roles:
storage:
# inherited: admin (Address), admin_active (bool)
borrower: Address
principal: u128 = 0
outstanding: u128 = 0
originated_at: u64 = 0
due_at: u64 = 0
# 0 = pending, 1 = active, 2 = repaid, 3 = defaulted
state: u64 = 0
@construct
def initialize(admin_addr: Address):
self.admin = admin_addr
self.admin_active = True
@tx
def originate(borrower_addr: Address, principal_amount: u128, term_seconds: u64):
require(ctx.caller == self.admin, "only_admin")
require(self.state == 0, "already_originated")
require(principal_amount > 0, "principal_must_be_positive")
require(term_seconds > 0, "term_must_be_positive")
self.borrower = borrower_addr
self.principal = principal_amount
self.outstanding = principal_amount
self.originated_at = ctx.block_timestamp
self.due_at = ctx.block_timestamp + term_seconds
self.state = 1
@tx
def repay(amount: u128):
require(self.state == 1, "loan_not_active")
require(ctx.caller == self.borrower, "only_borrower")
require(amount > 0, "amount_must_be_positive")
require(amount <= self.outstanding, "exceeds_outstanding")
self.outstanding = self.outstanding - amount
if self.outstanding == 0:
self.state = 2
@tx
def declare_default():
require(ctx.caller == self.admin, "only_admin")
require(self.state == 1, "loan_not_active")
require(ctx.block_timestamp > self.due_at, "loan_not_overdue")
require(self.outstanding > 0, "loan_already_repaid")
self.state = 3
@tx
def write_off():
require(ctx.caller == self.admin, "only_admin")
require(self.state == 3, "loan_not_defaulted")
self.outstanding = 0
@view
def loan_state() -> u64:
return self.state
@view
def outstanding_balance() -> u128:
return self.outstanding
@view
def is_overdue() -> bool:
return self.state == 1 and ctx.block_timestamp > self.due_at
@view
def borrower_address() -> Address:
return self.borrowerState Machine
pending (0)
│
│ originate(borrower, principal, term) [admin only]
▼
active (1)
│ │
│ repay(amt) │ declare_default() [admin, after due_at]
│ (repeat) ▼
│ defaulted (3)
│ │
▼ │ write_off() [admin only]
repaid (2) ▼
outstanding = 0Lifecycle
initialize(admin_address)
originate(borrower, 1_000_000, 2_592_000) # 30-day term
# borrower repays in two instalments (interest computed off-chain)
repay(600_000) # outstanding = 400_000
repay(400_000) # outstanding = 0 → state = repaid
# alternative: default path
# (block_timestamp > due_at)
declare_default()
write_off() # outstanding zeroed for accountingKey Design Points
Off-chain interest — outstanding starts equal to principal. Each repay call must include any accrued interest. The admin system computes the correct amount and the borrower transfers exactly that sum. This keeps the contract simple and auditable.
`state` guards every transition — each function checks that the loan is in the correct state before proceeding. The state integer encodes the full lifecycle: you cannot repay a defaulted loan or default an already-repaid one.
`declare_default` requires `block_timestamp > due_at` — the admin cannot declare default before the term expires, even if no payment has been made. This protects the borrower from premature enforcement.
`write_off` is separate from `declare_default` — defaulting records the event; write-off zeros the outstanding for accounting closure. The two steps are intentionally separated so an audit trail exists between the default declaration and the balance write-down.
`Roles` vs `Ownable` — Roles names the privileged account admin rather than owner, signaling that this is an operational role (lender / loan servicer) rather than a token governance role.