Author: Hover Labs, [email protected]
Kolibri is a protocol that issues kUSD, an algorithmic stable coin that is soft pegged to the US dollar. Kolibri functions by using over collateralized debt positions / CDPs (called “ovens”) which collateralize a synthetic asset. When synthetic assets are borrowed against a CDP, an interest rate (called a “stability fee”) is charged.
As of today, the stability fee is placed into two funds: (1) the stability fund and (2) the developer fund. The developer fund is meant to be a discretionary fund used to fund development projects on Kolibri. The stability fund is meant to be a liquidator of last resort, who will help to liquidate underwater ovens (Oven’s whose collateral is worth less than the loan).
Interest on ovens is accrued every 60 seconds (according to the timestamps in Tezos blocks). Interest accruals are done via linear approximation each time an action is taken on an oven. If an oven owes
i kUSD in loans and fees, then the interest that an oven owes is:
newInterest = i * (1 + (interestRatePerPeriod * numPeriods))
If we know there are
n ovens and that they each owe
in in interest, then the total interest owed to they system is:
totalInterest = i0
+ … + in-1
In practice, interest accumulation in the Kolibri system is more complicated, since computation on blockchains are both bounded (ex. We can’t calculate
n steps without running out of gas) and expensive (each calculation costs money).
To solve this problem, Kolibri uses a global accumulator (stored in the Minter) and a series of local accumulators (stored in each oven).
The global accumulator started at 1.0. Each time the Minter is interacted with, it updates the global accumulator according to the following formula:
newGlobalInterestIndex = oldGlobalInterestIndex * (1 + (interestRatePerPeriod * numPeriods))
When a new oven is created, it sets its local accumulator to the current value of the global accumulator. The oven interacts with the Minter, we can determine the newly accrued interest since the oven last interacted with the Minter, by comparing the values of local and global accumulators:
accumulatorRatio = globalAccumulator / localAccumulator newInterestOwedByOven = accumulatorRatio * totalPrinciple
And the local accumulator is set to the global accumulator.
The oven keeps track of two variables:
- Amount borrowed
- Amount owed in interest
When a repayment is processed, the repayment is first credited against the amount owed in interest, and then the remaining amount credited to the the amount borrowed. Any interest amounts are paid to developer and stability funds, and any amount of borrowed principle is simply burnt.
This system has the limitation that the developer and stability fund only receive interest at repayment time. This means that fund inflows to the developer and stability fund are highly dependent upon the amount of kUSD being repaid. Since the stability fund acts as a last defense for the protocol, and several  proposals have been put forth to utilize the dev fund, it would be useful to have predictable and regular in flows to these funds at accrual time, rather than repayment time.
The local and global accumulator system shall remain intact. This system is required to know how much an individual oven owes, which is important to process liquidations and repayments.
A new global accumulator, total borrowed, will be introduced to the minter. This accumulator keeps track of the total amount of outstanding loans and interest the system has. When someone interacts with the minter, we can calculate the amount of interest accrued from the last interaction by:
newTotalBorrowedAmount = oldBorrowedAmount * (1 + (interestRatePerPeriod * numPeriods) newInterest = newTotalBorrowedAmount - oldBorrowedAmount
We could then change our algorithms such that:
- newInterestAmount is always minted to the developer and stability funds immediately
- On repayment all tokens are burned, regardless of if they repay interest or principle
- On liquidation, all tokens are burned, except for the liquidation fee, which is paid to the funds
- On borrow operations, after interest is accrued, totalBorrowed is increased by borrowAmount
- On repayment operations, after interest is accrued, totalBorrowed is decreased by repayAmount
- On liquidate, after interest is accrued, totalBorrowed is decreased by the total value of the oven being liquidated.
Lastly, tracking principle and interest amounts in the ovens are no longer useful. However, migrating existing ovens is prohibitively hard, and the developers of this proposal advocate for optionality in the future.
This scheme is useful because it allows us to recognize kUSD tokens that we know we are going to eventually realize, immediately. This provides more predictable cash inflows, as opposed to variable cash inflows when ovens are repaid.
This has several benefits:
The stability fund can grow faster, and provides a more resilient backstop against black swan events
The dev fund has a consistent income. As Kolibri decentralizes, it’s likely that community members may request money for the dev fund for recurring expenses (Twitter subscriptions, web hosting, etc)
KIP-010 proposes a savings rate, and having more recurring income streams provides the proposal more flexibility in the interest rate.
Implementation of the above algorithm in the Minter is trivial and reference code can be provided.
Migration to a new minter contract presents a more difficult set of tasks. The contract must:
Pass a 4 day voting period
Remain in the timelock for 3 days
When the timelock is executed, the new Minter contract must know the correct amount with totalAmount Borrowed. We can calculate this amount off chain and originate the contract, but it is nearly assured that the value will change during the duration of the 7 day governance process.
One possible way to do this is to allow the system to pause for a temporary period of time while the value is calculated or initialized. One could do this by adding two additional pieces of state to the minter:
isMigrated(boolean): Whether the Minter has been migrated
migrationOwner(address): The address who is allowed to perform the migration.
All operations in the minter (borrow, repay, deposit, withdraw, liquidate) would fail if migrated was false.
A migration entrypoint could be defined as follows:
def migrate(self, newBorrowedAmount): sp.verify(sp.sender == self.data.migrationMultisig, “Not allowed to migrate”) sp.verify(self.data.isMigrated == False, “already migrated”) self.data.isMigrated = True self.data.totalBorrowedAmount = newBorrowedAmount
This would allow us to ferry a value calculated off chain to be on chain. There are several possibilities for a migrator, including:
- A centralized entity (Hover Labs, a community member, or otherwise)
- A multisig compromised of trusted community members
- A full on governance vote in either the Kolibri DAO or a new DAO originated specifically for this purpose
Each of these approaches has risk in the time the system will remain paused for. Importantly, the longer the system is paused, the more likely it is that the price of XTZ/USD will drift and ovens may become undercollateralized (or underwater)!
Since the global interest index will not continue to update while the system is disabled due to migration, any interest that would have occurred during that period is effectively forgiven. With a 19.5% stability fee and 4.9M outstanding kUSD, this means that the protocol will lose about $1.80 in interest per minute while it is paused, which is negligible.
Recovery in Case of Failure
As a final defense, the system also has a pause guardian that pauses the borrow, withdraw and liquidate functions, but not the repay or deposit functions. The system can be paused with a 1 of 3 multisig (held by Hover Labs) but can only be restarted via governance.
In a scenario where the system was unavailable due to a price drift, we could do a “slow restart” of the system by:
- Rotating in the new minter (all functions disabled due to migration)
- Pausing the system with the pause guardian (all functions disabled due to migration, borrow/withdraw/liquidate disabled due to pause)
- Migrating the system (enables deposit/repay, which allows users to shore up any positions that may have materially changed)
- Unpausing the system via Governance.
To be clear, due to the seven day delay on restarting the system, the pause guardian should be used as a last resort, and not as the plan of record.
To make migrations easier in the future, the Minter should provide getters (or Views) for the global interest index and the total borrowed amount. This would effectively remove the need for any off chain coordination during future migrations.