Skip to content

Instantly share code, notes, and snippets.

@richshaw
Created July 22, 2021 19:21
Show Gist options
  • Save richshaw/65eeb747824458c3fbb87c117f1ecbf4 to your computer and use it in GitHub Desktop.
Save richshaw/65eeb747824458c3fbb87c117f1ecbf4 to your computer and use it in GitHub Desktop.
Video Rental loan Ethereum smart contract
// 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