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

Temple Loving Care #2

merged 19 commits into from
Feb 17, 2023

Conversation

shero0x1337
Copy link
Contributor

@shero0x1337 shero0x1337 commented Jan 31, 2023

Temple Fix rate lending protocol

Specification: https://docs.google.com/document/d/1euRvN5BLSJWOCfWk8ltfNJhof2t65PfAoLx20ZRsNDo/edit

  • A fixed rate isolated lending protocol. It is made to support both DAI and OUD borrowing against temple.
  • The price of temple and the debt token are hardcoded. Temple is based on TPI and stable-coins are hardcoded to 1.
  • Interest rate is accrued for a certain fixed window debtCollector as opposed to a continues curve.
  • Bad debt position will remove all debt from the system and transfer equivalent backing collateral to debtCollector. Address owned by temple

src/TLC.sol Outdated Show resolved Hide resolved
test/TLC.t.sol Outdated Show resolved Hide resolved
test/TLC.t.sol Outdated Show resolved Hide resolved
test/TLC.t.sol Outdated Show resolved Hide resolved
src/TLC.sol Outdated Show resolved Hide resolved
src/TLC.sol Outdated Show resolved Hide resolved
src/TLC.sol Outdated Show resolved Hide resolved
src/TLC.sol Outdated Show resolved Hide resolved
src/TLC.sol Outdated Show resolved Hide resolved
src/TLC.sol Outdated Show resolved Hide resolved
src/TLC.sol Outdated Show resolved Hide resolved
src/TLC.sol Outdated Show resolved Hide resolved
src/TLC.sol Outdated
import {Operators} from "./common/access/Operators.sol";

contract TLC is Ownable, Operators {

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

shero0x1337 and others added 2 commits February 6, 2023 16:55
remove fees

rename contract

allow deposit debt from generalized account

cleanup nits

gas optimization
src/TempleLineOfCredit.sol Outdated Show resolved Hide resolved
_removeOperator(_address);
}

function setDebtPrice(uint256 _debtPrice) external onlyOperators {

Choose a reason for hiding this comment

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

Can we add events for these 4 setters? Could be useful later down the line to index in subgraph

/// @notice Supported collateral token address
IERC20 public immutable collateralToken;

/// @notice Collateral token price
Copy link

@frontier159 frontier159 Feb 7, 2023

Choose a reason for hiding this comment

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

What precision do these prices need to be in? Always 1e18? Or actually it doesn't matter as long as collateralPrice precision == debtPrice precision?

Is the price meant to be in USD terms or some other basis?

For reference, RAMOS TPI is in terms of 10_000, so 9700 == 0.97
Might be worth keeping consistent cross-contract

Copy link
Contributor Author

@shero0x1337 shero0x1337 Feb 7, 2023

Choose a reason for hiding this comment

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

yeah doesn't matter as long as the debt and collateral precision are the same

/// @notice Collateral token price
uint256 public collateralPrice;

/// @notice Requited collateral backing to not be in bad debt in percentage
Copy link

@frontier159 frontier159 Feb 7, 2023

Choose a reason for hiding this comment

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

typo - should be "Required"

So to get 120%, you enter 120? Or some other precision? Worth adding to the comment imo.

If that's right, that means there's no possibility for 0.5% if that was required? Worth making as 10_000 precision just in case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah doesn't hurt to increase percision

uint256 totalDebt = positions[account].debtAmount;
uint256 periodsPerYear = 365 days / interestRatePeriod;
uint256 periodsElapsed = block.timestamp - positions[account].createdAt; // divided by interestRatePeriod
totalDebt += (((totalDebt * interestRateBps) / 10000 / periodsPerYear) * periodsElapsed) / interestRatePeriod;
Copy link

@frontier159 frontier159 Feb 7, 2023

Choose a reason for hiding this comment

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

periodsElapsed can be renamed as secondsElapsed now? It doesn't represent a period
There's still some truncation as a result of this (slither would warn you). I think this is equivalent to:

    // Add on the accrued (simple) interest up to now.
    totalDebt += totalDebt * interestRateBps * secondsElapsed / 10_000 / periodsPerYear / interestRatePeriod;

ie move the multiplications to the front.


Or are you expecting that this calc should be in whole days only (ie debt only increases once per day)? In which case:

  • 0 seconds after createdAt: no accrued interest
  • ...
  • 86399 seconds after createdAt: no accrued interest
  • 86400 seconds after createdAt: 1 day of accrued interest

In which case I think you want:

// Total debt increases by full periods only (ie discrete days worth of interest)
uint256 fullPeriodsElapsed = (block.timestamp - positions[account].createdAt) / interestRatePeriod;
totalDebt += totalDebt * interestRateBps * fullPeriodsElapsed / 10_000 / periodsPerYear;

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 made this continuous instead of discrete

positions[msg.sender].createdAt = block.timestamp;
}

debtBalance -= amount;

Choose a reason for hiding this comment

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

This will revert if the amount to borrow is more than the DAI tokens in debtBalance (ie the operator needs to depositDebt() to add more DAI). I think it would be nicer UX (for the user) to revert with a custom error at the top of this function, ie fail early

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point


// 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;

Choose a reason for hiding this comment

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

I don't think the if() is required here or in borrow(), if there's no truncation in the getTotalDebtAmount() function. So can just always set:

positions[msg.sender].createdAt = block.timestamp;

Definitely worth tests which check the edge cases carefully

* @dev Allows borrower to deposit collateral
* @param amount is the amount to deposit
*/
function postCollateral(uint256 amount) external {

Choose a reason for hiding this comment

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

Wonder if we should be cautious and mark nonReentrant just to be sure. Eg any flash loan scenarios which could post collateral->borrow->repay?

src/TempleLineOfCredit.sol Outdated Show resolved Hide resolved
src/TempleLineOfCredit.sol Outdated Show resolved Hide resolved
ERC20Mock public debtToken;
uint256 public debtPrice;

address admin = address(0x1);

Choose a reason for hiding this comment

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

Can use makeAddr("admin") to add a label which is useful in debugging

https://book.getfoundry.sh/reference/forge-std/make-addr#makeaddr

bob and alice have the same address here...

assertEq(tlc.collateralPrice(), collateralPrice);
assertEq(address(tlc.debtToken()), address(debtToken));
assertEq(tlc.debtPrice(), debtPrice);
}

Choose a reason for hiding this comment

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

checks missing for: interestRatePeriod, debtBalance, debtCollector

assertEq(address(tlc.debtToken()), address(debtToken));
assertEq(tlc.debtPrice(), debtPrice);
}

Choose a reason for hiding this comment

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

Should also check all function access. Ie that they are only callable by the expected. I've found something like this is pretty handy:

    function expectOnlyOperators() internal {
        vm.prank(alice);
        vm.expectRevert(abi.encodeWithSelector(Operators.OnlyOperators.selector, alice));
    }

    function expectOnlyOwner() internal {
        vm.prank(alice);
        vm.expectRevert("Ownable: caller is not the owner");
    }

    function test_AddOperatorAccess() public {
        expectOnlyOwner();
        tlc.addOperator(alice);
    }

    function test_XXXAccess() public {
        expectOnlyYYY();
        XXX;
    }

...for all restricted functions...

assertEq(uint256(0), aliceCreatedAt);
assertEq((totalDebt * debtPrice / collateralPrice), collateralToken.balanceOf(debtCollector));
}
}

Choose a reason for hiding this comment

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

Missing tests for:

  1. setCollateralPrice
  2. setDebtCollector
  3. setCollateralizationRatio
  4. getDebtAmount
  5. removeDebt

tlc.borrow(borrowAmount);
}

function testBorrowAccuresInterest(uint32 periodElapsed) external {

Choose a reason for hiding this comment

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

typo: Accures

fuzz tests - awesome! If using this, should bund the periodElapsed to something reasonable for a timestamp, eg:

uint256 periodElapsed = bound(_periodElapsed, 0, 5 * 365 days);

However I think instead of just copy/pasting the formula to check against back in the test, we should also check against known/expected values. This relates to whether we expect the debt to increase continuously (every second) or discretely (when each new day that passes).

So should check a few known edge cases explicitly.

I also found this useful to dump the inputs/outputs of the fuzz:

vm.writeLine("out.txt", string.concat(string.concat(vm.toString(periodElapsed), " -> "), vm.toString(expectedTotalDebt)));

Need to add this to foundry.toml to use that tho:

fs_permissions = [{ access = "read-write", path = "./"}]

function _initDeposit(uint256 depositAmount) internal {
vm.startPrank(admin);
debtToken.approve(address(tlc), depositAmount);
tlc.depositDebt(admin, depositAmount);

Choose a reason for hiding this comment

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

Can we check that the event is emitted too, with expected events?

Same for all functions to be honest.

vm.startPrank(admin);
uint256 depositAmount = uint256(100_000e18);
debtToken.approve(address(tlc), depositAmount);
tlc.depositDebt(admin, depositAmount);

Choose a reason for hiding this comment

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

Should check that a deposit amount of 0 reverts with InvalidAmount

Also, can we check that the event is emitted too, with expected events?

Same for all functions to be honest.

uint256 repayAmount = uint(50_000e18);
_borrow(alice, uint(100_000e18), borrowAmount);
uint256 debtBalanceBefore = tlc.debtBalance();

Choose a reason for hiding this comment

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

Also need to check where more than one interest rate period has passsed?

* @param repayAmount is the amount to repay
*/
function repay(uint256 repayAmount) external {
if (repayAmount == 0) revert InvalidAmount(repayAmount);

Choose a reason for hiding this comment

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

I wonder if these user functions should fail if block.timestamp == positions[msg.sender].createdAt. Basically blocking any flash borrow->repay type cycle

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm, don't see that being particularly exploitable

uint256 totalDebt = tlc.getTotalDebtAmount(alice);
assertTrue(tlc.getCurrentCollaterilizationRatio(alice) < minCollateralizationRatio);
vm.prank(admin);
tlc.liquidate(alice);

Choose a reason for hiding this comment

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

Should check events are emitted.

Also I don't think these edge cases are tested:

  • collateralSeized > positions[debtor].collateralAmount
  • positions[account].debtAmount == 0

Definitely worth you running forge coverage to see what's covered/what's not:

forge coverage --report lcov && genhtml lcov.info -o report --branch-coverage --legend

@shero0x1337 shero0x1337 merged commit 5af95e3 into stage Feb 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants