> For the complete documentation index, see [llms.txt](https://docs.kton.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kton.io/protocol-internals/08-loan-and-validator-model.md).

# The Loan and Validator Model

This chapter is the technical specification of how pooled Gram becomes a validator stake and how the resulting interest flows back into the KTON pool. It is the engine behind the appreciating exchange rate. KTON does not stake Gram directly with the TON Elector. Instead, the Pool lends Gram to per-validator **controller** contracts, each controller stakes its own balance (validator funds plus the borrowed Gram) with the Elector, and after the stake is recovered the controller repays the loan with interest. All yield in the protocol comes from this validator-loan interest. There is no other source.

Every op-code, field, and constant below is taken directly from the contract source: `controller.func`, `pool.func`, `network_config_utils.func`, and `docs/controller.md`.

## Roles and contracts in a loan

| Actor            | Contract                             | Responsibility in the loan                                                                                                                                 |
| ---------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Validator wallet | (external)                           | Initiates the loan request and the stake; holds its own operating funds on the controller.                                                                 |
| Controller       | `controller.func`                    | One per validator per round-parity. Borrows from the Pool, stakes with the Elector, recovers, and repays.                                                  |
| Pool             | `pool.func`                          | Custodies pooled Gram, decides the loan size, extends credit, books the debt, and folds repaid profit into `total_balance`.                                |
| Approver         | role on the controller               | Approves a controller to borrow and sets `approver_set_profit_share`.                                                                                      |
| Interest manager | role on the Pool                     | Sets `interest_rate`, `min/max loan per validator`, `disbalance_tolerance`, and `credit_start_prior_elections_end`; receives per-loan and per-round stats. |
| Elector          | TON system contract (config param 1) | Native validator election and stake escrow. Not a KTON contract.                                                                                           |

Controller authentication is entirely address based. The controller address is derived from `[validator, pool, governor, halter, approver]`. Rotating any of those forces a fresh controller deployment.

A controller moves through a fixed state machine (`controller.func`):

```
REST (0) -> SENT_BORROWING_REQUEST (1) -> REST -> SENT_STAKE_REQUEST (2)
  -> FUNDS_STAKEN (3) -> SENT_RECOVER_REQUEST (4) -> REST
INSOLVENT (5) is a terminal-ish error state reached when the controller cannot repay.
```

## 1. Loan request: controller side

The validator wallet sends `controller::send_request_loan` (op `0x6335b11a`) to its controller. The handler enforces (`controller.func`):

* `assert_state!(state::REST)` and `assert_sender!(sender_address, validator)`.
* `msg_value >= MIN_REQUEST_LOAN_VALUE` (1 Gram), else `error::too_low_request_loan_value` (`0xf604`). This value covers controller and pool gas.
* The request body carries `min_loan`, `max_loan`, `max_interest`, and an optional `acceptable_profit_share` (share units). `interest` is provisionally set to `max_interest`.
* `acceptable_profit_share >= approver_set_profit_share`, else `error::profit_share_mismatch` (`0xfa06`). The validator must accept at least the profit share the approver demands.
* `approved?` must be true, else `error::controller_not_approved` (`0xfa00`).
* `borrowed_amount` must be 0, else `error::multiple_loans_are_prohibited` (`0xfa01`). One loan at a time.

It then checks the **election window** from network config (params 15 and 34, read via `network_config_utils.func`):

```
(elections_start_before, _, elections_end_before) = get_validator_config()   ;; config param 15
(utime_since, utime_until, vset)                  = get_current_validator_set() ;; config param 34
throw_unless(too_early_loan_request, now() > utime_until - elections_start_before)  ;; 0xfa02, elections started
throw_unless(too_early_loan_request, now() > utime_until - elections_end_before - allowed_borrow_start_prior_elections_end)  ;; controller-allowed start
throw_unless(too_late_loan_request,  now() < utime_until - elections_end_before)    ;; 0xfa03, elections still open
```

A loan can only be requested while the Elector's election for the next validation round is open. `allowed_borrow_start_prior_elections_end` is a per-controller window set by the approver (default `65536`, permissive).

Before forwarding, the controller checks it can afford the worst case. `elector_fine = max_recommended_punishment_for_validator_misbehaviour(max_loan + balance)` (config param 40, default 101 Gram if unset; see `network_config_utils.func`), `interest_payment = muldiv(max_loan, max_interest, SHARE_BASIS)`, and the validator's own funds (`balance - borrowed_amount`) must cover `ENSURABLE_BALANCE_FOR_STAKING + elector_fine + interest_payment`, else `error::too_high_loan_request_amount` (`0xfa04`). If `allocation` is nonzero, `max_loan <= allocation` is also required.

On success the controller forwards `pool::request_loan` (op `0xe642c965`) carrying the request plus its static data, using `CARRY_ALL_REMAINING_MESSAGE_VALUE`, and transitions to `SENT_BORROWING_REQUEST`.

## 2. Loan grant: pool side

The Pool handles `pool::request_loan` (op `0xe642c965`) in `pool.func`. It re-validates everything independently:

* `assert_state!(state::NORMAL)` and `throw_if(borrowing_request_in_closed_round, current_round_closed?)` (`0xf104`).
* The same election-window triple is re-checked against config params 15 and 34, producing `too_early_borrowing_request` (`0xf105`) and `too_late_borrowing_request` (`0xf106`). If `credit_start_prior_elections_end` is nonzero, an additional earliest-borrow gate applies; on the live public pool this value is 0, so that extra check is disabled.
* The sender is verified to be the controller derived from `[controller_id, validator]`: `assert_sender!(sender_address, _get_controller_address(controller_id, validator))`.
* `max_interest >= interest_rate`, else `error::interest_too_low` (`0xf100`). The validator must accept at least the pool's current rate.

### Sizing the loan

The Pool computes how much it can safely lend. First, `creditable_funds`, the cash actually available after reserving for queued withdrawals and storage:

```
creditable_funds = balance
                 - muldiv_extra(requested_for_withdrawal, total_balance, supply)  ;; Gram owed to pending withdrawals
                 - MIN_TONS_FOR_STORAGE                                            ;; 10 Gram (pool)
                 - 1                                                               ;; safety margin
```

Then the **disbalance cap**, which limits how much of the pool a single validator can draw so that no one controller concentrates the stake:

```
available_funds = min(
    creditable_funds,
    muldiv((DISBALANCE_TOLERANCE_BASIS / 2) + disbalance_tolerance, total_balance, DISBALANCE_TOLERANCE_BASIS) - borrowed
)
```

`DISBALANCE_TOLERANCE_BASIS = 2 << 8 = 512` (`pool.func`, the comment notes "2 and not 1 is intentional"). The base term `512/2 = 256` is exactly 50% of `total_balance`. `disbalance_tolerance` widens that ceiling: on the live public pool it is 255, raising the per-validator cap close to the full balance. `borrowed` is what is already lent out this round, so the cap is the remaining room under the ceiling.

The request's `min_loan` and `max_loan` are then clamped to the interest-manager's per-validator bounds:

```
min_loan = max(min_loan, min_loan_per_validator)
max_loan = min(max_loan, max_loan_per_validator)
throw_unless(contradicting_borrowing_params, min_loan <= max_loan)   ;; 0xf101
actual_loan = min(available_funds, max_loan)
throw_unless(not_enough_funds_for_loan, actual_loan >= min_loan)     ;; 0xf102
```

Live per-validator bounds are 1.00 / 2,000,000 Gram (public) and 1.00 / 2,506,300 Gram (private).

### Interest and the credit message

Interest is fixed at grant time:

```
interest = muldiv(actual_loan, interest_rate, SHARE_BASIS)
```

`SHARE_BASIS = 256 * 256 * 256 = 16,777,216` (2^24, defined in `types.func`). `interest_rate` is a per-round rate in those share units, set by the interest manager. On the live pools the raw value is `10800`, which is `10800 / 16,777,216 ≈ 0.06437%` of principal per loan. Interest is per-loan, not annualized here; the displayed APY is computed off-chain by annualizing realized round profit.

The Pool then sends exactly one `controller::credit` message (op `0x1690c604`). The split between message value and body is the key mechanic:

```
.store_coins(actual_loan)             ;; message VALUE  = cash transferred (the principal)
...
.store_body_header(controller::credit, query_id)
.store_coins(actual_loan + interest)  ;; message BODY   = debt recorded (principal + interest)
```

**The message VALUE is the cash (`actual_loan`); the BODY is the debt (`actual_loan + interest`).** The controller receives `actual_loan` in real Gram but is recorded as owing `actual_loan + interest`. Interest is part of the debt from the first instant of the loan; it is not accrued over time.

The Pool books the loan with `~add_loan(sender_address, actual_loan, interest)`, which accumulates `expected += loan_body + interest` for the round and stores `[already_borrowed, accounted_interest]` per controller in `borrowers_dict`. The running per-validator total must satisfy `total_loan <= max_loan_per_validator`, else `error::total_credit_too_high` (`0xf103`). The dictionary depth is bounded: `cell_depth(borrowers_dict) < MAX_LOAN_DICT_DEPTH` (12), else `error::credit_book_too_deep` (`0xf401`). Finally a `SERVICE_NOTIFICATION_AMOUNT` (0.02 Gram) stat message goes to the interest manager.

### Controller receives the credit

Back in `controller.func`, the `controller::credit` handler (`assert_sender!(sender_address, pool)`):

```
credit_amount = in_msg_body~load_coins()             ;; = actual_loan + interest (the debt)
borrowed_amount += credit_amount
throw_unless(credit_interest_too_high, credit_amount < muldiv(msg_value, interest + SHARE_BASIS, SHARE_BASIS) + ONE_TON)  ;; 0xfa05
interest = muldiv(credit_amount - msg_value, SHARE_BASIS, msg_value)
```

`msg_value` here is the cash (`actual_loan`), so `credit_amount - msg_value = interest`, and the controller recomputes its effective `interest` share and clamps it as a sanity check (`error::credit_interest_too_high`, `0xfa05`). `borrowed_amount` now holds the full debt including interest. The state returns to `REST`, and `borrowing_time` is stamped if it was zero.

## 3. Staking with the Elector

With cash in hand and state `REST`, the validator sends `controller::new_stake` (op `0x4e73744b`, aliased from `elector::new_stake`). The handler enforces, among other checks:

* `assert_state!(state::REST)`, `assert_sender!(sender_address, validator)`.
* `query_id > 0` (so the Elector replies), `msg_value >= ELECTOR_OPERATION_VALUE` (1.03 Gram).
* `value >= MIN_STAKE_TO_SEND` (50,000 Gram). This is the TON validator-stake floor, not a user deposit minimum.
* **Same-round credit rule**: if `borrowed_amount` is nonzero, the stake must be sent in the same election round the funds were borrowed for:

```
throw_unless(wrongly_used_credit, (borrowing_time > utime_since) & (now() < utime_until - elections_end_before))  ;; 0xf904
```

Borrowed Gram cannot be carried across rounds. It must be staked into the election it was borrowed for, or returned. The controller forwards the validated `new_stake` to the Elector (`elector_address()` from config param 1) with `PAY_FEES_SEPARATELY`, records `stake_amount_sent = value - ELECTOR_OPERATION_VALUE`, snapshots the current validator-set hash, resets `validator_set_changes_count = 0`, and moves to `SENT_STAKE_REQUEST`. On `elector::new_stake_ok` (`0xf374484c`) it advances to `FUNDS_STAKEN`; on `elector::new_stake_error` (`0xee6f454c`) or a bounced `new_stake` it returns to `REST`.

While funds are staked, the validator must periodically call `controller::update_validator_hash` (op `0xf0fd2250`) to track validator-set changes (incrementing `validator_set_changes_count`). If the validator is late by more than `GRACE_PERIOD` (600 s), anyone may call it and collect `HASH_UPDATE_FINE` (10 Gram) from the controller. This watchdog incentive keeps the recovery clock advancing even if the validator goes dark.

## 4. Recovery and settlement

Stake cannot be pulled back immediately. The Elector holds it across the validation round plus the unfreeze hold (`stake_held_for`). The controller must wait for the validator set to change enough times before recovering, because the Elector accrues credits in several phases (surplus return, complaint rewards, and the final unfreeze) and the controller must take them all in one shot.

`controller::recover_stake` (op `0xeb373a05`) enforces (`controller.func`):

```
assert_state!(state::FUNDS_STAKEN)
throw_unless(too_early_stake_recover_attempt_count, validator_set_changes_count >= 2)         ;; 0xf600
time_since_unfreeze = now() - validator_set_change_time - stake_held_for
throw_unless(too_early_stake_recover_attempt_time, (validator_set_changes_count > 2) | (time_since_unfreeze > 60))  ;; 0xf601
throw_unless(too_low_recover_stake_value, msg_value >= ELECTOR_OPERATION_VALUE)                ;; 0xf602
```

It sends `elector::recover_stake` (op `0x47657424`) with `CARRY_ALL_REMAINING_MESSAGE_VALUE` and moves to `SENT_RECOVER_REQUEST`. As with the hash watchdog, once `time_since_unfreeze >= GRACE_PERIOD` and `borrowed_amount > 0`, a non-validator caller may trigger recovery and collect `STAKE_RECOVER_FINE` (10 Gram); before the grace period only the validator may call it.

### The repayment amount: greater of two

When the Elector replies with `elector::recover_stake_ok` (op `0xf96f7324`), the controller computes profit and the amount to return:

```
profit = msg_value - stake_amount_sent
amount_to_return = max(
    borrowed_amount,                                            ;; full debt: principal + fixed interest
    muldiv(borrowed_amount, SHARE_BASIS, SHARE_BASIS + interest)  ;; restore principal (strip the interest factor)
      + muldiv(approver_set_profit_share, profit, SHARE_BASIS)    ;; + approver's share of realized profit
)
```

This is the V2 revenue-sharing rule. `borrowed_amount` is principal plus fixed interest. The second branch reconstructs the bare principal (since `borrowed_amount = principal * (SHARE_BASIS + interest) / SHARE_BASIS`, the inverse `muldiv` recovers the principal) and adds `approver_set_profit_share` percent of the round's realized profit. The pool is repaid **the greater of the two**. In normal conditions the fixed interest dominates; when a round is unusually profitable the profit-share branch wins, so the upside is shared with the pool (and therefore all KTON holders) rather than kept entirely by the validator.

The repayment is sent to the Pool as `pool::loan_repayment` (op `0xdfdca27b`) with `PAY_FEES_SEPARATELY`, but only if the controller can keep `MIN_TONS_FOR_STORAGE` (2 Gram) in reserve. If it cannot, it transitions to `INSOLVENT` (state 5) and does not repay until topped up. On a successful send it zeroes `borrowed_amount`, `borrowing_time`, `interest`, `stake_amount_sent`, and `stake_at`, returning to `REST`. An `elector::recover_stake_error` (op `0xfffffffe`) means the Elector held zero credits; the controller halts itself and goes `INSOLVENT` to avoid repeated fine drain.

### Unused-loan return and watchdog

If a controller borrowed but never won the election (so the funds were never staked), the loan must be returned. `controller::return_unused_loan` (op `0xed7378a6`) requires `state == REST`, `borrowed_amount > 0`, and `utime_since > borrowing_time` (the loan belongs to a prior round). It sends the full `borrowed_amount` back via `pool::loan_repayment`. Once the controller is past `GRACE_PERIOD`, anyone may trigger this and collect `STAKE_RECOVER_FINE` (10 Gram), the watchdog bounty that guarantees loaned Gram returns to the pool even if the validator abandons it. The governor can also force a return from an `INSOLVENT` controller via `governor::return_available_funds`.

## 5. Pool-side settlement and profit accounting

The Pool handles `pool::loan_repayment` (op `0xdfdca27b`) by calling `~close_loan(sender_address, msg_value)` against the previous round's borrowers first, then the current round's. `close_loan` looks up the borrower's recorded `was_borrowed`, removes it from the dictionary, and books:

```
profit += amount - was_borrowed   ;; amount is the repaid value, was_borrowed is the recorded debt
returned += amount
active_borrowers -= 1
```

Because `borrowed_amount` already included interest, a clean repayment of exactly the debt yields zero marginal profit here; the protocol's positive profit comes from the interest that was baked into the debt at grant time (and any profit-share excess). When the last active borrower closes (`active_borrowers == 0`), the round can finalize via `update_round`.

`finalize_lending_round` (in `pool.func`) closes the books for the round:

```
profit -= FINALIZE_ROUND_FEE                               ;; 1 Gram, taken off the top of round profit
fee     = max(muldiv(governance_fee_share, profit, SHARE_BASIS), 0)   ;; governance commission on yield
profit -= fee                                              ;; routed to the treasury role
profit  = max(profit, - total_balance)                     ;; losses socialized, floored
total_balance += profit                                    ;; auto-compound; supply unchanged
```

`governance_fee_share` is 16% on the live public pool. The 1 Gram `FINALIZE_ROUND_FEE` and the governance fee are taken on the round's yield, not on principal. `total_balance += profit` while `supply` stays fixed is exactly what makes each KTON redeem for more Gram over time. A round stat message (`interest_manager::stats`) reports `borrowed`, `returned`, `profit`, the new `total_balance`, and `supply` to the interest manager, who may then retune `interest_rate` for future rounds.

## Why this is the entire yield engine

There is no other yield path. The Pool's only productive use of capital is lending to controllers, and the only return is the fixed interest (or the larger profit-share) that controllers fold back through `loan_repayment` and `finalize_lending_round`. Validator performance determines realized profit: a validator that wins elections and validates honestly returns interest on schedule, while a slashed or absent validator can produce zero or negative round profit, which is socialized across all holders down to the `-total_balance` floor. Loan sizing (`creditable_funds`, the disbalance cap, per-validator min/max) bounds the risk any one validator poses, and the election-window gating plus the same-round credit rule ensure borrowed Gram is only ever exposed for the single election it was borrowed for.

## Op-code and constant reference

| Name                                           | Op-code / value                       | Source                      |
| ---------------------------------------------- | ------------------------------------- | --------------------------- |
| `controller::send_request_loan`                | `0x6335b11a`                          | op-codes.func               |
| `pool::request_loan`                           | `0xe642c965`                          | op-codes.func               |
| `controller::credit`                           | `0x1690c604`                          | op-codes.func               |
| `controller::new_stake` / `elector::new_stake` | `0x4e73744b`                          | op-codes.func               |
| `elector::new_stake_ok` / `_error`             | `0xf374484c` / `0xee6f454c`           | op-codes.func               |
| `controller::recover_stake`                    | `0xeb373a05`                          | op-codes.func               |
| `elector::recover_stake`                       | `0x47657424`                          | op-codes.func               |
| `elector::recover_stake_ok` / `_error`         | `0xf96f7324` / `0xfffffffe`           | op-codes.func               |
| `pool::loan_repayment`                         | `0xdfdca27b`                          | op-codes.func               |
| `controller::return_unused_loan`               | `0xed7378a6`                          | op-codes.func               |
| `controller::update_validator_hash`            | `0xf0fd2250`                          | op-codes.func               |
| `SHARE_BASIS`                                  | `16,777,216` (2^24)                   | types.func                  |
| `DISBALANCE_TOLERANCE_BASIS`                   | `512` (2 << 8)                        | pool.func                   |
| `MAX_LOAN_DICT_DEPTH`                          | `12`                                  | pool.func                   |
| `FINALIZE_ROUND_FEE`                           | `1 Gram`                              | pool.func                   |
| `MIN_TONS_FOR_STORAGE` (pool / controller)     | `10 Gram` / `2 Gram`                  | pool.func / controller.func |
| `ELECTOR_OPERATION_VALUE`                      | `1.03 Gram`                           | controller.func             |
| `MIN_REQUEST_LOAN_VALUE`                       | `1 Gram`                              | controller.func             |
| `MIN_STAKE_TO_SEND`                            | `50,000 Gram` (validator-stake floor) | controller.func             |
| `GRACE_PERIOD`                                 | `600 s`                               | controller.func             |
| `HASH_UPDATE_FINE` / `STAKE_RECOVER_FINE`      | `10 Gram` / `10 Gram`                 | controller.func             |

Next: **Round Lifecycle and Timing**


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.kton.io/protocol-internals/08-loan-and-validator-model.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
