On-chain Components
1. Collateral Management Logic
2. Lending Pool (Issuance & Redemption logic)
3. Position Registry
4. Price Oracle
5. Audit (Update Validation Logic)
Written in Aiken and developed for TrustLevel
Building
aiken build
# Or include traces for testing/debugging:
# (Note that collateral and lending_pool contracts individually exceed tx size limit if compiled verbose)
aiken build -t verbose
Testing
To run all tests:
aiken check
To run only tests matching the string foo, do:
aiken check -m foo
Concurrency and Double-Satisfaction
To address the risk of double-satisfaction exploits, the loanable asset in the lending pool is contained in only 1 utxo.
As a consequence, only 1 transaction at a time can ever spend the lending pool’s utxo and trigger its validation rules to be evaluated.
To allow users to concurrently take loans and repay active ones, the “loan” and “repayment” interactions are broken up into 2 blockchain transactions. This is similar to the request-and-batching done by dexes.
In the first part, the user submits their request - either loan takeout or loan repayment. In this transaction, they will only be spending their own collateral utxo, so other users are not affected.
The second part, which is the fulfillment of the user’s request, is done in the backend. This is where we immediately chain the transaction that spends the lending pool’s only utxo, to send to the user the loaned asset they requested. The lending pool’s new utxo can then be persistently stored, immediately ready to be used for the next fulfillment tx without waiting on the network for finality.
Input/Redeemer Indexing
The redeemer indexing design pattern is also implemented in the protocol, to reduce double-satisfaction risks, and more importantly to significantly reduce contract execution costs.
Process
-
Initialization
-
Protocol owner deploys the compiled Plutus validators into
utxo’s as reference scripts, and initialize the UTXOs containing the protocol’s settings. -
Protocol owner deposits the loanable asset into the
lending_poolcontract address.
-
-
Usage
-
User:
-
User deposits their collateral asset into the
collateralmanager contract address. -
User may withdraw their deposited collateral if it is not locked in an active loan.
-
User takes out a loan by:
-
Tx 1 (by user) User submits a loan request tx. This locks their collateral deposit and records the amount they want to borrow.
-
Tx 2 (chained to Tx 1 in the backend) A tx fulfilling the loan request of the user is automatically chained to the user’s loan request. This spends the corresponding amount of loanable asset from the
lending_poolcontract and sends it to the requesting user.
-
-
User repays an active loan through the same request-fulfill mechanism.
-
-
Oracle Data Provider:
- Trusted oracle data provider runs a service that periodically checks relevant dexes for the current price of the collateral asset against the loanable asset. The oracle price UTXO is then updated with the latest price data.
-
Admin:
- Admin (or anyone) runs a service that periodically checks all open loan positions for under-collateralization or maturity. If any are found, liquidation txs will be submitted for those positions.
-
Protocol Spec / Validator Operation
UTXOs
The current design of this protocol involves 7 validators that hold the following UTXOs respectively.
-
refscripts.ak:UTXOs containing the protocol’s compiled validator code as reference scripts.
-
settings.ak:UTXOs containing settings that are used by different components of the protocol.
-
collateral.ak:UTXOs containing users’ collateral assets, and a reference to their loan metadata if they have an open loan position.
-
position_registry.ak:UTXOs containing metadata of open loan positions (position UTXOs).
-
lending_pool.ak:The single UTXO containing the loanable asset. The datum in this UTXO also contains the lending pool settings such as the collateral asset, its price, loan-to-value ratio, and the list of interest rates for different loan terms.
-
oracle.ak:-
UTXO’s containing updated pricing data for the collateral asset (price UTXOs). This is used in the loan issuance logic.
-
A single UTXO containing the
oraclesettings (settings UTXO) - the list of trusted providers.
-
-
audit.ak:UTXO containing the protocol’s updated “health score” based on a number of key factors.
Position Tokens
The metadata of each open loan position are contained in UTXOs held at the position_registry contract (“position UTXO”). These position UTXOs are created when a user borrows from the lending pool, and are removed when a loan position is closed.
When a loan position is opened, a unique pair of “position tokens” are minted. These are beacon tokens used to tag the collateral UTXO that is used for the loan, and its corresponding position UTXO held at the position_registry contract. The “position tokens” link them together. Uniqueness of the pair is ensured by using the UTXO id of the input collateral UTXO.
When a loan is closed (repaid or liquidated), the position tokens are burned.
The minting policy of these “position tokens” is contained in the lending_pool contract.
Oracle Tokens
The oracle contract mints oracle price tokens: OPRC. These are beacon tokens that are used to identify the UTXO(s) containing official price data for collateral assets.
Settings Tokens
The settings contract mints 2 beacon tokens once (on initialization only):
GCFG- The beacon token for the UTXO containing the global settings datumOCFG- The beacon token for the UTXO containing the oracle settings datum
Requirements when withdrawing collateral
Redeemer: WithdrawCollateral
Enforced by collateral.ak:
- ✅ The collateral UTXO must not be used in an open loan position.
- ✅ The owner of the collateral must sign the transaction.
Requirements when issuing loans
-
Part 1: User submits loan request
Redeemer:
BorrowRequestEnforced by
collateral.ak:- ✅ The collateral UTXO must not be used in an open loan position.
- ✅ The owner of the collateral must sign the transaction.
- ✅ The collateral asset must be returned back to the
collateralcontract address after the tx. - ✅ The collateral UTXO datum must be updated with
LoanRequestedstatus, together with the loan details.
Note: No need to restrict collateral inputs to only 1. Multiple collateral UTXOs that have
Availablestatus can be used to request for a loan. Their values will just have to be merged into 1 output containing the loan request datum. That is, only if the collateral is ADA. -
Part 2: Loan request fulfillment tx is chained
Redeemer:
BorrowProcessEnforced by
lending_pool.ak:- ✅ There must be 1 input utxo each from the
collateraland thelending_poolcontracts respectively. - ✅ The global settings UTXO and the oracle price UTXO must be included in the reference inputs.
- ✅ The collateral utxo datum must have the status
LoanRequested. - ✅ The oracle UTXO must contain updated price data for the collateral asset.
- ✅ The collateral asset utxo must contain sufficient amount of collateral asset for the loan being taken, as determined by its current price from the oracle, and the loan-to-value ratio setting.
- ✅ A unique pair of position tokens must be minted for the current loan position.
- ✅ The collateral asset must be returned back to the
collateralcontract with an updated datum containing the status ofLoanIssued, and tagged with one of the position token pair. - ✅ A position UTXO must be created at the
position_registrycontract, containing the other one of the position token pair. In its datum, the following loan information should be contained:- ✅ Borrower
- ✅ Collateral asset and amount
- ✅ Borrowed asset and amount
- ✅ Interest payable
- ✅ Loan term chosen
- ✅ Maturity date
- ✅ All excess assets (lovelace & everything else) from the
lending_poolutxo must be returned to thelending_poolcontract. - ✅ The lending pool utxo datum must not change.
- ✅ The tx validity range must be within the specified period in the lending pool settings.
Enforced by
collateral.ak:- ✅ A utxo from the
lending_poolcontract must be spent in the transaction.
- ✅ There must be 1 input utxo each from the
Requirements when repaying a loan
-
Part 1: User submits loan repayment request
Redeemer:
RepayRequestEnforced by
collateral.ak:- ✅ The global settings utxo must be one of the reference inputs.
- ✅ The collateral utxo must have the status
LoanIssued. - ✅ The owner of the collateral must sign the transaction.
- ✅ The collateral input must contain exactly 1 position token.
- ✅ The correct position UTXO must be one of the reference inputs.
- ✅ The output collateral utxo must:
- ✅ contain both the collateral asset and the repayment asset
- ✅ have its datum updated with
RepayRequestedstatus
Notes:
-
No need to restrict to only 1 collateral input in a tx here since
collateralvalidator requires the UTXO value to be returned back to the contract. Only the datum is changed. Even if multiple collateral UTXO’s are spent withRepayRequestredeemer, they will all have to be returned back to thecollateralcontract anyway, with datum changes ensured to be correct. -
Also no need to require not having an input from
lending_poolsince the only redeemers accepted bylending_poolall require the input collateral UTXOs to have the status not equal toLoanIssued. So, no unauthorized or unintended spending ofcollateralorlending_poolUTXO’s will be approved.
-
Part 2: Loan repayment request fulfillment tx is chained
Redeemer:
RepayProcessEnforced by
lending_pool.ak:- ✅ There must be 1 input utxo each from the
collateral, theposition_registry, and thelending_poolcontracts respectively. - ✅ The global settings UTXO must be included in the reference inputs.
- ✅ The input collateral utxo must be validated:
- ✅ that its address matches with the settings in the
lending_pooldatum; - ✅ that its datum has the status
RepayRequested - ✅ that it contains a position token
- ✅ that its address matches with the settings in the
- ✅ The input position utxo must be validated:
- ✅ that it contains the same position token contained in the input collateral utxo
- ✅ that its datum contains the loan metadata
- ✅ The tx validity range must be within the specified period in the lending pool settings.
- ✅ The tx must be finalized not later than the maturity date and time.
- ✅ The output lending pool utxo must be validated:
- ✅ that its datum does not change
- ✅ that the amount of the loanable asset it contains is the total of:
- ✅ The amount contained in the input lender utxo
- ✅ The borrowed amount
- ✅ The interest amount
- ✅ that it is going back to the
lending_poolcontract
- ✅ The output collateral utxo must be validated:
- ✅ that the amounts of lovelace and/or collateral assets is the same as in the input
- ✅ that the datum is updated with
Availablestatus - ✅ that it is going back to the
collateralcontract
- ✅ The position token pair must be burned
Note:
There’s no more need to check that there’s no output going to the
position_registrycontract. All the validations done above, should leave no more assets from the protocol that can be sent to any other address.Enforced by
collateral.ak:- ✅ A utxo from the
lending_poolcontract must be spent in the transaction.
Enforced by
position_registry.ak:- ✅ The correct collateral UTXO must be one of the inputs, containing the same position token as the one in the position UTXO.
- ✅ There must be 1 input utxo each from the
Requirements when liquidating a loan position
-
Part 1: Liquidator sells the collateral for the loaned asset at a dex
Redeemer:
LiquidateCollateralEnforced by
collateral.ak:- ✅ The following input utxos must be present:
- ✅ 1 from the
collateralcontract (the one to be liquidated) - ✅ 1 from the
position_registrycontract (position UTXO)
- ✅ 1 from the
- ✅ The global settings UTXO, and the oracle price UTXO must be included in the reference inputs.
- ✅ The collateral utxo must contain a position token.
- ✅ The position token contained in the position UTXO must be the same as that contained in the collateral UTXO.
- ✅ The price data contained in the oracle UTXO must be updated.
- ✅ The loan position must be elligible for liquidation, for either of the following reasons:
- ✅ It has become overdue, or
- ✅ Its value has fallen below the requirement to secure the position.
- ✅ The collateral value must be sent to one of the supported dexes’ order contract for swapping into the loanable asset.
- ✅ The swap order’s
success_receivermust be thecollateralcontract. - ✅ The swap order’s
success_receiver_datummust contain the hash of the position token; so that we can identify the resulting UTXO later, after the swap is executed. - ✅ The position token pair must be returned to the
position_registrycontract. - ✅ The position utxo datum must be updated to contain the key hash of the liquidator.
- ✅ The tx must be signed by the liquidator indicated in the updated position utxo datum.
- ✅ The tx validity range must be within the specified period in the lending pool settings.
Enforced by
position_registry.ak:- ✅ The correct collateral UTXO must be one of the inputs, containing the same position token as the one in the position UTXO.
- ✅ The following input utxos must be present:
-
Part 2: From the dex swap proceeds, liquidator repays the loan + interest
Redeemer:
SettleLiquidationEnforced by
lending_pool.ak:- ✅ The following utxos must be included in the tx inputs:
- ✅ 1 from the
collateralcontract (the swap result) - ✅ 1 from the
position_registrycontract (position UTXO) - ✅ 1 from the
lending_poolcontract (pool UTXO)
- ✅ 1 from the
- ✅ The global settings UTXO must be included in the reference inputs.
- ✅ The collateral input must contain a datum hash matching the hash of the position token in the position UTXO.
- ✅ The output lending pool utxo must be validated:
- ✅ that its datum does not change
- ✅ that the amount of the loanable asset it contains is the total of:
- ✅ The amount contained in the input lender utxo
- ✅ The borrowed amount
- ✅ The interest amount
- ✅ that it is going back to the
lending_poolcontract
- ✅ The tx is signed by the liquidator
- ✅ The position token pair is burned
Enforced by
collateral.ak:- ✅ A utxo from the
lending_poolcontract must be spent in the transaction.
Enforced by
position_registry.ak:- ✅ A utxo from the
lending_poolcontract must be spent in the transaction.
- ✅ The following utxos must be included in the tx inputs:
Requirements for minting/burning oracle beacon tokens
Redeemer: MintOracleBeacon | BurnOracleToken
Enforced by oracle.ak:
- ✅ The global settings UTXO must be included in the reference inputs.
- ✅ The global settings UTXO must contain the global settings beacon token.
- ✅ The admin specified in the input global settings UTXO must sign the tx.
Requirements when updating oracle price data
Redeemer: UpdateOraclePrice
Enforced by oracle.ak:
- ✅ The oracle price UTXO must be one of the inputs
- ✅ The oracle settings UTXO must be included in the reference inputs
- ✅ The oracle price UTXO must be returned with its updated datum still having the same structure.
- ✅ The tx must be signed by one of the authorized updaters.
Requirements when updating audit datum
Redeemer: UpdateAuditDatum
Enforced by audit.ak:
- ✅ The current audit UTXO containing the audit beacon token must be spent in the tx.
- ✅ The following UTXOs must be included in the reference inputs:
- ✅ the global settings UTXO
- ✅ the oracle settings UTXO
- ✅ the
lending_poolUTXO - ✅ the oracle price UTXO
- ✅ The output audit UTXO must be returned to the
auditcontract - ✅ The output audit datum must have the expected structure and contents
- ✅ The tx must be signed by one of the trusted oracle providers
Requirements when updating global settings
Redeemer: UpdateGlobalCfg
Enforced by settings.ak:
- ✅ The global settings UTXO must be spent in the tx.
- ✅ The global settings UTXO must contain the global settings beacon token.
- ✅ The global settings beacon token must be returned to the
settingscontract. - ✅ The admin specified in the input global settings UTXO must sign the tx.
Requirements when updating oracle settings
Redeemer: UpdateOracleCfg
Enforced by settings.ak:
- ✅ The oracle settings UTXO must be spent in the tx.
- ✅ The oracle settings UTXO must contain the oracle settings beacon token.
- ✅ The global settings UTXO must be included in the reference inputs.
- ✅ The global settings UTXO must contain the global settings beacon token.
- ✅ The oracle settings beacon token must be returned to the
settingscontract. - ✅ The oracle settings datum structure must be preserved.
- ✅ The admin specified in the global settings UTXO datum must sign the tx.
Requirements when minting/burning position tokens
-
Minting
Enforced by
lending_pool.ak:- ✅ There must be 1 input utxo from the
lending_poolcontract. This triggers all the validation requirements already.
- ✅ There must be 1 input utxo from the
-
Burning
Enforced by
lending_pool.ak:- ✅ There must be 1 input utxo from the
lending_poolcontract. This triggers all the validation requirements already.
- ✅ There must be 1 input utxo from the