Deliverable 5
Completing and Testing the Loan Smart Contract
Mayuri Shimpi (mayuripraveen.shimpi@sjsu.edu)
The Loan Smart contract
This deliverable centers around the Loan Smart Contract, serving as an intermediary to connect lenders and borrowers. The Credit Bureau Smart Contract creates a new loan that borrowers
and lenders can associate with. In our system, lenders can invest funds, and borrowers can specify the funds they require. After that, the interest rate is calculated based on the terms set by the EOA that initializes the Loan Smart Contract. The system allows borrowers to make payments and to track their credit scores, we update the CreditBureau
smart contract with the capability of updating the credit score of the borrower. The system also allows lenders to make withdrawals
In this deliverable, I have implemented the invest, borrow, and calculate interest methods as described below:
- Constructor: This constructor is defining and initializing variables for the Loan smart contract. The contract tracks the total loan amount, interest rate, number of payments, time intervals between payments, and a minimum credit score requirement. The variables _amountInvested and _amountBorrowed are initialized to 0 and are used for tracking investment and borrowing activity within the contract.
constructor(
uint totalAmount,
uint interestRatePerMil,
uint numPayments,
uint secondsBetweenPayments,
uint minCreditScore) {
_amountInvested = 0;
_amountBorrowed = 0;
_totalAmount = totalAmount;
_interestRatePerMil = interestRatePerMil;
_numPayments = numPayments;
_secondsBetweenPayments = secondsBetweenPayments;
_minCreditScore = minCreditScore;
}
- The invest() function: This function withdraws the amount specified by msg.value from the lender (msg.sender) and adds the balance to the loan smart contract. If sufficient funds are received, the timestamp of this transaction is utilized as the starting time for the loan. This timestamp is then employed to calculate the accrued interest.
function invest(uint amount) public payable {
require(amount + _amountInvested <= _totalAmount, "Exceeds total amount of investment");
_investors[msg.sender] += amount;
_amountInvested += amount;
if (isReady()) {
_timeLoanStart = block.timestamp;
}
}
- Testing the invest() function:
To write test cases for the invest function, we'll cover various scenarios to ensure its correctness. Here are some scenarios:
- Test when the investment amount is within the total amount allowed.
- Test when the loan becomes ready after the investment.
contract testSuite {
Loan loan;
function beforeAll() public {
// instantiate contract
loan = new Loan(1000, 5000, 12, 2592000, 650);
Assert.equal(uint(1), uint(1), "1 should be equal to 1");
}
function testInvestWithinTotalAmount() public {
uint amount = 500;
loan.invest(500);
Assert.equal(amount, loan._amountInvested(), "Interest calculation is incorrect");
}
// Test case to ensure loan becomes ready after the investment
function testLoanReadyAfterInvestment() public {
uint amount = 600;
loan.borrow(500);
loan.invest(amount);
Assert.equal(loan.isReady(), true, "Loan should be ready after investment");
}
}
- The borrow(uint amount) function: This function is utilized by the borrower (msg.sender) to request a loan. The borrower's credit score and the requested amount must meet the loan requirements. This method involves a call to "getScore()", which will be elaborated on below and may incur gas costs. Similarly, the findLoan operation mentioned earlier incurs gas costs for computation and may have already computed the same getScore. To prevent costly recomputations, these calls may be cached.
I haven't called the getScore() function yet; however, I plan to do so in the future.
The isReady() function returns whether or not the loan has been funded.
function borrow(uint amount) public {
uint creditScore = 750;
require(creditScore > _minCreditScore, "Insufficient credit score");
require(_borrower[msg.sender] == 0, "Can't borrow twice");
require(_amountBorrowed + amount <= _totalAmount, "Not enough ether left to borrow");
_borrower[msg.sender] = amount;
uint costOfLoan = calculateInterest(amount, _numPayments) + amount;
_borrowerExpectedPayment[msg.sender] = costOfLoan / _numPayments;
_amountBorrowed += amount;
if (isReady()) {
_timeLoanStart = block.timestamp;
}
}
function isReady() view public returns (bool) {
return _totalAmount == _amountInvested && _totalAmount == _amountBorrowed;
}
- Testing the borrow(uint amount) function:
To write test cases for this function, we'll cover various scenarios to ensure its correctness. Here are some scenarios:
- Test to ensure borrower can't borrow twice.
- Test case to ensure borrower can't borrow more than total amount available.
contract testSuite {
Loan loan;
function beforeAll() public {
// instantiate contract
loan = new Loan(1000, 5000, 12, 2592000, 650);
Assert.equal(uint(1), uint(1), "1 should be equal to 1");
}
// Test case to ensure borrower can't borrow twice
function testBorrowTwice() public {
loan = new Loan(1000, 5000, 12, 2592000, 650);
loan.borrow(100); // Borrow initial amount
(bool success, ) = address(loan).call(abi.encodeWithSignature("borrow(uint256)", 100));
Assert.ok(!success, "Borrower should not be able to borrow twice");
}
// Test case to ensure borrower can't borrow more than total amount available
function testBorrowExceedTotalAmount() public {
uint totalAmount = 500;
(bool success, ) = address(loan).call{value: totalAmount + 1}(abi.encodeWithSignature("borrow(uint256)", totalAmount + 1));
Assert.ok(!success, "Borrower should not be able to borrow more than total amount available");
}
}
- The calculateInterest(uint owed, uint numPayments) function: This function, named calculateInterest, is designed to compute the accrued interest on a loan based on the provided parameters.
function calculateInterest(uint owed, uint numPayments) public view returns (uint) {
if (numPayments <= 0) {
return 0;
}
uint ratePerMilPayment = (_interestRatePerMil * _secondsBetweenPayments) / SECONDS_PER_YEAR;
uint milAugmentInterest = owed * ((MILION + ratePerMilPayment) ** numPayments);
uint newAmountOwed = milAugmentInterest / (MILION ** numPayments);
return newAmountOwed - owed;
}
- Testing the calculateInterest(uint owed, uint numPayments) function:
To write test cases for the calculateInterest function, we'll cover various scenarios to ensure its correctness. Here are some scenarios:
- Test case to ensure interest calculation is correct when numPayments is greater than 0.
contract testSuite {
Loan loan;
function beforeAll() public {
// instantiate contract
loan = new Loan(1000, 5000, 12, 2592000, 650);
Assert.equal(uint(1), uint(1), "1 should be equal to 1");
}
// Test case to ensure interest calculation is correct when numPayments is greater than 0
function testCalculateInterestWithValidNumPayments() public {
uint owed = 100; // Sample amount owed
uint numPayments = 12; // Sample number of payments
uint interestRatePerMil = 5000; // Sample interest rate per mil
// Call the calculateInterest function and check the result
uint calculatedInterest = loan.calculateInterest(owed, numPayments);
uint expectedInterest = (1000 * ((1000000 + interestRatePerMil) ** 12)) / (1000000 ** 12) - 1000;
Assert.equal(calculatedInterest, expectedInterest, "Interest calculation is incorrect");
}
}
- The withdraw(uint amount) function: This function allows an investor to withdraw a certain amount of ether from the contract.
function withdraw(uint amount) public {
uint current$$$=address(this).balance;
require(current$$$ - amount >= 0, "Insufficient funds in the account");
uint numPaymentsSinceLastCalculated = numPaymentsBetweenTimestamps(
block.timestamp, _investorLastWithdraw[msg.sender]);
uint investorBalance = _investors[msg.sender];
investorBalance = investorBalance + calculateInterest(investorBalance,
numPaymentsSinceLastCalculated);
require(investorBalance >= amount, "Withdraw less or equal your investment");
_investors[msg.sender] = investorBalance - amount;
_investorLastWithdraw[msg.sender] = block.timestamp;
payable(msg.sender).transfer(amount);
}
Deploying the Loan Smart Contract:
Running the Test Suite:
Conclusion:These functions and some helper functions like isReady() (returns whether or not the loan has funded) concludes the implementation and unit testing of the Loan Smart Contract. Next, I will continue to examine the procedure of a borrower acquiring a loan and subsequently repaying it as defined in ALOE system [1]. We presume that the loan has already been established but has not yet received full funding or gathered the required number of borrowers. To monitor credit scores, we incorporate the following functions into the CreditBureau smart contract.
The function updateScoreBorrow notifies the credit bureau of a loan being issued for a specified amount. Similarly, the function updateScoreRepayment informs the Credit Bureau smart contract that a loan has received a repayment payment of a certain amount. Furthermore, the function getScore retrieves the credit score of a client based on the given parameters such as the requested amount, interest rate per mil, number of payments, and seconds between payments.
References:
[1] T. H. Austin, K. Potika and C. Pollett, 'Autonomous Lending Organization on Ethereum with Credit Scoring,' 2023 Silicon Valley Cybersecurity Conference (SVCC), San Jose, CA, USA, 2023.
[2] G. Wood, "Ethereum: A Secure Decentralised Generalised Transaction Ledger, EIP-150 Revision," Ethereum & Ethcore, gavin@ethcore.io
|