Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Temple Loving Care #2

Merged
merged 19 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,11 @@
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
branch = v1.3.0
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
branch = v4.8.1
[submodule "lib/openzeppelin-contracts-upgradeable"]
path = lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
branch = v4.8.1
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Temple-Loving-Care
Temple Fixed Rate Lending protocol
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at 045704
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts-upgradeable
5 changes: 5 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/

openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/
openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
14 changes: 0 additions & 14 deletions src/Counter.sol

This file was deleted.

308 changes: 308 additions & 0 deletions src/TLC.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
// SPDX-License-Identifier: UNLICENSED
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
pragma solidity ^0.8.13;


import {Ownable} from "openzeppelin-contracts/access/Ownable.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";

import {Operators} from "./common/access/Operators.sol";

contract TLC is Ownable, Operators {
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also - can't easily migrate user positions if we need a v2. pros/cons of making this upgradeable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#5

using SafeERC20 for IERC20;

struct Position {
uint256 collateralAmount;
uint256 debtAmount;
uint256 createdAt;
}

// Collateral Parameters

/// Supported collateral token address
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
address public collateralAddress;
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved

/// Collateral token price
uint256 public collateralPrice;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm - is this a fixed price rather than some oracle/on-chain price? At a minimum should have a setter? Would be good if you can explain how this works.

Also:

  • Is this meant to be a USD price, something else?
  • Is it expected to be in 18 dp's too?


/// Requited collateral backing to not be in bad debt
uint256 public collateralizationRatio;
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved

/// Total collateral posted
uint256 public collateralBalance;

//// Total debt taken out
uint256 public debtBalance;

/// Fixed interest rate
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
uint256 public interestRate;

/// Amount in seconds for interest to accumulate
uint256 public interestRatePeriod;

/// Fee for taking out a loan
uint256 public originationFee;
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved

/// Fee charged for debtor liquidation
uint256 public liquidationFee;

/// Address to send bad debt collateral
address public debtCollector;

/// Debt parameters

/// Debt token address
address public debtAddress;

/// Debt token price
uint256 public debtPrice;
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved


/// Mapping of user positions
mapping(address => Position) public positions;

event DepositDebt(uint256 amount);
event RemoveDebt(uint256 amount);
event PostCollateral(uint256 amount);
event Borrow(address account, uint256 amount);
event Repay(address account, uint256 amount);
event Withdraw(address account, uint256 amount);
event Liquidated(address account, uint256 debtAmount, uint256 collateralSeized);

error ZeroBalance(address account);
error InsufficentCollateral(uint256 maxCapacity, uint256 debtAmount);
error ExceededBorrowedAmount(address account, uint256 amountBorrowed, uint256 amountRepay);
error ExceededCollateralAmonut(address account, uint256 amountCollateral, uint256 collateralWithdraw);
error WillUnderCollaterlize(address account, uint256 withdrawalAmount);
error OverCollaterilized(address account);

constructor(
uint256 _interestRate,
uint256 _collateralizationRatio,
uint256 _interestRatePeriod,

address _collateralAddress,
uint256 _collateralPrice,

address _debtAddress,
uint256 _debtPrice,
uint256 _liquidationFee,
address _debtCollector

) {
interestRate = _interestRate;
collateralizationRatio = _collateralizationRatio;
interestRatePeriod = _interestRatePeriod;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be <= 365 days correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, something like 8 hours seems ideal here


collateralAddress = _collateralAddress;
collateralPrice = _collateralPrice;

debtAddress = _debtAddress;
debtPrice = _debtPrice;

liquidationFee = _liquidationFee;
debtCollector = _debtCollector;
}


function addOperator(address _address) external override onlyOwner {
_addOperator(_address);
}

function removeOperator(address _address) external override onlyOwner {
_removeOperator(_address);
}

/**
* @dev Get user principal amount
* @return principal amount
*/
function getDebtAmount() public view returns (uint256) {
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
return positions[msg.sender].debtAmount;
}

/**
* @dev Get user total debt incurred (principal + interest)
* @return total Debt
*/
function getTotalDebtAmount(address account) public view returns (uint256) {
uint totalDebt = positions[account].debtAmount;
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
uint256 periodsPerYear = 365 days / interestRatePeriod;
uint256 periodsElapsed = (block.timestamp / interestRatePeriod) - (positions[account].createdAt / interestRatePeriod);
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
totalDebt += ((totalDebt * interestRate) / 10000 / periodsPerYear) * periodsElapsed;
return totalDebt;
}

/**
* @dev Allows operator to depoist debt tokens
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
* @param amount is the amount to deposit
*/
function depositDebt(uint256 amount) external onlyOperators{
require(amount > 0, "Amount is zero !!");
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
if (amount == 0) {
revert ZeroBalance(msg.sender);
}
debtBalance += amount;
IERC20(debtAddress).safeTransferFrom(
msg.sender,
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
address(this),
amount
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An invariant exists where debtAddress.balanceOf(address(this)) >= debtBalance

Should have an assert for this whenever we increment/decrement?

At a minimum add fuzz testing for it

emit DepositDebt(amount);
}

/**
* @dev Allows operator to remove debt token
* @param amount is the amount to remove
*/
function removeDebt(uint256 amount) external onlyOperators{
require(amount > 0, "Amount is zero !!");
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
if (amount == 0) {
revert ZeroBalance(msg.sender);
}
debtBalance -= amount;
IERC20(debtAddress).safeTransfer(
msg.sender,
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
amount
);
emit RemoveDebt(amount);
}

/**
* @dev Allows borrower to deposit collateral
* @param amount is the amount to deposit
*/
function postCollateral(uint256 amount) external {
if (amount == 0) revert ZeroBalance(msg.sender);
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
positions[msg.sender].collateralAmount += amount;
collateralBalance += amount;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to track collateralBalance separately? When it's required, could just use IERC20(collateralAddress).balanceOf(address(this))

The risk is that it could get out of sync (although would then have to deal with others donating/sending collateral without calling this function)

At a minimium should have an invariant assertion whenever we increment/decrement that IERC20(collateralAddress).balanceOf(address(this)) >= collateralBalance

I think the other invariant which should always hold is:

sum(positions[user].collateralAmount) === collateralBalance

Could fuzz test that perhaps?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok I am removing tracking collateralBalance. Not that useful anyways

IERC20(collateralAddress).safeTransferFrom(
msg.sender,
address(this),
amount
);
}

function borrow(uint256 amount) external {
if (positions[msg.sender].debtAmount != 0) {
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
positions[msg.sender].debtAmount = getTotalDebtAmount(msg.sender);
}

uint256 maxBorrowCapacity = maxBorrowCapacity(msg.sender);
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
maxBorrowCapacity -= positions[msg.sender].debtAmount;

// TODO: Add fees for borrowing
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
positions[msg.sender].debtAmount += amount;

if (positions[msg.sender].debtAmount > maxBorrowCapacity) {
revert InsufficentCollateral(maxBorrowCapacity, positions[msg.sender].debtAmount);
}

// If more than 1 interest rate period has passed update the start-time
if (block.timestamp - positions[msg.sender].createdAt > interestRatePeriod || positions[msg.sender].createdAt == 0 ) {
positions[msg.sender].createdAt = block.timestamp;
}

debtBalance -= amount;
IERC20(debtAddress).safeTransfer(
msg.sender,
amount
);
emit Borrow(msg.sender, amount);
}

function maxBorrowCapacity(address account) public returns(uint256) {
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
return ((positions[account].collateralAmount * collateralPrice * 100) / debtPrice / collateralizationRatio);
}



/**
* @dev Allows borrower to with draw collateral if sufficient to not default on loan
* @param withdrawalAmount is the amount to withdraw
*/
function withdrawCollateral(uint256 withdrawalAmount) external {
if (withdrawalAmount > positions[msg.sender].collateralAmount) {
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
revert ExceededCollateralAmonut(msg.sender, positions[msg.sender].collateralAmount, withdrawalAmount);
}

uint256 maxBorrowCapacity = (((positions[msg.sender].collateralAmount - withdrawalAmount) * collateralPrice * 100) / debtPrice / collateralizationRatio);
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
if (positions[msg.sender].debtAmount > maxBorrowCapacity ) {
revert WillUnderCollaterlize(msg.sender, withdrawalAmount);
}

positions[msg.sender].collateralAmount -= withdrawalAmount;
collateralBalance -= withdrawalAmount;
IERC20(collateralAddress).safeTransfer(
msg.sender,
withdrawalAmount
);

emit Withdraw(msg.sender, withdrawalAmount);
}

/**
* @dev Allows borrower to repay borrowed amount
* @param repayAmount is the amount to repay
*/
function repay(uint256 repayAmount) external {
if (repayAmount == 0) revert ZeroBalance(msg.sender);
positions[msg.sender].debtAmount = getTotalDebtAmount(msg.sender);

if (repayAmount > positions[msg.sender].debtAmount) {
revert ExceededBorrowedAmount(msg.sender, positions[msg.sender].debtAmount, repayAmount);
}

positions[msg.sender].debtAmount -= repayAmount;
debtBalance += repayAmount;

uint256 periodsElapsed = (block.timestamp / interestRatePeriod) - (positions[msg.sender].createdAt / interestRatePeriod);
positions[msg.sender].createdAt += periodsElapsed * interestRatePeriod;
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
IERC20(debtAddress).safeTransferFrom(
msg.sender,
address(this),
repayAmount
);
emit Repay(msg.sender, repayAmount);
}

/**
* @dev Allows operator to liquidate debtors position
* @param debtor the account to liquidate
*/
function liquidate(address debtor) external onlyOperators {
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved

if (getCurrentCollaterilizationRatio(debtor) >= collateralizationRatio) {
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
revert OverCollaterilized(debtor);
}

uint256 totalDebtOwed = getTotalDebtAmount(debtor);
// TODO: Add liquidation fee
uint256 collateralSeized = (totalDebtOwed * debtPrice) / collateralPrice;

if (collateralSeized > positions[debtor].collateralAmount) {
collateralSeized = positions[debtor].collateralAmount;
}

positions[debtor].collateralAmount -= collateralSeized;
positions[debtor].debtAmount = 0;
positions[debtor].createdAt = 0;
IERC20(collateralAddress).safeTransfer(
shero0x1337 marked this conversation as resolved.
Show resolved Hide resolved
debtCollector,
collateralSeized
);

emit Liquidated(debtor, totalDebtOwed, collateralSeized);

}

function getCurrentCollaterilizationRatio(address account) public view returns(uint256) {
if (positions[account].debtAmount == 0) {
return 0;
} else {
return ((positions[account].collateralAmount * collateralPrice * 100) / getTotalDebtAmount(account) / debtPrice);
}
}

}
Loading