> 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/07-protocol-architecture.md).

# Protocol Architecture

This chapter is for engineers and integrators. It describes the on-chain components of KTON, what each one is responsible for, how messages flow between them, and how addresses are derived. Everything here is taken from the contract source (`pool.func`, `controller.func`, the payout NFT contracts, and the shared `op-codes.func`) and from live on-chain state.

KTON is a liquid staking protocol on The Open Network (TON). Users deposit Gram (the TON network asset) and receive KTON, a rate-appreciating jetton. The pooled Gram is lent to validator-controllers, which stake it with the TON Elector and earn validation rewards. Realized loan interest, net of fees, is folded into the pool's accounting at each round's end, so 1 KTON redeems for an increasing amount of Gram over time (currently about 1.035 Gram). The supply is never rebased: balances stay fixed and the exchange rate moves instead.

## Component map

| Component                 | Source                               | Responsibility                                                                                                                                                                                                |
| ------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Pool**                  | `pool.func`, `pool_storage.func`     | Custodies Gram; mints and burns KTON through the jetton minter; extends per-round loans to controllers; runs round rotation and profit accounting; holds every governance role address. The central contract. |
| **KTON jetton minter**    | DAOJettonMinter (`jetton_dao`)       | Mints and burns KTON on the Pool's instruction. Its admin is the Pool. Standard TEP-74 DAO-jetton with off-chain metadata.                                                                                    |
| **KTON jetton wallet**    | DAOJettonWallet (`jetton_dao`)       | Holds one user's KTON balance. Supports DAO voting (can lock the balance during a vote). 9 decimals.                                                                                                          |
| **Controller**            | `controller.func`                    | One per validator per round parity. Borrows from the Pool, forwards the stake to the Elector, recovers the stake after the freeze period, and repays the loan. Authenticated purely by address.               |
| **Payout NFT collection** | `payout_nft/nft-collection.func`     | Per-round, per-side distributor. Mints one "bill" NFT per pessimistic deposit or withdrawal, then cascades a burn at round end to pay each holder pro-rata.                                                   |
| **Payout NFT item**       | `payout_nft/nft-item.func`           | A single user's receipt bill. Self-destructs on burn, paying its owner at burn time.                                                                                                                          |
| **TON Elector**           | TON system contract (config param 1) | Native validator election and stake escrow. Not a KTON contract; the protocol only sends it the standard `new_stake` / `recover_stake` messages.                                                              |

The KTON jetton master and the public Pool are deployed separately. The Pool is registered as the minter's admin, so only the Pool can mint or trigger burns. The Payout collections are deployed lazily by the Pool, one per round per side, as the first deposit or withdrawal of a round arrives.

Live addresses:

* Public Pool: `EQA9HwEZD_tONfVz6lJS0PVKR5viEiEGyj9AuQewGQVnXPg0`
* KTON jetton master: `EQBuIhXNNkWf9AW9miNGNTSO_uFZ23ejfIWrieXge5f733mw`

## The Pool contract

The Pool is the protocol's hub. Its storage holds the staking accounting (`total_balance`, `interest_rate`, the optimistic and `deposits_open?` flags), two round-data structures (`current_round_borrowers` and `prev_round_borrowers`, each a borrowers dict plus `round_id`, `active_borrowers`, `borrowed`, `expected`, `returned`, and `profit`), the loan bounds per validator, the governance fee, the minter and payout-side addresses with their supplies, the role addresses, and the code cells of its child contracts.

It keeps the codes of three child contracts because it needs them either to deploy children or to recompute and authorize their addresses:

* `controller_code` for controller address authorization,
* `payout_code` for deploying the deposit and withdrawal payout collections,
* `pool_jetton_wallet_code` for computing the deposit payout's KTON wallet address.

### Message dispatch

`recv_internal` parses the message flags and the body header `(op, query_id)`, loads storage, and branches on `op`. A handful of op-codes (from `op-codes.func`) define the public surface:

| Op                                 | Code         | Sender        | Effect                                                                           |
| ---------------------------------- | ------------ | ------------- | -------------------------------------------------------------------------------- |
| `pool::deposit`                    | `0x47d54391` | user          | Take `DEPOSIT_FEE`, mint KTON (optimistic) or mint a deposit bill (pessimistic). |
| `pool::withdraw`                   | `0x319b0cdc` | jetton minter | The burn-notification handler. Pay Gram instantly or mint a withdrawal bill.     |
| `pool::request_loan`               | `0xe642c965` | controller    | Size and grant a loan; emit `controller::credit`.                                |
| `pool::loan_repayment`             | `0xdfdca27b` | controller    | Close a loan; finalize the round if it was the last open loan.                   |
| `pool::touch`                      | `0x4bc7c2df` | anyone        | Force a round update via the get-data path.                                      |
| `pool::deploy_controller`          | `0xb27edcad` | validator     | Deploy a fresh controller for the sender.                                        |
| `pool::get_conversion_rate_unsafe` | `0x4b7b42e6` | anyone        | Reply with the projected balance and supply.                                     |

Governance op-codes (`sudo::*`, `governor::*`, `halter::*`, `interest_manager::*`, `treasury::*`) are covered in the Governance chapter and are omitted here.

One detail worth pinning down: the minter's `burn_notification` is wired to land directly on the withdraw handler. In `op-codes.func`, `payout::burn_notification` is defined as `pool::withdraw`, that is, both equal `0x319b0cdc`. So when a user burns KTON, the minter's notification arrives at the Pool already on the unstake code path. The handler first asserts the sender is the minter, then (inside a `try`) decodes the burned amount and the two custom-payload bits and either pays Gram immediately or queues a withdrawal bill. If anything throws after the minter check, the `catch` re-mints the burned KTON back to the user, so value is never lost on a failed exit.

### Mint and burn helpers

The Pool never holds KTON itself; it instructs the minter. `pool_mint_helpers.func` builds these messages:

* `request_to_mint_pool_jettons` sends `payout::mint` (`0x1674b0a0`) to `jetton_minter` and increments the in-memory `supply`. For user mints it carries the balance (mode `CARRY_ALL_BALANCE`); for payout distribution mints it carries an explicit forward amount.
* `request_to_mint_deposit` and `request_to_mint_withdrawal` mint a bill on the relevant payout collection. If that collection is not yet deployed for the current round (`deposit_payout` or `withdrawal_payout` is null), the helper first computes the collection's state-init, derives its address, deploys it with `payout::init` (`0xf5aa8943`), and stores the address before minting the first bill.

For the deposit side it also pre-computes the deposit collection's own KTON jetton wallet address (using `pool_jetton_wallet_code`) and passes it in the init message, because at round end the Pool mints real KTON to that collection, which then distributes it.

## The KTON jetton (minter and wallets)

KTON is a standard DAO-jetton (TEP-74 with voting-capable wallets), 9 decimals, mintable, with the Pool as admin. Two op-codes connect it to the Pool:

* Mint: the Pool sends `payout::mint` / `0x1674b0a0` to the minter, which forwards an `internal_transfer` to the recipient's wallet.
* Burn: a user sends `jetton::burn` (`0x595f07bc`) to their own wallet; the wallet decreases its balance and notifies the minter, which sends a burn notification (op `0x319b0cdc`) to the Pool. As noted above, that op is the same code as `pool::withdraw`, so the unstake is handled in one hop from the minter.

The exchange rate is derived from Pool state, not stored on the jetton: minting uses `jettons = muldiv(Gram, supply, total_balance)` and redeeming uses `Gram = muldiv(jettons, total_balance, supply)`, bootstrapping 1:1 when `supply == 0`.

## The Payout NFT collection and item (the "bills")

When a deposit or withdrawal cannot settle immediately, the user receives a burnable payout NFT, a "bill", that records their share of the round. There are two collections per round, one for deposits and one for withdrawals, each deployed lazily by the Pool.

