From 12cf342a459fbb4b204550b774ffab386b73f87b Mon Sep 17 00:00:00 2001 From: smokey Date: Mon, 23 Jan 2023 21:10:37 -0500 Subject: [PATCH 01/15] init --- src/Counter.sol | 14 ----- src/TLC.sol | 91 +++++++++++++++++++++++++++++++++ src/common/access/Operators.sol | 57 +++++++++++++++++++++ test/Counter.t.sol | 9 ++-- 4 files changed, 152 insertions(+), 19 deletions(-) delete mode 100644 src/Counter.sol create mode 100644 src/TLC.sol create mode 100644 src/common/access/Operators.sol diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/TLC.sol b/src/TLC.sol new file mode 100644 index 0000000..9d422f0 --- /dev/null +++ b/src/TLC.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Operators} from "./common/access/Operators.sol"; + +contract TLC is Operators{ + + struct Token { + address tokenAddress; + uint256 price; + uint256 reserveBalance; + uint256 lastUpdatedAt; + } + + struct Reserve { + uint256 collateralAmount; + uint256 debtAmount; + uint256 createdAt; + } + + // Collateral Parameters + + /// Requited collaterial backing to not be in bad debt + uint256 public collateralizationRatio; + + /// Total collateral posted + uint256 public collateralBalance; + + //// Total debt taken out + uint256 public debtBalance; + + /// Fixed interest rate + uint256 public interestRate; + + /// Amount in seconds for interest to accumulate + uint256 public interestRatePeriod; + + /// Fee for taking out a loan + uint256 public originationFee; + // uint256 public liquidationPenalty; + + /// Mapping of user positions + mapping(address => Debt) public debts; + + Token public Reserve; + + Token public loan; + + + error ZeroBalance(); + + /** + * @dev Get user principal amount + * @return principal amount + */ + function getDebtAmount() public view returns (uint256) { + return debts[msg.sender].debtAmount; + } + + /** + * @dev Get user total debt incurred (principal + interest) + * @return total Debt + */ + function getTotalDebtAmount() public view returns (uint256) { + uint totalDebt = debts[msg.sender].debtAmount; + uint256 periodsPerYear = 365 days / interestRatePeriod; + uint256 periodsElapsed = (block.timestamp / interestRatePeriod) - (debts[msg.sender].createdAt / interestRatePeriod); + totalDebt += ((totalDebt * interestRate) / 10000 / periodsPerYear) * periodsElapsed; + return totalDebt; + } + + /** + * @dev This function allows the Bank owner to deposit the reserve (debt tokens) + * @param amount is the amount to deposit + */ + function reserveDeposit(uint256 amount) external onlyOperator{ + require(amount > 0, "Amount is zero !!"); + if (amount = 0) { + revert ZeroBalance(); + } + debtBalance += amount; + IERC20(collateral.tokenAddress).safeTransferFrom( + msg.sender, + address(this), + amount + ); + emit ReserveDeposit(amount); + } + + +} diff --git a/src/common/access/Operators.sol b/src/common/access/Operators.sol new file mode 100644 index 0000000..ee0422f --- /dev/null +++ b/src/common/access/Operators.sol @@ -0,0 +1,57 @@ +pragma solidity ^0.8.13; +// SPDX-License-Identifier: AGPL-3.0-or-later +// Origami (common/access/Operators.sol) + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/// @notice Inherit to add an Operator role which multiple addreses can be granted. +/// @dev Derived classes to implement addOperator() and removeOperator() +abstract contract Operators is Initializable { + /// @notice A set of addresses which are approved to run operations. + mapping(address => bool) public operators; + + event AddedOperator(address indexed account); + event RemovedOperator(address indexed account); + + error OnlyOperators(address caller); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + function __Operators_init() internal onlyInitializing { + __Operators_init_unchained(); + } + + function __Operators_init_unchained() internal onlyInitializing { + } + + function _addOperator(address _account) internal { + emit AddedOperator(_account); + operators[_account] = true; + } + + /// @notice Grant `_account` the operator role + /// @dev Derived classes to implement and add protection on who can call + function addOperator(address _account) external virtual; + + function _removeOperator(address _account) internal { + emit RemovedOperator(_account); + delete operators[_account]; + } + + /// @notice Revoke the operator role from `_account` + /// @dev Derived classes to implement and add protection on who can call + function removeOperator(address _account) external virtual; + + modifier onlyOperators() { + if (!operators[msg.sender]) revert OnlyOperators(msg.sender); + _; + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; +} \ No newline at end of file diff --git a/test/Counter.t.sol b/test/Counter.t.sol index 30235e8..b7c5b92 100644 --- a/test/Counter.t.sol +++ b/test/Counter.t.sol @@ -2,14 +2,13 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import "../src/Counter.sol"; +import "../src/TLC.sol"; -contract CounterTest is Test { - Counter public counter; +contract TLCTest is Test { + TLC public tlc; function setUp() public { - counter = new Counter(); - counter.setNumber(0); + tlc = new TLC(); } function testIncrement() public { From 36a2d6c786a34b4bc1ab43759976ac701abf1a81 Mon Sep 17 00:00:00 2001 From: smokey Date: Mon, 23 Jan 2023 21:10:47 -0500 Subject: [PATCH 02/15] forge install: openzeppelin-contracts v4.8.1 --- .gitmodules | 4 ++++ lib/openzeppelin-contracts | 1 + 2 files changed, 5 insertions(+) create mode 160000 lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index b2061c7..34a5f5b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,7 @@ 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 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..0457042 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 0457042d93d9dfd760dbaa06a4d2f1216fdbe297 From 7e6bb9ed238c9c308eb861f2d4515833cdcd6449 Mon Sep 17 00:00:00 2001 From: smokey Date: Mon, 23 Jan 2023 21:14:39 -0500 Subject: [PATCH 03/15] forge install: openzeppelin-contracts-upgradeable v4.8.1 --- .gitmodules | 4 ++++ lib/openzeppelin-contracts-upgradeable | 1 + 2 files changed, 5 insertions(+) create mode 160000 lib/openzeppelin-contracts-upgradeable diff --git a/.gitmodules b/.gitmodules index 34a5f5b..42bd6ae 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,7 @@ 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 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..7ec6d2a --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 7ec6d2a3117eb3487a5f9029203e80ceb89bd984 From 2dd68a4f7160a5345c828f642b7c22baff2e1892 Mon Sep 17 00:00:00 2001 From: smokey Date: Wed, 25 Jan 2023 22:29:01 -0500 Subject: [PATCH 04/15] Temple loving care skeleton --- remappings.txt | 5 + src/TLC.sol | 139 ++++++++++++++++++++++----- src/common/access/Operators.sol | 2 +- test/Counter.t.sol | 23 ----- test/TLC.t.sol | 161 ++++++++++++++++++++++++++++++++ 5 files changed, 283 insertions(+), 47 deletions(-) create mode 100644 remappings.txt delete mode 100644 test/Counter.t.sol create mode 100644 test/TLC.t.sol diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..639fc1c --- /dev/null +++ b/remappings.txt @@ -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/ \ No newline at end of file diff --git a/src/TLC.sol b/src/TLC.sol index 9d422f0..4f91c22 100644 --- a/src/TLC.sol +++ b/src/TLC.sol @@ -1,18 +1,18 @@ // SPDX-License-Identifier: UNLICENSED 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 Operators{ +contract TLC is Ownable, Operators { - struct Token { - address tokenAddress; - uint256 price; - uint256 reserveBalance; - uint256 lastUpdatedAt; - } + using SafeERC20 for IERC20; - struct Reserve { + struct Position { uint256 collateralAmount; uint256 debtAmount; uint256 createdAt; @@ -20,7 +20,13 @@ contract TLC is Operators{ // Collateral Parameters - /// Requited collaterial backing to not be in bad debt + /// Supported collateral token address + address public collateralAddress; + + /// Collateral token price + uint256 public collateralPrice; + + /// Requited collateral backing to not be in bad debt uint256 public collateralizationRatio; /// Total collateral posted @@ -37,24 +43,65 @@ contract TLC is Operators{ /// Fee for taking out a loan uint256 public originationFee; - // uint256 public liquidationPenalty; + + + /// Debt parameters + + /// Debt token address + address public debtAddress; + + /// Debt token price + uint256 public debtPrice; + /// Mapping of user positions - mapping(address => Debt) public debts; + mapping(address => Position) public positions; - Token public Reserve; + event DepositDebt(uint256 amount); + event PostCollateral(uint256 amount); + event Borrow(address account, uint256 amount); - Token public loan; + error ZeroBalance(address account); + error InsufficentCollateral(uint256 maxCapacity, uint256 debtAmount); + constructor( + uint256 _interestRate, + uint256 _collateralizationRatio, + uint256 _interestRatePeriod, - error ZeroBalance(); + address _collateralAddress, + uint256 _collateralPrice, + + address _debtAddress, + uint256 _debtPrice + + ) { + interestRate = _interestRate; + collateralizationRatio = _collateralizationRatio; + interestRatePeriod = _interestRatePeriod; + + collateralAddress = _collateralAddress; + collateralPrice = _collateralPrice; + + debtAddress = _debtAddress; + debtPrice = _debtPrice; + } + + + 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) { - return debts[msg.sender].debtAmount; + return positions[msg.sender].debtAmount; } /** @@ -62,30 +109,76 @@ contract TLC is Operators{ * @return total Debt */ function getTotalDebtAmount() public view returns (uint256) { - uint totalDebt = debts[msg.sender].debtAmount; + uint totalDebt = positions[msg.sender].debtAmount; uint256 periodsPerYear = 365 days / interestRatePeriod; - uint256 periodsElapsed = (block.timestamp / interestRatePeriod) - (debts[msg.sender].createdAt / interestRatePeriod); + uint256 periodsElapsed = (block.timestamp / interestRatePeriod) - (positions[msg.sender].createdAt / interestRatePeriod); totalDebt += ((totalDebt * interestRate) / 10000 / periodsPerYear) * periodsElapsed; return totalDebt; } /** - * @dev This function allows the Bank owner to deposit the reserve (debt tokens) + * @dev Allows operator to depoist debt tokens * @param amount is the amount to deposit */ - function reserveDeposit(uint256 amount) external onlyOperator{ + function depositDebt(uint256 amount) external onlyOperators{ require(amount > 0, "Amount is zero !!"); - if (amount = 0) { - revert ZeroBalance(); + if (amount == 0) { + revert ZeroBalance(msg.sender); } debtBalance += amount; - IERC20(collateral.tokenAddress).safeTransferFrom( + IERC20(debtAddress).safeTransferFrom( + msg.sender, + address(this), + amount + ); + emit DepositDebt(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); + positions[msg.sender].collateralAmount += amount; + collateralBalance += amount; + IERC20(collateralAddress).safeTransferFrom( msg.sender, address(this), amount ); - emit ReserveDeposit(amount); } + function borrow(uint256 amount) external { + if (positions[msg.sender].debtAmount != 0) { + positions[msg.sender].debtAmount = getTotalDebtAmount(); + } + + uint256 maxBorrowCapacity = maxBorrowCapacity(msg.sender); + maxBorrowCapacity -= positions[msg.sender].debtAmount; + + // TODO: Add fees for borrowing + 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) { + return ((positions[account].collateralAmount * collateralPrice * 100) / debtPrice / collateralizationRatio); + } } diff --git a/src/common/access/Operators.sol b/src/common/access/Operators.sol index ee0422f..641f38e 100644 --- a/src/common/access/Operators.sol +++ b/src/common/access/Operators.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; // SPDX-License-Identifier: AGPL-3.0-or-later // Origami (common/access/Operators.sol) -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {Initializable} from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; /// @notice Inherit to add an Operator role which multiple addreses can be granted. /// @dev Derived classes to implement addOperator() and removeOperator() diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index b7c5b92..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -import "../src/TLC.sol"; - -contract TLCTest is Test { - TLC public tlc; - - function setUp() public { - tlc = new TLC(); - } - - function testIncrement() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testSetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/TLC.t.sol b/test/TLC.t.sol new file mode 100644 index 0000000..eb1007d --- /dev/null +++ b/test/TLC.t.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {ERC20Mock} from "openzeppelin-contracts/mocks/ERC20Mock.sol"; +import "../src/TLC.sol"; +import "../src/common/access/Operators.sol"; + +contract TLCTest is Test { + + TLC public tlc; + + uint256 public interestRate; + uint256 public collateralizationRatio; + uint256 public interestRatePeriod; + + ERC20Mock public collateralToken; + uint256 public collateralPrice; + + ERC20Mock public debtToken; + uint256 public debtPrice; + + address admin = address(0x1); + address alice = address(0x2); + address bob = address(0x2); + + + function setUp() public { + + interestRate = 5; // 5% + collateralizationRatio = 120; + interestRatePeriod = 1 hours; + collateralToken = new ERC20Mock("TempleToken", "Temple", admin, uint(500_000e18)); + collateralPrice = 970; // 0.97 + debtToken = new ERC20Mock("DAI Token", "DAI", admin, uint(500_000e18)); + debtPrice = 1000; // 1 USD + + tlc = new TLC( + interestRate, + collateralizationRatio, + interestRatePeriod, + address(collateralToken), + collateralPrice, + address(debtToken), + debtPrice + ); + + tlc.addOperator(admin); + } + + function testInitalization() public { + assertEq(tlc.interestRate(), interestRate); + assertEq(tlc.collateralizationRatio(), collateralizationRatio); + assertEq(tlc.collateralAddress(), address(collateralToken)); + assertEq(tlc.collateralPrice(), collateralPrice); + assertEq(tlc.debtAddress(), address(debtToken)); + assertEq(tlc.debtPrice(), debtPrice); + } + + function testAddOperator() public { + assertEq(tlc.owner(), address(this)); + assertFalse(tlc.operators(alice)); + tlc.addOperator(alice); + assertTrue(tlc.operators(alice)); + } + + function testRemoveOperator() public { + assertEq(tlc.owner(), address(this)); + tlc.addOperator(alice); + assertTrue(tlc.operators(alice)); + tlc.removeOperator(alice); + assertFalse(tlc.operators(alice)); + } + + function testDepositDebtExpectRevertOnlyOperator() public { + vm.expectRevert(abi.encodeWithSelector(Operators.OnlyOperators.selector, address(this))); + tlc.depositDebt(uint(100_000e18)); + } + + function testdepositDebt() public { + vm.startPrank(admin); + uint256 depositAmount = uint256(100_000e18); + debtToken.approve(address(tlc), depositAmount); + tlc.depositDebt(depositAmount); + + assertEq(debtToken.balanceOf(address(tlc)), depositAmount); + assertEq(tlc.debtBalance(), depositAmount); + } + + function _initDeposit(uint256 depositAmount) internal { + vm.startPrank(admin); + debtToken.approve(address(tlc), depositAmount); + tlc.depositDebt(depositAmount); + vm.stopPrank(); + } + + function testPostCollateralZeroBalanceRevert() external { + _initDeposit(uint256(100_000e18)); + vm.expectRevert(abi.encodeWithSelector(TLC.ZeroBalance.selector, alice)); + vm.prank(alice); + uint256 collateralAmount = uint(0); + tlc.postCollateral(collateralAmount); + } + + function testPostCollateralPasses() external { + _initDeposit(uint256(100_000e18)); + uint256 collateralAmount = uint(200_000e18); + deal(address(collateralToken), alice, collateralAmount); + vm.startPrank(alice); + collateralToken.approve(address(tlc), collateralAmount); + tlc.postCollateral(collateralAmount); + vm.stopPrank(); + + assertEq(collateralToken.balanceOf(address(tlc)), collateralAmount); + assertEq(tlc.collateralBalance(), collateralAmount); + } + + function _postCollateral(address user, uint256 collateralAmount) internal { + _initDeposit(100_000e18); + deal(address(collateralToken), user, collateralAmount); + vm.startPrank(user); + collateralToken.approve(address(tlc), collateralAmount); + tlc.postCollateral(collateralAmount); + vm.stopPrank(); + } + + function testBorrowCapacity() external { + uint256 collateralAmount = uint(100_000e18); + uint256 expectedMaxBorrowCapacity = uint(97_000e18) * uint(100) / uint(120); + _postCollateral(alice, collateralAmount); + assertEq(tlc.maxBorrowCapacity(alice), expectedMaxBorrowCapacity); + } + + function testBorrowInsufficientCollateral() external { + uint256 collateralAmount = uint(100_000e18); + _postCollateral(alice, collateralAmount); + uint256 maxBorrowCapacity = tlc.maxBorrowCapacity(alice); + uint256 borrowAmount = maxBorrowCapacity + uint(1); + vm.expectRevert(abi.encodeWithSelector(TLC.InsufficentCollateral.selector, maxBorrowCapacity, borrowAmount)); + vm.prank(alice); + tlc.borrow(borrowAmount); + } + + function testBorrowPasses() external { + uint256 collateralAmount = uint(100_000e18); + _postCollateral(alice, collateralAmount); + uint256 tlcDebtBalance = debtToken.balanceOf(address(tlc)); + uint256 maxBorrowCapacity = tlc.maxBorrowCapacity(alice); + vm.prank(alice); + tlc.borrow(maxBorrowCapacity); + + (uint256 aliceCollateralAmount, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); + + assertEq(aliceCollateralAmount, collateralAmount); + assertEq(aliceDebtAmount, maxBorrowCapacity); + assertEq(aliceCreatedAt, block.timestamp); + assertEq(tlc.debtBalance(), tlcDebtBalance - maxBorrowCapacity); + assertEq(debtToken.balanceOf(alice), maxBorrowCapacity); + } + +} From 48ce56b142885a2daca4e5f51dca657bc2eb07fc Mon Sep 17 00:00:00 2001 From: shero0x1337 <95888397+shero0x1337@users.noreply.github.com> Date: Wed, 25 Jan 2023 22:42:55 -0500 Subject: [PATCH 05/15] Create README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ee92bd --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Temple-Loving-Care +Temple Fixed Rate Lending protocol From c51ee11858cd6b96f6cc18f7263fbf761cc91867 Mon Sep 17 00:00:00 2001 From: smokey Date: Tue, 31 Jan 2023 17:48:04 -0500 Subject: [PATCH 06/15] Support repay and liquidate --- src/TLC.sol | 136 ++++++++++++++++++++++++++++++++++++++++++++--- test/TLC.t.sol | 140 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 268 insertions(+), 8 deletions(-) diff --git a/src/TLC.sol b/src/TLC.sol index 4f91c22..bea4d72 100644 --- a/src/TLC.sol +++ b/src/TLC.sol @@ -44,6 +44,11 @@ contract TLC is Ownable, Operators { /// Fee for taking out a loan uint256 public originationFee; + /// Fee charged for debtor liquidation + uint256 public liquidationFee; + + /// Address to send bad debt collateral + address public debtCollector; /// Debt parameters @@ -58,12 +63,20 @@ contract TLC is Ownable, Operators { 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, @@ -73,7 +86,9 @@ contract TLC is Ownable, Operators { uint256 _collateralPrice, address _debtAddress, - uint256 _debtPrice + uint256 _debtPrice, + uint256 _liquidationFee, + address _debtCollector ) { interestRate = _interestRate; @@ -85,6 +100,9 @@ contract TLC is Ownable, Operators { debtAddress = _debtAddress; debtPrice = _debtPrice; + + liquidationFee = _liquidationFee; + debtCollector = _debtCollector; } @@ -108,10 +126,10 @@ contract TLC is Ownable, Operators { * @dev Get user total debt incurred (principal + interest) * @return total Debt */ - function getTotalDebtAmount() public view returns (uint256) { - uint totalDebt = positions[msg.sender].debtAmount; + function getTotalDebtAmount(address account) public view returns (uint256) { + uint totalDebt = positions[account].debtAmount; uint256 periodsPerYear = 365 days / interestRatePeriod; - uint256 periodsElapsed = (block.timestamp / interestRatePeriod) - (positions[msg.sender].createdAt / interestRatePeriod); + uint256 periodsElapsed = (block.timestamp / interestRatePeriod) - (positions[account].createdAt / interestRatePeriod); totalDebt += ((totalDebt * interestRate) / 10000 / periodsPerYear) * periodsElapsed; return totalDebt; } @@ -134,6 +152,23 @@ contract TLC is Ownable, Operators { 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 !!"); + if (amount == 0) { + revert ZeroBalance(msg.sender); + } + debtBalance -= amount; + IERC20(debtAddress).safeTransfer( + msg.sender, + amount + ); + emit RemoveDebt(amount); + } + /** * @dev Allows borrower to deposit collateral * @param amount is the amount to deposit @@ -151,7 +186,7 @@ contract TLC is Ownable, Operators { function borrow(uint256 amount) external { if (positions[msg.sender].debtAmount != 0) { - positions[msg.sender].debtAmount = getTotalDebtAmount(); + positions[msg.sender].debtAmount = getTotalDebtAmount(msg.sender); } uint256 maxBorrowCapacity = maxBorrowCapacity(msg.sender); @@ -181,4 +216,93 @@ contract TLC is Ownable, Operators { 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) { + revert ExceededCollateralAmonut(msg.sender, positions[msg.sender].collateralAmount, withdrawalAmount); + } + + uint256 maxBorrowCapacity = (((positions[msg.sender].collateralAmount - withdrawalAmount) * collateralPrice * 100) / debtPrice / collateralizationRatio); + 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; + 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 { + + if (getCurrentCollaterilizationRatio(debtor) >= collateralizationRatio) { + 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( + 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); + } + } + } diff --git a/test/TLC.t.sol b/test/TLC.t.sol index eb1007d..61bb3d1 100644 --- a/test/TLC.t.sol +++ b/test/TLC.t.sol @@ -20,20 +20,24 @@ contract TLCTest is Test { ERC20Mock public debtToken; uint256 public debtPrice; + uint256 public liquidationFee; + address admin = address(0x1); address alice = address(0x2); address bob = address(0x2); + address debtCollector = address(0x3); function setUp() public { - interestRate = 5; // 5% + interestRate = 50; // 5% collateralizationRatio = 120; interestRatePeriod = 1 hours; collateralToken = new ERC20Mock("TempleToken", "Temple", admin, uint(500_000e18)); collateralPrice = 970; // 0.97 debtToken = new ERC20Mock("DAI Token", "DAI", admin, uint(500_000e18)); debtPrice = 1000; // 1 USD + liquidationFee = 10; tlc = new TLC( interestRate, @@ -42,7 +46,9 @@ contract TLCTest is Test { address(collateralToken), collateralPrice, address(debtToken), - debtPrice + debtPrice, + liquidationFee, + debtCollector ); tlc.addOperator(admin); @@ -158,4 +164,134 @@ contract TLCTest is Test { assertEq(debtToken.balanceOf(alice), maxBorrowCapacity); } + function _borrow(address _account, uint256 collateralAmount, uint256 borrowAmount) internal { + _postCollateral(_account, collateralAmount); + vm.prank(_account); + tlc.borrow(borrowAmount); + } + + function testBorrowAccuresInterest(uint32 periodElapsed) external { + uint256 borrowAmount = uint(60_000e18); + _borrow(alice, uint(100_000e18), borrowAmount); + + uint256 borrowTimeStamp = block.timestamp; + + vm.warp(block.timestamp + (periodElapsed * interestRatePeriod)); + uint256 periodsPerYear = 365 days / interestRatePeriod; + uint256 periodsElapsed = (block.timestamp / interestRatePeriod) - (borrowTimeStamp / interestRatePeriod); + uint256 expectedTotalDebt = (borrowAmount) + ((borrowAmount * interestRate) / 10000 / periodsPerYear) * periodsElapsed; + + vm.startPrank(alice); + assertEq(expectedTotalDebt, tlc.getTotalDebtAmount(alice)); + vm.stopPrank(); + } + + + function testRepayZero() external { + uint256 borrowAmount = uint(60_000e18); + uint256 repayAmount = uint(0); + _borrow(alice, uint(100_000e18), borrowAmount); + vm.expectRevert(abi.encodeWithSelector(TLC.ZeroBalance.selector, alice)); + vm.startPrank(alice); + tlc.repay(0); + vm.stopPrank(); + } + + function testRepayExceededBorrow() external { + uint256 borrowAmount = uint(60_000e18); + uint256 repayAmount = uint(61_000e18); + _borrow(alice, uint(100_000e18), borrowAmount); + vm.expectRevert(abi.encodeWithSelector(TLC.ExceededBorrowedAmount.selector, alice, borrowAmount, repayAmount)); + vm.startPrank(alice); + tlc.repay(repayAmount); + vm.stopPrank(); + } + + function testRepaySuccess() external { + uint256 borrowAmount = uint(60_000e18); + uint256 repayAmount = uint(50_000e18); + _borrow(alice, uint(100_000e18), borrowAmount); + uint256 debtBalanceBefore = tlc.debtBalance(); + + vm.startPrank(alice); + debtToken.approve(address(tlc), repayAmount); + tlc.repay(repayAmount); + (uint256 aliceCollateralAmount, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); + vm.stopPrank(); + + assertEq(borrowAmount - repayAmount, aliceDebtAmount); + assertEq(debtBalanceBefore + repayAmount, tlc.debtBalance()); + assertEq(block.timestamp, aliceCreatedAt); + } + + function testWithdrawExceedCollateralAmount() external { + uint256 borrowAmount = uint(60_000e18); + uint256 collateralAmount = uint(100_000e18); + uint256 withdrawalAmount = uint(100_001e18); + _borrow(alice, collateralAmount, borrowAmount); + vm.expectRevert(abi.encodeWithSelector(TLC.ExceededCollateralAmonut.selector, alice, collateralAmount, withdrawalAmount)); + + vm.startPrank(alice); + tlc.withdrawCollateral(withdrawalAmount); + vm.stopPrank(); + } + + function testWithdrawWillUnderCollaterlizeLoan() external { + uint256 borrowAmount = uint(60_000e18); + uint256 collateralAmount = uint(100_000e18); + uint256 withdrawalAmount = uint(30_001e18); + _borrow(alice, collateralAmount, borrowAmount); + vm.expectRevert(abi.encodeWithSelector(TLC.WillUnderCollaterlize.selector, alice, withdrawalAmount)); + + vm.startPrank(alice); + tlc.withdrawCollateral(withdrawalAmount); + vm.stopPrank(); + } + + function testWithdrawalSuccess() external { + uint256 borrowAmount = uint(60_000e18); + uint256 collateralAmount = uint(100_000e18); + uint256 withdrawalAmount = uint(10_000e18); + _borrow(alice, collateralAmount, borrowAmount); + + uint256 collateralBalanceBefore = tlc.collateralBalance(); + uint256 aliceCollateralBalanceBefore = collateralToken.balanceOf(alice); + + vm.startPrank(alice); + tlc.withdrawCollateral(withdrawalAmount); + (uint256 aliceCollateralAmount, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); + vm.stopPrank(); + + assertEq(collateralBalanceBefore - withdrawalAmount, tlc.collateralBalance()); + assertEq(aliceCollateralAmount, collateralAmount - withdrawalAmount); + assertEq(collateralToken.balanceOf(alice), aliceCollateralBalanceBefore + withdrawalAmount); + } + + + function testLiquidateSufficientCollateral() external { + uint256 borrowAmount = uint(70_000e18); + uint256 collateralAmount = uint(100_000e18); + _borrow(alice, collateralAmount, borrowAmount); + vm.expectRevert(abi.encodeWithSelector(TLC.OverCollaterilized.selector, alice)); + + vm.prank(admin); + tlc.liquidate(alice); + } + + function testLiquidateUnderWaterPositionSucessfully() external { + uint256 borrowAmount = uint(70_000e18); + uint256 collateralAmount = uint(100_000e18); + _borrow(alice, collateralAmount, borrowAmount); + vm.warp(block.timestamp + 11500 days); + uint256 totalDebt = tlc.getTotalDebtAmount(alice); + assertTrue(tlc.getCurrentCollaterilizationRatio(alice) < collateralizationRatio); + vm.prank(admin); + tlc.liquidate(alice); + + (uint256 aliceCollateralAmount, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); + assertEq(uint256(0), aliceDebtAmount); + assertEq(collateralAmount - (totalDebt * debtPrice / collateralPrice), aliceCollateralAmount); + assertEq(uint256(0), aliceCreatedAt); + assertEq((totalDebt * debtPrice / collateralPrice), collateralToken.balanceOf(debtCollector)); + } } From b55b4155f9cc881cb13bfcc7d2709690ec1c34ec Mon Sep 17 00:00:00 2001 From: smokey Date: Mon, 6 Feb 2023 10:42:36 -0500 Subject: [PATCH 07/15] had setters remove fees rename contract allow deposit debt from generalized account cleanup nits gas optimization --- src/{TLC.sol => TempleLineOfCredit.sol} | 174 ++++++++++--------- test/{TLC.t.sol => TempleLineOfCredit.t.sol} | 69 ++++---- 2 files changed, 124 insertions(+), 119 deletions(-) rename src/{TLC.sol => TempleLineOfCredit.sol} (57%) rename test/{TLC.t.sol => TempleLineOfCredit.t.sol} (81%) diff --git a/src/TLC.sol b/src/TempleLineOfCredit.sol similarity index 57% rename from src/TLC.sol rename to src/TempleLineOfCredit.sol index bea4d72..3262e34 100644 --- a/src/TLC.sol +++ b/src/TempleLineOfCredit.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; @@ -8,7 +8,7 @@ import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol" import {Operators} from "./common/access/Operators.sol"; -contract TLC is Ownable, Operators { +contract TempleLineOfCredit is Ownable, Operators { using SafeERC20 for IERC20; @@ -20,57 +20,47 @@ contract TLC is Ownable, Operators { // Collateral Parameters - /// Supported collateral token address - address public collateralAddress; + /// @notice Supported collateral token address + IERC20 public immutable collateralToken; - /// Collateral token price + /// @notice Collateral token price uint256 public collateralPrice; - /// Requited collateral backing to not be in bad debt - uint256 public collateralizationRatio; + /// @notice Requited collateral backing to not be in bad debt in percentage + uint256 public minCollateralizationRatio; - /// Total collateral posted - uint256 public collateralBalance; - - //// Total debt taken out + /// @notice Total debt taken out uint256 public debtBalance; - /// Fixed interest rate - uint256 public interestRate; - - /// Amount in seconds for interest to accumulate - uint256 public interestRatePeriod; + /// @notice Fixed borrow interest rate in bpds + uint256 public immutable interestRateBps; - /// Fee for taking out a loan - uint256 public originationFee; + /// @notice Amount in seconds for interest to accumulate + uint256 public immutable interestRatePeriod; - /// Fee charged for debtor liquidation - uint256 public liquidationFee; - - /// Address to send bad debt collateral + /// @notice Address to send bad debt collateral address public debtCollector; /// Debt parameters - /// Debt token address - address public debtAddress; + /// @notice Debt token address + IERC20 public immutable debtToken; - /// Debt token price + /// @notice Debt token price uint256 public debtPrice; - - /// Mapping of user positions + /// @notice Mapping of user positions mapping(address => Position) public positions; event DepositDebt(uint256 amount); event RemoveDebt(uint256 amount); - event PostCollateral(uint256 amount); + event PostCollateral(address account, 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 InvalidAmount(uint256 amount); error InsufficentCollateral(uint256 maxCapacity, uint256 debtAmount); error ExceededBorrowedAmount(address account, uint256 amountBorrowed, uint256 amountRepay); error ExceededCollateralAmonut(address account, uint256 amountCollateral, uint256 collateralWithdraw); @@ -78,30 +68,28 @@ contract TLC is Ownable, Operators { error OverCollaterilized(address account); constructor( - uint256 _interestRate, - uint256 _collateralizationRatio, + uint256 _interestRateBps, + uint256 _minCollateralizationRatio, uint256 _interestRatePeriod, - address _collateralAddress, + address _collateralToken, uint256 _collateralPrice, - address _debtAddress, + address _debtToken, uint256 _debtPrice, - uint256 _liquidationFee, address _debtCollector ) { - interestRate = _interestRate; - collateralizationRatio = _collateralizationRatio; + interestRateBps = _interestRateBps; + minCollateralizationRatio = _minCollateralizationRatio; interestRatePeriod = _interestRatePeriod; - collateralAddress = _collateralAddress; + collateralToken = IERC20(_collateralToken); collateralPrice = _collateralPrice; - debtAddress = _debtAddress; + debtToken = IERC20(_debtToken); debtPrice = _debtPrice; - liquidationFee = _liquidationFee; debtCollector = _debtCollector; } @@ -114,12 +102,28 @@ contract TLC is Ownable, Operators { _removeOperator(_address); } + function setDebtPrice(uint256 _debtPrice) external onlyOperators { + debtPrice = _debtPrice; + } + + function setCollateralPrice(uint256 _collateralPrice) external onlyOperators { + collateralPrice = _collateralPrice; + } + + function setDebtCollector(address _debtCollector) external onlyOperators { + debtCollector = _debtCollector; + } + + function setCollateralizationRatio(uint256 _minCollateralizationRatio) external onlyOperators { + minCollateralizationRatio = _minCollateralizationRatio; + } + /** * @dev Get user principal amount * @return principal amount */ - function getDebtAmount() public view returns (uint256) { - return positions[msg.sender].debtAmount; + function getDebtAmount(address account) public view returns (uint256) { + return positions[account].debtAmount; } /** @@ -127,10 +131,10 @@ contract TLC is Ownable, Operators { * @return total Debt */ function getTotalDebtAmount(address account) public view returns (uint256) { - uint totalDebt = positions[account].debtAmount; + uint256 totalDebt = positions[account].debtAmount; uint256 periodsPerYear = 365 days / interestRatePeriod; - uint256 periodsElapsed = (block.timestamp / interestRatePeriod) - (positions[account].createdAt / interestRatePeriod); - totalDebt += ((totalDebt * interestRate) / 10000 / periodsPerYear) * periodsElapsed; + uint256 periodsElapsed = block.timestamp - positions[account].createdAt; // divided by interestRatePeriod + totalDebt += (((totalDebt * interestRateBps) / 10000 / periodsPerYear) * periodsElapsed) / interestRatePeriod; return totalDebt; } @@ -138,14 +142,13 @@ contract TLC is Ownable, Operators { * @dev Allows operator to depoist debt tokens * @param amount is the amount to deposit */ - function depositDebt(uint256 amount) external onlyOperators{ - require(amount > 0, "Amount is zero !!"); + function depositDebt(address account, uint256 amount) external onlyOperators{ if (amount == 0) { - revert ZeroBalance(msg.sender); + revert InvalidAmount(amount); } debtBalance += amount; - IERC20(debtAddress).safeTransferFrom( - msg.sender, + debtToken.safeTransferFrom( + account, address(this), amount ); @@ -157,12 +160,11 @@ contract TLC is Ownable, Operators { * @param amount is the amount to remove */ function removeDebt(uint256 amount) external onlyOperators{ - require(amount > 0, "Amount is zero !!"); if (amount == 0) { - revert ZeroBalance(msg.sender); + revert InvalidAmount(amount); } debtBalance -= amount; - IERC20(debtAddress).safeTransfer( + debtToken.safeTransfer( msg.sender, amount ); @@ -174,67 +176,74 @@ contract TLC is Ownable, Operators { * @param amount is the amount to deposit */ function postCollateral(uint256 amount) external { - if (amount == 0) revert ZeroBalance(msg.sender); + if (amount == 0) revert InvalidAmount(amount); positions[msg.sender].collateralAmount += amount; - collateralBalance += amount; - IERC20(collateralAddress).safeTransferFrom( + collateralToken.safeTransferFrom( msg.sender, address(this), amount ); + emit PostCollateral(msg.sender, amount); } function borrow(uint256 amount) external { - if (positions[msg.sender].debtAmount != 0) { - positions[msg.sender].debtAmount = getTotalDebtAmount(msg.sender); - } + if (amount == 0) revert InvalidAmount(amount); - uint256 maxBorrowCapacity = maxBorrowCapacity(msg.sender); - maxBorrowCapacity -= positions[msg.sender].debtAmount; + uint256 debtAmount = positions[msg.sender].debtAmount; + if (debtAmount != 0) { + debtAmount = getTotalDebtAmount(msg.sender); + } - // TODO: Add fees for borrowing - positions[msg.sender].debtAmount += amount; + uint256 borrowCapacity = _maxBorrowCapacity(positions[msg.sender].collateralAmount) - debtAmount; + debtAmount += amount; - if (positions[msg.sender].debtAmount > maxBorrowCapacity) { - revert InsufficentCollateral(maxBorrowCapacity, positions[msg.sender].debtAmount); + if (debtAmount > borrowCapacity) { + revert InsufficentCollateral(borrowCapacity, debtAmount); } + + positions[msg.sender].debtAmount = 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 ) { + if (block.timestamp - positions[msg.sender].createdAt >= interestRatePeriod || positions[msg.sender].createdAt == 0 ) { positions[msg.sender].createdAt = block.timestamp; } debtBalance -= amount; - IERC20(debtAddress).safeTransfer( + debtToken.safeTransfer( msg.sender, amount ); emit Borrow(msg.sender, amount); } - function maxBorrowCapacity(address account) public returns(uint256) { - return ((positions[account].collateralAmount * collateralPrice * 100) / debtPrice / collateralizationRatio); + function maxBorrowCapacity(address account) public view returns(uint256) { + return ((positions[account].collateralAmount * collateralPrice * 100) / debtPrice / minCollateralizationRatio); } + function _maxBorrowCapacity(uint256 collateralAmount) internal view returns (uint256) { + return collateralAmount * collateralPrice * 100 / debtPrice / minCollateralizationRatio; + } /** * @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) { - revert ExceededCollateralAmonut(msg.sender, positions[msg.sender].collateralAmount, withdrawalAmount); + + uint256 collateralAmount = positions[msg.sender].collateralAmount; + if (withdrawalAmount > collateralAmount) { + revert ExceededCollateralAmonut(msg.sender, collateralAmount, withdrawalAmount); } - uint256 maxBorrowCapacity = (((positions[msg.sender].collateralAmount - withdrawalAmount) * collateralPrice * 100) / debtPrice / collateralizationRatio); - if (positions[msg.sender].debtAmount > maxBorrowCapacity ) { + uint256 borrowCapacity = _maxBorrowCapacity(collateralAmount - withdrawalAmount); + + if (positions[msg.sender].debtAmount > borrowCapacity ) { revert WillUnderCollaterlize(msg.sender, withdrawalAmount); } positions[msg.sender].collateralAmount -= withdrawalAmount; - collateralBalance -= withdrawalAmount; - IERC20(collateralAddress).safeTransfer( + collateralToken.safeTransfer( msg.sender, withdrawalAmount ); @@ -247,7 +256,7 @@ contract TLC is Ownable, Operators { * @param repayAmount is the amount to repay */ function repay(uint256 repayAmount) external { - if (repayAmount == 0) revert ZeroBalance(msg.sender); + if (repayAmount == 0) revert InvalidAmount(repayAmount); positions[msg.sender].debtAmount = getTotalDebtAmount(msg.sender); if (repayAmount > positions[msg.sender].debtAmount) { @@ -256,10 +265,12 @@ contract TLC is Ownable, Operators { positions[msg.sender].debtAmount -= repayAmount; debtBalance += repayAmount; - - uint256 periodsElapsed = (block.timestamp / interestRatePeriod) - (positions[msg.sender].createdAt / interestRatePeriod); - positions[msg.sender].createdAt += periodsElapsed * interestRatePeriod; - IERC20(debtAddress).safeTransferFrom( + + // 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; + } + debtToken.safeTransferFrom( msg.sender, address(this), repayAmount @@ -273,12 +284,11 @@ contract TLC is Ownable, Operators { */ function liquidate(address debtor) external onlyOperators { - if (getCurrentCollaterilizationRatio(debtor) >= collateralizationRatio) { + if (getCurrentCollaterilizationRatio(debtor) >= minCollateralizationRatio) { revert OverCollaterilized(debtor); } uint256 totalDebtOwed = getTotalDebtAmount(debtor); - // TODO: Add liquidation fee uint256 collateralSeized = (totalDebtOwed * debtPrice) / collateralPrice; if (collateralSeized > positions[debtor].collateralAmount) { @@ -288,7 +298,7 @@ contract TLC is Ownable, Operators { positions[debtor].collateralAmount -= collateralSeized; positions[debtor].debtAmount = 0; positions[debtor].createdAt = 0; - IERC20(collateralAddress).safeTransfer( + collateralToken.safeTransfer( debtCollector, collateralSeized ); diff --git a/test/TLC.t.sol b/test/TempleLineOfCredit.t.sol similarity index 81% rename from test/TLC.t.sol rename to test/TempleLineOfCredit.t.sol index 61bb3d1..73d5761 100644 --- a/test/TLC.t.sol +++ b/test/TempleLineOfCredit.t.sol @@ -3,15 +3,15 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import {ERC20Mock} from "openzeppelin-contracts/mocks/ERC20Mock.sol"; -import "../src/TLC.sol"; +import "../src/TempleLineOfCredit.sol"; import "../src/common/access/Operators.sol"; -contract TLCTest is Test { +contract TempleLineOfCreditTest is Test { - TLC public tlc; + TempleLineOfCredit public tlc; - uint256 public interestRate; - uint256 public collateralizationRatio; + uint256 public interestRateBps; + uint256 public minCollateralizationRatio; uint256 public interestRatePeriod; ERC20Mock public collateralToken; @@ -20,8 +20,6 @@ contract TLCTest is Test { ERC20Mock public debtToken; uint256 public debtPrice; - uint256 public liquidationFee; - address admin = address(0x1); address alice = address(0x2); address bob = address(0x2); @@ -30,24 +28,22 @@ contract TLCTest is Test { function setUp() public { - interestRate = 50; // 5% - collateralizationRatio = 120; - interestRatePeriod = 1 hours; + interestRateBps = 500; // 5% + minCollateralizationRatio = 120; + interestRatePeriod = 60 seconds; collateralToken = new ERC20Mock("TempleToken", "Temple", admin, uint(500_000e18)); collateralPrice = 970; // 0.97 debtToken = new ERC20Mock("DAI Token", "DAI", admin, uint(500_000e18)); debtPrice = 1000; // 1 USD - liquidationFee = 10; - tlc = new TLC( - interestRate, - collateralizationRatio, + tlc = new TempleLineOfCredit( + interestRateBps, + minCollateralizationRatio, interestRatePeriod, address(collateralToken), collateralPrice, address(debtToken), debtPrice, - liquidationFee, debtCollector ); @@ -55,11 +51,11 @@ contract TLCTest is Test { } function testInitalization() public { - assertEq(tlc.interestRate(), interestRate); - assertEq(tlc.collateralizationRatio(), collateralizationRatio); - assertEq(tlc.collateralAddress(), address(collateralToken)); + assertEq(tlc.interestRateBps(), interestRateBps); + assertEq(tlc.minCollateralizationRatio(), minCollateralizationRatio); + assertEq(address(tlc.collateralToken()), address(collateralToken)); assertEq(tlc.collateralPrice(), collateralPrice); - assertEq(tlc.debtAddress(), address(debtToken)); + assertEq(address(tlc.debtToken()), address(debtToken)); assertEq(tlc.debtPrice(), debtPrice); } @@ -80,14 +76,14 @@ contract TLCTest is Test { function testDepositDebtExpectRevertOnlyOperator() public { vm.expectRevert(abi.encodeWithSelector(Operators.OnlyOperators.selector, address(this))); - tlc.depositDebt(uint(100_000e18)); + tlc.depositDebt(alice, uint(100_000e18)); } function testdepositDebt() public { vm.startPrank(admin); uint256 depositAmount = uint256(100_000e18); debtToken.approve(address(tlc), depositAmount); - tlc.depositDebt(depositAmount); + tlc.depositDebt(admin, depositAmount); assertEq(debtToken.balanceOf(address(tlc)), depositAmount); assertEq(tlc.debtBalance(), depositAmount); @@ -96,13 +92,13 @@ contract TLCTest is Test { function _initDeposit(uint256 depositAmount) internal { vm.startPrank(admin); debtToken.approve(address(tlc), depositAmount); - tlc.depositDebt(depositAmount); + tlc.depositDebt(admin, depositAmount); vm.stopPrank(); } function testPostCollateralZeroBalanceRevert() external { _initDeposit(uint256(100_000e18)); - vm.expectRevert(abi.encodeWithSelector(TLC.ZeroBalance.selector, alice)); + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InvalidAmount.selector, uint(0))); vm.prank(alice); uint256 collateralAmount = uint(0); tlc.postCollateral(collateralAmount); @@ -118,7 +114,6 @@ contract TLCTest is Test { vm.stopPrank(); assertEq(collateralToken.balanceOf(address(tlc)), collateralAmount); - assertEq(tlc.collateralBalance(), collateralAmount); } function _postCollateral(address user, uint256 collateralAmount) internal { @@ -142,7 +137,7 @@ contract TLCTest is Test { _postCollateral(alice, collateralAmount); uint256 maxBorrowCapacity = tlc.maxBorrowCapacity(alice); uint256 borrowAmount = maxBorrowCapacity + uint(1); - vm.expectRevert(abi.encodeWithSelector(TLC.InsufficentCollateral.selector, maxBorrowCapacity, borrowAmount)); + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InsufficentCollateral.selector, maxBorrowCapacity, borrowAmount)); vm.prank(alice); tlc.borrow(borrowAmount); } @@ -179,7 +174,7 @@ contract TLCTest is Test { vm.warp(block.timestamp + (periodElapsed * interestRatePeriod)); uint256 periodsPerYear = 365 days / interestRatePeriod; uint256 periodsElapsed = (block.timestamp / interestRatePeriod) - (borrowTimeStamp / interestRatePeriod); - uint256 expectedTotalDebt = (borrowAmount) + ((borrowAmount * interestRate) / 10000 / periodsPerYear) * periodsElapsed; + uint256 expectedTotalDebt = (borrowAmount) + ((borrowAmount * interestRateBps) / 10000 / periodsPerYear) * periodsElapsed; vm.startPrank(alice); assertEq(expectedTotalDebt, tlc.getTotalDebtAmount(alice)); @@ -191,7 +186,7 @@ contract TLCTest is Test { uint256 borrowAmount = uint(60_000e18); uint256 repayAmount = uint(0); _borrow(alice, uint(100_000e18), borrowAmount); - vm.expectRevert(abi.encodeWithSelector(TLC.ZeroBalance.selector, alice)); + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InvalidAmount.selector, repayAmount)); vm.startPrank(alice); tlc.repay(0); vm.stopPrank(); @@ -201,7 +196,7 @@ contract TLCTest is Test { uint256 borrowAmount = uint(60_000e18); uint256 repayAmount = uint(61_000e18); _borrow(alice, uint(100_000e18), borrowAmount); - vm.expectRevert(abi.encodeWithSelector(TLC.ExceededBorrowedAmount.selector, alice, borrowAmount, repayAmount)); + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.ExceededBorrowedAmount.selector, alice, borrowAmount, repayAmount)); vm.startPrank(alice); tlc.repay(repayAmount); vm.stopPrank(); @@ -216,7 +211,7 @@ contract TLCTest is Test { vm.startPrank(alice); debtToken.approve(address(tlc), repayAmount); tlc.repay(repayAmount); - (uint256 aliceCollateralAmount, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); + (, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); vm.stopPrank(); assertEq(borrowAmount - repayAmount, aliceDebtAmount); @@ -229,7 +224,7 @@ contract TLCTest is Test { uint256 collateralAmount = uint(100_000e18); uint256 withdrawalAmount = uint(100_001e18); _borrow(alice, collateralAmount, borrowAmount); - vm.expectRevert(abi.encodeWithSelector(TLC.ExceededCollateralAmonut.selector, alice, collateralAmount, withdrawalAmount)); + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.ExceededCollateralAmonut.selector, alice, collateralAmount, withdrawalAmount)); vm.startPrank(alice); tlc.withdrawCollateral(withdrawalAmount); @@ -241,7 +236,7 @@ contract TLCTest is Test { uint256 collateralAmount = uint(100_000e18); uint256 withdrawalAmount = uint(30_001e18); _borrow(alice, collateralAmount, borrowAmount); - vm.expectRevert(abi.encodeWithSelector(TLC.WillUnderCollaterlize.selector, alice, withdrawalAmount)); + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.WillUnderCollaterlize.selector, alice, withdrawalAmount)); vm.startPrank(alice); tlc.withdrawCollateral(withdrawalAmount); @@ -254,15 +249,15 @@ contract TLCTest is Test { uint256 withdrawalAmount = uint(10_000e18); _borrow(alice, collateralAmount, borrowAmount); - uint256 collateralBalanceBefore = tlc.collateralBalance(); + uint256 collateralBalanceBefore = collateralToken.balanceOf(address(tlc)); uint256 aliceCollateralBalanceBefore = collateralToken.balanceOf(alice); vm.startPrank(alice); tlc.withdrawCollateral(withdrawalAmount); - (uint256 aliceCollateralAmount, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); + (uint256 aliceCollateralAmount,,) = tlc.positions(alice); vm.stopPrank(); - assertEq(collateralBalanceBefore - withdrawalAmount, tlc.collateralBalance()); + assertEq(collateralBalanceBefore - withdrawalAmount, collateralToken.balanceOf(address(tlc))); assertEq(aliceCollateralAmount, collateralAmount - withdrawalAmount); assertEq(collateralToken.balanceOf(alice), aliceCollateralBalanceBefore + withdrawalAmount); } @@ -272,7 +267,7 @@ contract TLCTest is Test { uint256 borrowAmount = uint(70_000e18); uint256 collateralAmount = uint(100_000e18); _borrow(alice, collateralAmount, borrowAmount); - vm.expectRevert(abi.encodeWithSelector(TLC.OverCollaterilized.selector, alice)); + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.OverCollaterilized.selector, alice)); vm.prank(admin); tlc.liquidate(alice); @@ -282,9 +277,9 @@ contract TLCTest is Test { uint256 borrowAmount = uint(70_000e18); uint256 collateralAmount = uint(100_000e18); _borrow(alice, collateralAmount, borrowAmount); - vm.warp(block.timestamp + 11500 days); + vm.warp(block.timestamp + 1180 days); uint256 totalDebt = tlc.getTotalDebtAmount(alice); - assertTrue(tlc.getCurrentCollaterilizationRatio(alice) < collateralizationRatio); + assertTrue(tlc.getCurrentCollaterilizationRatio(alice) < minCollateralizationRatio); vm.prank(admin); tlc.liquidate(alice); From e727c1607df1891a2b3ecbffbd674e50a2f7999d Mon Sep 17 00:00:00 2001 From: smokey Date: Tue, 7 Feb 2023 16:20:16 -0500 Subject: [PATCH 08/15] fix nit --- src/TempleLineOfCredit.sol | 33 +++++++++++++++++++-------------- test/TempleLineOfCredit.t.sol | 2 +- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/TempleLineOfCredit.sol b/src/TempleLineOfCredit.sol index 3262e34..88ff0e6 100644 --- a/src/TempleLineOfCredit.sol +++ b/src/TempleLineOfCredit.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.13; +pragma solidity ^0.8.17; import {Ownable} from "openzeppelin-contracts/access/Ownable.sol"; @@ -26,7 +26,7 @@ contract TempleLineOfCredit is Ownable, Operators { /// @notice Collateral token price uint256 public collateralPrice; - /// @notice Requited collateral backing to not be in bad debt in percentage + /// @notice Required collateral backing to not be in bad debt in percentage uint256 public minCollateralizationRatio; /// @notice Total debt taken out @@ -217,7 +217,7 @@ contract TempleLineOfCredit is Ownable, Operators { } function maxBorrowCapacity(address account) public view returns(uint256) { - return ((positions[account].collateralAmount * collateralPrice * 100) / debtPrice / minCollateralizationRatio); + return _maxBorrowCapacity(positions[account].collateralAmount); } @@ -230,7 +230,7 @@ contract TempleLineOfCredit is Ownable, Operators { * @param withdrawalAmount is the amount to withdraw */ function withdrawCollateral(uint256 withdrawalAmount) external { - + if (withdrawalAmount == 0) revert InvalidAmount(withdrawalAmount); uint256 collateralAmount = positions[msg.sender].collateralAmount; if (withdrawalAmount > collateralAmount) { revert ExceededCollateralAmonut(msg.sender, collateralAmount, withdrawalAmount); @@ -283,35 +283,40 @@ contract TempleLineOfCredit is Ownable, Operators { * @param debtor the account to liquidate */ function liquidate(address debtor) external onlyOperators { + Position storage position = positions[debtor]; + uint256 totalDebtOwed = getTotalDebtAmount(debtor); - if (getCurrentCollaterilizationRatio(debtor) >= minCollateralizationRatio) { + if (_getCurrentCollaterilizationRatio(position.collateralAmount, position.debtAmount, totalDebtOwed) >= minCollateralizationRatio) { revert OverCollaterilized(debtor); } - uint256 totalDebtOwed = getTotalDebtAmount(debtor); uint256 collateralSeized = (totalDebtOwed * debtPrice) / collateralPrice; - if (collateralSeized > positions[debtor].collateralAmount) { - collateralSeized = positions[debtor].collateralAmount; + if (collateralSeized > position.collateralAmount) { + collateralSeized = position.collateralAmount; } - positions[debtor].collateralAmount -= collateralSeized; - positions[debtor].debtAmount = 0; - positions[debtor].createdAt = 0; + position.collateralAmount -= collateralSeized; + position.debtAmount = 0; + position.createdAt = 0; + collateralToken.safeTransfer( debtCollector, collateralSeized ); emit Liquidated(debtor, totalDebtOwed, collateralSeized); - } function getCurrentCollaterilizationRatio(address account) public view returns(uint256) { - if (positions[account].debtAmount == 0) { + _getCurrentCollaterilizationRatio(positions[account].collateralAmount, positions[account].debtAmount, getTotalDebtAmount(account)); + } + + function _getCurrentCollaterilizationRatio(uint256 collateralAmount, uint256 debtAmount, uint256 totalDebtAmount) public view returns(uint256) { + if (debtAmount == 0 ) { return 0; } else { - return ((positions[account].collateralAmount * collateralPrice * 100) / getTotalDebtAmount(account) / debtPrice); + return ((collateralAmount * collateralPrice * 100) / totalDebtAmount / debtPrice); } } diff --git a/test/TempleLineOfCredit.t.sol b/test/TempleLineOfCredit.t.sol index 73d5761..ebf486b 100644 --- a/test/TempleLineOfCredit.t.sol +++ b/test/TempleLineOfCredit.t.sol @@ -202,7 +202,7 @@ contract TempleLineOfCreditTest is Test { vm.stopPrank(); } - function testRepaySuccess() external { + function testRepaySuccess(uint256 repayAmount) external { uint256 borrowAmount = uint(60_000e18); uint256 repayAmount = uint(50_000e18); _borrow(alice, uint(100_000e18), borrowAmount); From aa27eca9f1c5eb658d0ac60f0e460a8e74f852f9 Mon Sep 17 00:00:00 2001 From: smokey Date: Tue, 7 Feb 2023 21:11:54 -0500 Subject: [PATCH 09/15] make interest rate continuous --- src/TempleLineOfCredit.sol | 45 ++++++++++++++--------------------- test/TempleLineOfCredit.t.sol | 14 ++++------- 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/src/TempleLineOfCredit.sol b/src/TempleLineOfCredit.sol index 88ff0e6..c9504ae 100644 --- a/src/TempleLineOfCredit.sol +++ b/src/TempleLineOfCredit.sol @@ -26,7 +26,7 @@ contract TempleLineOfCredit is Ownable, Operators { /// @notice Collateral token price uint256 public collateralPrice; - /// @notice Required collateral backing to not be in bad debt in percentage + /// @notice Required collateral backing to not be in bad debt in percentage with 100 decimal precision uint256 public minCollateralizationRatio; /// @notice Total debt taken out @@ -35,9 +35,6 @@ contract TempleLineOfCredit is Ownable, Operators { /// @notice Fixed borrow interest rate in bpds uint256 public immutable interestRateBps; - /// @notice Amount in seconds for interest to accumulate - uint256 public immutable interestRatePeriod; - /// @notice Address to send bad debt collateral address public debtCollector; @@ -62,6 +59,7 @@ contract TempleLineOfCredit is Ownable, Operators { error InvalidAmount(uint256 amount); error InsufficentCollateral(uint256 maxCapacity, uint256 debtAmount); + error InsufficentDebtToken(uint256 debtTokenBalance, uint256 borrowAmount); error ExceededBorrowedAmount(address account, uint256 amountBorrowed, uint256 amountRepay); error ExceededCollateralAmonut(address account, uint256 amountCollateral, uint256 collateralWithdraw); error WillUnderCollaterlize(address account, uint256 withdrawalAmount); @@ -70,7 +68,6 @@ contract TempleLineOfCredit is Ownable, Operators { constructor( uint256 _interestRateBps, uint256 _minCollateralizationRatio, - uint256 _interestRatePeriod, address _collateralToken, uint256 _collateralPrice, @@ -82,7 +79,6 @@ contract TempleLineOfCredit is Ownable, Operators { ) { interestRateBps = _interestRateBps; minCollateralizationRatio = _minCollateralizationRatio; - interestRatePeriod = _interestRatePeriod; collateralToken = IERC20(_collateralToken); collateralPrice = _collateralPrice; @@ -132,9 +128,8 @@ contract TempleLineOfCredit is Ownable, Operators { */ function getTotalDebtAmount(address account) public view returns (uint256) { 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; + uint256 secondsElapsed = block.timestamp - positions[account].createdAt; + totalDebt += (totalDebt * interestRateBps * secondsElapsed) / 10000 / 365 days; return totalDebt; } @@ -186,34 +181,33 @@ contract TempleLineOfCredit is Ownable, Operators { emit PostCollateral(msg.sender, amount); } - function borrow(uint256 amount) external { - if (amount == 0) revert InvalidAmount(amount); + function borrow(uint256 borrowAmount) external { + if (borrowAmount == 0) revert InvalidAmount(borrowAmount); uint256 debtAmount = positions[msg.sender].debtAmount; + if (borrowAmount > debtBalance) { + revert InsufficentDebtToken(debtBalance, borrowAmount); + } if (debtAmount != 0) { debtAmount = getTotalDebtAmount(msg.sender); } uint256 borrowCapacity = _maxBorrowCapacity(positions[msg.sender].collateralAmount) - debtAmount; - debtAmount += amount; + debtAmount += borrowAmount; if (debtAmount > borrowCapacity) { revert InsufficentCollateral(borrowCapacity, debtAmount); } positions[msg.sender].debtAmount = 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; - } + positions[msg.sender].createdAt = block.timestamp; - debtBalance -= amount; + debtBalance -= borrowAmount; debtToken.safeTransfer( msg.sender, - amount + borrowAmount ); - emit Borrow(msg.sender, amount); + emit Borrow(msg.sender, borrowAmount); } function maxBorrowCapacity(address account) public view returns(uint256) { @@ -222,7 +216,7 @@ contract TempleLineOfCredit is Ownable, Operators { function _maxBorrowCapacity(uint256 collateralAmount) internal view returns (uint256) { - return collateralAmount * collateralPrice * 100 / debtPrice / minCollateralizationRatio; + return collateralAmount * collateralPrice * 10000 / debtPrice / minCollateralizationRatio; } /** @@ -265,11 +259,8 @@ contract TempleLineOfCredit is Ownable, Operators { positions[msg.sender].debtAmount -= repayAmount; debtBalance += repayAmount; - - // 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; - } + positions[msg.sender].createdAt = block.timestamp; + debtToken.safeTransferFrom( msg.sender, address(this), @@ -316,7 +307,7 @@ contract TempleLineOfCredit is Ownable, Operators { if (debtAmount == 0 ) { return 0; } else { - return ((collateralAmount * collateralPrice * 100) / totalDebtAmount / debtPrice); + return ((collateralAmount * collateralPrice * 10000) / totalDebtAmount / debtPrice); } } diff --git a/test/TempleLineOfCredit.t.sol b/test/TempleLineOfCredit.t.sol index ebf486b..799b5ef 100644 --- a/test/TempleLineOfCredit.t.sol +++ b/test/TempleLineOfCredit.t.sol @@ -12,7 +12,6 @@ contract TempleLineOfCreditTest is Test { uint256 public interestRateBps; uint256 public minCollateralizationRatio; - uint256 public interestRatePeriod; ERC20Mock public collateralToken; uint256 public collateralPrice; @@ -29,8 +28,7 @@ contract TempleLineOfCreditTest is Test { function setUp() public { interestRateBps = 500; // 5% - minCollateralizationRatio = 120; - interestRatePeriod = 60 seconds; + minCollateralizationRatio = 12000; // 120 percent collateralToken = new ERC20Mock("TempleToken", "Temple", admin, uint(500_000e18)); collateralPrice = 970; // 0.97 debtToken = new ERC20Mock("DAI Token", "DAI", admin, uint(500_000e18)); @@ -39,7 +37,6 @@ contract TempleLineOfCreditTest is Test { tlc = new TempleLineOfCredit( interestRateBps, minCollateralizationRatio, - interestRatePeriod, address(collateralToken), collateralPrice, address(debtToken), @@ -165,16 +162,15 @@ contract TempleLineOfCreditTest is Test { tlc.borrow(borrowAmount); } - function testBorrowAccuresInterest(uint32 periodElapsed) external { + function testBorrowAccuresInterest(uint32 secondsElapsed) external { uint256 borrowAmount = uint(60_000e18); _borrow(alice, uint(100_000e18), borrowAmount); uint256 borrowTimeStamp = block.timestamp; - vm.warp(block.timestamp + (periodElapsed * interestRatePeriod)); - uint256 periodsPerYear = 365 days / interestRatePeriod; - uint256 periodsElapsed = (block.timestamp / interestRatePeriod) - (borrowTimeStamp / interestRatePeriod); - uint256 expectedTotalDebt = (borrowAmount) + ((borrowAmount * interestRateBps) / 10000 / periodsPerYear) * periodsElapsed; + vm.warp(block.timestamp + secondsElapsed); + uint256 secondsElapsed = block.timestamp - borrowTimeStamp; + uint256 expectedTotalDebt = (borrowAmount) + ((borrowAmount * interestRateBps * secondsElapsed) / 10000 / 365 days); vm.startPrank(alice); assertEq(expectedTotalDebt, tlc.getTotalDebtAmount(alice)); From 7a58613944462aa87a3283ac1216eb1f651b9d30 Mon Sep 17 00:00:00 2001 From: smokey Date: Tue, 7 Feb 2023 21:17:49 -0500 Subject: [PATCH 10/15] fix type --- src/TempleLineOfCredit.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TempleLineOfCredit.sol b/src/TempleLineOfCredit.sol index c9504ae..ead32ef 100644 --- a/src/TempleLineOfCredit.sol +++ b/src/TempleLineOfCredit.sol @@ -32,7 +32,7 @@ contract TempleLineOfCredit is Ownable, Operators { /// @notice Total debt taken out uint256 public debtBalance; - /// @notice Fixed borrow interest rate in bpds + /// @notice Fixed borrow interest rate in bps uint256 public immutable interestRateBps; /// @notice Address to send bad debt collateral From fcdb1c93156ce99ac7e07aad7a417064dbf767b3 Mon Sep 17 00:00:00 2001 From: smokey Date: Fri, 10 Feb 2023 17:24:26 -0500 Subject: [PATCH 11/15] support multi-asset borrow --- src/TempleLineOfCredit.sol | 405 +++++++++++++++++------------- test/TempleLineOfCredit.t.sol | 457 ++++++++++++++++++++++------------ 2 files changed, 522 insertions(+), 340 deletions(-) diff --git a/src/TempleLineOfCredit.sol b/src/TempleLineOfCredit.sol index ead32ef..1285470 100644 --- a/src/TempleLineOfCredit.sol +++ b/src/TempleLineOfCredit.sol @@ -8,16 +8,48 @@ import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol" import {Operators} from "./common/access/Operators.sol"; +interface IERC20Mint { + function mint(address to, uint256 amount) external; +} + contract TempleLineOfCredit is Ownable, Operators { using SafeERC20 for IERC20; + /// @notice debt position on all tokens struct Position { + /// @notice total collateral posted for this position uint256 collateralAmount; + mapping(address => TokenPosition) tokenPosition; + } + + /// @notice debt position on a specific token + struct TokenPosition { uint256 debtAmount; uint256 createdAt; } + enum TokenType { + MINT, + TRANSFER + } + + /// @notice relevant data related to a debt token + struct DebtToken { + /// @notice either a mint token or a transfer + TokenType tokenType; + /// @notice Fixed borrow interest rate in bps + uint256 interestRateBps; + /// @notice debt token price + uint256 tokenPrice; + /// @notice Required collateral backing to not be in bad debt in percentage with 100 decimal precisio + uint256 minCollateralizationRatio; + /// @notice flag to show if debt token is supported + bool isAllowed; + } + + mapping(address => DebtToken) public debtTokens; + // Collateral Parameters /// @notice Supported collateral token address @@ -26,66 +58,42 @@ contract TempleLineOfCredit is Ownable, Operators { /// @notice Collateral token price uint256 public collateralPrice; - /// @notice Required collateral backing to not be in bad debt in percentage with 100 decimal precision - uint256 public minCollateralizationRatio; - - /// @notice Total debt taken out - uint256 public debtBalance; - - /// @notice Fixed borrow interest rate in bps - uint256 public immutable interestRateBps; - /// @notice Address to send bad debt collateral address public debtCollector; - /// Debt parameters - - /// @notice Debt token address - IERC20 public immutable debtToken; - - /// @notice Debt token price - uint256 public debtPrice; - /// @notice Mapping of user positions mapping(address => Position) public positions; - event DepositDebt(uint256 amount); - event RemoveDebt(uint256 amount); + event SetCollateralPrice(uint256 price); + event SetDebtCollector(address debtCollector); + event AddDebtToken(address token); + event RemoveDebtToken(address token); + event DepositDebt(address debtToken, uint256 amount); + event RemoveDebt(address debtToken, uint256 amount); event PostCollateral(address account, 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); + event Borrow(address account, address debtToken, uint256 amount); + // event Repay(address account, uint256 amount); + // event Withdraw(address account, uint256 amount); + // event Liquidated(address account, uint256 debtAmount, uint256 collateralSeized); error InvalidAmount(uint256 amount); - error InsufficentCollateral(uint256 maxCapacity, uint256 debtAmount); - error InsufficentDebtToken(uint256 debtTokenBalance, uint256 borrowAmount); - 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); + error Unsupported(address token); + error InsufficentCollateral(address debtToken, uint256 maxCapacity, uint256 debtAmount); + // error InsufficentDebtToken(uint256 debtTokenBalance, uint256 borrowAmount); + // 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 _interestRateBps, - uint256 _minCollateralizationRatio, - address _collateralToken, uint256 _collateralPrice, - - address _debtToken, - uint256 _debtPrice, address _debtCollector ) { - interestRateBps = _interestRateBps; - minCollateralizationRatio = _minCollateralizationRatio; collateralToken = IERC20(_collateralToken); collateralPrice = _collateralPrice; - - debtToken = IERC20(_debtToken); - debtPrice = _debtPrice; - debtCollector = _debtCollector; } @@ -98,38 +106,58 @@ contract TempleLineOfCredit is Ownable, Operators { _removeOperator(_address); } - function setDebtPrice(uint256 _debtPrice) external onlyOperators { - debtPrice = _debtPrice; - } - function setCollateralPrice(uint256 _collateralPrice) external onlyOperators { collateralPrice = _collateralPrice; + emit SetCollateralPrice(_collateralPrice); } function setDebtCollector(address _debtCollector) external onlyOperators { debtCollector = _debtCollector; + emit SetDebtCollector(debtCollector); } - function setCollateralizationRatio(uint256 _minCollateralizationRatio) external onlyOperators { - minCollateralizationRatio = _minCollateralizationRatio; + function addDebtToken(address token, TokenType tokenType, uint256 interestRateBps, uint256 tokenPrice, uint256 minCollateralizationRatio) external onlyOperators { + DebtToken memory newDebtToken = DebtToken(tokenType, interestRateBps, tokenPrice, minCollateralizationRatio, true); + debtTokens[token] = newDebtToken; + emit AddDebtToken(token); } + + function removeDebtToken(address debtToken) external onlyOperators { + if (!debtTokens[debtToken].isAllowed) revert Unsupported(debtToken); + delete debtTokens[debtToken]; + emit AddDebtToken(debtToken); + } + + function setMinCollateralizationRatio(address debtToken, uint256 _minCollateralizationRatio) external onlyOperators { + if (!debtTokens[debtToken].isAllowed) revert Unsupported(debtToken); + debtTokens[debtToken].minCollateralizationRatio = _minCollateralizationRatio; + emit RemoveDebtToken(debtToken); + } + + /** * @dev Get user principal amount * @return principal amount */ - function getDebtAmount(address account) public view returns (uint256) { - return positions[account].debtAmount; + function getDebtAmount(address debtToken, address account) public view returns (TokenPosition memory) { + if (!debtTokens[debtToken].isAllowed) revert Unsupported(debtToken); + return positions[account].tokenPosition[debtToken]; } /** * @dev Get user total debt incurred (principal + interest) * @return total Debt */ - function getTotalDebtAmount(address account) public view returns (uint256) { - uint256 totalDebt = positions[account].debtAmount; - uint256 secondsElapsed = block.timestamp - positions[account].createdAt; - totalDebt += (totalDebt * interestRateBps * secondsElapsed) / 10000 / 365 days; + function getTotalDebtAmount(address debtToken, address account) public view returns (uint256) { + DebtToken memory debtTokenInfo = debtTokens[debtToken]; + if (!debtTokenInfo.isAllowed) revert Unsupported(debtToken); + + TokenPosition storage userPosition = positions[account].tokenPosition[debtToken]; + + uint256 totalDebt = userPosition.debtAmount; + uint256 secondsElapsed = block.timestamp - userPosition.createdAt; + totalDebt += (totalDebt * debtTokenInfo.interestRateBps * secondsElapsed) / 10000 / 365 days; return totalDebt; } @@ -137,178 +165,197 @@ contract TempleLineOfCredit is Ownable, Operators { * @dev Allows operator to depoist debt tokens * @param amount is the amount to deposit */ - function depositDebt(address account, uint256 amount) external onlyOperators{ + function depositDebt(address debtToken, address account, uint256 amount) external onlyOperators{ if (amount == 0) { revert InvalidAmount(amount); } - debtBalance += amount; - debtToken.safeTransferFrom( + if (!debtTokens[debtToken].isAllowed || debtTokens[debtToken].tokenType != TokenType.TRANSFER ) revert Unsupported(debtToken); + IERC20(debtToken).safeTransferFrom( account, address(this), amount ); - emit DepositDebt(amount); + emit DepositDebt(debtToken, amount); } /** * @dev Allows operator to remove debt token * @param amount is the amount to remove */ - function removeDebt(uint256 amount) external onlyOperators{ + function removeDebt(address debtToken, address account, uint256 amount) external onlyOperators{ if (amount == 0) { revert InvalidAmount(amount); } - debtBalance -= amount; - debtToken.safeTransfer( + if (!debtTokens[debtToken].isAllowed || debtTokens[debtToken].tokenType != TokenType.TRANSFER ) revert Unsupported(debtToken); + IERC20(debtToken).safeTransfer( msg.sender, amount ); - emit RemoveDebt(amount); + emit RemoveDebt(debtToken, amount); } /** * @dev Allows borrower to deposit collateral - * @param amount is the amount to deposit + * @param collateralAmount is the amount to deposit */ - function postCollateral(uint256 amount) external { - if (amount == 0) revert InvalidAmount(amount); - positions[msg.sender].collateralAmount += amount; + function postCollateral(uint256 collateralAmount) external { + if (collateralAmount == 0) revert InvalidAmount(collateralAmount); + positions[msg.sender].collateralAmount += collateralAmount; collateralToken.safeTransferFrom( msg.sender, address(this), - amount + collateralAmount ); - emit PostCollateral(msg.sender, amount); + emit PostCollateral(msg.sender, collateralAmount); } - function borrow(uint256 borrowAmount) external { - if (borrowAmount == 0) revert InvalidAmount(borrowAmount); - - uint256 debtAmount = positions[msg.sender].debtAmount; - if (borrowAmount > debtBalance) { - revert InsufficentDebtToken(debtBalance, borrowAmount); - } - if (debtAmount != 0) { - debtAmount = getTotalDebtAmount(msg.sender); - } - - uint256 borrowCapacity = _maxBorrowCapacity(positions[msg.sender].collateralAmount) - debtAmount; - debtAmount += borrowAmount; - - if (debtAmount > borrowCapacity) { - revert InsufficentCollateral(borrowCapacity, debtAmount); - } + function borrow(address[] memory tokens, uint256[] memory borrowAmounts) external { - positions[msg.sender].debtAmount = debtAmount; - positions[msg.sender].createdAt = block.timestamp; - - debtBalance -= borrowAmount; - debtToken.safeTransfer( - msg.sender, - borrowAmount - ); - emit Borrow(msg.sender, borrowAmount); - } + for (uint256 i =0; i < tokens.length; i++) { + + address debtToken = tokens[i]; + uint256 borrowAmount = borrowAmounts[i]; - function maxBorrowCapacity(address account) public view returns(uint256) { - return _maxBorrowCapacity(positions[account].collateralAmount); - } + if (borrowAmount == 0) revert InvalidAmount(borrowAmount); + DebtToken memory debtTokenInfo = debtTokens[debtToken]; + if (!debtTokenInfo.isAllowed) revert Unsupported(debtToken); - function _maxBorrowCapacity(uint256 collateralAmount) internal view returns (uint256) { - return collateralAmount * collateralPrice * 10000 / debtPrice / minCollateralizationRatio; - } + uint256 debtAmount = positions[msg.sender].tokenPosition[debtToken].debtAmount; + if (debtAmount != 0) { + debtAmount = getTotalDebtAmount(debtToken, msg.sender); + } - /** - * @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 == 0) revert InvalidAmount(withdrawalAmount); - uint256 collateralAmount = positions[msg.sender].collateralAmount; - if (withdrawalAmount > collateralAmount) { - revert ExceededCollateralAmonut(msg.sender, collateralAmount, withdrawalAmount); - } + uint256 borrowCapacity = _maxBorrowCapacity(positions[msg.sender].collateralAmount, debtTokenInfo.tokenPrice, debtTokenInfo.minCollateralizationRatio) - debtAmount; + debtAmount += borrowAmount; - uint256 borrowCapacity = _maxBorrowCapacity(collateralAmount - withdrawalAmount); - - if (positions[msg.sender].debtAmount > borrowCapacity ) { - revert WillUnderCollaterlize(msg.sender, withdrawalAmount); - } + if (debtAmount > borrowCapacity) { + revert InsufficentCollateral(debtToken, borrowCapacity, debtAmount); + } - positions[msg.sender].collateralAmount -= withdrawalAmount; - collateralToken.safeTransfer( - msg.sender, - withdrawalAmount - ); + TokenPosition storage userPosition = positions[msg.sender].tokenPosition[debtToken]; - emit Withdraw(msg.sender, withdrawalAmount); - } + userPosition.debtAmount = debtAmount; + userPosition.createdAt = block.timestamp; + - /** - * @dev Allows borrower to repay borrowed amount - * @param repayAmount is the amount to repay - */ - function repay(uint256 repayAmount) external { - if (repayAmount == 0) revert InvalidAmount(repayAmount); - positions[msg.sender].debtAmount = getTotalDebtAmount(msg.sender); + if (debtTokenInfo.tokenType == TokenType.TRANSFER){ + IERC20(debtToken).safeTransfer( + msg.sender, + borrowAmount + ); + } else { + IERC20Mint(debtToken).mint( + msg.sender, + borrowAmount + ); + + } - if (repayAmount > positions[msg.sender].debtAmount) { - revert ExceededBorrowedAmount(msg.sender, positions[msg.sender].debtAmount, repayAmount); + emit Borrow(msg.sender, debtToken, borrowAmount); } - - positions[msg.sender].debtAmount -= repayAmount; - debtBalance += repayAmount; - positions[msg.sender].createdAt = block.timestamp; - - debtToken.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 { - Position storage position = positions[debtor]; - uint256 totalDebtOwed = getTotalDebtAmount(debtor); - - if (_getCurrentCollaterilizationRatio(position.collateralAmount, position.debtAmount, totalDebtOwed) >= minCollateralizationRatio) { - revert OverCollaterilized(debtor); - } - - uint256 collateralSeized = (totalDebtOwed * debtPrice) / collateralPrice; - - if (collateralSeized > position.collateralAmount) { - collateralSeized = position.collateralAmount; - } - position.collateralAmount -= collateralSeized; - position.debtAmount = 0; - position.createdAt = 0; - - collateralToken.safeTransfer( - debtCollector, - collateralSeized - ); - - emit Liquidated(debtor, totalDebtOwed, collateralSeized); + function maxBorrowCapacity(address debtToken, address account) public view returns(uint256) { + DebtToken memory debtTokenInfo = debtTokens[debtToken]; + return _maxBorrowCapacity(positions[account].collateralAmount, debtTokenInfo.tokenPrice, debtTokenInfo.minCollateralizationRatio); } - function getCurrentCollaterilizationRatio(address account) public view returns(uint256) { - _getCurrentCollaterilizationRatio(positions[account].collateralAmount, positions[account].debtAmount, getTotalDebtAmount(account)); - } - function _getCurrentCollaterilizationRatio(uint256 collateralAmount, uint256 debtAmount, uint256 totalDebtAmount) public view returns(uint256) { - if (debtAmount == 0 ) { - return 0; - } else { - return ((collateralAmount * collateralPrice * 10000) / totalDebtAmount / debtPrice); - } + function _maxBorrowCapacity(uint256 collateralAmount, uint256 debtPrice, uint256 minCollateralizationRatio) internal view returns (uint256) { + return collateralAmount * collateralPrice * 10000 / debtPrice / minCollateralizationRatio; } +// /** +// * @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 == 0) revert InvalidAmount(withdrawalAmount); +// uint256 collateralAmount = positions[msg.sender].collateralAmount; +// if (withdrawalAmount > collateralAmount) { +// revert ExceededCollateralAmonut(msg.sender, collateralAmount, withdrawalAmount); +// } + +// uint256 borrowCapacity = _maxBorrowCapacity(collateralAmount - withdrawalAmount); + +// if (positions[msg.sender].debtAmount > borrowCapacity ) { +// revert WillUnderCollaterlize(msg.sender, withdrawalAmount); +// } + +// positions[msg.sender].collateralAmount -= withdrawalAmount; +// collateralToken.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 InvalidAmount(repayAmount); +// 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; +// positions[msg.sender].createdAt = block.timestamp; + +// debtToken.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 { +// Position storage position = positions[debtor]; +// uint256 totalDebtOwed = getTotalDebtAmount(debtor); + +// if (_getCurrentCollaterilizationRatio(position.collateralAmount, position.debtAmount, totalDebtOwed) >= minCollateralizationRatio) { +// revert OverCollaterilized(debtor); +// } + +// uint256 collateralSeized = (totalDebtOwed * debtPrice) / collateralPrice; + +// if (collateralSeized > position.collateralAmount) { +// collateralSeized = position.collateralAmount; +// } + +// position.collateralAmount -= collateralSeized; +// position.debtAmount = 0; +// position.createdAt = 0; + +// collateralToken.safeTransfer( +// debtCollector, +// collateralSeized +// ); + +// emit Liquidated(debtor, totalDebtOwed, collateralSeized); +// } + +// function getCurrentCollaterilizationRatio(address account) public view returns(uint256) { +// _getCurrentCollaterilizationRatio(positions[account].collateralAmount, positions[account].debtAmount, getTotalDebtAmount(account)); +// } + +// function _getCurrentCollaterilizationRatio(uint256 collateralAmount, uint256 debtAmount, uint256 totalDebtAmount) public view returns(uint256) { +// if (debtAmount == 0 ) { +// return 0; +// } else { +// return ((collateralAmount * collateralPrice * 10000) / totalDebtAmount / debtPrice); +// } +// } + } diff --git a/test/TempleLineOfCredit.t.sol b/test/TempleLineOfCredit.t.sol index 799b5ef..db2cbce 100644 --- a/test/TempleLineOfCredit.t.sol +++ b/test/TempleLineOfCredit.t.sol @@ -16,8 +16,12 @@ contract TempleLineOfCreditTest is Test { ERC20Mock public collateralToken; uint256 public collateralPrice; - ERC20Mock public debtToken; - uint256 public debtPrice; + ERC20Mock public daiToken; + uint256 public daiPrice; + uint256 public daiMinCollateralizationRatio; + uint256 public daiInterestRateBps; + TempleLineOfCredit.TokenType public daiTokenType; + address admin = address(0x1); address alice = address(0x2); @@ -27,20 +31,20 @@ contract TempleLineOfCreditTest is Test { function setUp() public { - interestRateBps = 500; // 5% - minCollateralizationRatio = 12000; // 120 percent collateralToken = new ERC20Mock("TempleToken", "Temple", admin, uint(500_000e18)); collateralPrice = 970; // 0.97 - debtToken = new ERC20Mock("DAI Token", "DAI", admin, uint(500_000e18)); - debtPrice = 1000; // 1 USD + + + daiToken = new ERC20Mock("DAI Token", "DAI", admin, uint(500_000e18)); + daiPrice = 1000; // 1 USD + daiMinCollateralizationRatio = 12000; + daiTokenType = TempleLineOfCredit.TokenType.TRANSFER; + daiInterestRateBps = 500; // 5% + tlc = new TempleLineOfCredit( - interestRateBps, - minCollateralizationRatio, address(collateralToken), collateralPrice, - address(debtToken), - debtPrice, debtCollector ); @@ -48,12 +52,9 @@ contract TempleLineOfCreditTest is Test { } function testInitalization() public { - assertEq(tlc.interestRateBps(), interestRateBps); - assertEq(tlc.minCollateralizationRatio(), minCollateralizationRatio); assertEq(address(tlc.collateralToken()), address(collateralToken)); assertEq(tlc.collateralPrice(), collateralPrice); - assertEq(address(tlc.debtToken()), address(debtToken)); - assertEq(tlc.debtPrice(), debtPrice); + assertEq(tlc.debtCollector(), debtCollector); } function testAddOperator() public { @@ -71,25 +72,148 @@ contract TempleLineOfCreditTest is Test { assertFalse(tlc.operators(alice)); } + function testSetCollateralPriceFailOnlyOperator() public { + vm.expectRevert(abi.encodeWithSelector(Operators.OnlyOperators.selector, address(this))); + tlc.setCollateralPrice(100); + } + + function testSetCollateralPriceSuccess() public { + uint256 collateralPrice = 970; + vm.prank(admin); + tlc.setCollateralPrice(collateralPrice); + assertEq(tlc.collateralPrice(), collateralPrice); + } + + function testSetDebtCollectorFailsOnlyOperator() public { + vm.expectRevert(abi.encodeWithSelector(Operators.OnlyOperators.selector, address(this))); + tlc.setDebtCollector(alice); + } + + function testSetDebtCollectorSuccess() public { + vm.prank(admin); + tlc.setDebtCollector(alice); + assertEq(tlc.debtCollector(), alice); + } + + function testAddDebtTokenFailsOnlyOperator() public { + vm.expectRevert(abi.encodeWithSelector(Operators.OnlyOperators.selector, address(this))); + tlc.addDebtToken(address(daiToken), daiTokenType, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); + } + + function testAddDebtTokenSuccess() public { + vm.prank(admin); + tlc.addDebtToken(address(daiToken), daiTokenType, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); + (TempleLineOfCredit.TokenType tokenType, uint256 interestRateBps, uint256 tokenPrice, uint256 minCollateralizationRatio, bool allowed) = tlc.debtTokens(address(daiToken)); + assertEq(daiInterestRateBps, interestRateBps); + assertEq(daiPrice, tokenPrice); + assertEq(daiMinCollateralizationRatio, minCollateralizationRatio); + assertEq(allowed, true); + } + + function testRemoveDebtTokenFailsOnlyOperator() public { + vm.expectRevert(abi.encodeWithSelector(Operators.OnlyOperators.selector, address(this))); + tlc.removeDebtToken(address(daiToken)); + } + + function testRemoveDebtTokenFailsNotSupported() public { + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.Unsupported.selector, address(daiToken))); + vm.prank(admin); + tlc.removeDebtToken(address(daiToken)); + } + + + function testRemoveDebtTokenSuccess() public { + vm.prank(admin); + tlc.addDebtToken(address(daiToken), daiTokenType, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); + vm.prank(admin); + tlc.removeDebtToken(address(daiToken)); + (,,,, bool allowed) = tlc.debtTokens(address(daiToken)); + assertEq(allowed, false); + } + + function testSetMinCollateralizationRatioFailsOnlyOperator() public { + vm.expectRevert(abi.encodeWithSelector(Operators.OnlyOperators.selector, address(this))); + tlc.setMinCollateralizationRatio(address(0x0), 0); + } + + function testSetMinCollateralizationRatioUnsupported() public { + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.Unsupported.selector, address(0x0))); + vm.prank(admin); + tlc.setMinCollateralizationRatio(address(0x0), 0); + } + + function testSetMinCollateralizationRatioSuccess() public { + uint256 newCollaterilizationRatio = 140; + vm.prank(admin); + tlc.addDebtToken(address(daiToken), daiTokenType, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); + vm.prank(admin); + tlc.setMinCollateralizationRatio(address(daiToken), newCollaterilizationRatio); + (,,, uint256 minCollateralizationRatio,) = tlc.debtTokens(address(daiToken)); + assertEq(minCollateralizationRatio, newCollaterilizationRatio); + } + function testDepositDebtExpectRevertOnlyOperator() public { vm.expectRevert(abi.encodeWithSelector(Operators.OnlyOperators.selector, address(this))); - tlc.depositDebt(alice, uint(100_000e18)); + tlc.depositDebt(address(daiToken), alice, uint(100_000e18)); + } + + function testDepositDebtExpectFailUnSupprotedTokenAddress() public { + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.Unsupported.selector, address(daiToken))); + vm.prank(admin); + tlc.depositDebt(address(daiToken), alice, uint(100_000e18)); + } + + function testDepositDebtExpectFailIncorrectTokenType() public { + vm.prank(admin); + tlc.addDebtToken(address(daiToken), TempleLineOfCredit.TokenType.MINT, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); + + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.Unsupported.selector, address(daiToken))); + vm.prank(admin); + tlc.depositDebt(address(daiToken), alice, uint(100_000e18)); } - function testdepositDebt() public { + function testdepositDebtSucess() public { vm.startPrank(admin); + tlc.addDebtToken(address(daiToken), TempleLineOfCredit.TokenType.TRANSFER, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); + uint256 depositAmount = uint256(100_000e18); - debtToken.approve(address(tlc), depositAmount); - tlc.depositDebt(admin, depositAmount); + daiToken.approve(address(tlc), depositAmount); + tlc.depositDebt(address(daiToken), admin, depositAmount); + assertEq(daiToken.balanceOf(address(tlc)), depositAmount); + } - assertEq(debtToken.balanceOf(address(tlc)), depositAmount); - assertEq(tlc.debtBalance(), depositAmount); + function testRemoveDebtExpectRevertOnlyOperator() public { + vm.expectRevert(abi.encodeWithSelector(Operators.OnlyOperators.selector, address(this))); + tlc.removeDebt(address(daiToken), alice, uint(100_000e18)); } + function testRemoveDebtExpectFailUnSupprotedTokenAddress() public { + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.Unsupported.selector, address(daiToken))); + vm.prank(admin); + tlc.removeDebt(address(daiToken), alice, uint(100_000e18)); + } + + + function testRemoveDebtSucess() public { + vm.startPrank(admin); + tlc.addDebtToken(address(daiToken), TempleLineOfCredit.TokenType.TRANSFER, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); + + uint256 depositAmount = uint256(100_000e18); + daiToken.approve(address(tlc), depositAmount); + tlc.depositDebt(address(daiToken), admin, depositAmount); + + uint256 priorDebtBalance = daiToken.balanceOf(address(tlc)); + uint256 removeAmount = uint256(55_000e18); + tlc.removeDebt(address(daiToken), admin, removeAmount); + assertEq(daiToken.balanceOf(address(tlc)), priorDebtBalance - removeAmount); + } + + function _initDeposit(uint256 depositAmount) internal { vm.startPrank(admin); - debtToken.approve(address(tlc), depositAmount); - tlc.depositDebt(admin, depositAmount); + tlc.addDebtToken(address(daiToken), TempleLineOfCredit.TokenType.TRANSFER, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); + daiToken.approve(address(tlc), depositAmount); + tlc.depositDebt(address(daiToken), admin, depositAmount); vm.stopPrank(); } @@ -122,167 +246,178 @@ contract TempleLineOfCreditTest is Test { vm.stopPrank(); } - function testBorrowCapacity() external { + function testBorrowCapacityCorrect() external { uint256 collateralAmount = uint(100_000e18); uint256 expectedMaxBorrowCapacity = uint(97_000e18) * uint(100) / uint(120); _postCollateral(alice, collateralAmount); - assertEq(tlc.maxBorrowCapacity(alice), expectedMaxBorrowCapacity); + assertEq(tlc.maxBorrowCapacity(address(daiToken), alice), expectedMaxBorrowCapacity); } function testBorrowInsufficientCollateral() external { uint256 collateralAmount = uint(100_000e18); _postCollateral(alice, collateralAmount); - uint256 maxBorrowCapacity = tlc.maxBorrowCapacity(alice); + uint256 maxBorrowCapacity = tlc.maxBorrowCapacity(address(daiToken), alice); uint256 borrowAmount = maxBorrowCapacity + uint(1); - vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InsufficentCollateral.selector, maxBorrowCapacity, borrowAmount)); + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InsufficentCollateral.selector, address(daiToken), maxBorrowCapacity, borrowAmount)); vm.prank(alice); - tlc.borrow(borrowAmount); + + address[] memory debtTokens = new address[](1); + debtTokens[0] = address(daiToken); + uint256[] memory borrowAmounts = new uint256[](1); + borrowAmounts[0] = borrowAmount; + + tlc.borrow(debtTokens, borrowAmounts); } function testBorrowPasses() external { uint256 collateralAmount = uint(100_000e18); _postCollateral(alice, collateralAmount); - uint256 tlcDebtBalance = debtToken.balanceOf(address(tlc)); - uint256 maxBorrowCapacity = tlc.maxBorrowCapacity(alice); + uint256 tlcDebtBalance = daiToken.balanceOf(address(tlc)); + uint256 maxBorrowCapacity = tlc.maxBorrowCapacity(address(daiToken), alice); + vm.prank(alice); - tlc.borrow(maxBorrowCapacity); + address[] memory debtTokens = new address[](1); + debtTokens[0] = address(daiToken); + uint256[] memory borrowAmounts = new uint256[](1); + borrowAmounts[0] = maxBorrowCapacity; + tlc.borrow(debtTokens, borrowAmounts); - (uint256 aliceCollateralAmount, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); + (uint256 aliceCollateralAmount ) = tlc.positions(alice); + TempleLineOfCredit.TokenPosition memory tp = tlc.getDebtAmount(address(daiToken), alice); assertEq(aliceCollateralAmount, collateralAmount); - assertEq(aliceDebtAmount, maxBorrowCapacity); - assertEq(aliceCreatedAt, block.timestamp); - assertEq(tlc.debtBalance(), tlcDebtBalance - maxBorrowCapacity); - assertEq(debtToken.balanceOf(alice), maxBorrowCapacity); + assertEq(tp.debtAmount, maxBorrowCapacity); + assertEq(tp.createdAt, block.timestamp); + assertEq(daiToken.balanceOf(alice), maxBorrowCapacity); } - function _borrow(address _account, uint256 collateralAmount, uint256 borrowAmount) internal { - _postCollateral(_account, collateralAmount); - vm.prank(_account); - tlc.borrow(borrowAmount); - } + // function _borrow(address _account, uint256 collateralAmount, uint256 borrowAmount) internal { + // _postCollateral(_account, collateralAmount); + // vm.prank(_account); + // tlc.borrow(borrowAmount); + // } - function testBorrowAccuresInterest(uint32 secondsElapsed) external { - uint256 borrowAmount = uint(60_000e18); - _borrow(alice, uint(100_000e18), borrowAmount); + // function testBorrowAccuresInterest(uint32 secondsElapsed) external { + // uint256 borrowAmount = uint(60_000e18); + // _borrow(alice, uint(100_000e18), borrowAmount); - uint256 borrowTimeStamp = block.timestamp; + // uint256 borrowTimeStamp = block.timestamp; - vm.warp(block.timestamp + secondsElapsed); - uint256 secondsElapsed = block.timestamp - borrowTimeStamp; - uint256 expectedTotalDebt = (borrowAmount) + ((borrowAmount * interestRateBps * secondsElapsed) / 10000 / 365 days); - - vm.startPrank(alice); - assertEq(expectedTotalDebt, tlc.getTotalDebtAmount(alice)); - vm.stopPrank(); - } - - - function testRepayZero() external { - uint256 borrowAmount = uint(60_000e18); - uint256 repayAmount = uint(0); - _borrow(alice, uint(100_000e18), borrowAmount); - vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InvalidAmount.selector, repayAmount)); - vm.startPrank(alice); - tlc.repay(0); - vm.stopPrank(); - } - - function testRepayExceededBorrow() external { - uint256 borrowAmount = uint(60_000e18); - uint256 repayAmount = uint(61_000e18); - _borrow(alice, uint(100_000e18), borrowAmount); - vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.ExceededBorrowedAmount.selector, alice, borrowAmount, repayAmount)); - vm.startPrank(alice); - tlc.repay(repayAmount); - vm.stopPrank(); - } - - function testRepaySuccess(uint256 repayAmount) external { - uint256 borrowAmount = uint(60_000e18); - uint256 repayAmount = uint(50_000e18); - _borrow(alice, uint(100_000e18), borrowAmount); - uint256 debtBalanceBefore = tlc.debtBalance(); - - vm.startPrank(alice); - debtToken.approve(address(tlc), repayAmount); - tlc.repay(repayAmount); - (, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); - vm.stopPrank(); - - assertEq(borrowAmount - repayAmount, aliceDebtAmount); - assertEq(debtBalanceBefore + repayAmount, tlc.debtBalance()); - assertEq(block.timestamp, aliceCreatedAt); - } - - function testWithdrawExceedCollateralAmount() external { - uint256 borrowAmount = uint(60_000e18); - uint256 collateralAmount = uint(100_000e18); - uint256 withdrawalAmount = uint(100_001e18); - _borrow(alice, collateralAmount, borrowAmount); - vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.ExceededCollateralAmonut.selector, alice, collateralAmount, withdrawalAmount)); - - vm.startPrank(alice); - tlc.withdrawCollateral(withdrawalAmount); - vm.stopPrank(); - } - - function testWithdrawWillUnderCollaterlizeLoan() external { - uint256 borrowAmount = uint(60_000e18); - uint256 collateralAmount = uint(100_000e18); - uint256 withdrawalAmount = uint(30_001e18); - _borrow(alice, collateralAmount, borrowAmount); - vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.WillUnderCollaterlize.selector, alice, withdrawalAmount)); - - vm.startPrank(alice); - tlc.withdrawCollateral(withdrawalAmount); - vm.stopPrank(); - } - - function testWithdrawalSuccess() external { - uint256 borrowAmount = uint(60_000e18); - uint256 collateralAmount = uint(100_000e18); - uint256 withdrawalAmount = uint(10_000e18); - _borrow(alice, collateralAmount, borrowAmount); - - uint256 collateralBalanceBefore = collateralToken.balanceOf(address(tlc)); - uint256 aliceCollateralBalanceBefore = collateralToken.balanceOf(alice); - - vm.startPrank(alice); - tlc.withdrawCollateral(withdrawalAmount); - (uint256 aliceCollateralAmount,,) = tlc.positions(alice); - vm.stopPrank(); - - assertEq(collateralBalanceBefore - withdrawalAmount, collateralToken.balanceOf(address(tlc))); - assertEq(aliceCollateralAmount, collateralAmount - withdrawalAmount); - assertEq(collateralToken.balanceOf(alice), aliceCollateralBalanceBefore + withdrawalAmount); - } - - - function testLiquidateSufficientCollateral() external { - uint256 borrowAmount = uint(70_000e18); - uint256 collateralAmount = uint(100_000e18); - _borrow(alice, collateralAmount, borrowAmount); - vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.OverCollaterilized.selector, alice)); - - vm.prank(admin); - tlc.liquidate(alice); - } - - function testLiquidateUnderWaterPositionSucessfully() external { - uint256 borrowAmount = uint(70_000e18); - uint256 collateralAmount = uint(100_000e18); - _borrow(alice, collateralAmount, borrowAmount); - vm.warp(block.timestamp + 1180 days); - uint256 totalDebt = tlc.getTotalDebtAmount(alice); - assertTrue(tlc.getCurrentCollaterilizationRatio(alice) < minCollateralizationRatio); - vm.prank(admin); - tlc.liquidate(alice); - - (uint256 aliceCollateralAmount, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); - assertEq(uint256(0), aliceDebtAmount); - assertEq(collateralAmount - (totalDebt * debtPrice / collateralPrice), aliceCollateralAmount); - assertEq(uint256(0), aliceCreatedAt); - assertEq((totalDebt * debtPrice / collateralPrice), collateralToken.balanceOf(debtCollector)); - } + // vm.warp(block.timestamp + secondsElapsed); + // uint256 secondsElapsed = block.timestamp - borrowTimeStamp; + // uint256 expectedTotalDebt = (borrowAmount) + ((borrowAmount * interestRateBps * secondsElapsed) / 10000 / 365 days); + + // vm.startPrank(alice); + // assertEq(expectedTotalDebt, tlc.getTotalDebtAmount(alice)); + // vm.stopPrank(); + // } + + + // function testRepayZero() external { + // uint256 borrowAmount = uint(60_000e18); + // uint256 repayAmount = uint(0); + // _borrow(alice, uint(100_000e18), borrowAmount); + // vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InvalidAmount.selector, repayAmount)); + // vm.startPrank(alice); + // tlc.repay(0); + // vm.stopPrank(); + // } + + // function testRepayExceededBorrow() external { + // uint256 borrowAmount = uint(60_000e18); + // uint256 repayAmount = uint(61_000e18); + // _borrow(alice, uint(100_000e18), borrowAmount); + // vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.ExceededBorrowedAmount.selector, alice, borrowAmount, repayAmount)); + // vm.startPrank(alice); + // tlc.repay(repayAmount); + // vm.stopPrank(); + // } + + // function testRepaySuccess(uint256 repayAmount) external { + // uint256 borrowAmount = uint(60_000e18); + // uint256 repayAmount = uint(50_000e18); + // _borrow(alice, uint(100_000e18), borrowAmount); + // uint256 debtBalanceBefore = tlc.debtBalance(); + + // vm.startPrank(alice); + // debtToken.approve(address(tlc), repayAmount); + // tlc.repay(repayAmount); + // (, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); + // vm.stopPrank(); + + // assertEq(borrowAmount - repayAmount, aliceDebtAmount); + // assertEq(debtBalanceBefore + repayAmount, tlc.debtBalance()); + // assertEq(block.timestamp, aliceCreatedAt); + // } + + // function testWithdrawExceedCollateralAmount() external { + // uint256 borrowAmount = uint(60_000e18); + // uint256 collateralAmount = uint(100_000e18); + // uint256 withdrawalAmount = uint(100_001e18); + // _borrow(alice, collateralAmount, borrowAmount); + // vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.ExceededCollateralAmonut.selector, alice, collateralAmount, withdrawalAmount)); + + // vm.startPrank(alice); + // tlc.withdrawCollateral(withdrawalAmount); + // vm.stopPrank(); + // } + + // function testWithdrawWillUnderCollaterlizeLoan() external { + // uint256 borrowAmount = uint(60_000e18); + // uint256 collateralAmount = uint(100_000e18); + // uint256 withdrawalAmount = uint(30_001e18); + // _borrow(alice, collateralAmount, borrowAmount); + // vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.WillUnderCollaterlize.selector, alice, withdrawalAmount)); + + // vm.startPrank(alice); + // tlc.withdrawCollateral(withdrawalAmount); + // vm.stopPrank(); + // } + + // function testWithdrawalSuccess() external { + // uint256 borrowAmount = uint(60_000e18); + // uint256 collateralAmount = uint(100_000e18); + // uint256 withdrawalAmount = uint(10_000e18); + // _borrow(alice, collateralAmount, borrowAmount); + + // uint256 collateralBalanceBefore = collateralToken.balanceOf(address(tlc)); + // uint256 aliceCollateralBalanceBefore = collateralToken.balanceOf(alice); + + // vm.startPrank(alice); + // tlc.withdrawCollateral(withdrawalAmount); + // (uint256 aliceCollateralAmount,,) = tlc.positions(alice); + // vm.stopPrank(); + + // assertEq(collateralBalanceBefore - withdrawalAmount, collateralToken.balanceOf(address(tlc))); + // assertEq(aliceCollateralAmount, collateralAmount - withdrawalAmount); + // assertEq(collateralToken.balanceOf(alice), aliceCollateralBalanceBefore + withdrawalAmount); + // } + + + // function testLiquidateSufficientCollateral() external { + // uint256 borrowAmount = uint(70_000e18); + // uint256 collateralAmount = uint(100_000e18); + // _borrow(alice, collateralAmount, borrowAmount); + // vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.OverCollaterilized.selector, alice)); + + // vm.prank(admin); + // tlc.liquidate(alice); + // } + + // function testLiquidateUnderWaterPositionSucessfully() external { + // uint256 borrowAmount = uint(70_000e18); + // uint256 collateralAmount = uint(100_000e18); + // _borrow(alice, collateralAmount, borrowAmount); + // vm.warp(block.timestamp + 1180 days); + // uint256 totalDebt = tlc.getTotalDebtAmount(alice); + // assertTrue(tlc.getCurrentCollaterilizationRatio(alice) < minCollateralizationRatio); + // vm.prank(admin); + // tlc.liquidate(alice); + + // (uint256 aliceCollateralAmount, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); + // assertEq(uint256(0), aliceDebtAmount); + // assertEq(collateralAmount - (totalDebt * debtPrice / collateralPrice), aliceCollateralAmount); + // assertEq(uint256(0), aliceCreatedAt); + // assertEq((totalDebt * debtPrice / collateralPrice), collateralToken.balanceOf(debtCollector)); + // } } From a69c5a8939e14e0cf4f970b0bee856ad64761507 Mon Sep 17 00:00:00 2001 From: smokey Date: Sun, 12 Feb 2023 16:05:43 -0500 Subject: [PATCH 12/15] Support different token pricing --- src/TempleLineOfCredit.sol | 211 +++++++++++++++++++++------------ src/mocks/OudRedeemer.sol | 12 ++ test/TempleLineOfCredit.t.sol | 215 +++++++++++++++++++--------------- 3 files changed, 270 insertions(+), 168 deletions(-) create mode 100644 src/mocks/OudRedeemer.sol diff --git a/src/TempleLineOfCredit.sol b/src/TempleLineOfCredit.sol index 1285470..5be262d 100644 --- a/src/TempleLineOfCredit.sol +++ b/src/TempleLineOfCredit.sol @@ -8,8 +8,14 @@ import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol" import {Operators} from "./common/access/Operators.sol"; -interface IERC20Mint { +interface IERC20MintBurn { function mint(address to, uint256 amount) external; + function burn(address from, uint256 amount) external; + +} + +interface IOudRedeemer { + function treasuryPriceIndex() external view returns (uint256); } contract TempleLineOfCredit is Ownable, Operators { @@ -34,6 +40,11 @@ contract TempleLineOfCredit is Ownable, Operators { TRANSFER } + enum TokenPrice { + STABLE, + TPI + } + /// @notice relevant data related to a debt token struct DebtToken { /// @notice either a mint token or a transfer @@ -41,30 +52,37 @@ contract TempleLineOfCredit is Ownable, Operators { /// @notice Fixed borrow interest rate in bps uint256 interestRateBps; /// @notice debt token price - uint256 tokenPrice; - /// @notice Required collateral backing to not be in bad debt in percentage with 100 decimal precisio + TokenPrice tokenPrice; + /// @notice Required collateral backing to not be in bad debt. Should have same precision as the collateral price uint256 minCollateralizationRatio; /// @notice flag to show if debt token is supported bool isAllowed; } + /// @notice mapping of debt token to its underlying information mapping(address => DebtToken) public debtTokens; + /// @notice list of all supported debtTokens + address[] public debtTokenList; + // Collateral Parameters /// @notice Supported collateral token address IERC20 public immutable collateralToken; - /// @notice Collateral token price - uint256 public collateralPrice; + /// @notice Collateral token price with 10_000 + TokenPrice public collateralPrice; + /// @notice contract to get TPI price + address public oudRedeemer; + /// @notice Address to send bad debt collateral address public debtCollector; /// @notice Mapping of user positions mapping(address => Position) public positions; - event SetCollateralPrice(uint256 price); + event SetCollateralPrice(TokenPrice price); event SetDebtCollector(address debtCollector); event AddDebtToken(address token); event RemoveDebtToken(address token); @@ -72,29 +90,32 @@ contract TempleLineOfCredit is Ownable, Operators { event RemoveDebt(address debtToken, uint256 amount); event PostCollateral(address account, uint256 amount); event Borrow(address account, address debtToken, uint256 amount); - // event Repay(address account, uint256 amount); - // event Withdraw(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 InvalidAmount(uint256 amount); error Unsupported(address token); error InsufficentCollateral(address debtToken, uint256 maxCapacity, uint256 debtAmount); // error InsufficentDebtToken(uint256 debtTokenBalance, uint256 borrowAmount); - // error ExceededBorrowedAmount(address account, uint256 amountBorrowed, uint256 amountRepay); - // error ExceededCollateralAmonut(address account, uint256 amountCollateral, uint256 collateralWithdraw); - // error WillUnderCollaterlize(address account, uint256 withdrawalAmount); + 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( address _collateralToken, - uint256 _collateralPrice, - address _debtCollector + TokenPrice _collateralPrice, + address _debtCollector, + + address _oudRedeemer ) { collateralToken = IERC20(_collateralToken); collateralPrice = _collateralPrice; debtCollector = _debtCollector; + oudRedeemer = _oudRedeemer; } @@ -106,7 +127,7 @@ contract TempleLineOfCredit is Ownable, Operators { _removeOperator(_address); } - function setCollateralPrice(uint256 _collateralPrice) external onlyOperators { + function setCollateralPrice(TokenPrice _collateralPrice) external onlyOperators { collateralPrice = _collateralPrice; emit SetCollateralPrice(_collateralPrice); } @@ -116,9 +137,10 @@ contract TempleLineOfCredit is Ownable, Operators { emit SetDebtCollector(debtCollector); } - function addDebtToken(address token, TokenType tokenType, uint256 interestRateBps, uint256 tokenPrice, uint256 minCollateralizationRatio) external onlyOperators { + function addDebtToken(address token, TokenType tokenType, uint256 interestRateBps, TokenPrice tokenPrice, uint256 minCollateralizationRatio) external onlyOperators { DebtToken memory newDebtToken = DebtToken(tokenType, interestRateBps, tokenPrice, minCollateralizationRatio, true); debtTokens[token] = newDebtToken; + debtTokenList.push(token); emit AddDebtToken(token); } @@ -126,6 +148,7 @@ contract TempleLineOfCredit is Ownable, Operators { function removeDebtToken(address debtToken) external onlyOperators { if (!debtTokens[debtToken].isAllowed) revert Unsupported(debtToken); delete debtTokens[debtToken]; + /// TODO: Remove from the debtTokenList emit AddDebtToken(debtToken); } @@ -161,6 +184,17 @@ contract TempleLineOfCredit is Ownable, Operators { return totalDebt; } + function getDebtTokenPrice(TokenPrice _price) public view returns (uint256 price, uint256 precision) { + + if (_price == TokenPrice.STABLE) { + return (10000, 10000); + } else if (_price == TokenPrice.TPI) { + // Get Token Price from redemeer + uint256 tpiPrice = IOudRedeemer(oudRedeemer).treasuryPriceIndex(); + return (tpiPrice, 10000); + } + } + /** * @dev Allows operator to depoist debt tokens * @param amount is the amount to deposit @@ -238,18 +272,16 @@ contract TempleLineOfCredit is Ownable, Operators { userPosition.debtAmount = debtAmount; userPosition.createdAt = block.timestamp; - if (debtTokenInfo.tokenType == TokenType.TRANSFER){ IERC20(debtToken).safeTransfer( msg.sender, borrowAmount ); } else { - IERC20Mint(debtToken).mint( + IERC20MintBurn(debtToken).mint( msg.sender, borrowAmount ); - } emit Borrow(msg.sender, debtToken, borrowAmount); @@ -262,59 +294,91 @@ contract TempleLineOfCredit is Ownable, Operators { } - function _maxBorrowCapacity(uint256 collateralAmount, uint256 debtPrice, uint256 minCollateralizationRatio) internal view returns (uint256) { - return collateralAmount * collateralPrice * 10000 / debtPrice / minCollateralizationRatio; + function _maxBorrowCapacity(uint256 collateralAmount, TokenPrice debtPrice, uint256 minCollateralizationRatio) internal view returns (uint256) { + (uint256 debtTokenPrice, uint256 debtPercision) = getDebtTokenPrice(debtPrice); + (uint256 collateralTokenPrice, uint256 collateralPrecision) = getDebtTokenPrice(collateralPrice); + return collateralAmount * collateralTokenPrice * debtPercision * 10000 / debtTokenPrice / collateralPrecision /minCollateralizationRatio; } -// /** -// * @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 == 0) revert InvalidAmount(withdrawalAmount); -// uint256 collateralAmount = positions[msg.sender].collateralAmount; -// if (withdrawalAmount > collateralAmount) { -// revert ExceededCollateralAmonut(msg.sender, collateralAmount, withdrawalAmount); -// } + /** + * @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 == 0) revert InvalidAmount(withdrawalAmount); + uint256 collateralAmount = positions[msg.sender].collateralAmount; + if (withdrawalAmount > collateralAmount) { + revert ExceededCollateralAmonut(msg.sender, collateralAmount, withdrawalAmount); + } -// uint256 borrowCapacity = _maxBorrowCapacity(collateralAmount - withdrawalAmount); - -// if (positions[msg.sender].debtAmount > borrowCapacity ) { -// revert WillUnderCollaterlize(msg.sender, withdrawalAmount); -// } + for (uint256 i =0; i < debtTokenList.length; i++) { -// positions[msg.sender].collateralAmount -= withdrawalAmount; -// collateralToken.safeTransfer( -// msg.sender, -// withdrawalAmount -// ); + address debtToken = debtTokenList[i]; + DebtToken memory debtTokenInfo = debtTokens[debtToken]; -// emit Withdraw(msg.sender, withdrawalAmount); -// } + uint256 borrowCapacity = _maxBorrowCapacity(collateralAmount - withdrawalAmount, debtTokenInfo.tokenPrice, debtTokenInfo.minCollateralizationRatio); + + if (positions[msg.sender].tokenPosition[debtToken].debtAmount > borrowCapacity ) { + revert WillUnderCollaterlize(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 InvalidAmount(repayAmount); -// positions[msg.sender].debtAmount = getTotalDebtAmount(msg.sender); + positions[msg.sender].collateralAmount -= withdrawalAmount; + collateralToken.safeTransfer( + msg.sender, + withdrawalAmount + ); -// if (repayAmount > positions[msg.sender].debtAmount) { -// revert ExceededBorrowedAmount(msg.sender, positions[msg.sender].debtAmount, repayAmount); -// } + emit Withdraw(msg.sender, withdrawalAmount); + } -// positions[msg.sender].debtAmount -= repayAmount; -// debtBalance += repayAmount; -// positions[msg.sender].createdAt = block.timestamp; + /** + * @dev Allows borrower to repay borrowed amount + * @param tokens is the list of debt tokens to repay + * @param repayAmounts is amount to repay + */ + function repay(address[] memory tokens, uint256[] memory repayAmounts) external { -// debtToken.safeTransferFrom( -// msg.sender, -// address(this), -// repayAmount -// ); -// emit Repay(msg.sender, repayAmount); -// } + for (uint256 i =0; i < tokens.length; i++) { + + address debtToken = tokens[i]; + uint256 repayAmount = repayAmounts[i]; + + if (repayAmount == 0) revert InvalidAmount(repayAmount); + + DebtToken memory debtTokenInfo = debtTokens[debtToken]; + if (!debtTokenInfo.isAllowed) revert Unsupported(debtToken); + + uint256 debtAmount = positions[msg.sender].tokenPosition[debtToken].debtAmount; + if (debtAmount != 0) { + debtAmount = getTotalDebtAmount(debtToken, msg.sender); + } + + if (repayAmount > debtAmount) { + revert ExceededBorrowedAmount(msg.sender, debtAmount, repayAmount); + } + + TokenPosition storage userPosition = positions[msg.sender].tokenPosition[debtToken]; + + userPosition.debtAmount = debtAmount - repayAmount; + userPosition.createdAt = block.timestamp; + + if (debtTokenInfo.tokenType == TokenType.TRANSFER){ + IERC20(debtToken).safeTransferFrom( + msg.sender, + address(this), + repayAmount + ); + } else { + IERC20MintBurn(debtToken).mint( + msg.sender, + repayAmount + ); + } + emit Repay(msg.sender, repayAmount); + } + } // /** // * @dev Allows operator to liquidate debtors position @@ -346,16 +410,19 @@ contract TempleLineOfCredit is Ownable, Operators { // emit Liquidated(debtor, totalDebtOwed, collateralSeized); // } -// function getCurrentCollaterilizationRatio(address account) public view returns(uint256) { -// _getCurrentCollaterilizationRatio(positions[account].collateralAmount, positions[account].debtAmount, getTotalDebtAmount(account)); -// } + function getCurrentCollaterilizationRatio(address debtToken, address account) public view returns(uint256) { + DebtToken memory debtTokenInfo = debtTokens[debtToken]; + _getCurrentCollaterilizationRatio(positions[account].collateralAmount, positions[msg.sender].tokenPosition[debtToken].debtAmount, debtTokenInfo.tokenPrice, getTotalDebtAmount(debtToken, account)); + } -// function _getCurrentCollaterilizationRatio(uint256 collateralAmount, uint256 debtAmount, uint256 totalDebtAmount) public view returns(uint256) { -// if (debtAmount == 0 ) { -// return 0; -// } else { -// return ((collateralAmount * collateralPrice * 10000) / totalDebtAmount / debtPrice); -// } -// } + function _getCurrentCollaterilizationRatio(uint256 collateralAmount, uint256 debtAmount, TokenPrice debtPrice, uint256 totalDebtAmount) public view returns(uint256) { + if (debtAmount == 0 ) { + return 0; + } else { + (uint256 debtTokenPrice, uint256 debtPercision) = getDebtTokenPrice(debtPrice); + (uint256 collateralTokenPrice, uint256 collateralPrecision) = getDebtTokenPrice(collateralPrice); + return ((collateralAmount * collateralTokenPrice * debtPercision * 10000) / totalDebtAmount / debtTokenPrice / collateralPrecision); + } + } } diff --git a/src/mocks/OudRedeemer.sol b/src/mocks/OudRedeemer.sol new file mode 100644 index 0000000..b63cf0f --- /dev/null +++ b/src/mocks/OudRedeemer.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.17; + + +contract OudRedeemer { + + constructor() {} + + function treasuryPriceIndex() external pure returns (uint256) { + return 9700; + } +} \ No newline at end of file diff --git a/test/TempleLineOfCredit.t.sol b/test/TempleLineOfCredit.t.sol index db2cbce..5096f94 100644 --- a/test/TempleLineOfCredit.t.sol +++ b/test/TempleLineOfCredit.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import {ERC20Mock} from "openzeppelin-contracts/mocks/ERC20Mock.sol"; import "../src/TempleLineOfCredit.sol"; +import "../src/mocks/OudRedeemer.sol"; import "../src/common/access/Operators.sol"; contract TempleLineOfCreditTest is Test { @@ -14,15 +15,18 @@ contract TempleLineOfCreditTest is Test { uint256 public minCollateralizationRatio; ERC20Mock public collateralToken; - uint256 public collateralPrice; + TempleLineOfCredit.TokenPrice public collateralPrice; ERC20Mock public daiToken; - uint256 public daiPrice; + TempleLineOfCredit.TokenPrice public daiPrice; uint256 public daiMinCollateralizationRatio; uint256 public daiInterestRateBps; TempleLineOfCredit.TokenType public daiTokenType; + OudRedeemer public oudRedeemer; + + address admin = address(0x1); address alice = address(0x2); address bob = address(0x2); @@ -32,20 +36,24 @@ contract TempleLineOfCreditTest is Test { function setUp() public { collateralToken = new ERC20Mock("TempleToken", "Temple", admin, uint(500_000e18)); - collateralPrice = 970; // 0.97 + // collateralPrice = 9700; // 0.97 + collateralPrice = TempleLineOfCredit.TokenPrice.TPI; // 0.97 daiToken = new ERC20Mock("DAI Token", "DAI", admin, uint(500_000e18)); - daiPrice = 1000; // 1 USD + daiPrice = TempleLineOfCredit.TokenPrice.STABLE; // 1 USD daiMinCollateralizationRatio = 12000; daiTokenType = TempleLineOfCredit.TokenType.TRANSFER; daiInterestRateBps = 500; // 5% + oudRedeemer = new OudRedeemer(); + tlc = new TempleLineOfCredit( address(collateralToken), collateralPrice, - debtCollector + debtCollector, + address(oudRedeemer) ); tlc.addOperator(admin); @@ -53,7 +61,7 @@ contract TempleLineOfCreditTest is Test { function testInitalization() public { assertEq(address(tlc.collateralToken()), address(collateralToken)); - assertEq(tlc.collateralPrice(), collateralPrice); + assertEq(uint(tlc.collateralPrice()), uint(collateralPrice)); assertEq(tlc.debtCollector(), debtCollector); } @@ -74,14 +82,14 @@ contract TempleLineOfCreditTest is Test { function testSetCollateralPriceFailOnlyOperator() public { vm.expectRevert(abi.encodeWithSelector(Operators.OnlyOperators.selector, address(this))); - tlc.setCollateralPrice(100); + tlc.setCollateralPrice(TempleLineOfCredit.TokenPrice.STABLE); } function testSetCollateralPriceSuccess() public { - uint256 collateralPrice = 970; + TempleLineOfCredit.TokenPrice collateralPrice = TempleLineOfCredit.TokenPrice.STABLE; vm.prank(admin); tlc.setCollateralPrice(collateralPrice); - assertEq(tlc.collateralPrice(), collateralPrice); + assertEq(uint(tlc.collateralPrice()), uint(collateralPrice)); } function testSetDebtCollectorFailsOnlyOperator() public { @@ -103,9 +111,9 @@ contract TempleLineOfCreditTest is Test { function testAddDebtTokenSuccess() public { vm.prank(admin); tlc.addDebtToken(address(daiToken), daiTokenType, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); - (TempleLineOfCredit.TokenType tokenType, uint256 interestRateBps, uint256 tokenPrice, uint256 minCollateralizationRatio, bool allowed) = tlc.debtTokens(address(daiToken)); + (TempleLineOfCredit.TokenType tokenType, uint256 interestRateBps, TempleLineOfCredit.TokenPrice tokenPrice, uint256 minCollateralizationRatio, bool allowed) = tlc.debtTokens(address(daiToken)); assertEq(daiInterestRateBps, interestRateBps); - assertEq(daiPrice, tokenPrice); + assertEq(uint(daiPrice), uint(tokenPrice)); assertEq(daiMinCollateralizationRatio, minCollateralizationRatio); assertEq(allowed, true); } @@ -250,6 +258,7 @@ contract TempleLineOfCreditTest is Test { uint256 collateralAmount = uint(100_000e18); uint256 expectedMaxBorrowCapacity = uint(97_000e18) * uint(100) / uint(120); _postCollateral(alice, collateralAmount); + console.log(expectedMaxBorrowCapacity); assertEq(tlc.maxBorrowCapacity(address(daiToken), alice), expectedMaxBorrowCapacity); } @@ -291,107 +300,121 @@ contract TempleLineOfCreditTest is Test { assertEq(daiToken.balanceOf(alice), maxBorrowCapacity); } - // function _borrow(address _account, uint256 collateralAmount, uint256 borrowAmount) internal { - // _postCollateral(_account, collateralAmount); - // vm.prank(_account); - // tlc.borrow(borrowAmount); - // } + function _borrow(address _account, uint256 collateralAmount, uint256 borrowAmount) internal { + _postCollateral(_account, collateralAmount); + vm.prank(_account); + address[] memory debtTokens = new address[](1); + debtTokens[0] = address(daiToken); + uint256[] memory borrowAmounts = new uint256[](1); + borrowAmounts[0] = borrowAmount; + tlc.borrow(debtTokens, borrowAmounts); + } - // function testBorrowAccuresInterest(uint32 secondsElapsed) external { - // uint256 borrowAmount = uint(60_000e18); - // _borrow(alice, uint(100_000e18), borrowAmount); + function testBorrowAccuresInterest(uint32 secondsElapsed) external { + uint256 borrowAmount = uint(60_000e18); + _borrow(alice, uint(100_000e18), borrowAmount); - // uint256 borrowTimeStamp = block.timestamp; + uint256 borrowTimeStamp = block.timestamp; - // vm.warp(block.timestamp + secondsElapsed); - // uint256 secondsElapsed = block.timestamp - borrowTimeStamp; - // uint256 expectedTotalDebt = (borrowAmount) + ((borrowAmount * interestRateBps * secondsElapsed) / 10000 / 365 days); + vm.warp(block.timestamp + secondsElapsed); + uint256 secondsElapsed = block.timestamp - borrowTimeStamp; + uint256 expectedTotalDebt = (borrowAmount) + ((borrowAmount * daiInterestRateBps * secondsElapsed) / 10000 / 365 days); - // vm.startPrank(alice); - // assertEq(expectedTotalDebt, tlc.getTotalDebtAmount(alice)); - // vm.stopPrank(); - // } + vm.startPrank(alice); + assertEq(expectedTotalDebt, tlc.getTotalDebtAmount(address(daiToken), alice)); + vm.stopPrank(); + } - // function testRepayZero() external { - // uint256 borrowAmount = uint(60_000e18); - // uint256 repayAmount = uint(0); - // _borrow(alice, uint(100_000e18), borrowAmount); - // vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InvalidAmount.selector, repayAmount)); - // vm.startPrank(alice); - // tlc.repay(0); - // vm.stopPrank(); - // } + function testRepayZero() external { + uint256 borrowAmount = uint(60_000e18); + uint256 repayAmount = uint(0); + _borrow(alice, uint(100_000e18), borrowAmount); + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InvalidAmount.selector, repayAmount)); + vm.startPrank(alice); + address[] memory debtTokens = new address[](1); + debtTokens[0] = address(daiToken); + uint256[] memory repayAmounts = new uint256[](1); + repayAmounts[0] = repayAmount; + tlc.repay(debtTokens, repayAmounts); + vm.stopPrank(); + } - // function testRepayExceededBorrow() external { - // uint256 borrowAmount = uint(60_000e18); - // uint256 repayAmount = uint(61_000e18); - // _borrow(alice, uint(100_000e18), borrowAmount); - // vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.ExceededBorrowedAmount.selector, alice, borrowAmount, repayAmount)); - // vm.startPrank(alice); - // tlc.repay(repayAmount); - // vm.stopPrank(); - // } + function testRepayExceededBorrow() external { + uint256 borrowAmount = uint(60_000e18); + uint256 repayAmount = uint(61_000e18); + _borrow(alice, uint(100_000e18), borrowAmount); + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.ExceededBorrowedAmount.selector, alice, borrowAmount, repayAmount)); + vm.startPrank(alice); + address[] memory debtTokens = new address[](1); + debtTokens[0] = address(daiToken); + uint256[] memory repayAmounts = new uint256[](1); + repayAmounts[0] = repayAmount; + tlc.repay(debtTokens, repayAmounts); + vm.stopPrank(); + } - // function testRepaySuccess(uint256 repayAmount) external { - // uint256 borrowAmount = uint(60_000e18); - // uint256 repayAmount = uint(50_000e18); - // _borrow(alice, uint(100_000e18), borrowAmount); - // uint256 debtBalanceBefore = tlc.debtBalance(); - - // vm.startPrank(alice); - // debtToken.approve(address(tlc), repayAmount); - // tlc.repay(repayAmount); - // (, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); - // vm.stopPrank(); - - // assertEq(borrowAmount - repayAmount, aliceDebtAmount); - // assertEq(debtBalanceBefore + repayAmount, tlc.debtBalance()); - // assertEq(block.timestamp, aliceCreatedAt); - // } + function testRepaySuccessSingleToken(uint256 repayAmount) external { + uint256 borrowAmount = uint(60_000e18); + uint256 repayAmount = uint(50_000e18); + _borrow(alice, uint(100_000e18), borrowAmount); - // function testWithdrawExceedCollateralAmount() external { - // uint256 borrowAmount = uint(60_000e18); - // uint256 collateralAmount = uint(100_000e18); - // uint256 withdrawalAmount = uint(100_001e18); - // _borrow(alice, collateralAmount, borrowAmount); - // vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.ExceededCollateralAmonut.selector, alice, collateralAmount, withdrawalAmount)); + vm.startPrank(alice); + daiToken.approve(address(tlc), repayAmount); + address[] memory debtTokens = new address[](1); + debtTokens[0] = address(daiToken); + uint256[] memory repayAmounts = new uint256[](1); + repayAmounts[0] = repayAmount; + tlc.repay(debtTokens, repayAmounts); + TempleLineOfCredit.TokenPosition memory tp = tlc.getDebtAmount(address(daiToken), alice); + vm.stopPrank(); - // vm.startPrank(alice); - // tlc.withdrawCollateral(withdrawalAmount); - // vm.stopPrank(); - // } + assertEq(borrowAmount - repayAmount, tp.debtAmount); + assertEq(block.timestamp, tp.createdAt); + } - // function testWithdrawWillUnderCollaterlizeLoan() external { - // uint256 borrowAmount = uint(60_000e18); - // uint256 collateralAmount = uint(100_000e18); - // uint256 withdrawalAmount = uint(30_001e18); - // _borrow(alice, collateralAmount, borrowAmount); - // vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.WillUnderCollaterlize.selector, alice, withdrawalAmount)); + function testWithdrawExceedCollateralAmount() external { + uint256 borrowAmount = uint(60_000e18); + uint256 collateralAmount = uint(100_000e18); + uint256 withdrawalAmount = uint(100_001e18); + _borrow(alice, collateralAmount, borrowAmount); + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.ExceededCollateralAmonut.selector, alice, collateralAmount, withdrawalAmount)); - // vm.startPrank(alice); - // tlc.withdrawCollateral(withdrawalAmount); - // vm.stopPrank(); - // } + vm.startPrank(alice); + tlc.withdrawCollateral(withdrawalAmount); + vm.stopPrank(); + } - // function testWithdrawalSuccess() external { - // uint256 borrowAmount = uint(60_000e18); - // uint256 collateralAmount = uint(100_000e18); - // uint256 withdrawalAmount = uint(10_000e18); - // _borrow(alice, collateralAmount, borrowAmount); + function testWithdrawWillUnderCollaterlizeLoan() external { + uint256 borrowAmount = uint(60_000e18); + uint256 collateralAmount = uint(100_000e18); + uint256 withdrawalAmount = uint(30_001e18); + _borrow(alice, collateralAmount, borrowAmount); + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.WillUnderCollaterlize.selector, alice, withdrawalAmount)); - // uint256 collateralBalanceBefore = collateralToken.balanceOf(address(tlc)); - // uint256 aliceCollateralBalanceBefore = collateralToken.balanceOf(alice); + vm.startPrank(alice); + tlc.withdrawCollateral(withdrawalAmount); + vm.stopPrank(); + } - // vm.startPrank(alice); - // tlc.withdrawCollateral(withdrawalAmount); - // (uint256 aliceCollateralAmount,,) = tlc.positions(alice); - // vm.stopPrank(); + function testWithdrawalSuccess() external { + uint256 borrowAmount = uint(60_000e18); + uint256 collateralAmount = uint(100_000e18); + uint256 withdrawalAmount = uint(10_000e18); + _borrow(alice, collateralAmount, borrowAmount); - // assertEq(collateralBalanceBefore - withdrawalAmount, collateralToken.balanceOf(address(tlc))); - // assertEq(aliceCollateralAmount, collateralAmount - withdrawalAmount); - // assertEq(collateralToken.balanceOf(alice), aliceCollateralBalanceBefore + withdrawalAmount); - // } + uint256 collateralBalanceBefore = collateralToken.balanceOf(address(tlc)); + uint256 aliceCollateralBalanceBefore = collateralToken.balanceOf(alice); + + vm.startPrank(alice); + tlc.withdrawCollateral(withdrawalAmount); + (uint256 aliceCollateralAmount) = tlc.positions(alice); + vm.stopPrank(); + + assertEq(collateralBalanceBefore - withdrawalAmount, collateralToken.balanceOf(address(tlc))); + assertEq(aliceCollateralAmount, collateralAmount - withdrawalAmount); + assertEq(collateralToken.balanceOf(alice), aliceCollateralBalanceBefore + withdrawalAmount); + } // function testLiquidateSufficientCollateral() external { From 4ad33767e7423c82f8e658f129fe7d7fdc915507 Mon Sep 17 00:00:00 2001 From: smokey Date: Sun, 12 Feb 2023 16:22:13 -0500 Subject: [PATCH 13/15] burn instead of mint --- src/TempleLineOfCredit.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TempleLineOfCredit.sol b/src/TempleLineOfCredit.sol index 5be262d..9b3880c 100644 --- a/src/TempleLineOfCredit.sol +++ b/src/TempleLineOfCredit.sol @@ -297,7 +297,7 @@ contract TempleLineOfCredit is Ownable, Operators { function _maxBorrowCapacity(uint256 collateralAmount, TokenPrice debtPrice, uint256 minCollateralizationRatio) internal view returns (uint256) { (uint256 debtTokenPrice, uint256 debtPercision) = getDebtTokenPrice(debtPrice); (uint256 collateralTokenPrice, uint256 collateralPrecision) = getDebtTokenPrice(collateralPrice); - return collateralAmount * collateralTokenPrice * debtPercision * 10000 / debtTokenPrice / collateralPrecision /minCollateralizationRatio; + return collateralAmount * collateralTokenPrice * debtPercision * 10000 / debtTokenPrice / collateralPrecision / minCollateralizationRatio; } /** @@ -371,7 +371,7 @@ contract TempleLineOfCredit is Ownable, Operators { repayAmount ); } else { - IERC20MintBurn(debtToken).mint( + IERC20MintBurn(debtToken).burn( msg.sender, repayAmount ); From 22699e028116d7af232caa4062448c06c6034c17 Mon Sep 17 00:00:00 2001 From: smokey Date: Tue, 14 Feb 2023 21:51:35 -0500 Subject: [PATCH 14/15] add more tests --- src/TempleLineOfCredit.sol | 151 ++++++++++------- test/TempleLineOfCredit.t.sol | 303 ++++++++++++++++++++++++---------- 2 files changed, 310 insertions(+), 144 deletions(-) diff --git a/src/TempleLineOfCredit.sol b/src/TempleLineOfCredit.sol index 9b3880c..f09e417 100644 --- a/src/TempleLineOfCredit.sol +++ b/src/TempleLineOfCredit.sol @@ -32,7 +32,7 @@ contract TempleLineOfCredit is Ownable, Operators { /// @notice debt position on a specific token struct TokenPosition { uint256 debtAmount; - uint256 createdAt; + uint256 lastUpdatedAt; } enum TokenType { @@ -86,22 +86,23 @@ contract TempleLineOfCredit is Ownable, Operators { event SetDebtCollector(address debtCollector); event AddDebtToken(address token); event RemoveDebtToken(address token); - event DepositDebt(address debtToken, uint256 amount); - event RemoveDebt(address debtToken, uint256 amount); + event DepositReserve(address debtToken, uint256 amount); + event RemoveReserve(address debtToken, uint256 amount); event PostCollateral(address account, uint256 amount); event Borrow(address account, address debtToken, uint256 amount); event Repay(address account, uint256 amount); event Withdraw(address account, uint256 amount); - // event Liquidated(address account, uint256 debtAmount, uint256 collateralSeized); + event Liquidated(address account, uint256 debtAmount, uint256 collateralSeized); error InvalidAmount(uint256 amount); + error InvalidArrayLength(); error Unsupported(address token); + error InvalidTokenPrice(TokenPrice tokenPrice); error InsufficentCollateral(address debtToken, uint256 maxCapacity, uint256 debtAmount); - // error InsufficentDebtToken(uint256 debtTokenBalance, uint256 borrowAmount); 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); + error OverCollaterilized(address account); constructor( address _collateralToken, @@ -179,12 +180,12 @@ contract TempleLineOfCredit is Ownable, Operators { TokenPosition storage userPosition = positions[account].tokenPosition[debtToken]; uint256 totalDebt = userPosition.debtAmount; - uint256 secondsElapsed = block.timestamp - userPosition.createdAt; + uint256 secondsElapsed = block.timestamp - userPosition.lastUpdatedAt; totalDebt += (totalDebt * debtTokenInfo.interestRateBps * secondsElapsed) / 10000 / 365 days; return totalDebt; } - function getDebtTokenPrice(TokenPrice _price) public view returns (uint256 price, uint256 precision) { + function getTokenPrice(TokenPrice _price) public view returns (uint256 price, uint256 precision) { if (_price == TokenPrice.STABLE) { return (10000, 10000); @@ -192,14 +193,14 @@ contract TempleLineOfCredit is Ownable, Operators { // Get Token Price from redemeer uint256 tpiPrice = IOudRedeemer(oudRedeemer).treasuryPriceIndex(); return (tpiPrice, 10000); - } + } } /** * @dev Allows operator to depoist debt tokens * @param amount is the amount to deposit */ - function depositDebt(address debtToken, address account, uint256 amount) external onlyOperators{ + function depositReserve(address debtToken, address account, uint256 amount) external onlyOperators{ if (amount == 0) { revert InvalidAmount(amount); } @@ -209,14 +210,14 @@ contract TempleLineOfCredit is Ownable, Operators { address(this), amount ); - emit DepositDebt(debtToken, amount); + emit DepositReserve(debtToken, amount); } /** * @dev Allows operator to remove debt token * @param amount is the amount to remove */ - function removeDebt(address debtToken, address account, uint256 amount) external onlyOperators{ + function removeReserve(address debtToken, address account, uint256 amount) external onlyOperators{ if (amount == 0) { revert InvalidAmount(amount); } @@ -225,7 +226,7 @@ contract TempleLineOfCredit is Ownable, Operators { msg.sender, amount ); - emit RemoveDebt(debtToken, amount); + emit RemoveReserve(debtToken, amount); } /** @@ -245,17 +246,26 @@ contract TempleLineOfCredit is Ownable, Operators { function borrow(address[] memory tokens, uint256[] memory borrowAmounts) external { - for (uint256 i =0; i < tokens.length; i++) { + if (tokens.length != borrowAmounts.length) { + revert InvalidArrayLength(); + } + + address debtToken; + uint256 borrowAmount; + DebtToken memory debtTokenInfo; + + for (uint256 i; i < tokens.length; ++i) { - address debtToken = tokens[i]; - uint256 borrowAmount = borrowAmounts[i]; + debtToken = tokens[i]; + borrowAmount = borrowAmounts[i]; if (borrowAmount == 0) revert InvalidAmount(borrowAmount); - DebtToken memory debtTokenInfo = debtTokens[debtToken]; + debtTokenInfo = debtTokens[debtToken]; if (!debtTokenInfo.isAllowed) revert Unsupported(debtToken); - uint256 debtAmount = positions[msg.sender].tokenPosition[debtToken].debtAmount; + TokenPosition storage userPosition = positions[msg.sender].tokenPosition[debtToken]; + uint256 debtAmount = userPosition.debtAmount; if (debtAmount != 0) { debtAmount = getTotalDebtAmount(debtToken, msg.sender); } @@ -267,10 +277,8 @@ contract TempleLineOfCredit is Ownable, Operators { revert InsufficentCollateral(debtToken, borrowCapacity, debtAmount); } - TokenPosition storage userPosition = positions[msg.sender].tokenPosition[debtToken]; - userPosition.debtAmount = debtAmount; - userPosition.createdAt = block.timestamp; + userPosition.lastUpdatedAt = block.timestamp; if (debtTokenInfo.tokenType == TokenType.TRANSFER){ IERC20(debtToken).safeTransfer( @@ -295,9 +303,9 @@ contract TempleLineOfCredit is Ownable, Operators { function _maxBorrowCapacity(uint256 collateralAmount, TokenPrice debtPrice, uint256 minCollateralizationRatio) internal view returns (uint256) { - (uint256 debtTokenPrice, uint256 debtPercision) = getDebtTokenPrice(debtPrice); - (uint256 collateralTokenPrice, uint256 collateralPrecision) = getDebtTokenPrice(collateralPrice); - return collateralAmount * collateralTokenPrice * debtPercision * 10000 / debtTokenPrice / collateralPrecision / minCollateralizationRatio; + (uint256 debtTokenPrice, uint256 debtPrecision) = getTokenPrice(debtPrice); + (uint256 collateralTokenPrice, uint256 collateralPrecision) = getTokenPrice(collateralPrice); + return collateralAmount * collateralTokenPrice * debtPrecision * 10000 / debtTokenPrice / collateralPrecision / minCollateralizationRatio; } /** @@ -311,7 +319,7 @@ contract TempleLineOfCredit is Ownable, Operators { revert ExceededCollateralAmonut(msg.sender, collateralAmount, withdrawalAmount); } - for (uint256 i =0; i < debtTokenList.length; i++) { + for (uint256 i; i < debtTokenList.length; ++i) { address debtToken = debtTokenList[i]; DebtToken memory debtTokenInfo = debtTokens[debtToken]; @@ -340,17 +348,23 @@ contract TempleLineOfCredit is Ownable, Operators { */ function repay(address[] memory tokens, uint256[] memory repayAmounts) external { - for (uint256 i =0; i < tokens.length; i++) { + address debtToken; + uint256 repayAmount; + DebtToken memory debtTokenInfo; + + for (uint256 i; i < tokens.length; ++i) { - address debtToken = tokens[i]; - uint256 repayAmount = repayAmounts[i]; + debtToken = tokens[i]; + repayAmount = repayAmounts[i]; if (repayAmount == 0) revert InvalidAmount(repayAmount); - DebtToken memory debtTokenInfo = debtTokens[debtToken]; + debtTokenInfo = debtTokens[debtToken]; if (!debtTokenInfo.isAllowed) revert Unsupported(debtToken); - uint256 debtAmount = positions[msg.sender].tokenPosition[debtToken].debtAmount; + + TokenPosition storage userPosition = positions[msg.sender].tokenPosition[debtToken]; + uint256 debtAmount = userPosition.debtAmount; if (debtAmount != 0) { debtAmount = getTotalDebtAmount(debtToken, msg.sender); } @@ -359,10 +373,8 @@ contract TempleLineOfCredit is Ownable, Operators { revert ExceededBorrowedAmount(msg.sender, debtAmount, repayAmount); } - TokenPosition storage userPosition = positions[msg.sender].tokenPosition[debtToken]; - userPosition.debtAmount = debtAmount - repayAmount; - userPosition.createdAt = block.timestamp; + userPosition.lastUpdatedAt = block.timestamp; if (debtTokenInfo.tokenType == TokenType.TRANSFER){ IERC20(debtToken).safeTransferFrom( @@ -380,48 +392,67 @@ contract TempleLineOfCredit is Ownable, Operators { } } -// /** -// * @dev Allows operator to liquidate debtors position -// * @param debtor the account to liquidate -// */ -// function liquidate(address debtor) external onlyOperators { -// Position storage position = positions[debtor]; -// uint256 totalDebtOwed = getTotalDebtAmount(debtor); + /** + * @dev Allows operator to liquidate debtors position + * @param debtor the account to liquidate + */ + function liquidate(address debtor, address debtToken) external onlyOperators { + + DebtToken memory debtTokenInfo = debtTokens[debtToken]; + if (!debtTokens[debtToken].isAllowed) revert Unsupported(debtToken); + + TokenPosition storage userPosition = positions[debtor].tokenPosition[debtToken]; + uint256 totalDebtOwed = userPosition.debtAmount; + uint256 collateralAmount = positions[debtor].collateralAmount; + if (totalDebtOwed != 0) { + totalDebtOwed = getTotalDebtAmount(debtToken, debtor); + } -// if (_getCurrentCollaterilizationRatio(position.collateralAmount, position.debtAmount, totalDebtOwed) >= minCollateralizationRatio) { -// revert OverCollaterilized(debtor); -// } + if (_getCurrentCollaterilizationRatio(collateralAmount, userPosition.debtAmount, debtTokenInfo.tokenPrice, totalDebtOwed) >= debtTokenInfo.minCollateralizationRatio) { + revert OverCollaterilized(debtor); + } -// uint256 collateralSeized = (totalDebtOwed * debtPrice) / collateralPrice; + (uint256 debtTokenPrice, uint256 debtPrecision) = getTokenPrice(debtTokenInfo.tokenPrice); + (uint256 collateralTokenPrice, uint256 collateralPrecision) = getTokenPrice(collateralPrice); -// if (collateralSeized > position.collateralAmount) { -// collateralSeized = position.collateralAmount; -// } -// position.collateralAmount -= collateralSeized; -// position.debtAmount = 0; -// position.createdAt = 0; + uint256 collateralSeized = (totalDebtOwed * debtTokenPrice * collateralPrecision) / collateralTokenPrice / debtPrecision; -// collateralToken.safeTransfer( -// debtCollector, -// collateralSeized -// ); + if (collateralSeized > collateralAmount) { + collateralSeized = collateralAmount; + } -// emit Liquidated(debtor, totalDebtOwed, collateralSeized); -// } + positions[debtor].collateralAmount -= collateralSeized; + + // Wipe out all of users other token debts + address debtToken; + for (uint256 i; i < debtTokenList.length; ++i) { + debtToken = debtTokenList[i]; + TokenPosition storage userPosition = positions[debtor].tokenPosition[debtToken]; + userPosition.debtAmount = 0; + userPosition.lastUpdatedAt = block.timestamp; + } + + collateralToken.safeTransfer( + debtCollector, + collateralSeized + ); + + emit Liquidated(debtor, totalDebtOwed, collateralSeized); + } function getCurrentCollaterilizationRatio(address debtToken, address account) public view returns(uint256) { DebtToken memory debtTokenInfo = debtTokens[debtToken]; - _getCurrentCollaterilizationRatio(positions[account].collateralAmount, positions[msg.sender].tokenPosition[debtToken].debtAmount, debtTokenInfo.tokenPrice, getTotalDebtAmount(debtToken, account)); + return _getCurrentCollaterilizationRatio(positions[account].collateralAmount, positions[account].tokenPosition[debtToken].debtAmount, debtTokenInfo.tokenPrice, getTotalDebtAmount(debtToken, account)); } function _getCurrentCollaterilizationRatio(uint256 collateralAmount, uint256 debtAmount, TokenPrice debtPrice, uint256 totalDebtAmount) public view returns(uint256) { if (debtAmount == 0 ) { return 0; } else { - (uint256 debtTokenPrice, uint256 debtPercision) = getDebtTokenPrice(debtPrice); - (uint256 collateralTokenPrice, uint256 collateralPrecision) = getDebtTokenPrice(collateralPrice); - return ((collateralAmount * collateralTokenPrice * debtPercision * 10000) / totalDebtAmount / debtTokenPrice / collateralPrecision); + (uint256 debtTokenPrice, uint256 debtPrecision) = getTokenPrice(debtPrice); + (uint256 collateralTokenPrice, uint256 collateralPrecision) = getTokenPrice(collateralPrice); + return ((collateralAmount * collateralTokenPrice * debtPrecision * 10000) / totalDebtAmount / debtTokenPrice / collateralPrecision); } } diff --git a/test/TempleLineOfCredit.t.sol b/test/TempleLineOfCredit.t.sol index 5096f94..547a70f 100644 --- a/test/TempleLineOfCredit.t.sol +++ b/test/TempleLineOfCredit.t.sol @@ -24,6 +24,14 @@ contract TempleLineOfCreditTest is Test { TempleLineOfCredit.TokenType public daiTokenType; + ERC20Mock public oudToken; + TempleLineOfCredit.TokenPrice public oudPrice; + uint256 public oudMinCollateralizationRatio; + uint256 public oudInterestRateBps; + TempleLineOfCredit.TokenType public oudTokenType; + + + OudRedeemer public oudRedeemer; @@ -48,7 +56,6 @@ contract TempleLineOfCreditTest is Test { oudRedeemer = new OudRedeemer(); - tlc = new TempleLineOfCredit( address(collateralToken), collateralPrice, @@ -56,6 +63,13 @@ contract TempleLineOfCreditTest is Test { address(oudRedeemer) ); + + oudToken = new ERC20Mock("OUD Token", "OUD", address(tlc), uint(500_000e18)); + oudPrice = TempleLineOfCredit.TokenPrice.TPI; + oudMinCollateralizationRatio = 11000; + oudTokenType = TempleLineOfCredit.TokenType.MINT; + oudInterestRateBps = 500; // 5% + tlc.addOperator(admin); } @@ -129,7 +143,6 @@ contract TempleLineOfCreditTest is Test { tlc.removeDebtToken(address(daiToken)); } - function testRemoveDebtTokenSuccess() public { vm.prank(admin); tlc.addDebtToken(address(daiToken), daiTokenType, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); @@ -160,15 +173,21 @@ contract TempleLineOfCreditTest is Test { assertEq(minCollateralizationRatio, newCollaterilizationRatio); } - function testDepositDebtExpectRevertOnlyOperator() public { + function testDepositExpectRevertOnlyOperator() public { vm.expectRevert(abi.encodeWithSelector(Operators.OnlyOperators.selector, address(this))); - tlc.depositDebt(address(daiToken), alice, uint(100_000e18)); + tlc.depositReserve(address(daiToken), alice, uint(100_000e18)); } - function testDepositDebtExpectFailUnSupprotedTokenAddress() public { + function testDepositReserveInvalidAmount() public { + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InvalidAmount.selector, 0)); + vm.prank(admin); + tlc.depositReserve(address(daiToken), alice, 0); + } + + function testDepositReserveExpectFailUnSupprotedTokenAddress() public { vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.Unsupported.selector, address(daiToken))); vm.prank(admin); - tlc.depositDebt(address(daiToken), alice, uint(100_000e18)); + tlc.depositReserve(address(daiToken), alice, uint(100_000e18)); } function testDepositDebtExpectFailIncorrectTokenType() public { @@ -177,51 +196,62 @@ contract TempleLineOfCreditTest is Test { vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.Unsupported.selector, address(daiToken))); vm.prank(admin); - tlc.depositDebt(address(daiToken), alice, uint(100_000e18)); + tlc.depositReserve(address(daiToken), alice, uint(100_000e18)); } - function testdepositDebtSucess() public { + function testDepositDebtSucess() public { vm.startPrank(admin); tlc.addDebtToken(address(daiToken), TempleLineOfCredit.TokenType.TRANSFER, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); uint256 depositAmount = uint256(100_000e18); daiToken.approve(address(tlc), depositAmount); - tlc.depositDebt(address(daiToken), admin, depositAmount); + tlc.depositReserve(address(daiToken), admin, depositAmount); assertEq(daiToken.balanceOf(address(tlc)), depositAmount); } - function testRemoveDebtExpectRevertOnlyOperator() public { + function testRemoveDebtInvalidAmount() public { + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InvalidAmount.selector, 0)); + vm.prank(admin); + tlc.removeReserve(address(daiToken), alice, 0); + } + + function testRemoveReserveExpectRevertOnlyOperator() public { vm.expectRevert(abi.encodeWithSelector(Operators.OnlyOperators.selector, address(this))); - tlc.removeDebt(address(daiToken), alice, uint(100_000e18)); + tlc.removeReserve(address(daiToken), alice, uint(100_000e18)); } - function testRemoveDebtExpectFailUnSupprotedTokenAddress() public { + function testRemoveReserveExpectFailUnSupprotedTokenAddress() public { vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.Unsupported.selector, address(daiToken))); vm.prank(admin); - tlc.removeDebt(address(daiToken), alice, uint(100_000e18)); + tlc.removeReserve(address(daiToken), alice, uint(100_000e18)); } - function testRemoveDebtSucess() public { + function testRemoveReserveSucess() public { vm.startPrank(admin); tlc.addDebtToken(address(daiToken), TempleLineOfCredit.TokenType.TRANSFER, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); uint256 depositAmount = uint256(100_000e18); daiToken.approve(address(tlc), depositAmount); - tlc.depositDebt(address(daiToken), admin, depositAmount); + tlc.depositReserve(address(daiToken), admin, depositAmount); uint256 priorDebtBalance = daiToken.balanceOf(address(tlc)); uint256 removeAmount = uint256(55_000e18); - tlc.removeDebt(address(daiToken), admin, removeAmount); + tlc.removeReserve(address(daiToken), admin, removeAmount); assertEq(daiToken.balanceOf(address(tlc)), priorDebtBalance - removeAmount); } - function _initDeposit(uint256 depositAmount) internal { + function _initDeposit(uint256 daiDepositAmount) internal { vm.startPrank(admin); + + // deposit DAI tlc.addDebtToken(address(daiToken), TempleLineOfCredit.TokenType.TRANSFER, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); - daiToken.approve(address(tlc), depositAmount); - tlc.depositDebt(address(daiToken), admin, depositAmount); + daiToken.approve(address(tlc), daiDepositAmount); + tlc.depositReserve(address(daiToken), admin, daiDepositAmount); + + // support OUD + tlc.addDebtToken(address(oudToken), TempleLineOfCredit.TokenType.MINT, oudInterestRateBps, oudPrice, oudMinCollateralizationRatio); vm.stopPrank(); } @@ -258,7 +288,6 @@ contract TempleLineOfCreditTest is Test { uint256 collateralAmount = uint(100_000e18); uint256 expectedMaxBorrowCapacity = uint(97_000e18) * uint(100) / uint(120); _postCollateral(alice, collateralAmount); - console.log(expectedMaxBorrowCapacity); assertEq(tlc.maxBorrowCapacity(address(daiToken), alice), expectedMaxBorrowCapacity); } @@ -278,58 +307,119 @@ contract TempleLineOfCreditTest is Test { tlc.borrow(debtTokens, borrowAmounts); } - function testBorrowPasses() external { + + function testBorrowFailInvalidArrayLength() external { uint256 collateralAmount = uint(100_000e18); _postCollateral(alice, collateralAmount); - uint256 tlcDebtBalance = daiToken.balanceOf(address(tlc)); - uint256 maxBorrowCapacity = tlc.maxBorrowCapacity(address(daiToken), alice); + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InvalidArrayLength.selector)); vm.prank(alice); - address[] memory debtTokens = new address[](1); + address[] memory debtTokens = new address[](2); debtTokens[0] = address(daiToken); + debtTokens[1] = address(oudToken); uint256[] memory borrowAmounts = new uint256[](1); - borrowAmounts[0] = maxBorrowCapacity; + borrowAmounts[0] = 100_100; tlc.borrow(debtTokens, borrowAmounts); - - (uint256 aliceCollateralAmount ) = tlc.positions(alice); - TempleLineOfCredit.TokenPosition memory tp = tlc.getDebtAmount(address(daiToken), alice); - - assertEq(aliceCollateralAmount, collateralAmount); - assertEq(tp.debtAmount, maxBorrowCapacity); - assertEq(tp.createdAt, block.timestamp); - assertEq(daiToken.balanceOf(alice), maxBorrowCapacity); } - function _borrow(address _account, uint256 collateralAmount, uint256 borrowAmount) internal { - _postCollateral(_account, collateralAmount); - vm.prank(_account); - address[] memory debtTokens = new address[](1); + function testBorrowTwoAssetsSucess() external { + uint256 collateralAmount = uint(100_000e18); + _postCollateral(alice, collateralAmount); + uint256 tlcDebtBalance = daiToken.balanceOf(address(tlc)); + uint256 maxBorrowCapacityDAI = tlc.maxBorrowCapacity(address(daiToken), alice); + uint256 maxBorrowCapacityOUD = tlc.maxBorrowCapacity(address(oudToken), alice); + + vm.prank(alice); + address[] memory debtTokens = new address[](2); debtTokens[0] = address(daiToken); - uint256[] memory borrowAmounts = new uint256[](1); - borrowAmounts[0] = borrowAmount; + debtTokens[1] = address(oudToken); + uint256[] memory borrowAmounts = new uint256[](2); + borrowAmounts[0] = maxBorrowCapacityDAI; + borrowAmounts[1] = maxBorrowCapacityOUD; + tlc.borrow(debtTokens, borrowAmounts); + + (uint256 aliceCollateralAmount ) = tlc.positions(alice); + + TempleLineOfCredit.TokenPosition memory tpDAI = tlc.getDebtAmount(address(daiToken), alice); + assertEq(aliceCollateralAmount, collateralAmount); + assertEq(tpDAI.debtAmount, maxBorrowCapacityDAI); + assertEq(tpDAI.lastUpdatedAt, block.timestamp); + assertEq(daiToken.balanceOf(alice), maxBorrowCapacityDAI); + + TempleLineOfCredit.TokenPosition memory tpOUD = tlc.getDebtAmount(address(oudToken), alice); + assertEq(tpOUD.debtAmount, maxBorrowCapacityOUD); + assertEq(tpOUD.lastUpdatedAt, block.timestamp); + assertEq(oudToken.balanceOf(alice), maxBorrowCapacityOUD); } + function testBorrowAccuresInterest(uint32 secondsElapsed) external { uint256 borrowAmount = uint(60_000e18); - _borrow(alice, uint(100_000e18), borrowAmount); + _borrow(alice, uint(100_000e18), borrowAmount, borrowAmount); uint256 borrowTimeStamp = block.timestamp; vm.warp(block.timestamp + secondsElapsed); uint256 secondsElapsed = block.timestamp - borrowTimeStamp; - uint256 expectedTotalDebt = (borrowAmount) + ((borrowAmount * daiInterestRateBps * secondsElapsed) / 10000 / 365 days); + uint256 expectedTotalDebtDAI = (borrowAmount) + ((borrowAmount * daiInterestRateBps * secondsElapsed) / 10000 / 365 days); + uint256 expectedTotalDebtOUD = (borrowAmount) + ((borrowAmount * oudInterestRateBps * secondsElapsed) / 10000 / 365 days); + vm.startPrank(alice); - assertEq(expectedTotalDebt, tlc.getTotalDebtAmount(address(daiToken), alice)); + assertEq(expectedTotalDebtDAI, tlc.getTotalDebtAmount(address(daiToken), alice)); + assertEq(expectedTotalDebtOUD, tlc.getTotalDebtAmount(address(oudToken), alice)); vm.stopPrank(); } + function _borrow(address _account, uint256 collateralAmount, uint256 daiBorrowAmount, uint256 oudBorrowAmount) internal { + _postCollateral(_account, collateralAmount); + vm.prank(_account); + address[] memory debtTokens = new address[](2); + debtTokens[0] = address(daiToken); + debtTokens[1] = address(oudToken); + uint256[] memory borrowAmounts = new uint256[](2); + borrowAmounts[0] = daiBorrowAmount; + borrowAmounts[1] = oudBorrowAmount; + + tlc.borrow(debtTokens, borrowAmounts); + } + + + function testBorrowAlreadyBorrowedSucess() external { + uint256 borrowDAIAmountFirst = uint(30_000e18); + uint256 borrowOUDAmountFirst = uint(20_000e18); + + _borrow(alice, uint(100_000e18), borrowDAIAmountFirst, borrowOUDAmountFirst); + + uint secondsElapsed = 200 days; + vm.warp(block.timestamp + secondsElapsed); + + uint256 borrowDAIAmountSecond = uint(30_000e18); + uint256 borrowOUDAmountSecond = uint(20_000e18); + + _borrow(alice, uint(100_000e18), borrowDAIAmountSecond, borrowOUDAmountSecond); + + + TempleLineOfCredit.TokenPosition memory tpDAI = tlc.getDebtAmount(address(daiToken), alice); + /// First Principle accured principle second borrow amount + assertEq(tpDAI.debtAmount, borrowDAIAmountFirst + ((borrowDAIAmountFirst * daiInterestRateBps * secondsElapsed) / 10000 / 365 days) + borrowDAIAmountSecond ); + assertEq(tpDAI.lastUpdatedAt, block.timestamp); + assertEq(daiToken.balanceOf(alice), borrowDAIAmountFirst + borrowDAIAmountSecond); + + + + TempleLineOfCredit.TokenPosition memory tpOUD = tlc.getDebtAmount(address(oudToken), alice); + /// First Principle accured principle second borrow amount + assertEq(tpOUD.debtAmount, borrowOUDAmountFirst + ((borrowOUDAmountFirst * oudInterestRateBps * secondsElapsed) / 10000 / 365 days) + borrowOUDAmountSecond ); + assertEq(tpOUD.lastUpdatedAt, block.timestamp); + assertEq(oudToken.balanceOf(alice), borrowOUDAmountFirst + borrowOUDAmountSecond); + } function testRepayZero() external { uint256 borrowAmount = uint(60_000e18); uint256 repayAmount = uint(0); - _borrow(alice, uint(100_000e18), borrowAmount); + _borrow(alice, uint(100_000e18), borrowAmount, borrowAmount); vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InvalidAmount.selector, repayAmount)); vm.startPrank(alice); address[] memory debtTokens = new address[](1); @@ -343,7 +433,7 @@ contract TempleLineOfCreditTest is Test { function testRepayExceededBorrow() external { uint256 borrowAmount = uint(60_000e18); uint256 repayAmount = uint(61_000e18); - _borrow(alice, uint(100_000e18), borrowAmount); + _borrow(alice, uint(100_000e18), borrowAmount, borrowAmount); vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.ExceededBorrowedAmount.selector, alice, borrowAmount, repayAmount)); vm.startPrank(alice); address[] memory debtTokens = new address[](1); @@ -354,30 +444,40 @@ contract TempleLineOfCreditTest is Test { vm.stopPrank(); } - function testRepaySuccessSingleToken(uint256 repayAmount) external { - uint256 borrowAmount = uint(60_000e18); - uint256 repayAmount = uint(50_000e18); - _borrow(alice, uint(100_000e18), borrowAmount); + function testRepaySuccessMultipleTokens() external { + uint256 borrowDAIAmount = uint(60_000e18); + uint256 borrowOUDAmount = uint(60_000e18); + uint256 repayDAIAmount = uint(50_000e18); + uint256 repayOUDAmount = uint(50_000e18); + + _borrow(alice, uint(100_000e18), borrowDAIAmount, borrowOUDAmount); vm.startPrank(alice); - daiToken.approve(address(tlc), repayAmount); - address[] memory debtTokens = new address[](1); + daiToken.approve(address(tlc), repayDAIAmount); + address[] memory debtTokens = new address[](2); debtTokens[0] = address(daiToken); - uint256[] memory repayAmounts = new uint256[](1); - repayAmounts[0] = repayAmount; + debtTokens[1] = address(oudToken); + uint256[] memory repayAmounts = new uint256[](2); + repayAmounts[0] = repayDAIAmount; + repayAmounts[1] = repayOUDAmount; tlc.repay(debtTokens, repayAmounts); - TempleLineOfCredit.TokenPosition memory tp = tlc.getDebtAmount(address(daiToken), alice); vm.stopPrank(); - assertEq(borrowAmount - repayAmount, tp.debtAmount); - assertEq(block.timestamp, tp.createdAt); + TempleLineOfCredit.TokenPosition memory tpDAI = tlc.getDebtAmount(address(daiToken), alice); + assertEq(borrowDAIAmount - repayDAIAmount, tpDAI.debtAmount); + assertEq(block.timestamp, tpDAI.lastUpdatedAt); + + TempleLineOfCredit.TokenPosition memory tpOUD = tlc.getDebtAmount(address(oudToken), alice); + assertEq(borrowOUDAmount - repayOUDAmount, tpOUD.debtAmount); + assertEq(block.timestamp, tpOUD.lastUpdatedAt); + } function testWithdrawExceedCollateralAmount() external { uint256 borrowAmount = uint(60_000e18); uint256 collateralAmount = uint(100_000e18); uint256 withdrawalAmount = uint(100_001e18); - _borrow(alice, collateralAmount, borrowAmount); + _borrow(alice, collateralAmount, borrowAmount, borrowAmount); vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.ExceededCollateralAmonut.selector, alice, collateralAmount, withdrawalAmount)); vm.startPrank(alice); @@ -389,7 +489,7 @@ contract TempleLineOfCreditTest is Test { uint256 borrowAmount = uint(60_000e18); uint256 collateralAmount = uint(100_000e18); uint256 withdrawalAmount = uint(30_001e18); - _borrow(alice, collateralAmount, borrowAmount); + _borrow(alice, collateralAmount, borrowAmount, borrowAmount); vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.WillUnderCollaterlize.selector, alice, withdrawalAmount)); vm.startPrank(alice); @@ -401,7 +501,7 @@ contract TempleLineOfCreditTest is Test { uint256 borrowAmount = uint(60_000e18); uint256 collateralAmount = uint(100_000e18); uint256 withdrawalAmount = uint(10_000e18); - _borrow(alice, collateralAmount, borrowAmount); + _borrow(alice, collateralAmount, borrowAmount, borrowAmount); uint256 collateralBalanceBefore = collateralToken.balanceOf(address(tlc)); uint256 aliceCollateralBalanceBefore = collateralToken.balanceOf(alice); @@ -416,31 +516,66 @@ contract TempleLineOfCreditTest is Test { assertEq(collateralToken.balanceOf(alice), aliceCollateralBalanceBefore + withdrawalAmount); } + function testLiquidateSufficientCollateral() external { + uint256 collateralAmount = uint(100_000e18); + uint256 borrowAmount = uint(30_000e18); + _borrow(alice, collateralAmount, borrowAmount, borrowAmount); + + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.OverCollaterilized.selector, alice)); + + vm.prank(admin); + tlc.liquidate(alice, address(daiToken)); + } + + function testLiquidateUnderWaterPositionSucessfully() external { + uint256 collateralAmount = uint(100_000e18); + uint256 borrowDAIAmount = uint(70_000e18); + uint256 borrowOUDAmount = uint(30_000e18); + _borrow(alice, collateralAmount, borrowDAIAmount, borrowOUDAmount); + vm.warp(block.timestamp + 1800 days); + + uint256 totalDebt = tlc.getTotalDebtAmount(address(daiToken), alice); + assertTrue(tlc.getCurrentCollaterilizationRatio(address(daiToken), alice) < daiMinCollateralizationRatio); // Position in bad debt + assertFalse(tlc.getCurrentCollaterilizationRatio(address(oudToken), alice) < oudMinCollateralizationRatio); // Position in good debt + vm.prank(admin); + tlc.liquidate(alice, address(daiToken)); + + (uint256 aliceCollateralAmount) = tlc.positions(alice); + (uint256 debtTokenPrice, uint256 debtPrecision) = tlc.getTokenPrice(daiPrice); + (uint256 collateralTokenPrice, uint256 collateralPrecision) = tlc.getTokenPrice(collateralPrice); + assertEq(collateralAmount - (totalDebt * debtTokenPrice * collateralPrecision / collateralTokenPrice / debtPrecision), aliceCollateralAmount); + + TempleLineOfCredit.TokenPosition memory tpDAI = tlc.getDebtAmount(address(daiToken), alice); + assertEq(0, tpDAI.debtAmount); + assertEq(block.timestamp, tpDAI.lastUpdatedAt); + + TempleLineOfCredit.TokenPosition memory tpOUD = tlc.getDebtAmount(address(oudToken), alice); + assertEq(0, tpOUD.debtAmount); + assertEq(block.timestamp, tpOUD.lastUpdatedAt); + } + + function testLiquidateUnderWaterPositionCollateralExceedAmountThatCanBeSiezedSucessfully() external { + uint256 collateralAmount = uint(100_000e18); + uint256 borrowDAIAmount = uint(70_000e18); + uint256 borrowOUDAmount = uint(30_000e18); + _borrow(alice, collateralAmount, borrowDAIAmount, borrowOUDAmount); + vm.warp(block.timestamp + 18000 days); + + uint256 totalDebt = tlc.getTotalDebtAmount(address(daiToken), alice); + assertTrue(tlc.getCurrentCollaterilizationRatio(address(daiToken), alice) < daiMinCollateralizationRatio); // Position in bad debt + assertTrue(tlc.getCurrentCollaterilizationRatio(address(oudToken), alice) < oudMinCollateralizationRatio); // Position in good debt + vm.prank(admin); + tlc.liquidate(alice, address(daiToken)); + + (uint256 aliceCollateralAmount) = tlc.positions(alice); + assertEq(0, aliceCollateralAmount); - // function testLiquidateSufficientCollateral() external { - // uint256 borrowAmount = uint(70_000e18); - // uint256 collateralAmount = uint(100_000e18); - // _borrow(alice, collateralAmount, borrowAmount); - // vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.OverCollaterilized.selector, alice)); - - // vm.prank(admin); - // tlc.liquidate(alice); - // } - - // function testLiquidateUnderWaterPositionSucessfully() external { - // uint256 borrowAmount = uint(70_000e18); - // uint256 collateralAmount = uint(100_000e18); - // _borrow(alice, collateralAmount, borrowAmount); - // vm.warp(block.timestamp + 1180 days); - // uint256 totalDebt = tlc.getTotalDebtAmount(alice); - // assertTrue(tlc.getCurrentCollaterilizationRatio(alice) < minCollateralizationRatio); - // vm.prank(admin); - // tlc.liquidate(alice); - - // (uint256 aliceCollateralAmount, uint256 aliceDebtAmount, uint256 aliceCreatedAt) = tlc.positions(alice); - // assertEq(uint256(0), aliceDebtAmount); - // assertEq(collateralAmount - (totalDebt * debtPrice / collateralPrice), aliceCollateralAmount); - // assertEq(uint256(0), aliceCreatedAt); - // assertEq((totalDebt * debtPrice / collateralPrice), collateralToken.balanceOf(debtCollector)); - // } + TempleLineOfCredit.TokenPosition memory tpDAI = tlc.getDebtAmount(address(daiToken), alice); + assertEq(0, tpDAI.debtAmount); + assertEq(block.timestamp, tpDAI.lastUpdatedAt); + + TempleLineOfCredit.TokenPosition memory tpOUD = tlc.getDebtAmount(address(oudToken), alice); + assertEq(0, tpOUD.debtAmount); + assertEq(block.timestamp, tpOUD.lastUpdatedAt); + } } From a954dff4d003af3aeb9a37e4f22035512c80fe64 Mon Sep 17 00:00:00 2001 From: smokey Date: Wed, 15 Feb 2023 19:42:25 -0500 Subject: [PATCH 15/15] cleanup --- src/TempleLineOfCredit.sol | 150 ++++++++++++++++++++-------------- test/TempleLineOfCredit.t.sol | 45 ++++++---- 2 files changed, 119 insertions(+), 76 deletions(-) diff --git a/src/TempleLineOfCredit.sol b/src/TempleLineOfCredit.sol index f09e417..ce1083e 100644 --- a/src/TempleLineOfCredit.sol +++ b/src/TempleLineOfCredit.sol @@ -22,15 +22,15 @@ contract TempleLineOfCredit is Ownable, Operators { using SafeERC20 for IERC20; - /// @notice debt position on all tokens + /// @notice A user's posted collateral, debt positions across all debt tokens. struct Position { /// @notice total collateral posted for this position uint256 collateralAmount; - mapping(address => TokenPosition) tokenPosition; + mapping(address => DebtPosition) debtPosition; } /// @notice debt position on a specific token - struct TokenPosition { + struct DebtPosition { uint256 debtAmount; uint256 lastUpdatedAt; } @@ -41,7 +41,9 @@ contract TempleLineOfCredit is Ownable, Operators { } enum TokenPrice { + /// @notice equal to 1 USD STABLE, + /// @notice treasury price index TPI } @@ -84,6 +86,7 @@ contract TempleLineOfCredit is Ownable, Operators { event SetCollateralPrice(TokenPrice price); event SetDebtCollector(address debtCollector); + event SetMinCollateralizationRatio(address debtToken, uint256 minCollateralizationRatio); event AddDebtToken(address token); event RemoveDebtToken(address token); event DepositReserve(address debtToken, uint256 amount); @@ -98,7 +101,7 @@ contract TempleLineOfCredit is Ownable, Operators { error InvalidArrayLength(); error Unsupported(address token); error InvalidTokenPrice(TokenPrice tokenPrice); - error InsufficentCollateral(address debtToken, uint256 maxCapacity, uint256 debtAmount); + error InsufficentCollateral(address debtToken, uint256 maxCapacity, uint256 borrowAmount); error ExceededBorrowedAmount(address account, uint256 amountBorrowed, uint256 amountRepay); error ExceededCollateralAmonut(address account, uint256 amountCollateral, uint256 collateralWithdraw); error WillUnderCollaterlize(address account, uint256 withdrawalAmount); @@ -149,24 +152,32 @@ contract TempleLineOfCredit is Ownable, Operators { function removeDebtToken(address debtToken) external onlyOperators { if (!debtTokens[debtToken].isAllowed) revert Unsupported(debtToken); delete debtTokens[debtToken]; - /// TODO: Remove from the debtTokenList - emit AddDebtToken(debtToken); + uint256 debtTokensLength = debtTokenList.length; + for (uint256 i; i < debtTokensLength; ++i) { + if (debtTokenList[i] == debtToken) { + // Switch the last item into this place then pop off the end. + debtTokenList[i] = debtTokenList[debtTokenList.length - 1]; + debtTokenList.pop(); + break; + } + } + emit RemoveDebtToken(debtToken); } function setMinCollateralizationRatio(address debtToken, uint256 _minCollateralizationRatio) external onlyOperators { if (!debtTokens[debtToken].isAllowed) revert Unsupported(debtToken); debtTokens[debtToken].minCollateralizationRatio = _minCollateralizationRatio; - emit RemoveDebtToken(debtToken); + emit SetMinCollateralizationRatio(debtToken, _minCollateralizationRatio); } /** - * @dev Get user principal amount - * @return principal amount + * @dev Get current user debt position. (Doesn't account for recently accured interest) + * @return debt amount */ - function getDebtAmount(address debtToken, address account) public view returns (TokenPosition memory) { + function getDebtPosition(address debtToken, address account) external view returns (DebtPosition memory) { if (!debtTokens[debtToken].isAllowed) revert Unsupported(debtToken); - return positions[account].tokenPosition[debtToken]; + return positions[account].debtPosition[debtToken]; } /** @@ -174,10 +185,10 @@ contract TempleLineOfCredit is Ownable, Operators { * @return total Debt */ function getTotalDebtAmount(address debtToken, address account) public view returns (uint256) { - DebtToken memory debtTokenInfo = debtTokens[debtToken]; + DebtToken storage debtTokenInfo = debtTokens[debtToken]; if (!debtTokenInfo.isAllowed) revert Unsupported(debtToken); - TokenPosition storage userPosition = positions[account].tokenPosition[debtToken]; + DebtPosition storage userPosition = positions[account].debtPosition[debtToken]; uint256 totalDebt = userPosition.debtAmount; uint256 secondsElapsed = block.timestamp - userPosition.lastUpdatedAt; @@ -185,11 +196,19 @@ contract TempleLineOfCredit is Ownable, Operators { return totalDebt; } + /** + * @dev Get List of all supported debt tokens + * @return array of debt toknes + */ + function getDebtTokenList() external view returns (address[] memory) { + return debtTokenList; + } + function getTokenPrice(TokenPrice _price) public view returns (uint256 price, uint256 precision) { if (_price == TokenPrice.STABLE) { return (10000, 10000); - } else if (_price == TokenPrice.TPI) { + } else { // Get Token Price from redemeer uint256 tpiPrice = IOudRedeemer(oudRedeemer).treasuryPriceIndex(); return (tpiPrice, 10000); @@ -197,7 +216,9 @@ contract TempleLineOfCredit is Ownable, Operators { } /** - * @dev Allows operator to depoist debt tokens + * @dev Allows operator to deposit debt tokens + * @param debtToken debt token to deposit + * @param account account to take debtToken from * @param amount is the amount to deposit */ function depositReserve(address debtToken, address account, uint256 amount) external onlyOperators{ @@ -215,6 +236,8 @@ contract TempleLineOfCredit is Ownable, Operators { /** * @dev Allows operator to remove debt token + * @param debtToken debt token to deposit + * @param account account to take debt token from * @param amount is the amount to remove */ function removeReserve(address debtToken, address account, uint256 amount) external onlyOperators{ @@ -244,6 +267,11 @@ contract TempleLineOfCredit is Ownable, Operators { emit PostCollateral(msg.sender, collateralAmount); } + /** + * @dev Allows user to borrow debt tokens + * @param tokens list of debt tokens to borrow + * @param borrowAmounts list of amounts to borrow + */ function borrow(address[] memory tokens, uint256[] memory borrowAmounts) external { if (tokens.length != borrowAmounts.length) { @@ -252,7 +280,10 @@ contract TempleLineOfCredit is Ownable, Operators { address debtToken; uint256 borrowAmount; - DebtToken memory debtTokenInfo; + uint256 debtAmount; + uint256 borrowCapacity; + DebtToken storage debtTokenInfo; + DebtPosition storage userPosition; for (uint256 i; i < tokens.length; ++i) { @@ -264,19 +295,16 @@ contract TempleLineOfCredit is Ownable, Operators { debtTokenInfo = debtTokens[debtToken]; if (!debtTokenInfo.isAllowed) revert Unsupported(debtToken); - TokenPosition storage userPosition = positions[msg.sender].tokenPosition[debtToken]; - uint256 debtAmount = userPosition.debtAmount; - if (debtAmount != 0) { - debtAmount = getTotalDebtAmount(debtToken, msg.sender); - } + userPosition = positions[msg.sender].debtPosition[debtToken]; + debtAmount = userPosition.debtAmount == 0 ? 0 : getTotalDebtAmount(debtToken, msg.sender); - uint256 borrowCapacity = _maxBorrowCapacity(positions[msg.sender].collateralAmount, debtTokenInfo.tokenPrice, debtTokenInfo.minCollateralizationRatio) - debtAmount; - debtAmount += borrowAmount; - - if (debtAmount > borrowCapacity) { - revert InsufficentCollateral(debtToken, borrowCapacity, debtAmount); + + borrowCapacity = _maxBorrowCapacity(positions[msg.sender].collateralAmount, debtTokenInfo.tokenPrice, debtTokenInfo.minCollateralizationRatio) - debtAmount; + if (borrowAmount > borrowCapacity) { + revert InsufficentCollateral(debtToken, borrowCapacity, borrowAmount); } + debtAmount += borrowAmount; userPosition.debtAmount = debtAmount; userPosition.lastUpdatedAt = block.timestamp; @@ -297,7 +325,7 @@ contract TempleLineOfCredit is Ownable, Operators { } function maxBorrowCapacity(address debtToken, address account) public view returns(uint256) { - DebtToken memory debtTokenInfo = debtTokens[debtToken]; + DebtToken storage debtTokenInfo = debtTokens[debtToken]; return _maxBorrowCapacity(positions[account].collateralAmount, debtTokenInfo.tokenPrice, debtTokenInfo.minCollateralizationRatio); } @@ -319,17 +347,20 @@ contract TempleLineOfCredit is Ownable, Operators { revert ExceededCollateralAmonut(msg.sender, collateralAmount, withdrawalAmount); } - for (uint256 i; i < debtTokenList.length; ++i) { + address debtToken; + DebtToken storage debtTokenInfo; + uint256 borrowCapacity; + uint256 debtTokensLength = debtTokenList.length; + for (uint256 i; i < debtTokensLength; ++i) { - address debtToken = debtTokenList[i]; - DebtToken memory debtTokenInfo = debtTokens[debtToken]; + debtToken = debtTokenList[i]; + debtTokenInfo = debtTokens[debtToken]; - uint256 borrowCapacity = _maxBorrowCapacity(collateralAmount - withdrawalAmount, debtTokenInfo.tokenPrice, debtTokenInfo.minCollateralizationRatio); + borrowCapacity = _maxBorrowCapacity(collateralAmount - withdrawalAmount, debtTokenInfo.tokenPrice, debtTokenInfo.minCollateralizationRatio); - if (positions[msg.sender].tokenPosition[debtToken].debtAmount > borrowCapacity ) { + if (positions[msg.sender].debtPosition[debtToken].debtAmount > borrowCapacity ) { revert WillUnderCollaterlize(msg.sender, withdrawalAmount); } - } positions[msg.sender].collateralAmount -= withdrawalAmount; @@ -350,7 +381,9 @@ contract TempleLineOfCredit is Ownable, Operators { address debtToken; uint256 repayAmount; - DebtToken memory debtTokenInfo; + uint256 debtAmount; + DebtToken storage debtTokenInfo; + DebtPosition storage userPosition; for (uint256 i; i < tokens.length; ++i) { @@ -362,12 +395,8 @@ contract TempleLineOfCredit is Ownable, Operators { debtTokenInfo = debtTokens[debtToken]; if (!debtTokenInfo.isAllowed) revert Unsupported(debtToken); - - TokenPosition storage userPosition = positions[msg.sender].tokenPosition[debtToken]; - uint256 debtAmount = userPosition.debtAmount; - if (debtAmount != 0) { - debtAmount = getTotalDebtAmount(debtToken, msg.sender); - } + userPosition = positions[msg.sender].debtPosition[debtToken]; + debtAmount = userPosition.debtAmount == 0 ? 0 : getTotalDebtAmount(debtToken, msg.sender); if (repayAmount > debtAmount) { revert ExceededBorrowedAmount(msg.sender, debtAmount, repayAmount); @@ -395,40 +424,37 @@ contract TempleLineOfCredit is Ownable, Operators { /** * @dev Allows operator to liquidate debtors position * @param debtor the account to liquidate + * @param debtToken specific debt token that is in debt */ function liquidate(address debtor, address debtToken) external onlyOperators { - DebtToken memory debtTokenInfo = debtTokens[debtToken]; + DebtToken storage debtTokenInfo = debtTokens[debtToken]; if (!debtTokens[debtToken].isAllowed) revert Unsupported(debtToken); - TokenPosition storage userPosition = positions[debtor].tokenPosition[debtToken]; - uint256 totalDebtOwed = userPosition.debtAmount; + DebtPosition storage userPosition = positions[debtor].debtPosition[debtToken]; + uint256 totalDebtOwed = userPosition.debtAmount == 0 ? 0 : getTotalDebtAmount(debtToken, debtor); uint256 collateralAmount = positions[debtor].collateralAmount; - if (totalDebtOwed != 0) { - totalDebtOwed = getTotalDebtAmount(debtToken, debtor); - } - - if (_getCurrentCollaterilizationRatio(collateralAmount, userPosition.debtAmount, debtTokenInfo.tokenPrice, totalDebtOwed) >= debtTokenInfo.minCollateralizationRatio) { - revert OverCollaterilized(debtor); - } (uint256 debtTokenPrice, uint256 debtPrecision) = getTokenPrice(debtTokenInfo.tokenPrice); (uint256 collateralTokenPrice, uint256 collateralPrecision) = getTokenPrice(collateralPrice); + if (_getCurrentCollaterilizationRatio(collateralAmount, userPosition.debtAmount, totalDebtOwed, debtTokenPrice, debtPrecision, collateralTokenPrice, collateralPrecision) >= debtTokenInfo.minCollateralizationRatio) { + revert OverCollaterilized(debtor); + } uint256 collateralSeized = (totalDebtOwed * debtTokenPrice * collateralPrecision) / collateralTokenPrice / debtPrecision; - if (collateralSeized > collateralAmount) { collateralSeized = collateralAmount; } - positions[debtor].collateralAmount -= collateralSeized; + positions[debtor].collateralAmount = collateralAmount - collateralSeized; // Wipe out all of users other token debts - address debtToken; - for (uint256 i; i < debtTokenList.length; ++i) { - debtToken = debtTokenList[i]; - TokenPosition storage userPosition = positions[debtor].tokenPosition[debtToken]; + address _debtToken; + uint256 debtTokensLength = debtTokenList.length; + for (uint256 i; i < debtTokensLength; ++i) { + _debtToken = debtTokenList[i]; + userPosition = positions[debtor].debtPosition[_debtToken]; userPosition.debtAmount = 0; userPosition.lastUpdatedAt = block.timestamp; } @@ -441,17 +467,17 @@ contract TempleLineOfCredit is Ownable, Operators { emit Liquidated(debtor, totalDebtOwed, collateralSeized); } - function getCurrentCollaterilizationRatio(address debtToken, address account) public view returns(uint256) { - DebtToken memory debtTokenInfo = debtTokens[debtToken]; - return _getCurrentCollaterilizationRatio(positions[account].collateralAmount, positions[account].tokenPosition[debtToken].debtAmount, debtTokenInfo.tokenPrice, getTotalDebtAmount(debtToken, account)); + function getCurrentCollaterilizationRatio(address debtToken, address account) external view returns(uint256) { + (uint256 debtTokenPrice, uint256 debtPrecision) = getTokenPrice(debtTokens[debtToken].tokenPrice); + (uint256 collateralTokenPrice, uint256 collateralPrecision) = getTokenPrice(collateralPrice); + return _getCurrentCollaterilizationRatio(positions[account].collateralAmount, positions[account].debtPosition[debtToken].debtAmount, getTotalDebtAmount(debtToken, account), debtTokenPrice, debtPrecision, collateralTokenPrice, collateralPrecision); } - function _getCurrentCollaterilizationRatio(uint256 collateralAmount, uint256 debtAmount, TokenPrice debtPrice, uint256 totalDebtAmount) public view returns(uint256) { + function _getCurrentCollaterilizationRatio(uint256 collateralAmount, uint256 debtAmount, uint256 totalDebtAmount, uint256 debtTokenPrice, uint256 debtPrecision, uint256 collateralTokenPrice, uint256 collateralPrecision) public view returns(uint256) { if (debtAmount == 0 ) { return 0; } else { - (uint256 debtTokenPrice, uint256 debtPrecision) = getTokenPrice(debtPrice); - (uint256 collateralTokenPrice, uint256 collateralPrecision) = getTokenPrice(collateralPrice); + return ((collateralAmount * collateralTokenPrice * debtPrecision * 10000) / totalDebtAmount / debtTokenPrice / collateralPrecision); } } diff --git a/test/TempleLineOfCredit.t.sol b/test/TempleLineOfCredit.t.sol index 547a70f..92d8a7d 100644 --- a/test/TempleLineOfCredit.t.sol +++ b/test/TempleLineOfCredit.t.sol @@ -126,6 +126,7 @@ contract TempleLineOfCreditTest is Test { vm.prank(admin); tlc.addDebtToken(address(daiToken), daiTokenType, daiInterestRateBps, daiPrice, daiMinCollateralizationRatio); (TempleLineOfCredit.TokenType tokenType, uint256 interestRateBps, TempleLineOfCredit.TokenPrice tokenPrice, uint256 minCollateralizationRatio, bool allowed) = tlc.debtTokens(address(daiToken)); + assertEq(tlc.debtTokenList(0), address(daiToken)); assertEq(daiInterestRateBps, interestRateBps); assertEq(uint(daiPrice), uint(tokenPrice)); assertEq(daiMinCollateralizationRatio, minCollateralizationRatio); @@ -149,6 +150,7 @@ contract TempleLineOfCreditTest is Test { vm.prank(admin); tlc.removeDebtToken(address(daiToken)); (,,,, bool allowed) = tlc.debtTokens(address(daiToken)); + assertEq(tlc.getDebtTokenList().length, 0); assertEq(allowed, false); } @@ -341,13 +343,13 @@ contract TempleLineOfCreditTest is Test { (uint256 aliceCollateralAmount ) = tlc.positions(alice); - TempleLineOfCredit.TokenPosition memory tpDAI = tlc.getDebtAmount(address(daiToken), alice); + TempleLineOfCredit.DebtPosition memory tpDAI = tlc.getDebtPosition(address(daiToken), alice); assertEq(aliceCollateralAmount, collateralAmount); assertEq(tpDAI.debtAmount, maxBorrowCapacityDAI); assertEq(tpDAI.lastUpdatedAt, block.timestamp); assertEq(daiToken.balanceOf(alice), maxBorrowCapacityDAI); - TempleLineOfCredit.TokenPosition memory tpOUD = tlc.getDebtAmount(address(oudToken), alice); + TempleLineOfCredit.DebtPosition memory tpOUD = tlc.getDebtPosition(address(oudToken), alice); assertEq(tpOUD.debtAmount, maxBorrowCapacityOUD); assertEq(tpOUD.lastUpdatedAt, block.timestamp); assertEq(oudToken.balanceOf(alice), maxBorrowCapacityOUD); @@ -373,7 +375,9 @@ contract TempleLineOfCreditTest is Test { } function _borrow(address _account, uint256 collateralAmount, uint256 daiBorrowAmount, uint256 oudBorrowAmount) internal { - _postCollateral(_account, collateralAmount); + if (collateralAmount != 0) { + _postCollateral(_account, collateralAmount); + } vm.prank(_account); address[] memory debtTokens = new address[](2); debtTokens[0] = address(daiToken); @@ -386,6 +390,21 @@ contract TempleLineOfCreditTest is Test { } + function testBorrowAlreadyBorrowedFailInsufficientCollateral() external { + uint256 borrowDAIAmountFirst = uint(30_000e18); + uint256 borrowOUDAmountFirst = uint(20_000e18); + + _borrow(alice, uint(100_000e18), borrowDAIAmountFirst, borrowOUDAmountFirst); + + uint256 borrowDAIAmountSecond = tlc.maxBorrowCapacity(address(daiToken), alice) - borrowDAIAmountFirst + 1; + uint256 borrowOUDAmountSecond = uint(10_000e18); + console.log(borrowDAIAmountSecond, tlc.maxBorrowCapacity(address(daiToken), alice)); + + vm.expectRevert(abi.encodeWithSelector(TempleLineOfCredit.InsufficentCollateral.selector, address(daiToken), borrowDAIAmountSecond - 1, borrowDAIAmountSecond)); + _borrow(alice, 0, borrowDAIAmountSecond, borrowOUDAmountSecond); + } + + function testBorrowAlreadyBorrowedSucess() external { uint256 borrowDAIAmountFirst = uint(30_000e18); uint256 borrowOUDAmountFirst = uint(20_000e18); @@ -401,15 +420,13 @@ contract TempleLineOfCreditTest is Test { _borrow(alice, uint(100_000e18), borrowDAIAmountSecond, borrowOUDAmountSecond); - TempleLineOfCredit.TokenPosition memory tpDAI = tlc.getDebtAmount(address(daiToken), alice); + TempleLineOfCredit.DebtPosition memory tpDAI = tlc.getDebtPosition(address(daiToken), alice); /// First Principle accured principle second borrow amount assertEq(tpDAI.debtAmount, borrowDAIAmountFirst + ((borrowDAIAmountFirst * daiInterestRateBps * secondsElapsed) / 10000 / 365 days) + borrowDAIAmountSecond ); assertEq(tpDAI.lastUpdatedAt, block.timestamp); assertEq(daiToken.balanceOf(alice), borrowDAIAmountFirst + borrowDAIAmountSecond); - - - - TempleLineOfCredit.TokenPosition memory tpOUD = tlc.getDebtAmount(address(oudToken), alice); + + TempleLineOfCredit.DebtPosition memory tpOUD = tlc.getDebtPosition(address(oudToken), alice); /// First Principle accured principle second borrow amount assertEq(tpOUD.debtAmount, borrowOUDAmountFirst + ((borrowOUDAmountFirst * oudInterestRateBps * secondsElapsed) / 10000 / 365 days) + borrowOUDAmountSecond ); assertEq(tpOUD.lastUpdatedAt, block.timestamp); @@ -463,11 +480,11 @@ contract TempleLineOfCreditTest is Test { tlc.repay(debtTokens, repayAmounts); vm.stopPrank(); - TempleLineOfCredit.TokenPosition memory tpDAI = tlc.getDebtAmount(address(daiToken), alice); + TempleLineOfCredit.DebtPosition memory tpDAI = tlc.getDebtPosition(address(daiToken), alice); assertEq(borrowDAIAmount - repayDAIAmount, tpDAI.debtAmount); assertEq(block.timestamp, tpDAI.lastUpdatedAt); - TempleLineOfCredit.TokenPosition memory tpOUD = tlc.getDebtAmount(address(oudToken), alice); + TempleLineOfCredit.DebtPosition memory tpOUD = tlc.getDebtPosition(address(oudToken), alice); assertEq(borrowOUDAmount - repayOUDAmount, tpOUD.debtAmount); assertEq(block.timestamp, tpOUD.lastUpdatedAt); @@ -545,11 +562,11 @@ contract TempleLineOfCreditTest is Test { (uint256 collateralTokenPrice, uint256 collateralPrecision) = tlc.getTokenPrice(collateralPrice); assertEq(collateralAmount - (totalDebt * debtTokenPrice * collateralPrecision / collateralTokenPrice / debtPrecision), aliceCollateralAmount); - TempleLineOfCredit.TokenPosition memory tpDAI = tlc.getDebtAmount(address(daiToken), alice); + TempleLineOfCredit.DebtPosition memory tpDAI = tlc.getDebtPosition(address(daiToken), alice); assertEq(0, tpDAI.debtAmount); assertEq(block.timestamp, tpDAI.lastUpdatedAt); - TempleLineOfCredit.TokenPosition memory tpOUD = tlc.getDebtAmount(address(oudToken), alice); + TempleLineOfCredit.DebtPosition memory tpOUD = tlc.getDebtPosition(address(oudToken), alice); assertEq(0, tpOUD.debtAmount); assertEq(block.timestamp, tpOUD.lastUpdatedAt); } @@ -570,11 +587,11 @@ contract TempleLineOfCreditTest is Test { (uint256 aliceCollateralAmount) = tlc.positions(alice); assertEq(0, aliceCollateralAmount); - TempleLineOfCredit.TokenPosition memory tpDAI = tlc.getDebtAmount(address(daiToken), alice); + TempleLineOfCredit.DebtPosition memory tpDAI = tlc.getDebtPosition(address(daiToken), alice); assertEq(0, tpDAI.debtAmount); assertEq(block.timestamp, tpDAI.lastUpdatedAt); - TempleLineOfCredit.TokenPosition memory tpOUD = tlc.getDebtAmount(address(oudToken), alice); + TempleLineOfCredit.DebtPosition memory tpOUD = tlc.getDebtPosition(address(oudToken), alice); assertEq(0, tpOUD.debtAmount); assertEq(block.timestamp, tpOUD.lastUpdatedAt); }