Loan contract
In our project we will also implement PSP34 token. This token will represent a loan of a user who borrowed some assets. Upon borrowing assets the contract will mint an NFT to them, which will hold the information about their loan, namely the user who borrowed the assets, address of the asset which was used as collateral, how much collateral was deposited, what asset was borrowed, and how much, the liquidation price of the loan, timestamp of when was the loan performed, and information whether the loan is liquidated or not. This data will be stored in a separate storage trait, which we will derive in our NFT contract. We do this to separate storage from the logic, and we will do this in the lending contract as well. We do not want anybody to just mint and burn these, so we will implement the Ownable extension in our NFT. The mint and burn logic will be covered differently, we will not be using the mintable and burnable extensions.
The LoanContract
will contain several methods defined in the Loan
trait.
These methods are restricted and can be called only by an owner of the contract.
There is not too much logic to split it, so everything will be implemented
in the body of the contract.
Definition of the Loan
trait
In the traits/loan.rs
, we will define a Loan
trait.
That trait contains three super traits: PSP34
, PSP34Metadata
, and Ownable
.
Also, the trait contains several methods, and the definition of the LoanInfo
(that structure is used during interacting with the contract
so it is defined in the traits
instead of the body of the contract).
LoanRef
can be used by other developers to do a cross contract call to LoanContract
.
use openbrush::{
contracts::traits::{
ownable::*,
psp34::{
extensions::metadata::*,
*,
},
},
traits::{
AccountId,
Balance,
Timestamp,
},
};
#[cfg(feature = "std")]
use ink::storage::traits::StorageLayout;
#[derive(Default, Debug, Clone, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(StorageLayout, scale_info::TypeInfo))]
pub struct LoanInfo {
pub borrower: AccountId,
pub collateral_token: AccountId,
pub collateral_amount: Balance,
pub borrow_token: AccountId,
pub borrow_amount: Balance,
pub liquidation_price: Balance,
pub timestamp: Timestamp,
pub liquidated: bool,
}
#[openbrush::wrapper]
pub type LoanRef = dyn Loan + PSP34 + PSP34Metadata + Ownable;
#[openbrush::trait_definition]
pub trait Loan: PSP34 + PSP34Metadata + Ownable {
/// This function initalizes data of a loan and mint token inside it
#[ink(message)]
fn create_loan(&mut self, loan_info: LoanInfo) -> Result<(), PSP34Error>;
/// This function frees data of a loan and burn token inside it
#[ink(message)]
fn delete_loan(&mut self, initiator: AccountId, loan_id: Id) -> Result<(), PSP34Error>;
/// This function will be used when the user repays their loan only partially
#[ink(message)]
fn update_loan(
&mut self,
loan_id: Id,
new_borrow_amount: Balance,
new_timestamp: Timestamp,
new_collateral_amount: Balance,
) -> Result<(), PSP34Error>;
/// This function will set a loan to liquidated
#[ink(message)]
fn liquidate_loan(&mut self, loan_id: Id) -> Result<(), PSP34Error>;
/// Function returns `LoanInfo` by `Id`
#[ink(message)]
fn get_loan_info(&self, loan_id: Id) -> Result<LoanInfo, PSP34Error>;
}
Add dependencies
In addition to the dependencies imported in the PSP34
documentation, we will also add the ownable
dependency the same way as in the
ownable documentation. We will be using LoanContract
as a dependency in our lending contract to instantiate it. So we need to also add
the "rlib"
crate type to have the ability to import the LoanContract
as a dependency.
Implement the contract
We want a basic PSP34 token with metadata and ownable extensions,
so we will add these to our contract. We will add a openbrush::contract
macro to our contract and add some imports:
#![cfg_attr(not(feature = "std"), no_std, no_main)]
/// This contract will represent the loan of a user
#[openbrush::implementation(Ownable, PSP34, PSP34Metadata)]
#[openbrush::contract]
pub mod loan {
use openbrush::traits::String;
use lending_project::traits::loan::*;
use openbrush::{
modifiers,
storage::Mapping,
traits::Storage,
};
Note: If some default OpenBrush implementation is added to contract as a part of
#[openbrush::implementation]
macro, you don't have to add imports on your module. For example, if you addPSP34
to your contract, you don't have to adduse openbrush::traits::psp34::*;
to your module.
Define the storage
We will derive the storage traits related to PSP34
, PSP34Metadata
, and
Ownable
and declare the fields related to these traits. Also, we will declare
fields related to Loan
itself.
/// Define the storage for PSP34 data, Metadata data and Ownable data
#[ink(storage)]
#[derive(Storage)]
pub struct LoanContract {
#[storage_field]
psp34: psp34::Data,
#[storage_field]
ownable: ownable::Data,
#[storage_field]
metadata: metadata::Data,
// Fields of current contract
/// mapping from token id to `LoanInfo`
loan_info: Mapping<Id, LoanInfo>,
/// the id of last loan
last_loan_id: Id,
}
Implement the extension traits
We will be using these extensions in our NFT token, so we will implement them inside #[openbrush::implementation]
macro.
#[openbrush::implementation(Ownable, PSP34, PSP34Metadata)]
#[openbrush::contract]
mod contract {
...
}
Implement the Loan trait
We will implement the Loan
trait.
All functions except one are restricted by the only_owner
modifier.
impl Loan for LoanContract {
#[modifiers(only_owner)]
#[ink(message)]
fn create_loan(&mut self, mut loan_info: LoanInfo) -> Result<(), PSP34Error> {
let loan_id = self._get_next_loan_id_and_increase()?;
if self.loan_info.get(&loan_id).is_some() {
return Err(PSP34Error::Custom(String::from("This loan id already exists!")))
}
loan_info.liquidated = false;
self.loan_info.insert(&loan_id, &loan_info);
self._mint_to(loan_info.borrower, loan_id)
}
#[modifiers(only_owner)]
#[ink(message)]
fn delete_loan(&mut self, initiator: AccountId, loan_id: Id) -> Result<(), PSP34Error> {
self.loan_info.remove(&loan_id);
self._burn_from(initiator, loan_id)
}
#[modifiers(only_owner)]
#[ink(message)]
fn update_loan(
&mut self,
loan_id: Id,
new_borrow_amount: Balance,
new_timestamp: Timestamp,
new_collateral_amount: Balance,
) -> Result<(), PSP34Error> {
self._update_loan(loan_id, new_borrow_amount, new_timestamp, new_collateral_amount)
}
#[modifiers(only_owner)]
#[ink(message)]
fn liquidate_loan(&mut self, loan_id: Id) -> Result<(), PSP34Error> {
self._liquidate_loan(loan_id)
}
#[ink(message)]
fn get_loan_info(&self, loan_id: Id) -> Result<LoanInfo, PSP34Error> {
let loan_info = self.loan_info.get(&loan_id);
if loan_info.is_none() {
return Err(PSP34Error::Custom(String::from("Loan does not exist")))
}
Ok(loan_info.unwrap())
}
}
Define the constructor and add functions
Finally, we will define the constructor where we will set the name and the symbol of the token and then initialize the owner of the token (that owner will be able to mint and burn the tokens). We will also add several helper functions.
impl LoanContract {
/// constructor with name and symbol
#[ink(constructor, payable)]
pub fn new() -> Self {
let mut instance = Self::default();
instance.last_loan_id = Id::U128(1);
instance._set_attribute(
Id::U8(1u8),
String::from("LoanContract NFT"),
String::from("L-NFT"),
);
instance._init_with_owner(Self::env().caller());
instance
}
/// Internal function to update data of a loan
fn _update_loan(
&mut self,
loan_id: Id,
new_borrow_amount: Balance,
new_timestamp: Timestamp,
new_collateral_amount: Balance,
) -> Result<(), PSP34Error> {
let loan_info = self.loan_info.get(&loan_id);
if loan_info.is_none() {
return Err(PSP34Error::Custom(String::from("This loan does not exist!")))
}
let mut loan_info = loan_info.unwrap();
loan_info.collateral_amount = new_collateral_amount;
loan_info.borrow_amount = new_borrow_amount;
loan_info.timestamp = new_timestamp;
self.loan_info.insert(&loan_id, &loan_info);
Ok(())
}
/// Internal function to set loan to liquidated
fn _liquidate_loan(&mut self, loan_id: Id) -> Result<(), PSP34Error> {
let loan_info = self.loan_info.get(&loan_id);
if loan_info.is_none() {
return Err(PSP34Error::Custom(String::from("This loan does not exist!")))
}
let mut loan_info = loan_info.unwrap();
loan_info.liquidated = true;
self.loan_info.insert(&loan_id, &loan_info);
Ok(())
}
/// Internal function to return the id of a new loan and to increase it in the storage
fn _get_next_loan_id_and_increase(&mut self) -> Result<Id, PSP34Error> {
match &mut self.last_loan_id {
Id::U128(id) => {
let result = Id::U128(id.clone());
*id += 1;
Ok(result)
}
_ => Err(PSP34Error::Custom(String::from("Not expected Id!"))),
}
}
}