For the complete documentation index, see llms.txt. This page is also available as Markdown.

Governance, Roles and Upgrades

KTON's pool is not controlled by a single owner key, and it is not controlled by token-holder voting at the pool level. Instead, authority is split across eight named role slots, each holding a specific, limited power. Some powers carry on-chain timelocks (a mandatory waiting period before the action takes effect), and the most sensitive power of all, replacing the contract's code, is locked behind the longest timelock and a separate role from the one that governs day-to-day parameters. This chapter walks through every role, the separation of powers between them, the code-upgrade mechanism, the governor-migration handshake, and the halt/unhalt safety controls. All op-codes, fields, and constants below are cited from the deployed FunC sources.

The eight role slots

The pool packs all of its authority into one storage tuple. The shape is defined in roles_helper.func and loaded by ~load_roles in pool_storage.func:

roles = [sudoer, sudoer_set_at, governor, governor_update_after,
         interest_manager, halter, approver, treasury]

The accessors in roles_helper.func index this tuple by position (0 INDEX for sudoer, 1 INDEX for sudoer_set_at, and so on through 7 INDEX for treasury). Two of the eight slots are not addresses at all: sudoer_set_at (index 1) and governor_update_after (index 3) are 48-bit timestamps that drive the timelocks described later. The other six slots are contract addresses (slices). Any documentation that lists only six roles, and omits the two timestamp slots, is describing an older layout.

~load_roles parses the addresses across two cells: sudoer, sudoer_set_at, governor, governor_update_after, and interest_manager sit in the first cell, then a referenced cell holds halter, approver, and treasury. There is a backward-compatibility detail worth noting: if the inner cell has run out of bits when the parser reaches the treasury slot (ds.slice_bits() ? ... : interest_manager), the treasury defaults to the interest-manager address. save_roles writes the same two-cell structure back.

Slot
Index
Type
Held by

sudoer

0

address

Superuser key (code/data upgrades, arbitrary messages)

sudoer_set_at

1

timestamp (48-bit)

When the current sudoer was installed; starts the 2-day clock

governor

2

address

Central day-to-day authority

governor_update_after

3

timestamp (48-bit)

Earliest time a prepared governor migration may execute

interest_manager

4

address

Sets interest rate and loan operational params

halter

5

address

Emergency stop (halt / partial-halt)

approver

6

address

Approves controllers to borrow (used on the controller side)

treasury

7

address

Passive fee sink

Each role's powers

Governor

The governor is the central operating authority. In pool.func it is the only sender accepted for the following operations (each guarded by assert_sender!(sender_address, get_governor(roles))):

  • governor::set_sudoer (0x79e7c016): installs a new sudoer address and stamps sudoer_set_at to now(), which restarts the sudoer timelock.

  • governor::unhalt (0x7247e7a5): clears halted? back to false.

  • governor::prepare_governance_migration (0x9971881c): arms a future governor change (the first step of the two-step migration).

  • governor::set_roles (0x5e517f36): updates governor (only after migration matures), interest_manager, halter, approver, and treasury.

  • governor::set_deposit_settings (0x9bf5561c): sets optimistic_deposit_withdrawals, deposits_open?, and instant_withdrawal_fee.

  • governor::set_governance_fee (0x2aaa96a0): sets governance_fee_share.

Crucially, the governor cannot upgrade the contract's code and cannot set the interest rate. Those powers belong to the sudoer and the interest-manager respectively. This is the core of the separation of powers: the role that tunes economics day to day is structurally unable to rewrite the contract or move the validator interest rate.

Sudoer

The sudoer is the superuser, but it is the most constrained in time. It can do two things, both handled in sudoer_requests.func and both gated by the 2-day quarantine:

  • sudo::send_message (0x270695fb): process_sudo_request loads an 8-bit send mode and a message cell, then issues send_raw_message(message, mode). This lets the sudoer emit an arbitrary message with an arbitrary send-mode on behalf of the pool.

  • sudo::upgrade (0x96e7f528): process_sudo_upgrade_request loads up to three maybe-refs, data, code, and after_upgrade. If data is present it calls set_data(data); if code is present it calls set_code(code); if after_upgrade is present it executes that continuation. It then throw(1) to unwind.

Both sudo handlers begin with assert_sender!(sender, sudoer) and throw_unless(sudoer::quarantine, now() > sudoer_set_at + SUDOER_QUARANTINE). The only guardrail on the sudoer is the timelock; once the quarantine has elapsed, the sudoer can replace code and storage outright. The sudoer cannot install itself: only the governor can call set_sudoer, and doing so resets the clock.

Interest-manager

The interest-manager governs the loan economics, not the user-facing pool flags. In pool.func, guarded by assert_sender!(sender_address, get_interest_manager(roles)) and assert_state!(state::NORMAL):

  • interest_manager::set_interest (0xc9f04485): sets interest_rate (a 24-bit share value).

  • interest_manager::set_operational_params (0x4485c9f0): sets min_loan_per_validator, max_loan_per_validator (with max >= min enforced via error::contradicting_operational_params), disbalance_tolerance (an 8-bit value, DISBALANCE_BIT_SIZE), and credit_start_prior_elections_end.

The interest-manager also receives per-round statistics. It cannot touch the governance fee, the sudoer, the deposit settings, or the halt state.

Halter

The halter is the emergency-stop role. It can pause but never resume:

  • halter::halt (0x139a1b4e): sets halted? = true. The handler intentionally does not end_parse the message, so any trailing data can carry an off-chain reason.

  • halter::partial_halt (0x77778888): reads two booleans, stop_optimistic and close_deposits. Setting the first flips optimistic_deposit_withdrawals to false; setting the second flips deposits_open? to false.

The halter cannot call unhalt. Only the governor can clear halted?. This split means an emergency key can freeze the pool quickly but cannot quietly un-freeze it.

Approver

The approver lives on the controller side of the protocol. It approves controllers to request loans and is part of the profit-share setup (approver_set_profit_share). It has no authority over pool state, fees, halting, or upgrades.

Treasury

The treasury is a passive fee sink. It receives the governance fee and other protocol fees routed out of the pool. It holds no operational authority of any kind: it cannot send pool messages, change parameters, halt, or upgrade.

Separation of powers, at a glance

Action
Required role
Timelock

Replace code / data (sudo::upgrade)

sudoer

2 days (SUDOER_QUARANTINE)

Send arbitrary pool message (sudo::send_message)

sudoer

2 days (SUDOER_QUARANTINE)

Install a new sudoer (set_sudoer)

governor

none (but resets sudoer clock)

Prepare governor migration

governor

none to arm

Execute governor migration (via set_roles)

governor

1 day (GOVERNOR_QUARANTINE)

Set interest_manager / halter / approver / treasury

governor

none (instant)

Set interest rate / loan params

interest_manager

none

Set deposit settings / governance fee

governor

none

Halt / partial-halt

halter

none

Unhalt

governor

none

No single role can both rewrite the contract and operate it day to day; the upgrade key (sudoer) and the operating key (governor) are deliberately different roles, and the only role that can mint a new sudoer (governor) cannot itself perform the upgrade.

The upgrade mechanism

Code upgrades are sudoer-only, executed through sudo::upgrade (0x96e7f528), and they are not possible until the sudoer has been in place for at least SUDOER_QUARANTINE. From sudoer_requests.func:

The flow is:

  1. The governor installs the intended sudoer with governor::set_sudoer, which stamps sudoer_set_at = now().

  2. Two days must pass. Until then, every sudo::upgrade (and every sudo::send_message) reverts with sudoer::quarantine (0xa000).

  3. After the quarantine, the sudoer sends sudo::upgrade with optional data, code, and after_upgrade refs. The handler applies set_data and/or set_code, optionally runs the after_upgrade continuation, then unwinds.

