Voltar para Documentação

Docs Técnicas

Simple Loan

A minimal loan lifecycle contract: originate, repay in instalments, declare default. Composes Roles for a separate admin address.

O conteúdo abaixo vem das fontes técnicas do repositório e é prerenderizado no site para leitura direta por pessoas, crawlers e agentes.

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

python
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.borrower

State 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 = 0

Lifecycle

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 accounting

Key Design Points

Off-chain interestoutstanding 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.