The collection stores `issued_bills` (the running total of bill weight), its admin (the Pool), an optional `distribution` cell, the collection content, and a small linked-list cursor (`next_item_index`, `prev`, `current`, `next`, and the next item's state-init). Each item stores `inited?`, `owner`, `collection`, `index`, the bill `amount`, and `prev` / `next` pointers. Bill op-codes live in `payout_nft/op-codes.func`:

| Op                   | Code         | Meaning                                  |
| -------------------- | ------------ | ---------------------------------------- |
| `mint_nft`           | `0x1674b0a0` | Mint a bill of a given weight to a user. |
| `start_distribution` | `0x1140a64f` | Begin paying out a round's bills.        |
| `burn`               | `0xf127fe4e` | Burn one bill.                           |
| `burn_notification`  | `0xed58b0b2` | Item-to-collection burn report.          |
| `init_nft`           | `0x132f9a45` | Initialize a newly deployed item.        |

Bill weight differs by side. A deposit bill's weight is the Gram deposited; a withdrawal bill's weight is the KTON redeemed. At round end the Pool fires `start_distribution` at the collection (see below), and the collection walks the linked list of bills, burning each one in a single automatic cascade. Each burned item pays its owner-at-burn-time `muldiv(bill_weight, remaining_volume, issued_bills)`, then self-destructs.

This is why the bill must not be transferred or sold. The payout goes to whoever owns the bill at burn time, and the on-chain metadata literally warns against sending it on to other contracts. A user also cannot usefully burn a bill early: the collection rejects a burn before distribution has started.

## The Controller contracts

A controller is the bridge between the Pool's Gram and the TON Elector. The Pool itself never talks to the Elector; controllers do. There is one controller per validator per round parity (even and odd rounds), so a validator participating continuously runs two controllers.

Controller storage holds its `state` (rest, sent borrowing request, sent stake request, funds staken, or halted) plus an active-credit flag, the four authorization addresses, round-control fields (`saved_validator_set_hash`, `validator_set_changes_count`, `validator_set_change_time`, `stake_held_for`), the funds fields (`borrowed_amount`, `borrowing_time`), and the V2 fields `allocation`, `allowed_borrow_start_prior_elections_end`, `approver_set_profit_share`, and `acceptable_profit_share`.

Key controller op-codes (from `op-codes.func`):

| Op                               | Code         | Direction                                           |
| -------------------------------- | ------------ | --------------------------------------------------- |
| `controller::send_request_loan`  | `0x6335b11a` | validator to controller                             |
| `controller::credit`             | `0x1690c604` | pool to controller                                  |
| `controller::new_stake`          | `0x4e73744b` | validator to controller, then controller to Elector |
| `controller::recover_stake`      | `0xeb373a05` | trigger to controller                               |
| `controller::return_unused_loan` | `0xed7378a6` | trigger to controller                               |
| `controller::top_up`             | `0xd372158c` | pool deploy / funding                               |
| `controller::approve`            | `0x7b4b42e6` | approver                                            |

The controller borrows only while the election window is open, forwards the borrowed Gram to the Elector with `elector::new_stake` (`0x4e73744b`), and after the validator-set changes and the stake-held period elapse it recovers the stake with `elector::recover_stake` (`0x47657424`). On the Elector's `recover_stake_ok`, the controller computes `profit = received - stake_sent` and repays the Pool the greater of the full fixed-interest debt or the principal plus the agreed profit share:

```
amount_to_return = max(
    borrowed_amount,
    muldiv(borrowed_amount, SHARE_BASIS, SHARE_BASIS + interest)
        + muldiv(approver_set_profit_share, profit, SHARE_BASIS)
)
```

It then sends `pool::loan_repayment` (`0xdfdca27b`) back to the Pool. The detailed loan lifecycle, watchdog fines, and the profit-share model are covered in the next chapter.

### Controller address derivation

Controllers are authenticated by address, not by a stored owner field, so the address must be reproducible from a fixed set of inputs. `build_controller_address` in `pool.func` assembles the controller's static data from:

```
[ controller_id, validator, pool (my_address), governor, approver, halter ]
```

(the approver and halter are packed together in a referenced cell). That static data goes into `controller_init_state`, and `calc_address` hashes the state-init to produce the address in the controller workchain.

The practical consequence: the controller address is bound to the validator and to the pool, governor, approver, and halter role addresses. Rotating any of those addresses (for example, changing the governor) changes every controller address. The old controller would no longer pass the Pool's address check, so the validator must deploy a fresh controller. The Pool exposes `get_controller_address(controller_id, validator)` as a get-method so off-chain code can compute the same address.

When a validator sends `pool::deploy_controller` (`0xb27edcad`), the Pool rebuilds the address and init-state for the sender and deploys the controller with `controller::top_up` (`0xd372158c`), carrying the remaining message value.

## End-to-end message flows

### Deposit (optimistic, the live mode)

Both live pools run with the optimistic flag on, so a deposit mints KTON immediately.

```
User ──pool::deposit (0x47d54391)──▶ Pool
        Pool: deduct DEPOSIT_FEE (1 Gram), require deposit_amount > 0
        Pool: amount = muldiv_extra(deposit_amount, expected_supply, expected_balance)   ; projected end-of-round rate
Pool ──payout::mint (0x1674b0a0)──▶ KTON minter ──internal_transfer──▶ User's KTON wallet
        Pool: total_balance += deposit_amount
```

In pessimistic mode the Pool instead mints a deposit bill (`request_to_mint_deposit`), and the real KTON is minted at round finalization.

### Unstake (queued / pessimistic, the real exit path)

A withdrawal is not instant. On the live pools the instant path is economically disabled (the instant-withdrawal fee is set to roughly 100%), so the genuine exit is the queued path that settles when the round finalizes, about 36 hours.

```
User ──jetton::burn (0x595f07bc)──▶ User's KTON wallet
Wallet ──burn_notification (0x319b0cdc == pool::withdraw)──▶ Pool
        Pool: assert sender == minter; require msg_value > WITHDRAWAL_FEE (0.5 Gram)
        Pool: approximate_amount = muldiv(jetton_amount, total_balance, supply)
        (instant branch only if optimistic ON, user opted in, state NORMAL, and liquidity suffices)
Pool ──mint_nft (0x1674b0a0)──▶ Withdrawal payout collection ──▶ Withdrawal bill to User
        ...
        at round finalization:
Pool ──start_distribution (0x1140a64f)──▶ Withdrawal collection
        collection cascades burns of all bills; each pays its owner muldiv(weight, remaining, issued_bills)
```

If the instant branch's `fill_or_kill` bit was set and instant liquidity is unavailable, the burn reverts (throw 105) and the KTON is refunded.

### Loan and round settlement

```
Validator ──send_request_loan (0x6335b11a)──▶ Controller
Controller ──pool::request_loan (0xe642c965)──▶ Pool
        Pool: re-check election window; size actual_loan; interest = muldiv(actual_loan, interest_rate, SHARE_BASIS)
Pool ──controller::credit (0x1690c604)──▶ Controller        ; message value = cash, body = principal + interest (debt)
Validator ──controller::new_stake (0x4e73744b)──▶ Controller ──elector::new_stake──▶ Elector
        ... validator-set changes and stake-held period elapse ...
Controller ──elector::recover_stake (0x47657424)──▶ Elector ──recover_stake_ok──▶ Controller
Controller ──pool::loan_repayment (0xdfdca27b)──▶ Pool
        Pool: close_loan; if last open loan, update_round() finalizes deposits/withdrawals and folds profit into total_balance
```

When the last loan of a round repays, `update_round` calls `finalize_deposit_withdrawal_round`, which (a) sends Gram to the withdrawal collection's `start_distribution` and decreases `supply` by the redeemed amount, and (b) mints the round's KTON to the deposit collection and increases `total_balance` by the deposited Gram. Round profit is added to `total_balance` while `supply` is unchanged, which is the auto-compounding mechanism: each KTON appreciates without any holder action and without rebasing.

## Component responsibilities at a glance

* **Pool**: custody, accounting, rate math, loan sizing, round rotation, role enforcement. Holds no KTON; never contacts the Elector directly.
* **Jetton minter / wallet**: the KTON token. Mint and burn only on the Pool's instruction (minter admin = Pool). Wallets hold balances and support DAO voting.
* **Controllers**: per-validator, per-parity loan and staking agents. Talk to the Elector; repay the Pool. Address-authenticated, so role rotation forces redeployment.
* **Payout collections / items**: round-scoped, side-scoped distributors of bills. Cascade-burn at round end and pay each holder pro-rata. Bills must never be transferred.
* **Elector**: the native TON system contract the controllers stake with. Outside the protocol's trust boundary; only the standard stake messages cross it.

Next: **The Loan and Validator Model**


---

# 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/07-protocol-architecture.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.
