Tinlake is Centrifuge's initial Ethereum-based, open, decentralized smart-contract based platform of asset pools bringing together pool issuers looking for financing and investors who seek to utilize the full potential of decentralized finance (DeFi).
Note that Tinlake has been replaced by the Centrifuge App, however legacy Tinlake pools as described below are still live on the App. Newer pools do not use the below smart contracts, but rather are launched on Centrifuge Chain.
Issuers can responsibly bridge real-world assets, such as invoices, mortgages or streaming royalties into DeFi and access bankless liquidity. They do this by tokenizing their financial assets into Non-Fungible Tokens (“NFTs”), using these NFTs as collateral in Tinlake Pools to draw funding.
Borrowers (the issuers) have individual assets with varying terms and varying durations drawn against their collateral. The collateral is represented as an NFT, which needs to be locked in the Tinlake contracts to draw a loan from it. Pooling the individual assets removes the cumbersome need of p2p financing for matching maturities, risk, and interest rates, and it allows investors to invest in a diversified portfolio of real world assets.
Investors earn yield on TIN and DROP, Tinlake’s two investment tokens that are minted in exchange. TIN, known as the “risk token,” takes the risk of defaults first but also receives higher returns. DROP, known as the “yield token,” is protected against defaults by the TIN token and receives stable (but usually lower) returns. This is similar to junior/senior investment structures common in traditional finance.
Tinlake is implemented in Solidity and deployed on Ethereum mainnet. The source-code can be found on Github:
Repository | Desc |
---|---|
Main Tinlake Repository | |
Tinlake Math Libary. Adopted from ds-math. | |
Tinlake ERC20 implementation. Re-uses the ERC20 implementation from DAI. | |
ERC721 implementation used for Tinlake. | |
Tinlake deploy scripts build with bash and seth | |
Tinlake proxy contract used for the borrower interactions | |
Tinlake actions is a libary used via delegate call by the proxy | |
Tinlake authentification pattern libary | |
Tinlake Maker adapter. MIP22 implementation | |
Tinlake RPC tests for the Maker integration. Tests run against live Mainnet or Kovan deployment. | |
Tinlake Maker integration system tests. | |
Tinlake Mainnet spells (changes on Mainnet deployments) | |
Tinlake Kovan spells (changes on Mainnet deployments) | |
Tinlake pool management | |
Contract for cancelling all orders in a pool. (Required for contract upgrades) |
Centrifuge has performed multiple audits of its codebase:
All audit reports can be found here.
No technology is perfect or perfectly secure. Centrifuge believes that working with skilled security researchers across the globe is crucial in identifying weaknesses in any technology. We welcome the contribution of external security researchers and look forward to awarding them for their invaluable contribution to the security of all our users. Read more about security here.
Each Tinlake pool is an individual deployment of the set of smart contracts. A full list of all deployed pools, including metadata, can be found here.
The design of Tinlake was inspired by the design of the MakerDAO contracts. Some design patterns and best practices like the auth pattern
have been adopted in the Tinlake contracts.
The current version of Tinlake v0.3.5 has around 15 contracts.
It is important to notice that a deployment of Tinlake can only manage one pool. The current multiple pools on tinlake.centrifuge.io are re-deployments of the same source code.
The reasoning behind this is to keep the logic as simple as possible.
At its core, Tinlake has two main modules:
Each of the modules consists of multiple contracts written in Solidity.
The main purpose of the lender module is to handle the investor requests and maintain the pool constraints.
On the other hand, the borrow module keeps track of the individual loans from the asset originators.
The Borrow module contract handles the individual loans of the issuers.
The main actions of an issuer are:
On the lender side of Tinlake, the there are three main interactions:
A user can create a supply order by locking DAI/stablecoin in Tinlake.
1Example:2- Alice wants to invest 100 DAI into the seniorTranche (DROP).3- She calls supplyOrder on the senior operator contract.4- Her DAI is transferred to the seniorTranche contract.5- The epoch is closed and the current DROP price of 1.5 DAI is calculated6- 60% of all supplyOrders can be fulfilled7- Alice calls the disburse method and she receives8 - (100 DAI* 0.6) / 1.5 = 40 DROP tokens on her address9- The remaining 100-60 = 40 DAI are a new supplyOrder in the next epoch10- Alice calls supplyOrder and changes the order to zero DAI11 - Alice receives the 40 DAI back
A user can redeem their DROP or TIN tokens in exchange for DAI/stablecoin with a redeem order.
The disburse method can be called to collect tokens from an executed supply order or DAI/stablecoin from a successfully executed redeem order.
For creating a new redeem or supply order, the disburse method needs to be called before.
The token price of a supply or redeem order is defined by the epoch
and not by the price.
From a high level perspective, the lender contracts need to track the investments, perform the interest rates calculations, and ensure that the junior tranche takes the losses first.
The following variables are needed to track the state of the these actions.
The total currency (ERC20) locked in the reserve contract. In live Tinlake pools as of time of writing, the currency is the DAI stablecoin.
Net Asset Value of all outstanding loans.
The pool value is the total value in a Tinlake pool. It includes the currency in the reserve and net asset value of the outstanding loans.
The seniorAsset is the amount which belongs to the senior investor (DROP) in a pool.
Expected SeniorAsset
SeniorDebt
SeniorDebt is the amount which accrues interest for the senior tranche.
SeniorBalance SeniorBalance is the amount of the seniorTranche which is not used for interest accumulation.
Example
1Tinlake Pool:2------------------------------------------3| NAV: 80 DAI | juniorAsset: 10 DAI |4| Reserve: 20 DAI | seniorAsset: 90 DAI |5------------------------------------------67In this pool, 80% of the pool value is used for loans.8Therefore, 80% of the seniorAsset should be used for interest accumulation.910seniorDebt: 90 DAI * 0.8 = 72 DAI11seniorBalance: 90 DAI - 72 DAI = 18 DAI1213Let's say the interest rate is 10%.14The seniorDebt would increase in one time period.1516seniorDebt: 72 DAI * 1.10 = 79.2 DAI17seniorBalance: = 18.0 DAI18seniorAsset: 97.2 DAI
The senior value represents the value of the senior/DROP tranche.
It is calculated as:
If loans are defaulting, the juniorAsset would cover the losses. If the entire juniorAsset is lost, the poolValue could be lower than the expectedSeniorAsset.
The poolValue can be also expressed as:
The juniorAsset is the amount of the poolValue which belongs to junior investors (TIN
).
The difference between the seniorAsset value poolValue is the juniorAsset.
In case of losses, they are first covered by the junior investors.
seniorSupply Is the total amount of minted ERC20 DROP tokens.
juniorSupply Is the total amount of minted ERC20 TIN tokens.
The seniorAssetRatio is defined as:
It describes the percentage of the poolValue which belongs to senior investors.
The juniorRatio is an important metric in the pool because it defines the protections of the junior investors.
The percentage of loan defaults in a pool has to be higher than the juniorRatio until the senior investors are affected.
In the contracts the seniorRatio is used.
The following investor actions increase the juniorRatio:
The following investor actions decrease the juniorRatio:
The lender state variables in Tinlake are changing either because of:
In an epoch execution, the orders which can be fulfilled are changing the lender state
Amount of DAI available in the reserve.
Notation: new reserve in the next epoch after of the execution of the current. describes the current.
Net asset value of all ongoing loans expressed in DAI. The NAV is not impacted by the orders but relevant for the constraint calculation.
Note: This is a simplification of the seniorAsset formula and does not contain losses. (Not relevant for the solver).
Note: This is a simplification of the juniorAsset formula.
1The coordinator contract manages the epochs for the investors.
closeEpoch
1function closeEpoch() external minimumEpochTimePassed
closeEpoch
creates a snapshot of the current lender stateexecuteEpoch
1function executeEpoch() public
executeEpoch
1function submitSolution() public
submitSolution
1function submitSolution(uint seniorRedeem, uint juniorRedeem,2 uint juniorSupply, uint seniorSupply) public returns(int)
A Tinlake deployment has two tranche modules deployed:
A tranche module has four contracts
1The Tranche contracts maintains the orders of investors.
function supplyOrder
1function supplyOrder(address usr, uint newSupplyAmount) public auth orderAllowed(usr)
The supplyOrder
function can be used to place or revoke an order. The method is called by the operator contract. The initial call is initiated by an investor.
function redeemOrder
1function redeemOrder(address usr, uint newRedeemAmount) public auth orderAllowed(usr) {
The redeemOrder
function can be used to place or revoke a redeem.
function disburse
1function disburse(address usr, uint endEpoch) public auth returns (uint payoutCurrencyAmount, uint payoutTokenAmount, uint remainingSupplyCurrency, uint remainingRedeemToken)
The disburse
function can be used after an epoch is over to receive currency and tokens. The collection can be used over multiple epochs.
Example
1Alice:2supplyOrder: 100 DAI34Epochs:5epoch n: 40% of all orders can be fulfilled tokenPrice: 1.26epoch n+1: 30% of all orders can be fulfilled tokenPrice: 1.5789Alice calls the disburse function in epoch n+2:1011epoch n: 100 DAI * 0.4 /1.2 = 33.33 DROP12epoch n: supplyOrder(amountLeft) = 60 DAI13epoch n+1: 60 DAI * 0.3/1.5 = 12 DROP1415Disburse Amount: 33.33 DROP + 12 DROP = 45.33 DROP
The contract maintains the supply and redeem orders for epochs. One for DROP and one for TIN.
If an epoch gets executed, the tranche contract mints new token or transfers currency to reserve. At any point in time, the contract can hold tokens or the stablecoin-currency. The balances are not considered as part of the Tinlake reserve.
The contract maintains the supply and redeem orders for epochs. One for DROP and one for TIN.
If an epoch gets executed, the tranche contract mints new token or transfers currency to reserve. At any point in time, the contract can hold tokens or the stablecoin-currency. The balances are not considered as part of the Tinlake reserve.
A locked amount for a supply or redeem order can be changed as long as the epoch is still ongoing. On the other side, users might have successfully supplied or redeemed their tokens or currency but didn't collect them.
1The assessor contract keeps track of the state2and the constraints of lender module3.
changeSeniorAsset
1function changeSeniorAsset(uint seniorSupply, uint seniorRedeem) external auth
calcJuniorTokenPrice
1function calcJuniorTokenPrice(uint nav_, uint reserve_) public view returns (uint)
calcSeniorTokenPrice
1function calcSeniorTokenPrice(uint nav_, uint reserve_) public view returns(uint)
1The reserve contracts holds the currency and offers methods for deposit and payout.
deposit
1function deposit(uint currencyAmount) public auth
msg.sender
1The operator contract manages the allowances for investors.
The loanID itself is an ERC721 NFT contract called Title
.
We need to distinguish between three different NFTs used in the Tinlake contracts.
Collateral NFT
Title NFT
1The Shelf contract handles all loan related actions.
The collateral NFTs are locked in the Shelf contract.
issue
1function issue(address registry_, uint token_) external note returns (uint)
This is the first step in the loan process. It issues (or creates) a new loan in Tinlake. Issuing a new loan requires the ownership of a collateral NFT that will be locked in the next step of the loan creation process. It combines a collateral NFT with a loan ID.
lock
1function lock(uint loan) external owner(loan) note
Locks the collateral NFT in the shelf. This requires the ID of an issued loan, and the ownership of both the corresponding loan NFT and the collateral NFT.
borrow
1function borrow(uint loan, uint currencyAmount) external owner(loan)
This starts the borrow process of a loan. The method can only be called if the collateral NFT is locked.
Calling borrow informs the system of the requested currencyAmount. This requires a max ceiling
(~max borrow amount) for the collateral NFT to be defined by an oracle in the NAV feed.
If no max ceiling has been provided in the NAV feed contract, the maximum borrow amount would be zero.
withdraw
1function withdraw(uint loan, uint currencyAmount, address usr) external owner(loan) note
repay
1function repay(uint loan, uint currencyAmount) external owner(loan) note
unlock
1function unlock(uint loan) external owner(loan) not
close
1function close(uint loan) external note
1The pile contract manages the interest rate accumulations for loans.
The default implementation of the Pile allows creating of different interest rate groups and assigning each loan a rate group. Each interest rate group has an interest rate that is calculated on a per second compounding basis.
Its task is to report the outstanding debt for each loan with the method debt(uint loan) returns (uint)
.
The method accrue(uint loan)
needs to be called by the Shelf before any modification of the debt is made to update the current debt. This is to ensure that any other methods relying on that data (such as the Ceiling contract) get the most up to date debt().
Whenever decDebt
and incDebt
are called, first the debt is updated with the compounded interest and then the debt is increased or decreased by the specified amount.
incDebt
1function incDebt(uint loan, uint currencyAmount) external auth note
Increases the debt of a loan by a currencyAmount.
decDebt
1function decDebt(uint loan, uint currencyAmount) external auth note
debt
1function debt(uint loan) external view returns (uint)
claim
1function claim(uint loan, address usr) public auth note
1The collector contract can seize defaulted loans.
seize
1function seize(uint loan) external
file price
1function file(bytes32 what, uint loan, address buyer, uint nftPrice) external auth
collect
1function collect(uint loan, address buyer) external auth
The collector functionality is part of Tinlake, but it is not in active usage.
The main purpose of the NAV feed is to maintain the priced values of collateral NFTs and to calculate the NAV.
The NAV feed maintains risk groups for individual collateral NFTs.
A risk group has the following properties:
risk group ID
thresholdRatio
ceilingRatio
interestRate
recoveryRatePD
For a high level introduction to the NAV in Tinlake, please visit the Pool Valuation (NAV) documentation. In this document, the NAV formulas as seen as given.
The focus of this section is how to efficiently implement the NAV calculation on-chain in Solidity.
The reader should be familiar with the following financial concepts:
From a finance perspective, the current NAV implemented in Tinlake is a simple one-cash flow DCF (Discounted Cash Flow) valuation approach.
The idea is to have it on-chain for full transparency on how token prices are calculated in Tinlake.
The NAV contract serves two purposes in the Tinlake system.
The future value is the expected amount of a loan repayment. In the most cases all the loans will be fully repaid. However, a certain percentage may default. The underlying collateral will be sold and a certain amount should be recoverable.
In Tinlake this is expressed in the expected return factor.
It includes a given probability of default (PD) and loss given default (LGD) per each loan risk group.
This can be expressed as one variable.
1Expected Loss = PD * LGD
1Expected Return = 1 - ExpectedLoss
Note, Expected Return
is also denoted Recovery Rate in finance. In the Solidity contract, the variable is called recoveryRatePD.
Calculation for the future value of a loan.
1P.....principal (loan borrow amount)2i.....interest rate per second3m.....maturityDate (unixTimestamp)4now...unix timestamp now5ER....expected return factor (0-1)6FV.... future value of a loan
Example:
Alice wants to borrow 100 DAI for 2 years with 5% interest per year. The probability of default over two years is 1% and loss given default is 20%.
ExpectedLoss: 0.01 _ 0.2 = 0.002 ExpectedReturnFactor: 1 - 0.002 = 0.998 FV = 100 DAI _ 1.05^(2023-2021) _ 0.998 = 100 _ 1.05^2 * 0.998 = 110.0295 DAI
Note: For illustration, time and interest is in years instead of seconds.
1FV...future value of a loan2d....discount rate of a loan3m.....maturityDate (unixTimestamp)4now...unix timestamp now5PV.....present value of a loan
It is important to note that the present value of the loans is depending on the block.timestamp in Solidity.
Example:
Alice loan has a future value of 110.0295 DAI.
Let's assume a discount rate of 3.00%.
In the year 2022 the present value would be:
p = 110.0295/(1.03^(2023-2022)) = 106.82 DAI
Note: For illustration time and interest is in year instead of seconds.
The total discounting is the sum over all present values of the loans before the maturity date.
1td....total Discounting2pv.....present value of a loan
An overdue loan in Tinlake is defined as a loan with now > maturityDate
and isWrittenOff(loan) == false
.
Each individual loan can be immediately written off if maturityDate > now
. The standard write-off function is a public
method which can be called by everyone.
However, if a loan is not writtenOff, it is considered as overdue.
In that case the presentValue should be equal to future value until the loan is written-off.
For all loans where (maturityDate < now && isWrittenOff(i) == false)
If loan is not repaid after the maturity date, it will be moved into a write-group.
A write-off group has three attributes
The write-group has a different interest rate for the loan debt and a writeOff factor. Most Tinlake pools have around 3-4 different write off groups with different factors (0% - 100%.
1wf....write off factor2debt... debt of the loan
For all loans where (maturityDate < now && isWrittenOff(i) == true)
The debt will still continue to accrue interest.
For calculating the current token prices, the current NAV is an important variable in the Tinlake contracts.
1TD... totalDiscounting2TW... totalWriteOffs
All loans with a maturity date in the future and an open debt are part of the totalDiscounting
. An overdue loan after the maturity date is part of the totalWriteOffs
From a complexity perspective, the totalDiscount
calculation is gas-consuming operation.
A naive implementation would be a iteration over all loans check if the maturity date is in the future. Calculate the present value of each loan and add them together to get the totalDiscount
.
This would have a total runtime of O(n) and would be very expensive from the gas perspective.
The loans with the same write-off group can be grouped together in the pile contract and can be ignored from a runtime perspective.
This implementation is just done theoretically to introduce a simple solution.
In the first implementation of NAV, we grouped the different loans together by maturity date in a linked-list. Instead of iterating over all loans, we only needed to iterate over all different maturity dates in the future.
Bucket A bucket includes the future value of all loans with the same maturity date.
The following sections describes how the NAV is implemented in Tinlake.
n
are all buckets between now and the future.Le't's assume total discounting
td is already given from yesterday. It is possible to calculate the current total discounting
based on it.
Note: Assuming no new borrow or repay events and no overdue loans
1t....time
1t...time passed since last update
The formula is correct if, between lastUpdate
and lastUpdate+t
, no new loan is overdue or no new loan has been borrowed or repaid.
If loans are overdue between t
and t+n
, they would exist incorrectly in the totalDiscount
.
Therefore a errTotalDiscount
is calculated which needs to be removed from totalDiscount
for it to be correct, in case of new overdue loans.
errTotalDiscount
The errTotalDiscount
is defined as the sum of all new loans which are overdue since the last update applying the total Discount formula.
Note the following, which is mathematically the same
Given it is a way to avoid a negative pow operation in Solidity.
We can define errTotalDiscount
as the following
For all loans where maturityDate < now and maturityDate > lastUpdate
.
In the final Solidity implementation, we don't iterate over all loans to calculate the errTotalDiscount
. The loan are grouped in buckets
. All loans with the same maturity date are grouped into one bucket.
1// Solidity pseudo code2 uint errTotalDiscount = 0;3 uint nnow = uniqueDayTimestamp(block.timestamp);4 // find all new overdue loans since the last update5 // calculate the discount of the overdue loans which is needed6 // for the total discount calculation7 for(uint i = lastNAVUpdate; i < nnow; i = i + 1 days) {8 uint b = buckets[i];9 if (b != 0) {10 errTotalDiscount = safeAdd(errPV, rmul(b, rpow(discountRate.value, safeSub(nnow, i), ONE)));11 }12 }1314 uint totalDiscount = safeSub(rmul(latestDiscount, rpow(discountRate.value, safeSub(nnow, lastNAVUpdate), ONE)), errTotalDiscount),
If a new loan is borrowed or repaid, first the latest NAV is calculated as described in the section above.
If a new loan is borrowed as a first step the NAV is updated to the latestblock.timestamp
.
Afterwards the new loan needs to be added to the totalDiscount
and latestNAV
.
If the demand of investments or redemptions is higher than the available capital, new investments or redemption would result in a first come, first serve situation. Especially if multiple parties want to redeem their tokens after loans from borrowers are repaid. On Ethereum this would result in transaction front-running and very high gas fees for all participants.
To avoid such a situation, the supply and redemption orders happen in epochs.
During an epoch, every whitelisted investor can create supply or a redeem order for DROP or TIN.
After the minimum epoch time has passed, anyone can execute the epoch.
An epoch can be executed after it is closed to calculate how many supply and redeem orders can be fulfilled.
The goal is to fulfill as many orders as possible without violating any pool constraints.
Pool constraints define restrictions in a pool like a max amount of currency in a reserve or maintain a healthy ratio between DROP and TIN.
Order Types In Tinlake, there exists four different order types:
OrderType | Priority | Description |
---|---|---|
seniorRedeemOrder | Priority 1 | Sum of all senior redeem orders in an epoch has the highest priority. First, the seniorRedeemOrder should be maximized. |
juniorRedeemOrder | Priority 2 | Sum of all junior redeem orders in an epoch has the second highest priority. |
juniorSupplyOrder | Priority 3 | Sum of all junior supply orders in an epoch. |
seniorSupplyOrder | Priority 4 | Sum of all senior supply orders in an epoch. |
SeniorRedeemOrders have the highest priority. The goal is to maximize the seniorRedeemOrders and afterwards consider the other order types. The supplyOrders can help to increase the seniorRedeem fulfillment rate.
The idea is to fulfill the maximum amount of orders. If it is not possible to fulfill 100% of all invest orders, the fulfillment rate is calculated and applied to all orders equally. This means all investors from the same order type can supply or redeem the same percentage of their original investment.
For each order type the fulfillment rate is calculated depending on the pool constraints.
The amount left is automatically re-ordered in the next epoch
.
After an epoch
is closed, the current token prices are calculated. The tokenPrice reflects a current valuation of the portfolio expressed in the NAV.
An epoch can have an arbitrary length. It only requires a minimum epoch time defined by the issuer. After the minimum epoch time has passed, any user can close the epoch.
The fulfillment of order types is constricted by the so-called pool constraints. Some constraints arise naturally, like the maximum number of redeems restricted by the amount of DAI available in the reserve. Others are defined by the issuer to maintain a healthy pool state.
The following constraints are defined by the asset originator
Constraints | Desc |
---|---|
C1: Currency Constraint | It is not possible to allow more redeems than there is currency in the reserve. (after considering the new investments) |
C2: Max Reserve Constraint | The total amount of DAI in the reserve is restricted by the maxReserve parameter. The maxReserve can be updated by the issuer. |
C3: MaxSeniorRatio | New supplies and new redemptions are not allowed to violate maxSeniorRatio. A maxSeniorRatio implicitly guarantees a minimum juniorRatio. The juniorRatio protects the senior investors. |
If all (maximum) invest and redeem orders can be fulfilled without violating any constraints all orders will be executed upon epoch close.
If all (maximum) invest and redeem orders can be fulfilled without violating any constraints all orders will be executed upon epoch close.
Instead of finding the optimal solution on-chain through smart contract calculations, optimal solutions can be calculated off-chain and submitted to the pool. In this approach, the smart contracts only validate and score submitted solutions. The smart contracts allow anyone to submit a solution in the epoch execution state. They then verify that the solution is valid and accept the best submitted solution according to the execution priorities.
This approach reduces the complexity and costs of the contracts and allows to easily add new constraints like a minimumReserve
for example.
The decision function should maximize the orders in Tinlake without violating any of the constraints.
The priority of the different order types can be encoded into a decision function where weights enforce the priority.
In literature, this approach is called goal programming.
The problem of how many orders a Tinlake pool can fulfill without violating any constraints can be seen as a linear programming problem (LP).
Source: Wikipedia
In Tinlake's case, the feasible region of the LP problem is restricted by the maximum orders and by the pool constraints.
If in an epoch execution it is not possible to fulfill all orders. the pool opens a submission period.
If the pool is in a submission period, it is open to everyone to submit solution.
A restricted list of allowed solution submitters would centralize the epoch execution process and is therefore avoided.
However, it is important to notice that an attacker could potentially submit a non-optimal solution.
For preventing the execution of a non-optimal solution a challenge period has been added.
The challenge period needs to pass before the epoch can execute the first valid submitted solution.
From a risk perspective it only requires one honest submitter for having the optimal solution.
Off-chain Solver
We included a solver library (based on wasm) in the Tinlake UI so everyone can submit the optimal solution as easy as possible.
It is easy to detect off-chain if a non-optimal solution has been submitted to the contracts.
1. Max Reserve Constraint After the epoch is executed
The orders are restricted by the currency available in the pool after an epoch execution. This constraint considers new investments through supply orders.
1Example:2Reserve: 5 DAI3DROP supplyOrder: 10 DAI4DROP redeemOrder: 15 DAI56Solution:7DROP supplyOrder: 10 DAI8DROP redeemOrder: 15 DAI
This is a helper constraint. A submitted solution is not allowed to be higher than the total orders.
This is a helper constraint. Negative values are not possible.
The decision function in the Tinlake contracts uses different weights to achieve the following order:
Weights
Max Function
See More: Centrifuge Design Doc: Solver - Decision Function
In a Tinlake pool, two different types of constraints exist
A submitted solution always needs to satisfy the core constraints.
It is possible that a current pool state violates the pool constraints.
The seniorRatio can be higher or lower than the min and max ratio. This can happen by a NAV decrease or increase.
The reserve can violate the maxReserve if a high repayment happened or the asset originator changed the maxReserve.
If the ratio or maxReserve violates a constraint, we define a pool as unhealthy.
Unhealthy State In the case of a pool being in a unhealthy state, there a two possibilities in an epoch execution:
It is not possible to detect on-chain in which of the two cases a pool is.
1Example:2reserve: 100k DAI3maxReserve: 110 DAI45seniorRedeemOrder: 20k6The 20k seniorRedeem orders could fix the unhealthy state.78Instead, if we only would have 5k redeemOrders, we could only improve the reserve constraint.910(We are assuming here the redeemOrders would not violate any other constraints)
In case of an unhealthy state, the pool accepts improvements of the current situation as a solution until it sees a solution which satisfies all constraints.
The highest priority is to fix the seniorRatio if it is broken.
1Example Broken Senior Ratio:2current seniorRatio: 0.9534maxSeniorRatio: 0.85minSeniorRatio: 0.7
The second priority is to fix the maxReserve constraint if it is broken too.
Ratio Improvement Score If a submitted solution is outside of the senior Ratio range.
The score of the ratio is calculated in the following way:
This results in the following scoring behavior. The closer the ratio is to the minRatio or maxRatio, the higher the score.
Note: The scoring function only applies if the current ratio is not within the range.
Reserve Improvement Score If the current reserve is higher than the maxReserve, the improvement score is:
If a first improvement is submitted the score of the current state are used as a baseline The submitted solution needs to have a higher score than the current state. If the current orders would not improve the state, no new orders are accepted.
1Example:2reserve Constraint is violated but only new supplyOrders exist
The senior investors are getting a fixed interest rate on their investment for the currency of ongoing loans. Currency which is in the reserve does not accrue interest. Only DAI which is used for loans can accrue interest.
The current NAV multiplied with the current seniorRatio is the interest bearing amount for the seniorAsset.
Rebalancing In rebalancing, the total seniorAsset value stays the same. Only the relation between seniorDebt and seniorBalance changes.
With every epoch that has executed supply/redeem transactions, the relation between senior und junior tranches changes.
The rebalancing is happening as part of the epochExecute.
Loan Borrow/Repay
If a loan is repaid or borrowed, it changes the NAV and reserve. The borrow and repayment amounts are updates to the balance between seniorDebt
and seniorBalance
.
JuniorAsset increase The juniorAsset is defined as the difference between the poolValue and the seniorAsset. Since the NAV is continuously increasing, it results in a juniorAsset increase. This increase of the juniorAsset is only considered in the rebalancing at the end of an epoch.
The solver is currently run from our Tinlake Bot, as well as manually callable from the Tinlake UI.
These are both Javascript environments. However, no linear solver exists in vanilla Javascript which fits our requirements.
Therefore, a while back we ported CLP, a well tested C++ based linear solver, using WebAssembly:
The clp-wasm library contains a C++ wrapper which makes sure the precision we need (with the usage of the fixed point arithmetic) is achieved.
The actual input and constraints are then defined in tinlake.js, our JS client library for Tinlake.
The code for this can be found here.
Some DROP tokens of specific Tinlake pools are accepted as collateral in Maker. In this case, a Debt ceiling
is defined my Maker governance. In exchange for providing liquidity, DROP tokens are locked in a Maker vault as collateral. Some pools also include an additional over-collateralization as an additional protection. In a liquidation scenario, the overcollateralization would be paid by TIN token holders.
Important metrics to use DROP as a collateral in Maker are:
If the DROP token of a specific Tinlake pool is accepted as collateral in Maker, a debt ceiling
is defined my Maker governance.
Tinlake itself maintains a variable called creditline
.
The asset originator can change the creditline by calling
The creditline needs to be <=
debt ceiling.
The creditline defines how much an asset originator wants to borrow at maximum from the Maker vault.
Raise/Sink A raise or sink of the creditline is not triggering a borrowing or repayment of DAI.
It only indicates that it will happen soon and ensures that the pool is ready for it.
The pool check if a borrow from Maker would violate any of the constraints.
A borrow from Maker results in a seniorRatio increase.
From the constraint perspective, the Maker DROP is already considered as part after the raise.
It is not possible to redeem the required TIN for the Maker creditline.
1Example: Raise creditline 10k DAI2seniorRatioRange: 0,10 - 0,934seniorAsset: 10k5juniorAsset: 200k6seniorRatio: 0,1178It is possible for nearly all TIN investors to leave the pool.910raise creditline: 890k (no overcollateralization)11validate constraint perspective:12seniorAsset: 900k13juniorAsset: 200k14seniorRatio: 0,91516It would be not possible for TIN investors to redeem after the raise.
From the constraint perspective in the epoch, it doesn't matter if the amount is already borrowed or not.
Draw Borrows DAI from Maker and uses DROP as collateral.
The Maker community can define a over-collateralizaton for DROP to be accept as collateral in Maker.
1Example:2Overcollateralization of 110%34dropPrice: 1.55draw Amount: 100 DAI6collateralValue: 100 DAI 1.10 = 110 DAI7collateral in DROP: 110 DAI/1.5 = 73.33 DROP
The required over-collateralization of DROP for Maker is paid by TIN investors in case of liquidation scenario.
1Example:23State:4seniorAsset: 80 DAI | 80 DROP | dropPrice: 1.05juniorAsset: 20 DAI | 20 TIN | tinPrice: 1.06Reserve: 100 DAI7dropPrice: 1.08seniorRatio: 0.89--10Maker draw:11draw: 10 DAI from Maker12Overcollateralization: 110%13Assumption: draw is not violating constraints14---15drawAmount: 10 DAI16mint: 11 DROP (10 * 1.1/1 = 11 DROP)17transfer DROP to MKR18deposit: 10 DAI into the reserve19increase seniorAsset: 11 DAI2021State:22seniorAsset: 91 DAI | 91 DROP | dropPrice: 1.023juniorAsset: 19 DAI | 20 TIN | tinPrice: 19/20 = 0.9524Reserve: 110 DAI25seniorRatio: 91/110: 0.82
In case of a liquidation the TIN investors would have to pay for the additional DROP.
1In the example above the TIN price would2drop to 0.95 in case of a liquidation.