diff --git a/.gitignore b/.gitignore index 9776e47..6ed2830 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ cache/ out/ +broadcast/ .DS_Store remappings.txt \ No newline at end of file diff --git a/deployments/Deploy.sol b/deployments/Deploy.sol new file mode 100644 index 0000000..e479192 --- /dev/null +++ b/deployments/Deploy.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.14; + +import "forge-std/Test.sol"; +import {Proxy} from "../src/proxy/Proxy.sol"; +import {WETH} from "solmate/tokens/WETH.sol"; +import {Beacon} from "../src/proxy/Beacon.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {Account} from "../src/core/Account.sol"; +import {LEther} from "../src/tokens/LEther.sol"; +import {LToken} from "../src/tokens/LToken.sol"; +import {IOracle} from "oracle/core/IOracle.sol"; +import {Registry} from "../src/core/Registry.sol"; +import {RiskEngine} from "../src/core/RiskEngine.sol"; +import {WETHOracle} from "oracle/weth/WETHOracle.sol"; +import {OracleFacade} from "oracle/core/OracleFacade.sol"; +import {AccountManager} from "../src/core/AccountManager.sol"; +import {AccountFactory} from "../src/core/AccountFactory.sol"; +import {DefaultRateModel} from "../src/core/DefaultRateModel.sol"; +import {ChainlinkOracle} from "oracle/chainlink/ChainlinkOracle.sol"; +import {ControllerFacade} from "controller/core/ControllerFacade.sol"; +import {AggregatorV3Interface} from "oracle/chainlink/AggregatorV3Interface.sol"; + +contract Deploy is Test { + // Kovan + address constant TREASURY = 0xc6E058a257eD5EFD6F14DB90dF58754d6963d542; + address constant WETH9 = 0xd0A1E359811322d97991E03f863a0C30C2cF029C; + address constant DAI = 0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa; + address constant ETHUSD = 0x9326BFA02ADD2366b30bacB125260Af641031331; + address constant DAIUSD = 0x777A68032a88E5A84678A77Af2CD65A7b3c0775a; + + Registry registryImpl; + Registry registry; + Account account; + AccountManager accountManagerImpl; + AccountManager accountManager; + RiskEngine riskEngine; + Beacon beacon; + AccountFactory accountFactory; + DefaultRateModel rateModel; + OracleFacade oracle; + ControllerFacade controller; + LEther lEthImpl; + LEther lEth; + LToken lToken; + LToken lDai; + WETHOracle wethOracle; + ChainlinkOracle chainlinkOracle; + + + function run() public { + vm.startBroadcast(); + + // Registry + registryImpl = new Registry(); + registry = Registry(address(new Proxy(address(registryImpl)))); + registry.init(); + + // Account + account = new Account(); + registry.setAddress("ACCOUNT", address(account)); + + // Account Manager + accountManagerImpl = new AccountManager(); + accountManager = AccountManager(payable(address(new Proxy(address(accountManagerImpl))))); + registry.setAddress("ACCOUNT_MANAGER", address(accountManager)); + accountManager.init(registry); + + // Risk Engine + riskEngine = new RiskEngine(registry); + registry.setAddress("RISK_ENGINE", address(riskEngine)); + + // Beacon + beacon = new Beacon(address(account)); + registry.setAddress("ACCOUNT_BEACON", address(beacon)); + + // Account Factory + accountFactory = new AccountFactory(address(beacon)); + registry.setAddress("ACCOUNT_FACTORY", address(accountFactory)); + + // Rate Model + rateModel = new DefaultRateModel(1e17, 3e17, 35e17, 2102400e18); + registry.setAddress("RATE_MODEL", address(rateModel)); + + // Oracle Facade + oracle = new OracleFacade(); + registry.setAddress("ORACLE", address(oracle)); + + // Controller Facade + controller = new ControllerFacade(); + registry.setAddress("CONTROLLER", address(controller)); + controller.toggleTokenAllowance(WETH9); + controller.toggleTokenAllowance(DAI); + + // initDep + accountManager.initDep(); + riskEngine.initDep(); + + // LETH + lEthImpl = new LEther(); + lEth = LEther(payable(address(new Proxy(address(lEthImpl))))); + lEth.init(ERC20(WETH9), "LEther", "LETH", registry, 3e15, TREASURY); + registry.setLToken(WETH9, address(lEth)); + accountManager.toggleCollateralStatus(WETH9); + lEth.initDep("RATE_MODEL"); + + // LDAI + lToken = new LToken(); + lDai = LToken(address(new Proxy(address(lToken)))); + lDai.init(ERC20(DAI), "LDai", "LDAI", registry, 3e15, TREASURY); + registry.setLToken(DAI, address(lDai)); + accountManager.toggleCollateralStatus(DAI); + lDai.initDep("RATE_MODEL"); + + // WETH Oracle + wethOracle = new WETHOracle(); + oracle.setOracle(address(0), IOracle(wethOracle)); + oracle.setOracle(WETH9, IOracle(wethOracle)); + + // Chainlink Oracle + chainlinkOracle = new ChainlinkOracle(AggregatorV3Interface(ETHUSD)); + chainlinkOracle.setFeed(DAI, AggregatorV3Interface(DAIUSD)); + oracle.setOracle(DAI, chainlinkOracle); + + // Log Contract Addresses + console.log("Registry Impl", address(registryImpl)); + console.log("Registry", address(registry)); + console.log("Account", address(account)); + console.log("Account Manager Impl", address(accountManagerImpl)); + console.log("Account Manager", address(accountManager)); + console.log("Risk Engine", address(riskEngine)); + console.log("Beacon", address(beacon)); + console.log("Account Factory", address(accountFactory)); + console.log("Rate Model", address(rateModel)); + console.log("Oracle Facade", address(oracle)); + console.log("Controller Facade", address(controller)); + console.log("LEther Impl", address(lEthImpl)); + console.log("LEther", address(lEth)); + console.log("LToken", address(lToken)); + console.log("LDai", address(lDai)); + console.log("WETH Oracle", address(wethOracle)); + console.log("ChainlinkOracle", address(chainlinkOracle)); + + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/deployments/scripts/README.md b/deployments/scripts/README.md deleted file mode 100644 index 7e7e8f0..0000000 --- a/deployments/scripts/README.md +++ /dev/null @@ -1,31 +0,0 @@ - -# Contract Deployment - -The _deployment_script_ can be used to deploy any contract. - -## Requirements - -Python3 - -## How to run the script - -```bash -python deployment_script.py ... -``` - -## How to use the script - -In order to deploy a contract or a series of contracts, -you will need to add the name of the contract to the contracts_data -object. - -The object has three properties: - -1. args: constructor args to deploy the contract. These args can be other contracts in the contracts_data or any constant data. - 1. If the arg is another contract, it will take the address of that contract and pass it as an argument. -2. address: address where the contract will be deployed or is already deployed. -3. src: destination of the contract. - -## Output of the script - -Will display the contract object of all the deployed contracts along. diff --git a/deployments/scripts/deployment_script.py b/deployments/scripts/deployment_script.py deleted file mode 100644 index ffb1021..0000000 --- a/deployments/scripts/deployment_script.py +++ /dev/null @@ -1,130 +0,0 @@ -import os -import subprocess -import sys - -PRIVATE_KEY = "" -RPC_URL = "" - - -def get_deployment_address(output): - count = 0 - i = 0 - address = "" - while count < 2: - if output[i] == ":": - count += 1 - i += 1 - i += 1 - while output[i] != "\n": - address += output[i] - i += 1 - return address - - -def add_basic_commands(commands): - commands += [ - "forge", - "create", - "--legacy", - "--rpc-url", - RPC_URL - ] - - -def add_private_key(commands): - commands += [ - "--private-key", - PRIVATE_KEY - ] - - -def add_args(commands, args=[]): - if not args: - return - commands += ["--constructor-args"] - for arg in args: - if arg in contracts_data: - commands.append(contracts_data[arg]['address']) - else: - commands.append(arg) - - -def add_src(contract, commands): - commands += [contract["src"]] - - -def deploy(contract): - commands = [] - add_basic_commands(commands) - add_args(commands, contract["args"]) - add_private_key(commands) - add_src(contract, commands) - print(commands) - os.chdir("../") - output = subprocess.run( - commands, - stdout=subprocess.PIPE, - text=True - ) - contract["address"] = get_deployment_address(output.stdout) - - -contracts_data = { - "RiskEngine": { - "args": ["FeedAggregator"], - "address": "0x3cfbf9cd019a8f936f56f69da81e6fcf3626b058", - "src": "src/core/RiskEngine.sol:RiskEngine" - }, - "UserRegistry": { - "args": [], - "address": "0x6db5119954d7626227476e3f5c6ff503258870d0", - "src": "src/core/UserRegistry.sol:UserRegistry" - }, - "DefaultRateModel": { - "args": [], - "address": "0x12b6687510d78c05ba6f3a421763934ca7784a11", - "src": "src/core/DefaultRateModel.sol:DefaultRateModel" - }, - "FeedAggregator": { - "args": ["WETH"], - "address": "0x8e9e2604b3e221ffbbe8c63048e89aa0c45e925d", - "src": "src/priceFeeds/FeedAggregator.sol:FeedAggregator" - }, - "Account": { - "args": [], - "address": "0xf8cfff57a017f588d8aecd5aacac9a6345612745", - "src": "src/core/Account.sol:Account" - }, - "Beacon": { - "args": ["Account"], - "address": "0x61ddaf7853a9e9183602db775a07aa18006ff97c", - "src": "src/proxy/Beacon.sol:Beacon" - }, - "AccountFactory": { - "args": ["Beacon"], - "address": "0xfaa4a292aaeb8c498dc1adb44afafee003f077d7", - "src": "src/core/AccountFactory.sol:AccountFactory" - }, - "AccountManager": { - "args": ["RiskEngine", "AccountFactory", "UserRegistry"], - "address": "", - "src": "src/core/AccountManager.sol:AccountManager" - }, - "WETH": { - "address": "0xfaa4a292aaeb8c498dc1adb44afafee003f077d7" - } -} - - -def deploy_contracts(contracts): - for contract in contracts: - deploy(contracts_data[contract]) - for contract in contracts: - print(contracts_data[contract]) - - -if __name__ == "__main__": - RPC_URL = sys.argv[1] - PRIVATE_KEY = sys.argv[2] - contracts = sys.argv[3:] - deploy_contracts(contracts) diff --git a/deployments/scripts/setup_contracts.sh b/deployments/scripts/setup_contracts.sh deleted file mode 100644 index 0a7f638..0000000 --- a/deployments/scripts/setup_contracts.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/bin/bash -set -eo pipefail - -RPC_URL= -PRIVATE_KEY= - -# Since we use relative paths, cd into the script's directory before running it -PATH_TO_SRC=../src -PATH_TO_LIB=../lib - -forge build --force # Compile everything - -# -# Deploy Contracts -# - -deploy() { - # stdout from forge create - local output - - if (($# > 1)) - then - local path=$1 # path to contract - shift - output=$(forge create --rpc-url $RPC_URL --private-key $PRIVATE_KEY $path \ - --constructor-args $@) - else - output=$(forge create --rpc-url $RPC_URL --private-key $PRIVATE_KEY $1) - fi - - # Extract deployed address from stdout - echo $output | grep "Deployed to:" | cut -d" " -f10 -} - -ERC20=$(deploy ${PATH_TO_SRC}/test/utils/TestERC20.sol:TestERC20 \ - "TestERC20" "TEST" 18) -echo "ERC20: ${ERC20}" - -REGISTRY=$(deploy ${PATH_TO_SRC}/core/Registry.sol:Registry) -echo "Registry: ${REGISTRY}" - -forge create --rpc-url $RPC_URL --private-key $PRIVATE_KEY ${PATH_TO_SRC}/core/Registry.sol:Registry - -RATE_MODEL=$(deploy ${PATH_TO_SRC}/core/DefaultRateModel.sol:DefaultRateModel) -echo "Rate Model: ${RATE_MODEL}" - -RISK_ENGINE=$(deploy ${PATH_TO_SRC}/core/RiskEngine.sol:RiskEngine ${REGISTRY}) -echo "Risk Engine: ${RISK_ENGINE}" - -ACCOUNT_MANAGER=$(deploy ${PATH_TO_SRC}/core/AccountManager.sol:AccountManager \ - ${REGISTRY}) -echo "Account Manager: ${ACCOUNT_MANAGER}" - -ACCOUNT=$(deploy ${PATH_TO_SRC}/core/Account.sol:Account) -echo "Account: ${ACCOUNT}" - -BEACON=$(deploy ${PATH_TO_SRC}/proxy/Beacon.sol:Beacon ${ACCOUNT}) -echo "Beacon: ${BEACON}" - -ACCOUNT_FACTORY=$(deploy ${PATH_TO_SRC}/core/AccountFactory.sol:AccountFactory \ - ${BEACON}) -echo "Account Factory: ${ACCOUNT_FACTORY}" - -LETHER=$(deploy ${PATH_TO_SRC}/tokens/LEther.sol:LEther ${REGISTRY} 1) -echo "LEther: ${LETHER}" - -LERC20=$(deploy ${PATH_TO_SRC}/tokens/LERC20.sol:LERC20 \ - "LTestERC20" "LERC20" 18 ${ERC20} ${REGISTRY} 1) -echo "LERC20: ${LERC20}" - -ORACLE=$(deploy ${PATH_TO_LIB}/oracle/src/core/OracleFacade.sol:OracleFacade) -echo "Oracle: ${ORACLE}" - -CONTROLLER=$(deploy \ ${PATH_TO_LIB}/controller/src/core/ControllerFacade.sol:ControllerFacade) -echo "Controller: ${CONTROLLER}" - -# -# Register Contracts -# - -setAddress() { - local registryKey - registryKey=$(cast --from-utf8 $1 | cast --to-bytes32) || exit $? - cast send --legacy --rpc-url $RPC_URL --private-key $PRIVATE_KEY $REGISTRY \ - "setAddress(bytes32, address)" $registryKey $2 - echo "Successfully registered $2 as $1 with key ${registryKey}" -} - -setAddress "ORACLE" $ORACLE -setAddress "CONTROLLER" $CONTROLLER -setAddress "RATE_MODEL" $RATE_MODEL -setAddress "RISK_ENGINE" $RISK_ENGINE -setAddress "ACCOUNT_FACTORY" $ACCOUNT_FACTORY -setAddress "ACCOUNT_MANAGER" $ACCOUNT_MANAGER - -setLToken() { - cast send --legacy --rpc-url $RPC_URL --private-key $PRIVATE_KEY $REGISTRY \ - "setLToken(address, address)" $1 $2 - echo "Successfully set up LToken contract $2 for underlying token $1" -} - -setLToken 0x0000000000000000000000000000000000000000 $LETHER -setLToken $ERC20 $LERC20 - -# -# Initialize Contracts -# - -initialize() { - cast send --legacy --rpc-url $RPC_URL --private-key $PRIVATE_KEY $1 "initialize()" - echo "Successfully Initialized $1" -} - -initialize $RISK_ENGINE -initialize $ACCOUNT_MANAGER -initialize $LETHER -initialize $LERC20 \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std index 37a3fe4..5645100 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 37a3fe48c3a4d8239cda93445f0b5e76b1507436 +Subproject commit 564510058ab3db01577b772c275e081e678373f2 diff --git a/src/core/AccountManager.sol b/src/core/AccountManager.sol index b0365d2..746071f 100644 --- a/src/core/AccountManager.sol +++ b/src/core/AccountManager.sol @@ -228,7 +228,6 @@ contract AccountManager is Pausable, IAccountManager { if (registry.LTokenFor(token) == address(0)) revert Errors.LTokenUnavailable(); _repay(account, token, amt); - emit Repay(account, msg.sender, token, amt); } /** @@ -327,19 +326,16 @@ contract AccountManager is Pausable, IAccountManager { /* Internal Functions */ /* -------------------------------------------------------------------------- */ - function _repay(address account, address token, uint value) internal { + function _repay(address account, address token, uint amt) internal { ILToken LToken = ILToken(registry.LTokenFor(token)); LToken.updateState(); - uint shares; - if (value == type(uint256).max) { - shares = LToken.borrowsOf(account); - value = LToken.convertToAssets(shares); - } else shares = LToken.convertToShares(value); - account.withdraw(address(LToken), token, value); - if (LToken.collectFrom(account, value, shares)) + if (amt == type(uint256).max) amt = LToken.getBorrowBalance(account); + account.withdraw(address(LToken), token, amt); + if (LToken.collectFrom(account, amt)) IAccount(account).removeBorrow(token); if (IERC20(token).balanceOf(account) == 0) IAccount(account).removeAsset(token); + emit Repay(account, msg.sender, token, amt); } function _updateTokensIn(address account, address[] memory tokensIn) diff --git a/src/core/DefaultRateModel.sol b/src/core/DefaultRateModel.sol index 545f931..02ee7c5 100644 --- a/src/core/DefaultRateModel.sol +++ b/src/core/DefaultRateModel.sol @@ -41,19 +41,17 @@ contract DefaultRateModel is IRateModel { where util = borrows / (liquidity - reserves + borrows) @param liquidity total balance of the underlying asset in the pool @param borrows balance of underlying assets borrowed from the pool - @param reserves balance of underlying assets reserved for the protocol @return uint borrow rate per block */ function getBorrowRatePerBlock( uint liquidity, - uint borrows, - uint reserves + uint borrows ) external view returns (uint) { - uint util = _utilization(liquidity, borrows, reserves); + uint util = _utilization(liquidity, borrows); return c3.mul( ( util.mul(c1) @@ -64,16 +62,12 @@ contract DefaultRateModel is IRateModel { ); } - function _utilization( - uint liquidity, - uint borrows, - uint reserves - ) - internal - pure + function _utilization(uint liquidity, uint borrows) + internal + pure returns (uint) { - uint totalAssets = liquidity + borrows - reserves; + uint totalAssets = liquidity + borrows; return (totalAssets == 0) ? 0 : borrows.div(totalAssets); } } \ No newline at end of file diff --git a/src/interface/core/IRateModel.sol b/src/interface/core/IRateModel.sol index 7b82ede..acee4ee 100644 --- a/src/interface/core/IRateModel.sol +++ b/src/interface/core/IRateModel.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.10; interface IRateModel { function getBorrowRatePerBlock( uint liquidity, - uint borrows, - uint reserves + uint borrows ) external view returns (uint); } \ No newline at end of file diff --git a/src/interface/tokens/ILToken.sol b/src/interface/tokens/ILToken.sol index 6d4b423..834bab5 100644 --- a/src/interface/tokens/ILToken.sol +++ b/src/interface/tokens/ILToken.sol @@ -8,14 +8,16 @@ import {IOwnable} from "../utils/IOwnable.sol"; import {IRegistry} from "../core/IRegistry.sol"; import {IRateModel} from "../core/IRateModel.sol"; -interface ILToken is IERC20, IERC4626, IOwnable { +interface ILToken { function init( ERC20 _asset, string calldata _name, string calldata _symbol, IRegistry _registry, - uint _reserveFactor + uint _reserveFactor, + address treasury ) external; + function initDep(string calldata) external; function registry() external returns (IRegistry); @@ -24,9 +26,8 @@ interface ILToken is IERC20, IERC4626, IOwnable { function updateState() external; function lendTo(address account, uint value) external returns (bool); - function collectFrom(address account, uint value, uint shares) external returns (bool); + function collectFrom(address account, uint value) external returns (bool); + + function getBorrows() external view returns (uint); function getBorrowBalance(address account) external view returns (uint); - function borrowsOf(address) external returns (uint256); - function convertToShares(uint256 assets) external view returns (uint256); - function convertToAssets(uint256 shares) external view returns (uint256); } \ No newline at end of file diff --git a/src/test/account/RepayFlow.t.sol b/src/test/account/RepayFlow.t.sol index 6d06f06..7ed4d62 100644 --- a/src/test/account/RepayFlow.t.sol +++ b/src/test/account/RepayFlow.t.sol @@ -3,8 +3,10 @@ pragma solidity ^0.8.10; import {TestBase} from "../utils/TestBase.sol"; import {IAccount} from "../../interface/core/IAccount.sol"; +import {PRBMathUD60x18} from "prb-math/PRBMathUD60x18.sol"; contract RepayFlowTest is TestBase { + using PRBMathUD60x18 for uint; address public account; address public borrower = cheats.addr(1); @@ -82,4 +84,22 @@ contract RepayFlowTest is TestBase { assertEq(riskEngine.getBorrows(account), 0); } + + function testMaxRepayERC20WithInterest(uint96 depositAmt, uint96 borrowAmt) + public + { + // Setup + cheats.assume(borrowAmt > 0); + cheats.assume(MAX_LEVERAGE.mul(depositAmt) > borrowAmt); + deposit(borrower, account, address(erc20), depositAmt); + borrow(borrower, account, address(erc20), borrowAmt); + cheats.roll(block.number + 100); + + // Test + cheats.prank(borrower); + accountManager.repay(account, address(erc20), type(uint).max); + + assertEq(lErc20.getBorrowBalance(account), 0); + assertEq(lErc20.getBorrows(), 0); + } } \ No newline at end of file diff --git a/src/test/account/RepayInParts.t.sol b/src/test/account/RepayInParts.t.sol new file mode 100644 index 0000000..9309889 --- /dev/null +++ b/src/test/account/RepayInParts.t.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {TestBase} from "../utils/TestBase.sol"; +import {IAccount} from "../../interface/core/IAccount.sol"; +import {PRBMathUD60x18} from "prb-math/PRBMathUD60x18.sol"; + +contract RepayInParts is TestBase { + using PRBMathUD60x18 for uint; + + address public account; + address public borrower = cheats.addr(1); + + function setUp() public { + setupContracts(); + account = openAccount(borrower); + } + + // Borrow - Repay - Repay Max + function testRepayInParts1(uint96 depositAmt, uint96 borrowAmt, uint96 repayAmt) + public + { + // Setup + cheats.assume(borrowAmt > repayAmt); + cheats.assume(MAX_LEVERAGE.mul(depositAmt) > borrowAmt); + deposit(borrower, account, address(erc20), depositAmt); + borrow(borrower, account, address(erc20), borrowAmt); + cheats.roll(block.number + 100); + + // Test + cheats.prank(borrower); + accountManager.repay(account, address(erc20), repayAmt); + + // Increment block number + cheats.roll(block.number + 100); + + cheats.prank(borrower); + accountManager.repay(account, address(erc20), type(uint).max); + + assertEq(riskEngine.getBorrows(account), 0); + assertEq(lErc20.getBorrows(), 0); + } + + // Borrow1 - Borrow2 - Repay Max + function testRepayInParts2(uint96 depositAmt, uint96 borrowAmt, uint96 borrow1) + public + { + // Setup + cheats.assume(borrowAmt > borrow1); + cheats.assume(MAX_LEVERAGE.mul(depositAmt) > borrowAmt); + + // Lending Pool + address lender = address(5); + erc20.mint(lender, borrowAmt); + cheats.startPrank(lender); + erc20.approve(address(lErc20), type(uint).max); + lErc20.deposit(borrowAmt, lender); + cheats.stopPrank(); + + deposit(borrower, account, address(erc20), depositAmt); + + // Borrow1 + cheats.startPrank(borrower); + if (lErc20.previewDeposit(borrow1) > 0) + accountManager.borrow(account, address(erc20), borrow1); + cheats.stopPrank(); + + cheats.roll(block.number + 10); + erc20.mint(account, type(uint128).max); + + // Borrow2 + cheats.startPrank(borrower); + if (lErc20.previewDeposit(borrowAmt - borrow1) > 0) + accountManager.borrow(account, address(erc20), borrowAmt - borrow1); + cheats.stopPrank(); + cheats.roll(block.number + 10); + + cheats.prank(borrower); + accountManager.repay(account, address(erc20), type(uint).max); + + assertEq(riskEngine.getBorrows(account), 0); + assertEq(lErc20.getBorrows(), 0); + } + + // Borrow1 - Borrow2 - Repay1 - Repay Max + function testRepayInParts3( + uint96 depositAmt, + uint96 borrowAmt, + uint96 borrow1, + uint96 repayAmt + ) + public + { + // Setup + cheats.assume(borrowAmt > repayAmt); + cheats.assume(borrowAmt > borrow1); + cheats.assume(MAX_LEVERAGE.mul(depositAmt) > borrowAmt); + + // Lending Pool + address lender = address(5); + erc20.mint(lender, borrowAmt); + cheats.startPrank(lender); + erc20.approve(address(lErc20), type(uint).max); + lErc20.deposit(borrowAmt, lender); + cheats.stopPrank(); + + deposit(borrower, account, address(erc20), depositAmt); + + // Borrow1 + cheats.startPrank(borrower); + if (lErc20.previewDeposit(borrow1) > 0) + accountManager.borrow(account, address(erc20), borrow1); + cheats.stopPrank(); + + cheats.roll(block.number + 10); + erc20.mint(account, type(uint128).max); + + // Borrow2 + cheats.startPrank(borrower); + if (lErc20.previewDeposit(borrowAmt - borrow1) > 0) + accountManager.borrow(account, address(erc20), borrowAmt - borrow1); + cheats.stopPrank(); + cheats.roll(block.number + 10); + + // Repay1 + cheats.prank(borrower); + accountManager.repay(account, address(erc20), repayAmt); + cheats.roll(block.number + 10); + + // Max Repay + cheats.prank(borrower); + accountManager.repay(account, address(erc20), type(uint).max); + + assertEq(riskEngine.getBorrows(account), 0); + assertEq(lErc20.getBorrows(), 0); + } + + // Borrow1 - Repay1 - Borrow2 - Repay Max + function testRepayInParts4( + uint96 depositAmt, + uint96 borrowAmt, + uint96 borrow1 + ) + public + { + // Setup + cheats.assume(MAX_LEVERAGE.mul(depositAmt) > borrowAmt); + cheats.assume(borrowAmt > borrow1); + + uint repayAmt = borrow1 / 2; + + // Lending Pool + address lender = address(5); + erc20.mint(lender, borrowAmt); + cheats.startPrank(lender); + erc20.approve(address(lErc20), type(uint).max); + lErc20.deposit(borrowAmt, lender); + cheats.stopPrank(); + + deposit(borrower, account, address(erc20), depositAmt); + + // Borrow1 + cheats.startPrank(borrower); + if (lErc20.previewDeposit(borrow1) > 0) + accountManager.borrow(account, address(erc20), borrow1); + cheats.stopPrank(); + + cheats.roll(block.number + 10); + erc20.mint(account, type(uint128).max); + + // Repay1 + cheats.prank(borrower); + accountManager.repay(account, address(erc20), repayAmt); + cheats.roll(block.number + 10); + + // Borrow2 + cheats.startPrank(borrower); + if (lErc20.previewDeposit(borrowAmt - borrow1) > 0) + accountManager.borrow(account, address(erc20), borrowAmt - borrow1); + cheats.stopPrank(); + cheats.roll(block.number + 10); + + // Max Repay + cheats.prank(borrower); + accountManager.repay(account, address(erc20), type(uint).max); + + assertEq(riskEngine.getBorrows(account), 0); + assertEq(lErc20.getBorrows(), 0); + } +} \ No newline at end of file diff --git a/src/test/account/Reserves.t.sol b/src/test/account/Reserves.t.sol new file mode 100644 index 0000000..a492422 --- /dev/null +++ b/src/test/account/Reserves.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import {TestBase} from "../utils/TestBase.sol"; +import {PRBMathUD60x18} from "prb-math/PRBMathUD60x18.sol"; + +contract ReserveTests is TestBase { + using PRBMathUD60x18 for uint; + + address public account; + address lp = cheats.addr(100); + address borrower = cheats.addr(101); + + function setUp() public { + setupContracts(); + account = openAccount(borrower); + } + + function testReserves(uint96 deposit, uint96 borrow) public { + cheats.assume(borrow > 0); + cheats.assume(MAX_LEVERAGE.mul(deposit) > borrow); + + // LP deposits assets + erc20.mint(lp, borrow); + erc20.mint(borrower, deposit); + cheats.startPrank(lp); + erc20.approve(address(lErc20), borrow); + uint shares = lErc20.deposit(borrow, lp); + cheats.stopPrank(); + + // Borrower borrows and max repays after 100 blocks + cheats.startPrank(borrower); + erc20.approve(address(accountManager), deposit); + accountManager.deposit(account, address(erc20), deposit); + accountManager.borrow(account, address(erc20), borrow); + cheats.roll(block.number + 100); + accountManager.repay(account, address(erc20), type(uint).max); + cheats.stopPrank(); + + // LP removes all liq + cheats.prank(lp); + lErc20.redeem(shares, lp, lp); + + assertEq(erc20.balanceOf(address(lErc20)), lErc20.getReserves()); + } + + function testReserves2(uint96 deposit, uint96 borrow) public { + cheats.assume(borrow > 0); + cheats.assume(MAX_LEVERAGE.mul(deposit) > borrow); + + // LP deposits assets + erc20.mint(lp, borrow); + erc20.mint(borrower, deposit); + cheats.startPrank(lp); + erc20.approve(address(lErc20), borrow); + uint shares = lErc20.deposit(borrow, lp); + cheats.stopPrank(); + + // Borrower borrows and max repays after 100 blocks + cheats.startPrank(borrower); + erc20.approve(address(accountManager), deposit); + accountManager.deposit(account, address(erc20), deposit); + accountManager.borrow(account, address(erc20), borrow); + cheats.roll(block.number + 100); + accountManager.repay(account, address(erc20), type(uint).max); + cheats.stopPrank(); + + // Redeem Reserves + lErc20.redeemReserves(lErc20.getReserves()); + + // LP removes all liq + cheats.prank(lp); + lErc20.redeem(shares, lp, lp); + + // assertEq(erc20.balanceOf(address(lErc20)), lErc20.getReserves()); + } +} + diff --git a/src/test/tokens/LToken.t.sol b/src/test/tokens/LToken.t.sol index 797303d..1dd167c 100644 --- a/src/test/tokens/LToken.t.sol +++ b/src/test/tokens/LToken.t.sol @@ -56,9 +56,7 @@ contract LTokenTest is TestBase { // Test cheats.startPrank(address(accountManager)); - bool isBorrowBalanceZero = lErc20.collectFrom( - account, collectAmt, lErc20.convertToShares(collectAmt) - ); + bool isBorrowBalanceZero = lErc20.collectFrom(account, collectAmt); cheats.stopPrank(); uint borrowBalance = lErc20.getBorrowBalance(account); @@ -77,16 +75,14 @@ contract LTokenTest is TestBase { // Test cheats.startPrank(address(accountManager)); - lErc20.collectFrom( - account, collectAmt, lErc20.convertToShares(collectAmt) - ); + lErc20.collectFrom(account, collectAmt); cheats.stopPrank(); } function testCollectFromAuthError(uint lendAmt) public { // Test cheats.expectRevert(Errors.AccountManagerOnly.selector); - lErc20.collectFrom(account, lendAmt, 0); + lErc20.collectFrom(account, lendAmt); } function testGetBorrowBalance( diff --git a/src/test/utils/TestBase.sol b/src/test/utils/TestBase.sol index bcf8056..1eb2115 100644 --- a/src/test/utils/TestBase.sol +++ b/src/test/utils/TestBase.sol @@ -30,16 +30,19 @@ contract TestBase is Test { uint lenderID = 5; address lender = cheats.addr(lenderID); + uint treasuryID = 420; + address treasury = cheats.addr(treasuryID); + // Test ERC20 Tokens WETH weth; TestERC20 erc20; // LTokens LEther lEthImplementation; - ILEther lEth; + LEther lEth; LToken lErc20Implementation; - ILToken lErc20; + LToken lErc20; // Core Contracts RiskEngine riskEngine; @@ -109,12 +112,12 @@ contract TestBase is Test { accountFactory = new AccountFactory(address(beacon)); lEthImplementation = new LEther(); - lEth = ILEther(address(new Proxy(address(lEthImplementation)))); - lEth.init(weth, "LEther", "LEth", registry, 0); + lEth = LEther(payable(address(new Proxy(address(lEthImplementation))))); + lEth.init(weth, "LEther", "LEth", registry, 1e17, treasury); lErc20Implementation = new LToken(); - lErc20 = ILToken(address(new Proxy(address(lErc20Implementation)))); - lErc20.init(erc20, "LTestERC20", "LERC20", registry, 0); + lErc20 = LToken(address(new Proxy(address(lErc20Implementation)))); + lErc20.init(erc20, "LTestERC20", "LERC20", registry, 1e17, treasury); } function register() private { diff --git a/src/tokens/LToken.sol b/src/tokens/LToken.sol index 431ceb7..9369b9f 100644 --- a/src/tokens/LToken.sol +++ b/src/tokens/LToken.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.10; import {Errors} from "../utils/Errors.sol"; +import {ERC4626} from "./utils/ERC4626.sol"; import {Pausable} from "../utils/Pausable.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; -import {ERC4626} from "./utils/ERC4626.sol"; +import {ILToken} from "../interface/tokens/ILToken.sol"; import {IRegistry} from "../interface/core/IRegistry.sol"; import {PRBMathUD60x18} from "prb-math/PRBMathUD60x18.sol"; import {IRateModel} from "../interface/core/IRateModel.sol"; @@ -13,8 +14,9 @@ import {IRateModel} from "../interface/core/IRateModel.sol"; @title Lending Token @notice Lending token with ERC4626 implementation */ -contract LToken is Pausable, ERC4626 { +contract LToken is Pausable, ERC4626, ILToken { using PRBMathUD60x18 for uint; + // using FixedPointMathLib for uint; /* -------------------------------------------------------------------------- */ /* STATE VARIABLES */ @@ -32,20 +34,34 @@ contract LToken is Pausable, ERC4626 { /// @notice Account Manager address public accountManager; - /// @notice Total amount of reserves - uint public reserves; + /// @notice Protocol Treasury + address public treasury; + + /// @notice unused + uint public borrowFeeRate; /// @notice Total amount of borrows uint public borrows; + /// @notice Cumulative borrow index + uint public borrowIndex; + + /// @notice Total amount of reserves + uint public reserves; + /// @notice Reserve Factor uint public reserveFactor; /// @notice Block number of when the state of the LToken was last updated uint public lastUpdated; + struct BorrowData { + uint index; + uint balance; + } + /// @notice Mapping of account to borrow amount - mapping (address => uint) public borrowsOf; + mapping (address => BorrowData) public borrowData; /* -------------------------------------------------------------------------- */ /* EVENTS */ @@ -74,13 +90,15 @@ contract LToken is Pausable, ERC4626 { @param _symbol Symbol of LToken @param _registry Address of Registry @param _reserveFactor Reserve Factor + @param _treasury Protocol Treasury */ function init( ERC20 _asset, string calldata _name, string calldata _symbol, IRegistry _registry, - uint _reserveFactor + uint _reserveFactor, + address _treasury ) external { if (initialized) revert Errors.ContractAlreadyInitialized(); initialized = true; @@ -88,6 +106,8 @@ contract LToken is Pausable, ERC4626 { initERC4626(_asset, _name, _symbol); registry = _registry; reserveFactor = _reserveFactor; + borrowIndex = 1e18; + treasury = _treasury; } /** @@ -113,9 +133,10 @@ contract LToken is Pausable, ERC4626 { returns (bool isFirstBorrow) { updateState(); - isFirstBorrow = (borrowsOf[account] == 0); - borrowsOf[account] += convertToShares(amt); + isFirstBorrow = (borrowData[account].balance == 0); borrows += amt; + borrowData[account].balance = getBorrowBalance(account) + amt; + borrowData[account].index = borrowIndex; asset.transfer(account, amt); return isFirstBorrow; } @@ -126,23 +147,15 @@ contract LToken is Pausable, ERC4626 { @param amt Amount of token to collect @return isNotInDebt Returns if the account has pending borrows or not */ - function collectFrom(address account, uint amt, uint shares) + function collectFrom(address account, uint amt) external accountManagerOnly returns (bool) { borrows -= amt; - borrowsOf[account] -= shares; - return (borrowsOf[account] == 0); - } - - /** - @notice Returns Borrow balance of given account - @param account Address of account - @return borrowBalance Amount of underlying tokens borrowed - */ - function getBorrowBalance(address account) external view returns (uint) { - return previewRedeem(borrowsOf[account]); + borrowData[account].balance = getBorrowBalance(account) - amt; + borrowData[account].index = borrowIndex; + return (borrowData[account].balance == 0); } /* -------------------------------------------------------------------------- */ @@ -151,26 +164,47 @@ contract LToken is Pausable, ERC4626 { /** @notice Returns total amount of underlying assets - totalAssets = underlying balance + totalBorrows - totalReservers + delta - delta = totalBorrows * RateFactor * (1e18 - reserveFactor) + totalAssets = liquidity + totalBorrows - totalReserves @return totalAssets Total amount of underlying assets */ function totalAssets() public view override returns (uint) { - uint delta = (borrows == 0 || lastUpdated == block.number) ? 0 - : borrows.mul(_getRateFactor()).mul(1e18 - reserveFactor); - return asset.balanceOf(address(this)) + borrows - reserves + delta; + return asset.balanceOf(address(this)) + getBorrows() - getReserves(); + } + + /// @notice Current total borrows owed to the pool + function getBorrows() public view returns (uint) { + return borrows.mul(1e18 + getRateFactor()); + } + + /// @notice Current total reserves in the pool + function getReserves() public view returns (uint) { + return reserves.mul(1e18 + getRateFactor()); } /// @notice Updates state of the lending pool function updateState() public { if (lastUpdated == block.number) return; - uint rateFactor = _getRateFactor(); + uint rateFactor = getRateFactor(); uint interestAccrued = borrows.mul(rateFactor); borrows += interestAccrued; reserves += interestAccrued.mul(reserveFactor); + borrowIndex += borrowIndex.mul(rateFactor); lastUpdated = block.number; } + /** + @notice Returns Borrow balance of given account + @param account Address of account + @return borrowBalance Amount of underlying tokens borrowed + */ + function getBorrowBalance(address account) public view returns (uint) { + uint balance = borrowData[account].balance; + return (balance == 0) ? 0 : + (borrowIndex.mul(1e18 + getRateFactor())) + .div(borrowData[account].index) + .mul(balance); + } + /* -------------------------------------------------------------------------- */ /* INTERNAL FUNCTIONS */ /* -------------------------------------------------------------------------- */ @@ -179,17 +213,17 @@ contract LToken is Pausable, ERC4626 { @dev Rate Factor = Block Delta * Interest Rate Per Block Block Delta = Number of blocks since last update */ - function _getRateFactor() internal view returns (uint) { - return (block.number - lastUpdated).fromUint() - .mul(rateModel.getBorrowRatePerBlock( + function getRateFactor() internal view returns (uint) { + uint blockDelta = block.number - lastUpdated; + return (blockDelta == 0) ? 0 : (blockDelta * 1e18).mul( + rateModel.getBorrowRatePerBlock( asset.balanceOf(address(this)), - borrows, - reserves + borrows ) ); } - function afterDeposit(uint, uint) internal override { updateState(); } + function beforeDeposit(uint, uint) internal override { updateState(); } function beforeWithdraw(uint, uint) internal override { updateState(); } /* -------------------------------------------------------------------------- */ @@ -197,15 +231,18 @@ contract LToken is Pausable, ERC4626 { /* -------------------------------------------------------------------------- */ /** - @notice Transfers reserves from the LP to the specified address + @notice Transfers reserves from the LP to the treasury @dev Emits ReservesRedeemed(to, amt) - @param to Recipient address @param amt Amount of token to transfer */ - function redeemReserves(address to, uint amt) external adminOnly { + function redeemReserves(uint amt) external adminOnly { updateState(); reserves -= amt; - emit ReservesRedeemed(to, amt); - asset.transfer(to, amt); + emit ReservesRedeemed(treasury, amt); + asset.transfer(treasury, amt); + } + + function setBorrowFeeRate(uint _borrowFeeRate) external adminOnly { + borrowFeeRate = _borrowFeeRate; } } \ No newline at end of file diff --git a/src/tokens/utils/ERC4626.sol b/src/tokens/utils/ERC4626.sol index 78f9ed4..9c27c2e 100644 --- a/src/tokens/utils/ERC4626.sol +++ b/src/tokens/utils/ERC4626.sol @@ -46,6 +46,8 @@ abstract contract ERC4626 is CustomERC20 { //////////////////////////////////////////////////////////////*/ function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) { + beforeDeposit(assets, shares); + // Check for rounding error since we round down in previewDeposit. require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); @@ -55,11 +57,11 @@ abstract contract ERC4626 is CustomERC20 { _mint(receiver, shares); emit Deposit(msg.sender, receiver, assets, shares); - - afterDeposit(assets, shares); } function mint(uint256 shares, address receiver) public virtual returns (uint256 assets) { + beforeDeposit(assets, shares); + assets = previewMint(shares); // No need to check for rounding error, previewMint rounds up. // Need to transfer before minting or ERC777s could reenter. @@ -68,8 +70,6 @@ abstract contract ERC4626 is CustomERC20 { _mint(receiver, shares); emit Deposit(msg.sender, receiver, assets, shares); - - afterDeposit(assets, shares); } function withdraw( @@ -181,5 +181,5 @@ abstract contract ERC4626 is CustomERC20 { function beforeWithdraw(uint256 assets, uint256 shares) internal virtual {} - function afterDeposit(uint256 assets, uint256 shares) internal virtual {} + function beforeDeposit(uint256 assets, uint256 shares) internal virtual {} }