-
Notifications
You must be signed in to change notification settings - Fork 0
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
Conversation
Support repay and liquidate
src/TLC.sol
Outdated
import {Operators} from "./common/access/Operators.sol"; | ||
|
||
contract TLC is Ownable, Operators { | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove fees rename contract allow deposit debt from generalized account cleanup nits gas optimization
Refactor
src/TempleLineOfCredit.sol
Outdated
_removeOperator(_address); | ||
} | ||
|
||
function setDebtPrice(uint256 _debtPrice) external onlyOperators { |
There was a problem hiding this comment.
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
src/TempleLineOfCredit.sol
Outdated
/// @notice Supported collateral token address | ||
IERC20 public immutable collateralToken; | ||
|
||
/// @notice Collateral token price |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
src/TempleLineOfCredit.sol
Outdated
/// @notice Collateral token price | ||
uint256 public collateralPrice; | ||
|
||
/// @notice Requited collateral backing to not be in bad debt in percentage |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
src/TempleLineOfCredit.sol
Outdated
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; |
There was a problem hiding this comment.
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;
There was a problem hiding this comment.
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
src/TempleLineOfCredit.sol
Outdated
positions[msg.sender].createdAt = block.timestamp; | ||
} | ||
|
||
debtBalance -= amount; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point
src/TempleLineOfCredit.sol
Outdated
|
||
// 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; |
There was a problem hiding this comment.
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
src/TempleLineOfCredit.sol
Outdated
* @dev Allows borrower to deposit collateral | ||
* @param amount is the amount to deposit | ||
*/ | ||
function postCollateral(uint256 amount) external { |
There was a problem hiding this comment.
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
?
ERC20Mock public debtToken; | ||
uint256 public debtPrice; | ||
|
||
address admin = address(0x1); |
There was a problem hiding this comment.
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); | ||
} |
There was a problem hiding this comment.
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); | ||
} | ||
|
There was a problem hiding this comment.
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)); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing tests for:
- setCollateralPrice
- setDebtCollector
- setCollateralizationRatio
- getDebtAmount
- removeDebt
test/TempleLineOfCredit.t.sol
Outdated
tlc.borrow(borrowAmount); | ||
} | ||
|
||
function testBorrowAccuresInterest(uint32 periodElapsed) external { |
There was a problem hiding this comment.
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 = "./"}]
test/TempleLineOfCredit.t.sol
Outdated
function _initDeposit(uint256 depositAmount) internal { | ||
vm.startPrank(admin); | ||
debtToken.approve(address(tlc), depositAmount); | ||
tlc.depositDebt(admin, depositAmount); |
There was a problem hiding this comment.
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.
test/TempleLineOfCredit.t.sol
Outdated
vm.startPrank(admin); | ||
uint256 depositAmount = uint256(100_000e18); | ||
debtToken.approve(address(tlc), depositAmount); | ||
tlc.depositDebt(admin, depositAmount); |
There was a problem hiding this comment.
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(); | ||
|
There was a problem hiding this comment.
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?
src/TempleLineOfCredit.sol
Outdated
* @param repayAmount is the amount to repay | ||
*/ | ||
function repay(uint256 repayAmount) external { | ||
if (repayAmount == 0) revert InvalidAmount(repayAmount); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
test/TempleLineOfCredit.t.sol
Outdated
uint256 totalDebt = tlc.getTotalDebtAmount(alice); | ||
assertTrue(tlc.getCurrentCollaterilizationRatio(alice) < minCollateralizationRatio); | ||
vm.prank(admin); | ||
tlc.liquidate(alice); |
There was a problem hiding this comment.
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
Oud support
Temple Fix rate lending protocol
Specification: https://docs.google.com/document/d/1euRvN5BLSJWOCfWk8ltfNJhof2t65PfAoLx20ZRsNDo/edit
debtCollector
. Address owned by temple