As a rapidly growing industry in the blockchain ecosystem, (decentralized finance) DeFi is spurring innovation and revolution in spending, sending, locking, and loaning cryptocurrency tokens.
One of the many goals of blockchain is to make financial instruments available to everyone. A loan in blockchain DeFi can be used in combination with lending, borrowing, spot trading, margin trading, and flash loans.
With DeFi, end users can quickly and easily access loans without having to submit their passports or background checks like in the traditional banking system.
In this tutorial, you learn about a basic loan system as you use Ignite CLI to build a loan module.
You will learn how to
Scaffold a blockchain
Scaffold a Cosmos SDK loan module
Scaffold a list for loan objects
Create messages in the loan module to interact with the loan object
Interact with other Cosmos SDK modules
Use an escrow module account
Add application messages for a loan system
Request loan
Approve loan
Repay loan
Liquidate loan
Cancel loan
Note: The code in this tutorial is written specifically for this learning experience and is intended only for educational purposes. This tutorial code is not intended to be used in production.
For the sake of simplicity, define every parameter as a string.
The request-loan message creates a new loan object and locks the tokens to be spent as fee and collateral into an escrow account. Describe these conditions in the module keeper x/loan/keeper/msg_server_request_loan.go:
Copy
package keeper
import("context""github.com/username/loan/x/loan/types"
sdk "github.com/cosmos/cosmos-sdk/types")func(k msgServer)RequestLoan(goCtx context.Context, msg *types.MsgRequestLoan)(*types.MsgRequestLoanResponse,error){
ctx := sdk.UnwrapSDKContext(goCtx)// Create a new Loan with the following user inputvar loan = types.Loan{
Amount: msg.Amount,
Fee: msg.Fee,
Collateral: msg.Collateral,
Deadline: msg.Deadline,
State:"requested",
Borrower: msg.Creator,}// TODO: collateral has to be more than the amount (+fee?)// moduleAcc := sdk.AccAddress(crypto.AddressHash([]byte(types.ModuleName)))// Get the borrower address
borrower,_:= sdk.AccAddressFromBech32(msg.Creator)// Get the collateral as sdk.Coins
collateral, err := sdk.ParseCoinsNormalized(loan.Collateral)if err !=nil{panic(err)}// Use the module account as escrow account
sdkError := k.bankKeeper.SendCoinsFromAccountToModule(ctx, borrower, types.ModuleName, collateral)if sdkError !=nil{returnnil, sdkError
}// Add the loan to the keeper
k.AppendLoan(
ctx,
loan,)return&types.MsgRequestLoanResponse{},nil}
Since this function is using the bankKeeper with the function SendCoinsFromAccountToModule, you must add the SendCoinsFromAccountToModule function to x/loan/types/expected_keepers.go like this:
When a loan is created, a certain input validation is required. You want to throw error messages in case the end user tries impossible inputs.
You can describe message validation errors in the modules types directory.
Add the following code to the ValidateBasic() function in the /x/loan/types/message_request_loan.go file:
Copy
func(msg *MsgRequestLoan)ValidateBasic()error{_, err := sdk.AccAddressFromBech32(msg.Creator)
amount, err := sdk.ParseCoinsNormalized(msg.Amount)
fee,_:= sdk.ParseCoinsNormalized(msg.Fee)
collateral,_:= sdk.ParseCoinsNormalized(msg.Collateral)if err !=nil{return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress,"invalid creator address (%s)", err)}if!amount.IsValid(){return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest,"amount is not a valid Coins object")}if amount.Empty(){return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest,"amount is empty")}if!fee.IsValid(){return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest,"fee is not a valid Coins object")}if!collateral.IsValid(){return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest,"collateral is not a valid Coins object")}if collateral.Empty(){return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest,"collateral is empty")}returnnil}
Congratulations, you have created the request-loan message.
You can run the chain and test your first message.
After a loan request has been published, another account can approve the loan and agree to the terms of the borrower.
The message approve-loan has one parameter, the id.
Specify the type of id as uint. By default, ids are stored as uint.
Copy
ignite scaffold message approve-loan id:uint
This message must be available for all loan types that are in "requested" status.
The loan approval sends the requested coins for the loan to the borrower and sets the loan state to "approved".
Modify the x/loan/keeper/msg_server_approve_loan.go to implement this logic:
Copy
package keeper
import("context""fmt""github.com/username/loan/x/loan/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors")func(k msgServer)ApproveLoan(goCtx context.Context, msg *types.MsgApproveLoan)(*types.MsgApproveLoanResponse,error){
ctx := sdk.UnwrapSDKContext(goCtx)
loan, found := k.GetLoan(ctx, msg.Id)if!found {returnnil, sdkerrors.Wrapf(sdkerrors.ErrKeyNotFound,"key %d doesn't exist", msg.Id)}// TODO: for some reason the error doesn't get printed to the terminalif loan.State !="requested"{returnnil, sdkerrors.Wrapf(types.ErrWrongLoanState,"%v", loan.State)}
lender,_:= sdk.AccAddressFromBech32(msg.Creator)
borrower,_:= sdk.AccAddressFromBech32(loan.Borrower)
amount, err := sdk.ParseCoinsNormalized(loan.Amount)if err !=nil{returnnil, sdkerrors.Wrap(types.ErrWrongLoanState,"Cannot parse coins in loan amount")}
k.bankKeeper.SendCoins(ctx, lender, borrower, amount)
loan.Lender = msg.Creator
loan.State ="approved"
k.SetLoan(ctx, loan)return&types.MsgApproveLoanResponse{},nil}
This module uses the SendCoins function of bankKeeper. Add this SendCoins function to the x/loan/types/expected_keepers.go file:
Copy
package types
import(
sdk "github.com/cosmos/cosmos-sdk/types")type BankKeeper interface{// Methods imported from bank should be defined hereSendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins)errorSendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins)error}
Now, define the ErrWrongLoanState new error type by adding it to the errors definitions in x/loan/types/errors.go:
After the coins have been successfully exchanged, the state of the loan is set to repayed.
To release tokens with the SendCoinsFromModuleToAccount function of bankKeepers, you need to add the SendCoinsFromModuleToAccount function to the x/loan/types/expected_keepers.go:
Copy
package types
import(
sdk "github.com/cosmos/cosmos-sdk/types")type BankKeeper interface{// Methods imported from bank should be defined hereSendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins)errorSendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins)errorSendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins)error}
Start the blockchain and use the two commands you already have available:
Copy
ignite chain serve -r
Use the -r flag to reset the blockchain state and start with a new database:
After the deadline is passed, a lender can liquidate a loan when the borrower does not repay the tokens. The message to liquidate-loan refers to the loan id:
Consider again updating your local repository with a git commit. After you test and use your loan module, consider publishing your code to a public repository for others to see your accomplishments.