Re-installing the sudoer (calling set_sudoer again) restarts the two-day clock, because sudoer_set_at is reset to the new now(). There is no governor-driven code upgrade, and there is no on-chain version-bump operation. The pool exposes get_code_version() in versioning.func, which simply returns a compile-time git_hash from auto/git-hash.func. That value is informational only; it is not a control surface and does not change as part of any upgrade transaction.

Governor migration (two-step, 1-day timelock)

Replacing the governor itself is a deliberate two-step handshake with its own timelock, defined in pool.func:

Step one, governor::prepare_governance_migration (0x9971881c): the current governor submits a governor_update_after timestamp. The handler stores it via set_governor_update_after, then enforces throw_unless(error::governor_update_too_soon, governor_update_after - now() > GOVERNOR_QUARANTINE), that is, the proposed effective time must be more than one day in the future. The error code for proposing too soon is error::governor_update_too_soon (0xa001).

Step two, governor::set_roles (0x5e517f36): the governor change is applied only inside set_roles, and only when its first boolean flag is set. At that point the handler enforces throw_unless(error::governor_update_not_matured, get_governor_update_after(roles) < now()), so the prepared timestamp must already have passed. The new governor address is then installed and governor_update_after is set to a sentinel far-future value (0xffffffffffff) so the slot cannot be re-used without a fresh prepare step. The error code for executing before maturity is error::governor_update_not_matured (0xa003).

The same set_roles message can also update interest_manager, halter, approver, and treasury in the same transaction, each behind its own boolean flag. Those four role changes are instant: only the governor slot itself carries the 1-day maturity check. This is the practical asymmetry of the model: rotating the emergency, interest, approval, and fee-sink keys is immediate, but rotating the central governor key requires a public, day-long waiting window.

Halt, partial-halt, unhalt and self-halt

Halting is the pool's circuit breaker. There are three ways it can engage and exactly one way it can be cleared.

Full halt. halter::halt (0x139a1b4e) sets halted? = true. Once halted, assert_not_halted!() (defined in asserts.func) makes the gated operations revert. Halting blocks deposits, loan requests, loan repayments, round upkeep, controller deployment, and similar pool operations.

Partial halt. halter::partial_halt (0x77778888) is a softer brake. It can flip optimistic_deposit_withdrawals off (forcing the queued, pessimistic path) and/or flip deposits_open? off (closing new deposits), without setting the global halted? flag.

Self-halt. The pool can halt itself. If, at round finalization, it cannot cover the queued withdrawals plus its storage reserve (MIN_TONS_FOR_STORAGE, 10 Gram), it sets halted? = true on its own and pays nobody until it is topped up. No human key is involved in this path; it is a safety latch.

Withdrawals stay open. This is the most important property of the halt design. The withdraw handler runs assert_not_halted!() inside a try block, so a halt during withdrawal does not destroy value: the burned KTON is re-minted back to the user (the catch branch calls request_to_mint_pool_jettons(from_address, jetton_amount, query_id, true)). The burn-notification op (pool::withdraw) is the path a user's KTON burn lands on, and it is structured so that any revert refunds the user's KTON rather than losing it. In other words, halting can stop new deposits and loan activity, but it is built so the protocol can always either pay out or hand back the user's tokens.

Only the governor unhalts. Clearing the halt requires governor::unhalt (0x7247e7a5), guarded by assert_sender!(sender_address, get_governor(roles)). The halter that pressed the button cannot release it; resumption is a separate, higher-trust role. This guarantees that an emergency stop is reviewed by the governing key before normal operation resumes.

Why this model matters

The role split turns a single point of failure into a layered one. The key that can rewrite the contract (sudoer) is different from the key that runs it (governor), is different again from the key that can freeze it (halter), and the key that profits from fees (treasury) has no power at all. The two most dangerous transitions, replacing code and replacing the governor, are both delayed by on-chain timelocks (2 days and 1 day respectively) that are visible to anyone watching the chain before they take effect. None of these controls are token-voted at the pool level; they are address-keyed with mandatory waiting periods, which is the protocol's chosen form of trust minimization.

Next: Payout NFT Receipts

Last updated