This document describes how the installment repayment schedule advances (or
does not advance) in response to repay_credit calls. It is the authoritative
prose complement to the Mermaid diagrams in docs/ARCHITECTURE.md.
A credit line may carry a repayment schedule configured via
set_repayment_schedule. The schedule tracks:
| Field | Type | Meaning |
|---|---|---|
amount_per_period |
i128 |
Principal that must be retired each period |
next_due_ts |
u64 |
Unix timestamp of the next instalment due date |
period_secs |
u64 |
Duration of each period in seconds |
periods_remaining |
u32 |
How many instalments are still outstanding |
next_due_ts advances by exactly one period_secs when a repay_credit
call satisfies both of the following conditions simultaneously:
- All accrued interest is cleared — the payment covers any outstanding interest before principal is counted.
- At least
amount_per_periodof principal is retired in the same call.
If either condition is not met, next_due_ts remains unchanged regardless of
the repayment amount.
let interest_cleared = repay_amount >= accrued_interest
let principal_paid = repay_amount - min(repay_amount, accrued_interest)
if interest_cleared && principal_paid >= amount_per_period {
next_due_ts += period_secs
periods_remaining -= 1
}
Edge case: when repay_amount == accrued_interest (interest-only), the
principal component is zero, which is strictly less than amount_per_period.
Therefore next_due_ts does not advance.
This is the correct and intentional behaviour:
- An interest payment reduces the outstanding balance but does not satisfy the instalment obligation.
- Advancing the due date on an interest-only payment would allow a borrower to defer principal indefinitely by making small interest payments.
credit_limit = 1_000_000
draw_amount = 600_000
rate_bps = 1_000 (10 % p.a.)
amount_per_period = 100_000
period_secs = 2_592_000 (30 days)
first_due_ts = T0 + period_secs
| Time | Action | Repay amount | next_due_ts |
Advances? |
|---|---|---|---|---|
| T0 + 15 days | Interest-only | ~8 219 | T0 + 30 days | No |
| T0 + 30 days | Interest + principal | ~16 438 + 100 000 | T0 + 60 days | Yes |
Repaying interest plus less than amount_per_period in principal also does
not advance the schedule. Even one stroop below the threshold is insufficient.
If a single repay_credit call retires enough principal to cover two or more
periods, the schedule still advances by exactly one period. The contract
processes instalments one at a time; the surplus principal reduces the
outstanding balance but does not pre-pay future instalments.
When periods_remaining reaches zero after an advance:
- The schedule is considered fully satisfied.
- Subsequent calls to
get_repayment_schedulereturnNone. is_delinquentreturnsfalse(no outstanding schedule → no delinquency).
stateDiagram-v2
[*] --> ScheduleActive : set_repayment_schedule
ScheduleActive --> ScheduleActive : repay_credit\n(interest only OR\nprincipal < amount_per_period)\nnext_due_ts unchanged
ScheduleActive --> ScheduleActive : repay_credit\n(interest + principal >= amount_per_period\nAND periods_remaining > 1)\nnext_due_ts += period_secs\nperiods_remaining -= 1
ScheduleActive --> ScheduleCleared : repay_credit\n(interest + principal >= amount_per_period\nAND periods_remaining == 1)\nschedule removed
ScheduleActive --> Delinquent : ledger.timestamp > next_due_ts\nAND installment not satisfied
Delinquent --> ScheduleActive : repay_credit\n(clears delinquent amount)\nnext_due_ts advances
ScheduleCleared --> [*]
The following tests in contracts/credit/tests/installment_interest_only_repay.rs
cover this state machine:
| Test | Scenario | Expected outcome |
|---|---|---|
interest_only_does_not_advance |
Repay = accrued interest only | next_due_ts unchanged |
interest_plus_installment_advances_one_period |
Repay = interest + amount_per_period |
next_due_ts += period_secs |
partial_principal_does_not_advance |
Repay = interest + (amount_per_period − 1) |
next_due_ts unchanged |
double_principal_advances_only_one_period |
Repay = interest + 2 × amount_per_period |
next_due_ts += period_secs (not ×2) |
zero_repay_does_not_advance |
Repay = 0 | next_due_ts unchanged |
sequential_interest_only_then_full_repay |
Two-step: interest-only → full repay | Step 1 unchanged; Step 2 advances |
contracts/credit/src/lifecycle.rs—advance_repayment_schedule_after_repaycontracts/credit/src/accrual.rs—apply_accrual, interest computationcontracts/credit/src/math_utils.rs—prorate_interest,Roundingdocs/PROTOCOL_SPEC.md—set_repayment_schedule,is_delinquententrypoint specs