Created
July 22, 2021 19:21
-
-
Save richshaw/65eeb747824458c3fbb87c117f1ecbf4 to your computer and use it in GitHub Desktop.
Video Rental loan Ethereum smart contract
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: GPL-3.0 | |
pragma solidity >=0.8.0 <0.9.0; | |
/* * | |
* "Video Rental" Loan | |
* Alice lends Bob x eth | |
* Bob owes principle + interest on borrowed eth until term ends | |
* If Bob does not repay borrowed eth before the term ends then | |
* Bob will incur a late fee in the form of a higher interest rate. | |
* */ | |
contract loan { | |
/* Things loans have */ | |
uint amount; // @todo just ether for this version expand to other tokens in the future | |
uint principalOutstanding; | |
uint interestRate; // decimal -2; Solidarity does not support decimals yet https://docs.soliditylang.org/en/latest/types.html#fixed-point-numbers | |
uint pastDueInterestRate; | |
uint previouslyAccurredInterest; | |
address lender; | |
address borrower; | |
uint termDays; | |
uint loanAcceptedTimestamp = 0; | |
uint lastPaymentTimestamp = 0; | |
/* Constants */ | |
uint decimals = 18; | |
uint denominator = 10**decimals; | |
constructor(uint _interestRate, | |
uint _pastDueInterestRate, | |
address _borrower, | |
uint _termDays) payable { | |
require(msg.value > 0, "No ether sent with contract"); | |
amount = msg.value; | |
interestRate = _interestRate; | |
pastDueInterestRate = _pastDueInterestRate; | |
lender = msg.sender; // The address creating the loan is the lender | |
borrower = _borrower; | |
termDays = _termDays; | |
require(msg.sender != borrower, "Lender and borrower cannot be same address"); | |
} | |
modifier onlyLender() { | |
require(isLender(),"Can only be accessed by lender"); | |
_; | |
} | |
modifier onlyBorrower() { | |
require(isBorrower(),"Can only be accessed by borrower"); | |
_; | |
} | |
modifier onlyLenderOrBorrower() { | |
require(isLenderOrBorrower(),"Can only be accessed by lender or borrower"); | |
_; | |
} | |
function isLender() private view returns(bool) { | |
return msg.sender == lender; | |
} | |
function isBorrower() private view returns(bool) { | |
return msg.sender == borrower; | |
} | |
function isLenderOrBorrower() private view returns(bool) { | |
return msg.sender == lender || msg.sender == borrower; | |
} | |
/* Things loans do */ | |
function viewLoanTerms() public view returns(uint, uint, uint, address, address, uint) { | |
return( | |
amount, | |
interestRate, | |
pastDueInterestRate, | |
lender, | |
borrower, | |
termDays | |
); | |
} | |
function acceptTerms() public onlyBorrower() { | |
require(loanAcceptedTimestamp == 0, "Loan terms have already been accepted"); | |
_sendPayment(borrower,amount); | |
loanAcceptedTimestamp = block.timestamp; | |
lastPaymentTimestamp = block.timestamp; | |
principalOutstanding = amount; | |
} | |
function _sendPayment(address _to, uint _amount) private { | |
(bool sent, bytes memory data) = _to.call{value: _amount}(""); | |
data; // Stops unused local var warning? | |
require(sent, "Failed to send Ether"); | |
} | |
function makeLoanPayment() public payable { | |
uint _amountCurrentlyOutstanding = amountCurrentlyOutstanding(); | |
uint _interest = _amountCurrentlyOutstanding - principalOutstanding; | |
uint _paymentAmount = msg.value; | |
require(_paymentAmount <= _amountCurrentlyOutstanding,"Payment cannot be greater than the total amount outstanding"); | |
require(_paymentAmount > 0,"No payment sent"); | |
if(_paymentAmount >= _interest) { | |
_paymentAmount = _paymentAmount - _interest; // Pay off interest first then principle | |
_interest = 0; | |
principalOutstanding = principalOutstanding - _paymentAmount; | |
} else { // Only pay off a part of accured interest | |
_interest = _interest - _paymentAmount; | |
previouslyAccurredInterest = previouslyAccurredInterest + _interest; | |
} | |
lastPaymentTimestamp = block.timestamp; | |
} | |
function amountCurrentlyOutstanding() public view returns(uint) { | |
require(loanAcceptedTimestamp > 0, "Loan terms have not been accepted, nothing outstanding"); | |
uint _secondsSinceLastPayment = block.timestamp - lastPaymentTimestamp; | |
uint _secondsInYear = 31536000; | |
//Normalize numbers to avoid overflow | |
uint _interestRate = _getCurrentInterestRate() * (10**(decimals-2)); | |
uint _principalOutstanding = principalOutstanding * denominator; | |
// Calculate current amount outstanding on the loan | |
uint _interestRatePerSecond = _interestRate / _secondsInYear; | |
uint _amountDue = _principalOutstanding * (_interestRatePerSecond * _secondsSinceLastPayment); | |
_amountDue = _amountDue / denominator ** 2 + _principalOutstanding / denominator; | |
_amountDue = _amountDue + previouslyAccurredInterest; | |
return(_amountDue); | |
} | |
function _getCurrentInterestRate() private view returns(uint) { | |
if(block.timestamp > loanAcceptedTimestamp + termDays * 1 days) { | |
return(pastDueInterestRate); | |
} else { | |
return(interestRate); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment