diff --git a/src/SizeView.sol b/src/SizeView.sol index 9453b336..b299039b 100644 --- a/src/SizeView.sol +++ b/src/SizeView.sol @@ -52,6 +52,7 @@ abstract contract SizeView is SizeStorage, ISizeView { function getUserView(address user) public view returns (UserView memory) { return UserView({ user: state.users[user], + account: user, collateralAmount: state.tokens.collateralToken.balanceOf(user), borrowAmount: state.tokens.borrowToken.balanceOf(user), debtAmount: state.tokens.debtToken.balanceOf(user) diff --git a/src/libraries/UserLibrary.sol b/src/libraries/UserLibrary.sol index 01bf333b..4d5221b8 100644 --- a/src/libraries/UserLibrary.sol +++ b/src/libraries/UserLibrary.sol @@ -10,6 +10,7 @@ struct User { struct UserView { User user; + address account; uint256 collateralAmount; uint256 borrowAmount; uint256 debtAmount; diff --git a/src/libraries/actions/Common.sol b/src/libraries/actions/Common.sol index 84a85708..5e410af4 100644 --- a/src/libraries/actions/Common.sol +++ b/src/libraries/actions/Common.sol @@ -17,9 +17,12 @@ library Common { state.tokens.debtToken.burn(fol.borrower, amount); loan.faceValue -= amount; + validateMinimumCredit(state, loan.getCredit()); + if (!loan.isFOL()) { fol.faceValue -= amount; fol.faceValueExited -= amount; + validateMinimumCredit(state, fol.getCredit()); } } diff --git a/src/libraries/actions/SelfLiquidateLoan.sol b/src/libraries/actions/SelfLiquidateLoan.sol index b006d37e..fbeb59ee 100644 --- a/src/libraries/actions/SelfLiquidateLoan.sol +++ b/src/libraries/actions/SelfLiquidateLoan.sol @@ -52,7 +52,6 @@ library SelfLiquidateLoan { Loan storage loan = state.loans[params.loanId]; - // credit := faceValue - exited :>= state.config.minimumCredit (by construction, see createSOL) uint256 credit = loan.getCredit(); Loan storage fol = state.getFOL(loan); diff --git a/test/BorrowAsMarketOrder.t.sol b/test/BorrowAsMarketOrder.t.sol index d8bb0196..e74e399e 100644 --- a/test/BorrowAsMarketOrder.t.sol +++ b/test/BorrowAsMarketOrder.t.sol @@ -417,7 +417,6 @@ contract BorrowAsMarketOrderTest is BaseTest { _deposit(alice, 100e18, 100e18); _deposit(bob, 100e18, 100e18); _lendAsLimitOrder(alice, 100e18, 12, 0.1e18, 12); - LoanOffer memory offerBefore = size.getUserView(alice).user.loanOffer; Vars memory _before = _state(); diff --git a/test/Repay.t.sol b/test/Repay.t.sol index a4388810..0a60498a 100644 --- a/test/Repay.t.sol +++ b/test/Repay.t.sol @@ -4,8 +4,10 @@ pragma solidity 0.8.20; import {BaseTest, Vars} from "./BaseTest.sol"; import {Errors} from "@src/libraries/Errors.sol"; + import {LoanStatus} from "@src/libraries/LoanLibrary.sol"; import {PERCENT} from "@src/libraries/MathLibrary.sol"; +import {RepayParams} from "@src/libraries/actions/Repay.sol"; import {Math} from "@src/libraries/MathLibrary.sol"; @@ -154,4 +156,36 @@ contract RepayTest is BaseTest { assertEq(_after.candy.borrowAmount, _before.candy.borrowAmount + faceValue / 2); assertTrue(!size.getLoan(loanId).repaid); } + + function test_Repay_repay_partial_cannot_leave_loan_below_minimumCredit() public { + _setPrice(1e18); + _deposit(alice, usdc, 100e6); + _deposit(bob, weth, 150e18); + _lendAsLimitOrder(alice, 100e18, 12, 0, 12); + uint256 amount = 10e18; + uint256 loanId = _borrowAsMarketOrder(bob, alice, amount, 12); + + vm.expectRevert( + abi.encodeWithSelector(Errors.CREDIT_LOWER_THAN_MINIMUM_CREDIT.selector, 4e18, size.config().minimumCredit) + ); + _repay(bob, loanId, 6e18); + assertGt(size.getCredit(loanId), size.config().minimumCredit); + } + + function test_Repay_repay_partial_cannot_leave_loan_below_minimumCredit(uint256 borrowAmount, uint256 repayAmount) + public + { + borrowAmount = bound(borrowAmount, size.config().minimumCredit, 100e18); + repayAmount = bound(repayAmount, 0, borrowAmount); + + _setPrice(1e18); + _deposit(alice, usdc, 100e6); + _deposit(bob, weth, 150e18); + _lendAsLimitOrder(alice, 100e18, 12, 0, 12); + uint256 loanId = _borrowAsMarketOrder(bob, alice, borrowAmount, 12); + + vm.prank(bob); + try size.repay(RepayParams({loanId: loanId, amount: repayAmount})) {} catch {} + assertGe(size.getCredit(loanId), size.config().minimumCredit); + } } diff --git a/test/invariants/CryticToFoundry.t.sol b/test/invariants/CryticToFoundry.t.sol index 5692c495..14ba2afa 100644 --- a/test/invariants/CryticToFoundry.t.sol +++ b/test/invariants/CryticToFoundry.t.sol @@ -33,4 +33,43 @@ contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { 47700905178432190716842576859681767948209730775316858409394951552214610302274 ); } + + function test_REPAY_01() public { + deposit(address(0x0), 0); + deposit(address(0xdeadbeef), 91460117); + borrowAsLimitOrder(4907270871702042502, 5894179853816920); + lendAsMarketOrder(address(0x0), 19031769674707116036212, 5004820998700516064, false); + repay(115792089237316195423570985008687907853269984665640564039457584007913129639935, 400); + } + + function test_REPAY_01_2() public { + deposit(address(0xdeadbeef), 0); + deposit(address(0x0), 0); + lendAsLimitOrder(0, 3471498, 0); + borrowAsMarketOrder( + address(0x0), + 24574995402710635646614640190156820935535387820397, + 90229103640261999611339698052518587384522780478451, + false, + 0, + 17145422572889695645779948228995886701899200731 + ); + repay(0, 23008551643797667129); + } + + function test_LOAN_05() public { + deposit(address(0xdeadbeef), 0); + deposit(address(0x0), 0); + lendAsLimitOrder(0, 4640020, 0); + borrowAsMarketOrder( + address(0x0), + 7104076085823381970015930982381262816774162739483, + 152749325598820291654851530442320910523826499512205, + false, + 0, + 20428906037535863295274218098285099272042586872 + ); + try this.repay(0, 14698502976581688395) {} catch {} + assertTrue(invariant_LOAN(), LOAN_05); + } } diff --git a/test/invariants/Helper.sol b/test/invariants/Helper.sol index 3beb24a0..8f799d90 100644 --- a/test/invariants/Helper.sol +++ b/test/invariants/Helper.sol @@ -12,6 +12,8 @@ abstract contract Helper is Deploy, PropertiesConstants { uint256 internal constant MAX_DURATION = 180 days; uint256 internal constant MAX_RATE = 2e18; uint256 internal constant MAX_TIME_BUCKETS = 24; + uint256 internal constant MIN_PRICE = 0.01e18; + uint256 internal constant MAX_PRICE = 10_000e18; function _getRandomUser(address user) internal pure returns (address) { return uint160(user) % 3 == 0 ? USER1 : uint160(user) % 3 == 1 ? USER2 : USER3; diff --git a/test/invariants/TargetFunctions.sol b/test/invariants/TargetFunctions.sol index 45805335..48934859 100644 --- a/test/invariants/TargetFunctions.sol +++ b/test/invariants/TargetFunctions.sol @@ -27,6 +27,7 @@ import {SelfLiquidateLoanParams} from "@src/libraries/actions/SelfLiquidateLoan. import {WithdrawParams} from "@src/libraries/actions/Withdraw.sol"; abstract contract TargetFunctions is Deploy, Helper, Properties, BaseTargetFunctions { + event L1(uint256 a); event L4(uint256 a, uint256 b, uint256 c, uint256 d); function setup() internal override { @@ -220,8 +221,8 @@ abstract contract TargetFunctions is Deploy, Helper, Properties, BaseTargetFunct __after(loanId); - lt(_after.sender.borrowAmount, _before.sender.borrowAmount, REPAY_01); - gt(_after.protocolBorrowAmount, _before.protocolBorrowAmount, REPAY_01); + lte(_after.sender.borrowAmount, _before.sender.borrowAmount, REPAY_01); + gte(_after.protocolBorrowAmount, _before.protocolBorrowAmount, REPAY_01); lt(_after.sender.debtAmount, _before.sender.debtAmount, REPAY_02); } @@ -259,7 +260,7 @@ abstract contract TargetFunctions is Deploy, Helper, Properties, BaseTargetFunct t(_before.isSenderLiquidatable, LIQUIDATE_03); } - function selfLiquidateLoan(uint256 loanId) public getSender { + function selfLiquidateLoan(uint256 loanId) internal getSender { __before(loanId); precondition(_before.activeLoans > 0); @@ -275,7 +276,7 @@ abstract contract TargetFunctions is Deploy, Helper, Properties, BaseTargetFunct lt(_after.sender.debtAmount, _before.sender.debtAmount, LIQUIDATE_02); } - function liquidateLoanWithReplacement(uint256 loanId, address borrower) public getSender { + function liquidateLoanWithReplacement(uint256 loanId, address borrower) internal getSender { __before(loanId); precondition(_before.activeLoans > 0); @@ -293,7 +294,7 @@ abstract contract TargetFunctions is Deploy, Helper, Properties, BaseTargetFunct lt(_after.borrower.debtAmount, _before.borrower.debtAmount, LIQUIDATE_02); } - function moveToVariablePool(uint256 loanId) public getSender { + function moveToVariablePool(uint256 loanId) internal getSender { __before(loanId); precondition(_before.activeLoans > 0); @@ -307,8 +308,7 @@ abstract contract TargetFunctions is Deploy, Helper, Properties, BaseTargetFunct } function setPrice(uint256 price) public { - uint256 oldPrice = priceFeed.getPrice(); - price = between(price, oldPrice * 1e18 / 1.2e18, priceFeed.getPrice() * 1.2e18 / 1e18); + price = between(price, MIN_PRICE, MAX_PRICE); priceFeed.setPrice(price); }