├── .gitignore ├── .gitmodules ├── foundry.toml ├── script └── Pool.sol ├── test ├── PoolLens.t.sol ├── mocks │ └── ERC20.sol ├── Factory.t.sol └── Pool.t.sol ├── README.md └── src ├── PoolLens.sol ├── Factory.sol └── Pool.sol /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | cache/ 3 | .env -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /script/Pool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.17; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | contract PoolScript is Script { 7 | function setUp() public {} 8 | 9 | function run() public { 10 | vm.broadcast(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/PoolLens.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.17; 3 | 4 | import "../src/PoolLens.sol"; 5 | import "forge-std/Test.sol"; 6 | import "../src/Pool.sol"; 7 | import "./mocks/ERC20.sol"; 8 | 9 | contract PoolLensTest is Test, Pool("", "",IERC20(address(new MockERC20(1000, 18))), IERC20(address(new MockERC20(1000, 18))), 1e18, 0.8e18, 1e15, 1e15, 0.1e18, 0.4e18, 0.6e18) { 10 | 11 | PoolLens lens; 12 | 13 | function setUp() public { 14 | lens = new PoolLens(); 15 | } 16 | 17 | function testGetDebtOf() external { 18 | // we define these variables for the lens contract to access 19 | lastTotalDebt = 1e18; 20 | lastAccrueInterestTime = block.timestamp; 21 | debtSharesSupply = 1e18 + 1; 22 | debtSharesBalanceOf[address(1)] = 1e18; 23 | 24 | assertEq(lens.getDebtOf(address(this), address(1)), getDebtOf(debtSharesBalanceOf[address(1)], debtSharesSupply, lastTotalDebt)); 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /test/mocks/ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.17; 3 | 4 | contract MockERC20 { 5 | 6 | uint8 public immutable decimals; 7 | uint public immutable totalSupply; 8 | mapping (address => uint) public balanceOf; 9 | mapping (address => mapping (address => uint)) public allowance; 10 | 11 | constructor (uint supply, uint8 _decimals) { 12 | decimals = _decimals; 13 | balanceOf[msg.sender] = totalSupply = supply; 14 | } 15 | 16 | function transfer(address to, uint amount) public returns (bool) { 17 | balanceOf[msg.sender] -= amount; 18 | balanceOf[to] += amount; 19 | return true; 20 | } 21 | 22 | function approve(address spender, uint amount) public returns (bool) { 23 | allowance[msg.sender][spender] = amount; 24 | return true; 25 | } 26 | 27 | function transferFrom(address from, address to, uint amount) public returns (bool) { 28 | allowance[from][msg.sender] -= amount; 29 | 30 | balanceOf[from] -= amount; 31 | balanceOf[to] += amount; 32 | return true; 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Surge Protocol 2 | 3 | Foundry project containing Surge Protocol's Solidity smart contracts 4 | 5 | ## Pre-requisites 6 | 7 | * [forge](https://github.com/foundry-rs/foundry) ^0.2.0 8 | * [Surge protocol overview](https://medium.com/surge-fi/introduction-to-surge-protocol-overview-34cc828d7c50) 9 | 10 | ## Compile 11 | `forge build` 12 | 13 | ## Test 14 | `forge test` 15 | 16 | ## Coverage report 17 | `forge coverage` 18 | 19 | ## For reviewers 20 | 21 | * Only the Factory contract is deployed by the protocol deployer. Pool contracts are deployed by users via the Factory. 22 | * PoolLens is not considered in scope of reviews and is not mission critical. It is only meant from off-chain frontend consumption. 23 | * The Factory will be deployed on a number of EVM chains. It should at least be compatible with the top L2s (Optimism and Arbitrum) and the [top 10 EVM chains by TVL](https://defillama.com/chains/EVM). 24 | * Unexpected loss of funds due to the operator role should be considered high severity bugs. 25 | * Loan and collateral contracts are considered by the Pool contracts as trusted, ERC20-compliant and non-rebasing token contracts. It's the users' responsibility to choose pools with valid non-malicious token contracts. This include re-entrancy risk due to external calls to these 2 contracts. 26 | * Pool contracts should be compliant with the ERC20 token standard. 27 | * Lenders and borrowers should never suffer a precision loss higher than 1/1e18 of their expected return. 28 | * Pool deployers should not be able to set parameters that cause the pool to behave unexpectedly at a future date (e.g. causing lenders not to be able to withdraw or borrowers to repay/remove collateral). 29 | -------------------------------------------------------------------------------- /test/Factory.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/Factory.sol"; 6 | import "./mocks/ERC20.sol"; 7 | import "../src/Pool.sol"; 8 | 9 | contract FactoryTest is Test { 10 | 11 | Factory factory; 12 | 13 | function setUp() public { 14 | factory = new Factory(address(this), "G"); 15 | } 16 | 17 | function testInitialValues() external { 18 | assertEq(factory.operator(), address(this)); 19 | assertEq(factory.MAX_FEE_MANTISSA(), 0.2e18); 20 | } 21 | 22 | function testDeployPool() external { 23 | Pool pool = factory.deploySurgePool(IERC20(address(0)), IERC20(address(1)), 1, 1, 1, 1, 1, 1, 1); 24 | assertEq(factory.getPoolsLength(), 1); 25 | assertEq(address(factory.pools(0)), address(pool)); 26 | assertEq(factory.isPool(pool), true); 27 | assertEq(address(pool.COLLATERAL_TOKEN()), address(0)); 28 | assertEq(address(pool.LOAN_TOKEN()), address(1)); 29 | assertEq(pool.MAX_COLLATERAL_RATIO_MANTISSA(), 1); 30 | assertEq(pool.SURGE_MANTISSA(), 1); 31 | assertEq(pool.COLLATERAL_RATIO_FALL_DURATION(), 1); 32 | assertEq(pool.COLLATERAL_RATIO_RECOVERY_DURATION(), 1); 33 | assertEq(pool.MIN_RATE(), 1); 34 | assertEq(pool.SURGE_RATE(), 1); 35 | assertEq(pool.MAX_RATE(), 1); 36 | assertEq(pool.symbol(), "G0"); 37 | assertEq(pool.name(), "Surge G0 Pool"); 38 | 39 | for (uint i = 1; i <= 100; i++) { 40 | factory.deploySurgePool(IERC20(address(0)), IERC20(address(1)), 1, 1, 1, 1, 1, 1, 1); 41 | } 42 | 43 | assertEq(factory.getPoolsLength(), 101); 44 | Pool latestPool = factory.pools(100); 45 | assertEq(latestPool.symbol(), "G100"); 46 | assertEq(latestPool.name(), "Surge G100 Pool"); 47 | } 48 | 49 | function testFee() external { 50 | assertEq(factory.feeMantissa(), 0); 51 | factory.setFeeRecipient(address(1)); 52 | assertEq(factory.feeRecipient(), address(1)); 53 | factory.setFeeMantissa(1); 54 | assertEq(factory.feeMantissa(), 1); 55 | (address feeRecipient, uint fee) = factory.getFee(); 56 | assertEq(feeRecipient, address(1)); 57 | assertEq(fee, 1); 58 | } 59 | 60 | function testChangeOperator() external { 61 | assertEq(factory.operator(), address(this)); 62 | factory.setPendingOperator(address(1)); 63 | assertEq(factory.pendingOperator(), address(1)); 64 | vm.prank(address(1)); 65 | factory.acceptOperator(); 66 | assertEq(factory.operator(), address(1)); 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/PoolLens.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.17; 3 | 4 | import "./Pool.sol"; 5 | import "./Factory.sol"; 6 | 7 | contract PoolLens { 8 | 9 | function getUtilizationMantissa(address pool) public view returns (uint) { 10 | uint _totalDebt = Pool(pool).lastTotalDebt(); 11 | IERC20 _loanToken = IERC20(Pool(pool).LOAN_TOKEN()); 12 | uint _loanTokenBalance = IERC20(_loanToken).balanceOf(pool); 13 | uint _supplied = _loanTokenBalance + _totalDebt; 14 | if(_supplied == 0) return 0; 15 | return _totalDebt * 1e18 / _supplied; 16 | } 17 | 18 | function getBorrowRateMantissa(address pool) public view returns (uint) { 19 | uint _util = getUtilizationMantissa(pool); 20 | uint _surgeMantissa = Pool(pool).SURGE_MANTISSA(); 21 | uint MIN_RATE = Pool(pool).MIN_RATE(); 22 | uint SURGE_RATE = Pool(pool).SURGE_RATE(); 23 | uint MAX_RATE = Pool(pool).MAX_RATE(); 24 | if(_util <= _surgeMantissa) { 25 | uint ratePerUnit = (SURGE_RATE - MIN_RATE) * 1e18 / _surgeMantissa; 26 | return ratePerUnit * _util / 1e18 + MIN_RATE; 27 | } else { 28 | uint excessUtil = (_util - _surgeMantissa); 29 | uint ratePerExcessUnit = (MAX_RATE - SURGE_RATE) * 1e18 / (1e18 - _surgeMantissa); 30 | return (ratePerExcessUnit * excessUtil / 1e18) + SURGE_RATE; 31 | } 32 | } 33 | 34 | function getCurrentTotalDebt(address pool) public view returns (uint256) { 35 | uint _totalDebt = Pool(pool).lastTotalDebt(); 36 | if(_totalDebt == 0) return 0; 37 | uint _lastAccrueInterestTime = Pool(pool).lastAccrueInterestTime(); 38 | if(_lastAccrueInterestTime == block.timestamp) return _totalDebt; 39 | uint _borrowRate = getBorrowRateMantissa(pool); 40 | uint _timeDelta = block.timestamp - _lastAccrueInterestTime; 41 | uint _interest = _totalDebt * _borrowRate / 1e18 * _timeDelta / 365 days; 42 | return _totalDebt + _interest; 43 | } 44 | 45 | function getInvestmentOf(address pool, address user) external view returns (uint256) { 46 | uint _totalDebt = getCurrentTotalDebt(pool); 47 | IERC20 _loanToken = IERC20(Pool(pool).LOAN_TOKEN()); 48 | uint _loanTokenBalance = IERC20(_loanToken).balanceOf(pool); 49 | uint _totalSupply = Pool(pool).totalSupply(); 50 | if(_totalSupply == 0) return 0; 51 | return Pool(pool).balanceOf(user) * (_totalDebt + _loanTokenBalance) / _totalSupply; 52 | } 53 | 54 | function getDebtOf(address pool, address user) external view returns (uint) { 55 | uint _debtSharesSupply = Pool(pool).debtSharesSupply(); 56 | if (_debtSharesSupply == 0) return 0; 57 | uint _totalDebt = getCurrentTotalDebt(pool); 58 | uint _userDebtShares = Pool(pool).debtSharesBalanceOf(user); 59 | uint debt = _userDebtShares * _totalDebt / _debtSharesSupply; 60 | if(debt * _debtSharesSupply < _userDebtShares * _totalDebt) debt++; 61 | return debt; 62 | } 63 | 64 | function getCollateralRatioMantissa(address pool) external view returns (uint) { 65 | uint _lastAccrueInterestTime = Pool(pool).lastAccrueInterestTime(); 66 | uint _lastCollateralRatioMantissa = Pool(pool).lastCollateralRatioMantissa(); 67 | 68 | if(_lastAccrueInterestTime == block.timestamp) return _lastCollateralRatioMantissa; 69 | 70 | uint _util = getUtilizationMantissa(pool); 71 | uint _surgeMantissa = Pool(pool).SURGE_MANTISSA(); 72 | uint _maxCollateralRatioMantissa = Pool(pool).MAX_COLLATERAL_RATIO_MANTISSA(); 73 | uint _collateralRatioFallDuration = Pool(pool).COLLATERAL_RATIO_FALL_DURATION(); 74 | uint _collateralRatioRecoveryDuration = Pool(pool).COLLATERAL_RATIO_RECOVERY_DURATION(); 75 | 76 | if(_util <= _surgeMantissa) { 77 | if(_lastCollateralRatioMantissa == _maxCollateralRatioMantissa) return _lastCollateralRatioMantissa; 78 | uint timeDelta = block.timestamp - _lastAccrueInterestTime; 79 | uint speed = _maxCollateralRatioMantissa / _collateralRatioRecoveryDuration; 80 | uint change = timeDelta * speed; 81 | if(_lastCollateralRatioMantissa + change > _maxCollateralRatioMantissa) { 82 | return _maxCollateralRatioMantissa; 83 | } else { 84 | return _lastCollateralRatioMantissa + change; 85 | } 86 | } else { 87 | if(_lastCollateralRatioMantissa == 0) return 0; 88 | uint timeDelta = block.timestamp - _lastAccrueInterestTime; 89 | uint speed = _maxCollateralRatioMantissa / _collateralRatioFallDuration; 90 | uint change = timeDelta * speed; 91 | if(_lastCollateralRatioMantissa < change) { 92 | return 0; 93 | } else { 94 | return _lastCollateralRatioMantissa - change; 95 | } 96 | } 97 | } 98 | 99 | function getSuppliedLoanTokens(address pool) external view returns (uint) { 100 | return getCurrentTotalDebt(pool) + IERC20(Pool(pool).LOAN_TOKEN()).balanceOf(pool); 101 | } 102 | 103 | function getSupplyRateMantissa(address pool) external view returns (uint) { 104 | uint _fee = Factory(address(Pool(pool).FACTORY())).feeMantissa(); 105 | uint _borrowRate = getBorrowRateMantissa(pool); 106 | uint _util = getUtilizationMantissa(pool); 107 | uint oneMinusFee = 1e18 - _fee; 108 | uint rateToPool = _borrowRate * oneMinusFee / 1e18; 109 | return _util * rateToPool / 1e18; 110 | } 111 | 112 | } -------------------------------------------------------------------------------- /src/Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.17; 3 | 4 | import "./Pool.sol"; 5 | 6 | /// @title Factory 7 | /// @author Moaz Mohsen & Nour Haridy 8 | /// @notice This contract is responsible for deploying new pools and providing them with the current fee and fee recipient 9 | contract Factory { 10 | 11 | address public operator; 12 | address public pendingOperator; 13 | address public feeRecipient; 14 | uint public feeMantissa; 15 | uint public constant MAX_FEE_MANTISSA = 0.2e18; 16 | bytes16 private constant _SYMBOLS = "0123456789abcdef"; 17 | bytes32 public immutable POOL_SYMBOL_PREFIX; 18 | mapping (Pool => bool) public isPool; 19 | Pool[] public pools; 20 | 21 | constructor(address _operator, string memory _poolSymbolPrefix) { 22 | operator = _operator; 23 | POOL_SYMBOL_PREFIX = pack(_poolSymbolPrefix); 24 | } 25 | 26 | function pack(string memory unpacked) internal pure returns (bytes32 packed) { 27 | require (bytes(unpacked).length < 32); 28 | assembly { 29 | packed := mload (add (unpacked, 31)) 30 | } 31 | } 32 | 33 | function unpack(bytes32 packed) internal pure returns (string memory unpacked) { 34 | uint l = uint (packed >> 248); 35 | require (l < 32); 36 | unpacked = string (new bytes (l)); 37 | assembly { 38 | mstore (add (unpacked, 31), packed) // Potentially writes into unallocated memory, which is fine 39 | } 40 | } 41 | 42 | /// @notice Get the number of deployed pools 43 | /// @return uint number of deployed pools 44 | /// @dev Useful for iterating on all pools 45 | function getPoolsLength() external view returns (uint) { 46 | return pools.length; 47 | } 48 | 49 | /// @notice Set the fee recipient for all pools 50 | /// @param _feeRecipient address of the new fee recipient 51 | /// @dev Only callable by the operator 52 | function setFeeRecipient(address _feeRecipient) external { 53 | require(msg.sender == operator, "Factory: not operator"); 54 | require(_feeRecipient != address(0), "Factory: feeRecipient zero"); 55 | feeRecipient = _feeRecipient; 56 | } 57 | 58 | /// @notice Set the fee for all pools 59 | /// @param _feeMantissa the new fee amount in Mantissa (scaled by 1e18) 60 | /// @dev Only callable by the operator 61 | function setFeeMantissa(uint _feeMantissa) external { 62 | require(msg.sender == operator, "Factory: not operator"); 63 | require(_feeMantissa <= MAX_FEE_MANTISSA, "Factory: fee too high"); 64 | if(_feeMantissa > 0) require(feeRecipient != address(0), "Factory: fee recipient is zero address"); 65 | feeMantissa = _feeMantissa; 66 | } 67 | 68 | /// @notice Set a pending operator for the factory 69 | /// @param _pendingOperator address of the new pending operator 70 | /// @dev Only callable by the operator 71 | function setPendingOperator(address _pendingOperator) external { 72 | require(msg.sender == operator, "Factory: not operator"); 73 | pendingOperator = _pendingOperator; 74 | } 75 | 76 | /// @notice Accept the pending operator 77 | /// @dev Only callable by the pending operator 78 | function acceptOperator() external { 79 | require(msg.sender == pendingOperator, "Factory: not pending operator"); 80 | operator = pendingOperator; 81 | pendingOperator = address(0); 82 | } 83 | 84 | /// @notice Get the fee and fee recipient for all pools 85 | /// @return address of the fee recipient and the fee amount in Mantissa (scaled by 1e18) 86 | /// @dev Used by pools to access the current fee and fee recipient 87 | function getFee() external view returns (address, uint) { 88 | uint _feeMantissa = feeMantissa; 89 | if(_feeMantissa == 0) return (address(0), 0); 90 | return (feeRecipient, _feeMantissa); 91 | } 92 | 93 | /// @dev Return the log in base 10, rounded down, of a positive value. Returns 0 if given 0. 94 | function log10(uint256 value) internal pure returns (uint256) { 95 | uint256 result = 0; 96 | unchecked { 97 | if (value >= 10 ** 64) { 98 | value /= 10 ** 64; 99 | result += 64; 100 | } 101 | if (value >= 10 ** 32) { 102 | value /= 10 ** 32; 103 | result += 32; 104 | } 105 | if (value >= 10 ** 16) { 106 | value /= 10 ** 16; 107 | result += 16; 108 | } 109 | if (value >= 10 ** 8) { 110 | value /= 10 ** 8; 111 | result += 8; 112 | } 113 | if (value >= 10 ** 4) { 114 | value /= 10 ** 4; 115 | result += 4; 116 | } 117 | if (value >= 10 ** 2) { 118 | value /= 10 ** 2; 119 | result += 2; 120 | } 121 | if (value >= 10 ** 1) { 122 | result += 1; 123 | } 124 | } 125 | return result; 126 | } 127 | 128 | 129 | /// @dev Converts a `uint256` to its ASCII `string` decimal representation. 130 | function toString(uint256 value) internal pure returns (string memory) { 131 | unchecked { 132 | uint256 length = log10(value) + 1; 133 | string memory buffer = new string(length); 134 | uint256 ptr; 135 | /// @solidity memory-safe-assembly 136 | assembly { 137 | ptr := add(buffer, add(32, length)) 138 | } 139 | while (true) { 140 | ptr--; 141 | /// @solidity memory-safe-assembly 142 | assembly { 143 | mstore8(ptr, byte(mod(value, 10), _SYMBOLS)) 144 | } 145 | value /= 10; 146 | if (value == 0) break; 147 | } 148 | return buffer; 149 | } 150 | } 151 | 152 | /// @notice Deploy a new Surge pool 153 | /// @param _collateralToken address of the collateral token 154 | /// @param _loanToken address of the loan token 155 | /// @param _maxCollateralRatioMantissa the maximum collateral ratio in Mantissa (scaled by 1e18) 156 | /// @param _surgeMantissa the surge utilization threshold in Mantissa (scaled by 1e18) 157 | /// @param _collateralRatioFallDuration the duration of the collateral ratio fall in seconds 158 | /// @param _collateralRatioRecoveryDuration the duration of the collateral ratio recovery in seconds 159 | /// @param _minRateMantissa the minimum interest rate in Mantissa (scaled by 1e18) 160 | /// @param _surgeRateMantissa the interest rate at the surge threshold in Mantissa (scaled by 1e18) 161 | /// @param _maxRateMantissa the maximum interest rate in Mantissa (scaled by 1e18) 162 | /// @return Pool the address of the deployed pool 163 | function deploySurgePool( 164 | IERC20 _collateralToken, 165 | IERC20 _loanToken, 166 | uint _maxCollateralRatioMantissa, 167 | uint _surgeMantissa, 168 | uint _collateralRatioFallDuration, 169 | uint _collateralRatioRecoveryDuration, 170 | uint _minRateMantissa, 171 | uint _surgeRateMantissa, 172 | uint _maxRateMantissa 173 | ) external returns (Pool) { 174 | string memory poolNumberString = toString(pools.length); 175 | string memory prefix = unpack(POOL_SYMBOL_PREFIX); 176 | Pool pool = new Pool( 177 | string(abi.encodePacked(prefix, poolNumberString)), 178 | string(abi.encodePacked("Surge ", prefix, poolNumberString, " Pool")), 179 | _collateralToken, 180 | _loanToken, 181 | _maxCollateralRatioMantissa, 182 | _surgeMantissa, 183 | _collateralRatioFallDuration, 184 | _collateralRatioRecoveryDuration, 185 | _minRateMantissa, 186 | _surgeRateMantissa, 187 | _maxRateMantissa 188 | ); 189 | isPool[pool] = true; 190 | emit PoolDeployed(pools.length, address(pool), address(_collateralToken), address(_loanToken), _maxCollateralRatioMantissa, _surgeMantissa, _collateralRatioFallDuration, _collateralRatioRecoveryDuration, _minRateMantissa, _surgeRateMantissa, _maxRateMantissa); 191 | pools.push(pool); 192 | return pool; 193 | } 194 | event PoolDeployed(uint poolId, address pool, address indexed collateralToken, address indexed loanToken, uint indexed maxCollateralRatioMantissa, uint surgeMantissa, uint collateralRatioFallDuration, uint collateralRatioRecoveryDuration, uint minRateMantissa, uint surgeRateMantissa, uint maxRateMantissa); 195 | } -------------------------------------------------------------------------------- /test/Pool.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/Pool.sol"; 6 | import "../src/Factory.sol"; 7 | import "../src/PoolLens.sol"; 8 | import "./mocks/ERC20.sol"; 9 | 10 | contract PoolTest is Test { 11 | 12 | Factory factory; 13 | PoolLens lens; 14 | 15 | function setUp() public { 16 | factory = new Factory(address(this), "G"); 17 | lens = new PoolLens(); 18 | } 19 | 20 | function testDepositWithdraw() external { 21 | uint amount = 1e36; 22 | MockERC20 collateralToken = new MockERC20(0, 18); 23 | MockERC20 loanToken = new MockERC20(amount, 18); 24 | Pool pool = factory.deploySurgePool(IERC20(address(collateralToken)), IERC20(address(loanToken)), 1e18, 0.8e18, 1e15, 1e15, 0.1e18, 0.4e18, 0.6e18); 25 | loanToken.approve(address(pool), amount); 26 | pool.deposit(amount); 27 | assertEq(loanToken.balanceOf(address(pool)), amount); 28 | assertEq(loanToken.balanceOf(address(this)), 0); 29 | assertEq(pool.balanceOf(address(this)), amount); 30 | assertEq(pool.totalSupply(), amount + pool.MINIMUM_LIQUIDITY()); 31 | vm.warp(block.timestamp + 30 minutes); 32 | pool.withdraw(type(uint).max); 33 | assertEq(loanToken.balanceOf(address(pool)), pool.MINIMUM_LIQUIDITY()); 34 | assertEq(loanToken.balanceOf(address(this)), amount - pool.MINIMUM_LIQUIDITY()); 35 | assertEq(pool.balanceOf(address(this)), 0); 36 | assertEq(pool.totalSupply(), pool.MINIMUM_LIQUIDITY()); 37 | } 38 | 39 | function testDepositWithdrawAll() public { 40 | uint amount = 1000000; // 1 usdc 41 | MockERC20 collateralToken = new MockERC20(0, 18); 42 | MockERC20 loanToken = new MockERC20(1001001, 18); 43 | Pool pool = factory.deploySurgePool(IERC20(address(collateralToken)), IERC20(address(loanToken)), 1e18, 0.8e18, 1e15, 1e15, 0.1e18, 0.4e18, 0.6e18); 44 | loanToken.approve(address(pool), amount); 45 | loanToken.transfer(address(1), 1000); // 0.1 cents 46 | vm.prank(address(1)); 47 | loanToken.approve(address(pool), 1000); 48 | vm.prank(address(1)); 49 | pool.deposit(1000); 50 | pool.deposit(amount); 51 | assertEq(pool.balanceOf(address(this)), amount * 2); 52 | loanToken.transfer(address(pool), 1); // to trigger x * y % denom > 0 53 | vm.warp(block.timestamp + 30 minutes); 54 | pool.withdraw(type(uint).max); 55 | assertEq(pool.balanceOf(address(this)), 0); 56 | assertEq(loanToken.balanceOf(address(this)), amount); 57 | } 58 | 59 | function testInterestAccrual() external { 60 | uint amount = 1e36; 61 | uint borrowAmount = amount / 2; 62 | MockERC20 collateralToken = new MockERC20(amount, 18); 63 | MockERC20 loanToken = new MockERC20(amount * 2, 18); 64 | Pool pool = factory.deploySurgePool(IERC20(address(collateralToken)), IERC20(address(loanToken)), 1e18, 0.5e18, 1e15, 1e15, 0.1e18, 0.4e18, 0.6e18); 65 | loanToken.approve(address(pool), type(uint).max); 66 | collateralToken.approve(address(pool), type(uint).max); 67 | pool.deposit(amount); 68 | pool.addCollateral(address(this), borrowAmount); 69 | pool.borrow(borrowAmount / 2); 70 | if(borrowAmount == 0) return; 71 | assertApproxEqAbs(pool.lastTotalDebt(), borrowAmount / 2, 1, "a"); 72 | assertEq(pool.debtSharesBalanceOf(address(this)), pool.lastTotalDebt(), "b"); 73 | pool.borrow(borrowAmount / 2); 74 | assertApproxEqAbs(pool.lastTotalDebt(), borrowAmount, 1, "c"); 75 | assertEq(pool.debtSharesBalanceOf(address(this)), pool.lastTotalDebt(), "d"); 76 | vm.warp(block.timestamp + 365 days); 77 | pool.withdraw(0); // accrues interest 78 | uint debt = pool.lastTotalDebt(); 79 | uint expectedDebt = borrowAmount + (borrowAmount * 0.4e18 * 365 days / (365 days * 1e18)); 80 | assertApproxEqAbs(debt, expectedDebt, 5, "e"); 81 | pool.repay(address(this), debt); 82 | vm.warp(block.timestamp + 30 minutes); 83 | pool.withdraw(debt); 84 | pool.removeCollateral(borrowAmount); 85 | } 86 | 87 | function testInterestAccrual20Util() external { 88 | MockERC20 collateralToken = new MockERC20(1000e18, 18); 89 | MockERC20 loanToken = new MockERC20(1000e18, 18); 90 | Pool pool = factory.deploySurgePool(IERC20(address(collateralToken)), IERC20(address(loanToken)), 1e18, 0.8e18, 1e15, 1e15, 0.1e18, 0.4e18, 0.6e18); 91 | loanToken.approve(address(pool), 1000e18); 92 | collateralToken.approve(address(pool), 1000e18); 93 | pool.deposit(100e18); 94 | pool.addCollateral(address(this), 20e18); 95 | pool.borrow(20e18); 96 | vm.warp(block.timestamp + 365 days); 97 | pool.repay(address(this), 23.5e18); 98 | assertEq(pool.lastTotalDebt(), 0); 99 | pool.withdraw(type(uint).max); 100 | pool.removeCollateral(20e18); 101 | } 102 | 103 | function testInterestAccrual90Util() external { 104 | MockERC20 collateralToken = new MockERC20(1000e18, 18); 105 | MockERC20 loanToken = new MockERC20(1000e18, 18); 106 | Pool pool = factory.deploySurgePool(IERC20(address(collateralToken)), IERC20(address(loanToken)), 1e18, 0.8e18, 1e15, 1e15, 0.1e18, 0.4e18, 0.6e18); 107 | loanToken.approve(address(pool), 1000e18); 108 | collateralToken.approve(address(pool), 1000e18); 109 | pool.deposit(125e18); 110 | vm.warp(block.timestamp + 30 minutes); 111 | pool.addCollateral(address(this), 90e18); 112 | pool.borrow(90e18); 113 | pool.withdraw(25e18); 114 | vm.warp(block.timestamp + 365 days); 115 | pool.repay(address(this), 135e18); 116 | assertEq(pool.lastTotalDebt(), 0); 117 | pool.withdraw(type(uint).max); 118 | pool.removeCollateral(90e18); 119 | } 120 | 121 | function testInterestAccrual100Util() external { 122 | MockERC20 collateralToken = new MockERC20(1000e18, 18); 123 | MockERC20 loanToken = new MockERC20(1000e18, 18); 124 | Pool pool = factory.deploySurgePool(IERC20(address(collateralToken)), IERC20(address(loanToken)), 1e18, 0.8e18, 1e15, 1e15, 0.1e18, 0.4e18, 0.6e18); 125 | loanToken.approve(address(pool), 1000e18); 126 | collateralToken.approve(address(pool), 1000e18); 127 | pool.deposit(125e18); 128 | vm.warp(block.timestamp + 30 minutes); 129 | pool.addCollateral(address(this), 100e18); 130 | pool.borrow(100e18); 131 | pool.withdraw(25e18); 132 | vm.warp(block.timestamp + 365 days); 133 | pool.repay(address(this), 160e18); 134 | assertEq(pool.lastTotalDebt(), 0); 135 | vm.warp(block.timestamp + 30 minutes); 136 | pool.withdraw(type(uint).max); 137 | pool.removeCollateral(100e18); 138 | } 139 | 140 | function testInterestAccrualFee() external { 141 | uint fee = 0.1e18; 142 | MockERC20 collateralToken = new MockERC20(1000e18, 18); 143 | MockERC20 loanToken = new MockERC20(1000e18, 18); 144 | Pool pool = factory.deploySurgePool(IERC20(address(collateralToken)), IERC20(address(loanToken)), 1e18, 0.8e18, 1e15, 1e15, 1e18, 1e18, 1e18); 145 | loanToken.approve(address(pool), 1000e18); 146 | collateralToken.approve(address(pool), 1000e18); 147 | factory.setFeeRecipient(address(1)); 148 | factory.setFeeMantissa(fee); 149 | pool.deposit(100e18); 150 | pool.addCollateral(address(this), 20e18); 151 | pool.borrow(20e18); 152 | vm.warp(block.timestamp + 365 days); 153 | vm.roll(block.number + 1); 154 | pool.withdraw(0); 155 | assertEq(pool.lastTotalDebt(), 40e18); 156 | vm.prank(address(1)); 157 | pool.withdraw(type(uint).max); 158 | assertApproxEqAbs(loanToken.balanceOf(address(1)), 2e18, 1); 159 | assertEq(pool.balanceOf(address(this)), 100e18); 160 | } 161 | 162 | function testBorrow() external { 163 | uint amount = 1e36; 164 | uint borrowAmount = amount / 2; 165 | MockERC20 collateralToken = new MockERC20(amount, 18); 166 | MockERC20 loanToken = new MockERC20(amount, 18); 167 | Pool pool = factory.deploySurgePool(IERC20(address(collateralToken)), IERC20(address(loanToken)), 1e18, 0.5e18, 1e15, 1e15, 0.1e18, 0.4e18, 0.6e18); 168 | loanToken.approve(address(pool), type(uint).max); 169 | collateralToken.approve(address(pool), type(uint).max); 170 | pool.deposit(amount); 171 | pool.addCollateral(address(this), borrowAmount); 172 | pool.borrow(borrowAmount); 173 | assertEq(pool.debtSharesBalanceOf(address(this)), borrowAmount); 174 | assertEq(pool.lastTotalDebt(), borrowAmount); 175 | pool.repay(address(this), borrowAmount); 176 | assertEq(pool.debtSharesBalanceOf(address(this)), 0); 177 | assertEq(pool.lastTotalDebt(), 0); 178 | vm.warp(block.timestamp + 30 minutes); 179 | pool.withdraw(borrowAmount); 180 | pool.removeCollateral(borrowAmount); 181 | } 182 | 183 | function testRepayAll() external { 184 | uint amount = 1e36; 185 | uint borrowAmount = amount / 2; 186 | MockERC20 collateralToken = new MockERC20(amount, 18); 187 | MockERC20 loanToken = new MockERC20(amount, 18); 188 | Pool pool = factory.deploySurgePool(IERC20(address(collateralToken)), IERC20(address(loanToken)), 1e18, 0.5e18, 1e15, 1e15, 0.1e18, 0.4e18, 0.6e18); 189 | loanToken.approve(address(pool), type(uint).max); 190 | collateralToken.approve(address(pool), type(uint).max); 191 | pool.deposit(amount); 192 | pool.addCollateral(address(this), borrowAmount); 193 | pool.borrow(borrowAmount); 194 | assertEq(pool.debtSharesBalanceOf(address(this)), borrowAmount); 195 | assertEq(pool.lastTotalDebt(), borrowAmount); 196 | pool.repay(address(this), type(uint).max); 197 | assertEq(pool.debtSharesBalanceOf(address(this)), 0); 198 | assertEq(pool.lastTotalDebt(), 0); 199 | vm.warp(block.timestamp + 30 minutes); 200 | pool.withdraw(borrowAmount); 201 | pool.removeCollateral(borrowAmount); 202 | } 203 | 204 | function testAddAndRemoveCollateral() external { 205 | uint amount = 1e18; 206 | uint borrowAmount = amount / 2; 207 | MockERC20 collateralToken = new MockERC20(amount, 18); 208 | MockERC20 loanToken = new MockERC20(amount, 18); 209 | Pool pool = factory.deploySurgePool(IERC20(address(collateralToken)), IERC20(address(loanToken)), 1e18, 0.5e18, 1e15, 1e15, 0.1e18, 0.4e18, 0.6e18); 210 | loanToken.approve(address(pool), type(uint).max); 211 | collateralToken.approve(address(pool), type(uint).max); 212 | pool.deposit(amount); 213 | pool.addCollateral(address(this), borrowAmount); 214 | pool.borrow(borrowAmount); 215 | vm.expectRevert(); 216 | pool.removeCollateral(borrowAmount); 217 | pool.repay(address(this), borrowAmount / 2); 218 | vm.expectRevert(); 219 | pool.removeCollateral((borrowAmount / 2) + 1); 220 | pool.removeCollateral(borrowAmount / 2); 221 | assertEq(pool.collateralBalanceOf(address(this)), borrowAmount / 2); 222 | vm.expectRevert(); 223 | pool.removeCollateral(1); 224 | } 225 | 226 | function testLiquidate() external { 227 | uint amount = 1e18; 228 | uint borrowAmount = amount / 2; 229 | MockERC20 collateralToken = new MockERC20(amount, 18); 230 | MockERC20 loanToken = new MockERC20(amount * 2, 18); 231 | Pool pool = factory.deploySurgePool(IERC20(address(collateralToken)), IERC20(address(loanToken)), 1e18, 0.5e18, 1e15, 1e15, 0.1e18, 0.4e18, 0.6e18); 232 | loanToken.approve(address(pool), type(uint).max); 233 | collateralToken.approve(address(pool), type(uint).max); 234 | pool.deposit(amount); 235 | pool.addCollateral(address(this), borrowAmount); 236 | pool.borrow(borrowAmount); 237 | vm.warp(block.timestamp + 365 days); 238 | vm.roll(block.number + 1); 239 | pool.withdraw(0); 240 | uint debtAmount = pool.lastTotalDebt(); 241 | pool.liquidate(address(this), debtAmount); 242 | assertEq(pool.lastTotalDebt(), 0); 243 | assertEq(pool.collateralBalanceOf(address(this)), 0); 244 | } 245 | 246 | function testLiquidateAll() external { 247 | uint amount = 1e18; 248 | uint borrowAmount = amount / 2; 249 | MockERC20 collateralToken = new MockERC20(amount, 18); 250 | MockERC20 loanToken = new MockERC20(amount * 2, 18); 251 | Pool pool = factory.deploySurgePool(IERC20(address(collateralToken)), IERC20(address(loanToken)), 1e18, 0.5e18, 1e15, 1e15, 0.1e18, 0.4e18, 0.6e18); 252 | loanToken.approve(address(pool), type(uint).max); 253 | collateralToken.approve(address(pool), type(uint).max); 254 | pool.deposit(amount); 255 | pool.addCollateral(address(this), borrowAmount); 256 | pool.borrow(borrowAmount); 257 | vm.warp(block.timestamp + 365 days); 258 | vm.warp(block.timestamp + 30 minutes); 259 | pool.withdraw(0); 260 | pool.liquidate(address(this), type(uint).max); 261 | assertEq(pool.lastTotalDebt(), 0); 262 | assertEq(pool.collateralBalanceOf(address(this)), 0); 263 | } 264 | 265 | function testSurgeRecovery() external { 266 | uint amount = 1e18; 267 | uint borrowAmount = amount / 2; 268 | MockERC20 collateralToken = new MockERC20(amount, 18); 269 | MockERC20 loanToken = new MockERC20(amount * 2, 18); 270 | Pool pool = factory.deploySurgePool(IERC20(address(collateralToken)), IERC20(address(loanToken)), 1e18, 0.5e18, 1e15, 1e15, 1000, 1000, 1000); 271 | loanToken.approve(address(pool), type(uint).max); 272 | collateralToken.approve(address(pool), type(uint).max); 273 | pool.deposit(amount); 274 | pool.addCollateral(address(this), borrowAmount); 275 | pool.borrow(borrowAmount); 276 | vm.warp(block.timestamp + 365 days); 277 | pool.withdraw(0); 278 | vm.warp(block.timestamp + (1e15 / 2)); 279 | pool.withdraw(0); 280 | assertEq(pool.lastCollateralRatioMantissa(), 0.5e18); 281 | vm.warp(block.timestamp + (1e15 / 2)); 282 | pool.withdraw(0); 283 | assertEq(pool.lastCollateralRatioMantissa(), 0); 284 | pool.repay(address(this), borrowAmount); 285 | vm.warp(block.timestamp + (1e15 / 2)); 286 | pool.withdraw(0); 287 | assertEq(pool.lastCollateralRatioMantissa(), 0.5e18); 288 | vm.warp(block.timestamp + (1e15 / 2)); 289 | pool.withdraw(0); 290 | assertEq(pool.lastCollateralRatioMantissa(), 1e18); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/Pool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.17; 3 | 4 | interface IERC20 { 5 | function balanceOf(address) external view returns(uint); 6 | function transferFrom(address sender, address recipient, uint256 amount) external returns(bool); 7 | function transfer(address, uint) external returns (bool); 8 | function decimals() external view returns (uint8); 9 | } 10 | 11 | interface IFactory { 12 | function getFee() external view returns (address to, uint feeMantissa); 13 | } 14 | 15 | /// @title Pool 16 | /// @author Moaz Mohsen & Nour Haridy 17 | /// @notice A Surge lending pool for a single collateral and loan token pair 18 | /// @dev This contract asssumes that the collateral and loan tokens are valid non-rebasing ERC20-compliant tokens 19 | contract Pool { 20 | 21 | IFactory public immutable FACTORY; 22 | IERC20 public immutable COLLATERAL_TOKEN; 23 | IERC20 public immutable LOAN_TOKEN; 24 | string public symbol; 25 | string public name; 26 | uint8 public constant decimals = 18; 27 | uint private constant RATE_CEILING = 100e18; // 10,000% borrow APR 28 | uint public constant MINIMUM_LIQUIDITY = 10 ** 3; 29 | uint public constant MIN_LOCKUP_DURATION = 30 minutes; 30 | uint public immutable MIN_RATE; 31 | uint public immutable SURGE_RATE; 32 | uint public immutable MAX_RATE; 33 | uint public immutable MAX_COLLATERAL_RATIO_MANTISSA; 34 | uint public immutable SURGE_MANTISSA; 35 | uint public immutable COLLATERAL_RATIO_FALL_DURATION; 36 | uint public immutable COLLATERAL_RATIO_RECOVERY_DURATION; 37 | bytes4 private constant TRANSFER_SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); 38 | bytes4 private constant TRANSFER_FROM_SELECTOR = bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); 39 | uint public lastCollateralRatioMantissa; 40 | uint public debtSharesSupply; 41 | mapping (address => uint) public debtSharesBalanceOf; 42 | uint public lastTotalDebt; 43 | uint public lastAccrueInterestTime; 44 | uint public totalSupply; 45 | mapping (address => mapping (address => uint)) public allowance; 46 | mapping (address => uint) public balanceOf; 47 | mapping (address => uint) public collateralBalanceOf; 48 | uint public lastLoanTokenBalance; 49 | mapping (address => uint) public lastDepositTimestamp; 50 | 51 | constructor( 52 | string memory _symbol, 53 | string memory _name, 54 | IERC20 _collateralToken, 55 | IERC20 _loanToken, 56 | uint _maxCollateralRatioMantissa, 57 | uint _surgeMantissa, 58 | uint _collateralRatioFallDuration, 59 | uint _collateralRatioRecoveryDuration, 60 | uint _minRateMantissa, 61 | uint _surgeRateMantissa, 62 | uint _maxRateMantissa 63 | ) { 64 | require(_collateralToken != _loanToken, "Pool: collateral and loan tokens are the same"); 65 | require(_collateralRatioFallDuration > 0, "Pool: _collateralRatioFallDuration too low"); 66 | require(_collateralRatioFallDuration <= _maxCollateralRatioMantissa, "Pool: _collateralRatioFallDuration too high"); 67 | require(_collateralRatioRecoveryDuration > 0, "Pool: _collateralRatioRecoveryDuration too low"); 68 | require(_collateralRatioRecoveryDuration <= _maxCollateralRatioMantissa, "Pool: _collateralRatioRecoveryDuration too high"); 69 | require(_maxCollateralRatioMantissa > 0, "Pool: _maxCollateralRatioMantissa too low"); 70 | require(_surgeMantissa < 1e18, "Pool: _surgeMantissa too high"); 71 | require(_minRateMantissa <= _surgeRateMantissa, "Pool: _minRateMantissa too high"); 72 | require(_surgeRateMantissa <= _maxRateMantissa, "Pool: _surgeRateMantissa too high"); 73 | require(_maxRateMantissa <= RATE_CEILING, "Pool: _maxRateMantissa too high"); 74 | symbol = _symbol; 75 | name = _name; 76 | FACTORY = IFactory(msg.sender); 77 | COLLATERAL_TOKEN = _collateralToken; 78 | LOAN_TOKEN = _loanToken; 79 | MAX_COLLATERAL_RATIO_MANTISSA = _maxCollateralRatioMantissa; 80 | SURGE_MANTISSA = _surgeMantissa; 81 | COLLATERAL_RATIO_FALL_DURATION = _collateralRatioFallDuration; 82 | COLLATERAL_RATIO_RECOVERY_DURATION = _collateralRatioRecoveryDuration; 83 | lastCollateralRatioMantissa = _maxCollateralRatioMantissa; 84 | MIN_RATE = _minRateMantissa; 85 | SURGE_RATE = _surgeRateMantissa; 86 | MAX_RATE = _maxRateMantissa; 87 | } 88 | 89 | function safeTransfer(IERC20 token, address to, uint value) internal { 90 | (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(TRANSFER_SELECTOR, to, value)); 91 | require(success && (data.length == 0 || abi.decode(data, (bool))), 'Pool: TRANSFER_FAILED'); 92 | } 93 | 94 | function safeTransferFrom(IERC20 token, address from, address to, uint value) internal { 95 | (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(TRANSFER_FROM_SELECTOR, from, to, value)); 96 | require(success && (data.length == 0 || abi.decode(data, (bool))), 'Pool: TRANSFER_FROM_FAILED'); 97 | } 98 | 99 | /// @notice Gets the current state of pool variables based on the current time 100 | /// @param _loanTokenBalance The current balance of the loan token in the pool 101 | /// @param _feeMantissa The fee to be charged on interest accrual 102 | /// @param _lastCollateralRatioMantissa The collateral ratio at the last interest accrual 103 | /// @param _totalSupply The total supply of pool tokens at the last interest accrual 104 | /// @param _lastAccrueInterestTime The last time interest was accrued 105 | /// @param _totalDebt The total debt of the pool at the last interest accrual 106 | /// @return _currentTotalSupply The current total supply of pool tokens 107 | /// @return _accruedFeeShares The accrued fee shares to be transferred to the fee recipient 108 | /// @return _currentCollateralRatioMantissa The current collateral ratio 109 | /// @return _currentTotalDebt The current total debt of the pool 110 | /// @dev This view function behaves as a pure function with the exception of immutable variables (which are constant) 111 | function getCurrentState( 112 | uint _loanTokenBalance, 113 | uint _feeMantissa, 114 | uint _lastCollateralRatioMantissa, 115 | uint _totalSupply, 116 | uint _lastAccrueInterestTime, 117 | uint _totalDebt 118 | ) internal view returns ( 119 | uint _currentTotalSupply, 120 | uint _accruedFeeShares, 121 | uint _currentCollateralRatioMantissa, 122 | uint _currentTotalDebt 123 | ) { 124 | 125 | // 1. Set default return values 126 | _currentTotalSupply = _totalSupply; 127 | _currentTotalDebt = _totalDebt; 128 | _currentCollateralRatioMantissa = _lastCollateralRatioMantissa; 129 | // _accruedFeeShares = 0; 130 | 131 | // 2. Get the time passed since the last interest accrual 132 | uint _timeDelta = block.timestamp - _lastAccrueInterestTime; 133 | 134 | // 3. If the time passed is 0, return the current values 135 | if(_timeDelta == 0) return (_currentTotalSupply, _accruedFeeShares, _currentCollateralRatioMantissa, _currentTotalDebt); 136 | 137 | // 4. Calculate the supplied value 138 | uint _supplied = _totalDebt + _loanTokenBalance; 139 | // 5. Calculate the utilization 140 | uint _util = getUtilizationMantissa(_totalDebt, _supplied); 141 | 142 | // 6. Calculate the collateral ratio 143 | _currentCollateralRatioMantissa = getCollateralRatioMantissa( 144 | _util, 145 | _lastAccrueInterestTime, 146 | block.timestamp, 147 | _lastCollateralRatioMantissa, 148 | COLLATERAL_RATIO_FALL_DURATION, 149 | COLLATERAL_RATIO_RECOVERY_DURATION, 150 | MAX_COLLATERAL_RATIO_MANTISSA, 151 | SURGE_MANTISSA 152 | ); 153 | 154 | // 7. If there is no debt, return the current values 155 | if(_totalDebt == 0) return (_currentTotalSupply, _accruedFeeShares, _currentCollateralRatioMantissa, _currentTotalDebt); 156 | 157 | // 8. Calculate the borrow rate 158 | uint _borrowRate = getBorrowRateMantissa(_util, SURGE_MANTISSA, MIN_RATE, SURGE_RATE, MAX_RATE); 159 | // 9. Calculate the interest 160 | uint _interest = _totalDebt * _borrowRate * _timeDelta / (365 days * 1e18); // does the optimizer optimize this? or should it be a constant? 161 | // 10. Update the total debt 162 | _currentTotalDebt += _interest; 163 | 164 | // 11. If there is no fee, return the current values 165 | if(_feeMantissa == 0) return (_currentTotalSupply, _accruedFeeShares, _currentCollateralRatioMantissa, _currentTotalDebt); 166 | // 12. Calculate the fee 167 | uint fee = _interest * _feeMantissa / 1e18; 168 | // 13. Calculate the accrued fee shares 169 | _accruedFeeShares = fee * _totalSupply / (_supplied + _interest - fee); // if supplied is 0, we will have returned at step 7 170 | // 14. Update the total supply 171 | _currentTotalSupply += _accruedFeeShares; 172 | } 173 | 174 | /// @notice Gets the current borrow rate in mantissa (scaled by 1e18) 175 | /// @param _util The utilization in mantissa (scaled by 1e18) 176 | /// @param _surgeMantissa The utilization at which the borrow rate will be at the surge rate in mantissa (scaled by 1e18) 177 | /// @param _minRateMantissa The minimum borrow rate at 0% utilization in mantissa (scaled by 1e18) 178 | /// @param _surgeRateMantissa The borrow rate at the surge utilization in mantissa (scaled by 1e18) 179 | /// @param _maxRateMantissa The maximum borrow rate at 100% utilization in mantissa (scaled by 1e18) 180 | /// @return uint The borrow rate in mantissa (scaled by 1e18) 181 | function getBorrowRateMantissa(uint _util, uint _surgeMantissa, uint _minRateMantissa, uint _surgeRateMantissa, uint _maxRateMantissa) internal pure returns (uint) { 182 | if(_util <= _surgeMantissa) { 183 | return (_surgeRateMantissa - _minRateMantissa) * 1e18 * _util / _surgeMantissa / 1e18 + _minRateMantissa; // is this optimized by the optimized? 184 | } else { 185 | uint excessUtil = _util - _surgeMantissa; 186 | return (_maxRateMantissa - _surgeRateMantissa) * 1e18 * excessUtil / (1e18 - _surgeMantissa) / 1e18 + _surgeRateMantissa; // is this optimized by the optimizer? 187 | } 188 | } 189 | 190 | /// @notice Gets the current pool utilization rate in mantissa (scaled by 1e18) 191 | /// @param _totalDebt The total debt of the pool 192 | /// @param _supplied The total supplied loan tokens of the pool 193 | /// @return uint The pool utilization rate in mantissa (scaled by 1e18) 194 | function getUtilizationMantissa(uint _totalDebt, uint _supplied) internal pure returns (uint) { 195 | if(_supplied == 0) return 0; 196 | return _totalDebt * 1e18 / _supplied; 197 | } 198 | 199 | /// @notice Converts a loan token amount to shares 200 | /// @param _tokenAmount The loan token amount to convert 201 | /// @param _supplied The total supplied loan tokens of the pool 202 | /// @param _sharesTotalSupply The total supply of shares of the pool 203 | /// @param roundUpCheck Whether to check and round up the shares amount 204 | /// @return uint The shares amount 205 | function tokenToShares(uint _tokenAmount, uint _supplied, uint _sharesTotalSupply, bool roundUpCheck) internal pure returns (uint) { 206 | if(_supplied == 0) return _tokenAmount; 207 | uint shares = _tokenAmount * _sharesTotalSupply / _supplied; 208 | if(roundUpCheck && shares * _supplied < _tokenAmount * _sharesTotalSupply) shares++; 209 | return shares; 210 | } 211 | 212 | /// @notice Gets the pool collateral ratio in mantissa (scaled by 1e18) 213 | /// @param _util The utilization in mantissa (scaled by 1e18) 214 | /// @param _lastAccrueInterestTime The last time the pool accrued interest 215 | /// @param _now The current time 216 | /// @param _lastCollateralRatioMantissa The last collateral ratio of the pool in mantissa (scaled by 1e18) 217 | /// @param _collateralRatioFallDuration The duration of the collateral ratio fall from max to 0 in seconds 218 | /// @param _collateralRatioRecoveryDuration The duration of the collateral ratio recovery from 0 to max in seconds 219 | /// @param _maxCollateralRatioMantissa The maximum collateral ratio of the pool in mantissa (scaled by 1e18) 220 | /// @param _surgeMantissa The utilization at which the surge threshold is triggered in mantissa (scaled by 1e18) 221 | /// @return uint The pool collateral ratio in mantissa (scaled by 1e18) 222 | function getCollateralRatioMantissa( 223 | uint _util, 224 | uint _lastAccrueInterestTime, 225 | uint _now, 226 | uint _lastCollateralRatioMantissa, 227 | uint _collateralRatioFallDuration, 228 | uint _collateralRatioRecoveryDuration, 229 | uint _maxCollateralRatioMantissa, 230 | uint _surgeMantissa 231 | ) internal pure returns (uint) { 232 | unchecked { 233 | if(_lastAccrueInterestTime == _now) return _lastCollateralRatioMantissa; 234 | 235 | // If utilization is less than or equal to surge, we are increasing collateral ratio 236 | if(_util <= _surgeMantissa) { 237 | // The collateral ratio can only increase if it is less than the max collateral ratio 238 | if(_lastCollateralRatioMantissa == _maxCollateralRatioMantissa) return _lastCollateralRatioMantissa; 239 | 240 | // If the collateral ratio can increase, we calculate the increase 241 | uint timeDelta = _now - _lastAccrueInterestTime; 242 | uint change = timeDelta * _maxCollateralRatioMantissa / _collateralRatioRecoveryDuration; 243 | 244 | // If the change in collateral ratio is greater than the max collateral ratio, we set the collateral ratio to the max collateral ratio 245 | if(_lastCollateralRatioMantissa + change >= _maxCollateralRatioMantissa) { 246 | return _maxCollateralRatioMantissa; 247 | } else { 248 | // Otherwise we increase the collateral ratio by the change 249 | return _lastCollateralRatioMantissa + change; 250 | } 251 | } else { 252 | // If utilization is greater than the surge, we are decreasing collateral ratio 253 | // The collateral ratio can only decrease if it is greater than 0 254 | if(_lastCollateralRatioMantissa == 0) return 0; 255 | 256 | // If the collateral ratio can decrease, we calculate the decrease 257 | uint timeDelta = _now - _lastAccrueInterestTime; 258 | uint change = timeDelta * _maxCollateralRatioMantissa / _collateralRatioFallDuration; 259 | 260 | // If the change in collateral ratio is greater than the collateral ratio, we set the collateral ratio to 0 261 | if(_lastCollateralRatioMantissa <= change) { 262 | return 0; 263 | } else { 264 | // Otherwise we decrease the collateral ratio by the change 265 | return _lastCollateralRatioMantissa - change; 266 | } 267 | } 268 | } 269 | } 270 | 271 | /// @notice Transfers pool tokens to the recipient 272 | /// @param to The address of the recipient 273 | /// @param amount The amount of pool tokens to transfer 274 | /// @return bool that indicates if the operation was successful 275 | function transfer(address to, uint amount) external returns (bool) { 276 | require(to != address(0), "Pool: to cannot be address 0"); 277 | require(lastDepositTimestamp[msg.sender] + MIN_LOCKUP_DURATION <= block.timestamp, "Pool: cannot transfer within lockup duration"); 278 | balanceOf[msg.sender] -= amount; 279 | unchecked { 280 | balanceOf[to] += amount; 281 | } 282 | emit Transfer(msg.sender, to, amount); 283 | return true; 284 | } 285 | 286 | /// @notice Transfers pool tokens on behalf of one address to another 287 | /// @param from The address of the sender 288 | /// @param to The address of the recipient 289 | /// @param amount The amount of pool tokens to transfer 290 | /// @return bool that indicates if the operation was successful 291 | function transferFrom(address from, address to, uint amount) external returns (bool) { 292 | require(to != address(0), "Pool: to cannot be address 0"); 293 | require(lastDepositTimestamp[from] + MIN_LOCKUP_DURATION <= block.timestamp, "Pool: cannot transfer within lockup duration"); 294 | if(from != msg.sender) { 295 | allowance[from][msg.sender] -= amount; 296 | } 297 | balanceOf[from] -= amount; 298 | unchecked { 299 | balanceOf[to] += amount; 300 | } 301 | emit Transfer(from, to, amount); 302 | return true; 303 | } 304 | 305 | /// @notice Approves an address to spend pool tokens on behalf of the sender 306 | /// @param spender The address of the spender 307 | /// @param amount The amount of pool tokens to approve 308 | /// @return bool that indicates if the operation was successful 309 | function approve(address spender, uint amount) external returns (bool) { 310 | allowance[msg.sender][spender] = amount; 311 | emit Approval(msg.sender, spender, amount); 312 | return true; 313 | } 314 | 315 | /// @notice Increases the allowance of an address to spend tokens on behalf of the sender 316 | /// @param spender The address of the spender 317 | /// @param addedValue The amount of tokens to increase the allowance by 318 | function increaseAllowance(address spender, uint addedValue) external returns (bool) { 319 | allowance[msg.sender][spender] += addedValue; 320 | emit Approval(msg.sender, spender, allowance[msg.sender][spender]); 321 | return true; 322 | } 323 | 324 | /// @notice Decreases the allowance of an address to spend tokens on behalf of the sender 325 | /// @param spender The address of the spender 326 | /// @param subtractedValue The amount of tokens to decrease the allowance by 327 | function decreaseAllowance(address spender, uint subtractedValue) external returns (bool) { 328 | allowance[msg.sender][spender] -= subtractedValue; 329 | emit Approval(msg.sender, spender, allowance[msg.sender][spender]); 330 | return true; 331 | } 332 | 333 | /// @notice Deposit loan tokens in exchange for pool tokens 334 | /// @param amount The amount of loan tokens to deposit 335 | function deposit(uint amount) external { 336 | (address _feeRecipient, uint _feeMantissa) = FACTORY.getFee(); 337 | ( 338 | uint _currentTotalSupply, 339 | uint _accruedFeeShares, 340 | uint _currentCollateralRatioMantissa, 341 | uint _currentTotalDebt 342 | ) = getCurrentState( 343 | lastLoanTokenBalance, 344 | _feeMantissa, 345 | lastCollateralRatioMantissa, 346 | totalSupply, 347 | lastAccrueInterestTime, 348 | lastTotalDebt 349 | ); 350 | 351 | if(_currentTotalSupply == 0) { 352 | _currentTotalSupply = MINIMUM_LIQUIDITY; 353 | balanceOf[address(0)] = MINIMUM_LIQUIDITY; 354 | emit Transfer(address(0), address(0), MINIMUM_LIQUIDITY); 355 | } 356 | 357 | uint _shares = tokenToShares(amount, (_currentTotalDebt + lastLoanTokenBalance), _currentTotalSupply, false); 358 | require(_shares > 0, "Pool: 0 shares"); 359 | _currentTotalSupply += _shares; 360 | 361 | // commit current state 362 | balanceOf[msg.sender] += _shares; 363 | totalSupply = _currentTotalSupply; 364 | // avoid recording accrue interest time if there is no change in total debt i.e. 0 interest 365 | if (lastTotalDebt != _currentTotalDebt) { 366 | lastTotalDebt = _currentTotalDebt; 367 | lastAccrueInterestTime = block.timestamp; 368 | } 369 | lastCollateralRatioMantissa = _currentCollateralRatioMantissa; 370 | emit Deposit(msg.sender, amount); 371 | emit Transfer(address(0), msg.sender, _shares); 372 | if(_accruedFeeShares > 0) { 373 | balanceOf[_feeRecipient] += _accruedFeeShares; 374 | emit Transfer(address(0), _feeRecipient, _accruedFeeShares); 375 | } 376 | lastDepositTimestamp[msg.sender] = block.timestamp; // commiting the last commit time for the user to prevent transfers within the lockup duration 377 | 378 | // interactions 379 | safeTransferFrom(LOAN_TOKEN, msg.sender, address(this), amount); 380 | 381 | // sync balance 382 | lastLoanTokenBalance = LOAN_TOKEN.balanceOf(address(this)); 383 | } 384 | 385 | /// @notice Withdraw loan tokens in exchange for pool tokens 386 | /// @param amount The amount of loan tokens to withdraw 387 | /// @dev If amount is type(uint).max, withdraws all loan tokens 388 | function withdraw(uint amount) external { 389 | require(lastDepositTimestamp[msg.sender] + MIN_LOCKUP_DURATION <= block.timestamp, "Pool: cannot transfer within lockup duration"); 390 | (address _feeRecipient, uint _feeMantissa) = FACTORY.getFee(); 391 | ( 392 | uint _currentTotalSupply, 393 | uint _accruedFeeShares, 394 | uint _currentCollateralRatioMantissa, 395 | uint _currentTotalDebt 396 | ) = getCurrentState( 397 | lastLoanTokenBalance, 398 | _feeMantissa, 399 | lastCollateralRatioMantissa, 400 | totalSupply, 401 | lastAccrueInterestTime, 402 | lastTotalDebt 403 | ); 404 | 405 | uint _shares; 406 | if (amount == type(uint).max) { 407 | amount = balanceOf[msg.sender] * (_currentTotalDebt + lastLoanTokenBalance) / _currentTotalSupply; 408 | _shares = balanceOf[msg.sender]; 409 | } else { 410 | _shares = tokenToShares(amount, (_currentTotalDebt + lastLoanTokenBalance), _currentTotalSupply, true); 411 | } 412 | _currentTotalSupply -= _shares; 413 | 414 | // commit current state 415 | balanceOf[msg.sender] -= _shares; 416 | totalSupply = _currentTotalSupply; 417 | // avoid recording accrue interest time if there is no change in total debt i.e. 0 interest 418 | if (lastTotalDebt != _currentTotalDebt) { 419 | lastTotalDebt = _currentTotalDebt; 420 | lastAccrueInterestTime = block.timestamp; 421 | } 422 | lastCollateralRatioMantissa = _currentCollateralRatioMantissa; 423 | emit Withdraw(msg.sender, amount); 424 | emit Transfer(msg.sender, address(0), _shares); 425 | if(_accruedFeeShares > 0) { 426 | balanceOf[_feeRecipient] += _accruedFeeShares; 427 | emit Transfer(address(0), _feeRecipient, _accruedFeeShares); 428 | } 429 | 430 | // interactions 431 | safeTransfer(LOAN_TOKEN, msg.sender, amount); 432 | 433 | // sync balance 434 | lastLoanTokenBalance = LOAN_TOKEN.balanceOf(address(this)); 435 | } 436 | 437 | /// @notice Deposit collateral tokens 438 | /// @param to The address to receive the collateral deposit 439 | /// @param amount The amount of collateral tokens to deposit 440 | function addCollateral(address to, uint amount) external { 441 | collateralBalanceOf[to] += amount; 442 | safeTransferFrom(COLLATERAL_TOKEN, msg.sender, address(this), amount); 443 | emit AddCollateral(to, msg.sender, amount); 444 | } 445 | 446 | /// @notice Gets the debt of a user 447 | /// @param _userDebtShares The amount of debt shares of the user 448 | /// @param _debtSharesSupply The total amount of debt shares 449 | /// @param _totalDebt The total amount of debt 450 | /// @return uint The debt of the user 451 | function getDebtOf(uint _userDebtShares, uint _debtSharesSupply, uint _totalDebt) internal pure returns (uint) { 452 | if (_debtSharesSupply == 0) return 0; 453 | uint debt = _userDebtShares * _totalDebt / _debtSharesSupply; 454 | if(debt * _debtSharesSupply < _userDebtShares * _totalDebt) debt++; 455 | return debt; 456 | } 457 | 458 | /// @notice Withdraw collateral tokens 459 | /// @param amount The amount of collateral tokens to withdraw 460 | function removeCollateral(uint amount) external { 461 | (address _feeRecipient, uint _feeMantissa) = FACTORY.getFee(); 462 | ( 463 | uint _currentTotalSupply, 464 | uint _accruedFeeShares, 465 | uint _currentCollateralRatioMantissa, 466 | uint _currentTotalDebt 467 | ) = getCurrentState( 468 | lastLoanTokenBalance, 469 | _feeMantissa, 470 | lastCollateralRatioMantissa, 471 | totalSupply, 472 | lastAccrueInterestTime, 473 | lastTotalDebt 474 | ); 475 | 476 | uint userDebt = getDebtOf(debtSharesBalanceOf[msg.sender], debtSharesSupply, _currentTotalDebt); 477 | if(userDebt > 0) { 478 | uint userCollateralRatioMantissa = userDebt * 1e36 / (collateralBalanceOf[msg.sender] - amount); 479 | require(userCollateralRatioMantissa <= (_currentCollateralRatioMantissa * 1e18), "Pool: user collateral ratio too high"); 480 | } 481 | 482 | // commit current state 483 | totalSupply = _currentTotalSupply; 484 | // avoid recording accrue interest time if there is no change in total debt i.e. 0 interest 485 | if (lastTotalDebt != _currentTotalDebt) { 486 | lastTotalDebt = _currentTotalDebt; 487 | lastAccrueInterestTime = block.timestamp; 488 | } 489 | lastCollateralRatioMantissa = _currentCollateralRatioMantissa; 490 | collateralBalanceOf[msg.sender] -= amount; 491 | emit RemoveCollateral(msg.sender, amount); 492 | if(_accruedFeeShares > 0) { 493 | balanceOf[_feeRecipient] += _accruedFeeShares; 494 | emit Transfer(address(0), _feeRecipient, _accruedFeeShares); 495 | } 496 | 497 | // interactions 498 | safeTransfer(COLLATERAL_TOKEN, msg.sender, amount); 499 | 500 | // sync balance 501 | lastLoanTokenBalance = LOAN_TOKEN.balanceOf(address(this)); 502 | } 503 | 504 | /// @notice Borrow loan tokens 505 | /// @param amount The amount of loan tokens to borrow 506 | function borrow(uint amount) external { 507 | (address _feeRecipient, uint _feeMantissa) = FACTORY.getFee(); 508 | ( 509 | uint _currentTotalSupply, 510 | uint _accruedFeeShares, 511 | uint _currentCollateralRatioMantissa, 512 | uint _currentTotalDebt 513 | ) = getCurrentState( 514 | lastLoanTokenBalance, 515 | _feeMantissa, 516 | lastCollateralRatioMantissa, 517 | totalSupply, 518 | lastAccrueInterestTime, 519 | lastTotalDebt 520 | ); 521 | 522 | uint _debtSharesSupply = debtSharesSupply; 523 | uint userDebt = getDebtOf(debtSharesBalanceOf[msg.sender], _debtSharesSupply, _currentTotalDebt) + amount; 524 | uint userCollateralRatioMantissa = userDebt * 1e36 / collateralBalanceOf[msg.sender]; 525 | require(userCollateralRatioMantissa <= (_currentCollateralRatioMantissa * 1e18), "Pool: user collateral ratio too high"); 526 | 527 | uint _newUtil = getUtilizationMantissa(_currentTotalDebt + amount, (_currentTotalDebt + lastLoanTokenBalance)); 528 | require(_newUtil <= SURGE_MANTISSA, "Pool: utilization too high"); 529 | 530 | uint _shares = tokenToShares(amount, _currentTotalDebt, _debtSharesSupply, true); 531 | _currentTotalDebt += amount; 532 | 533 | // commit current state 534 | debtSharesBalanceOf[msg.sender] += _shares; 535 | debtSharesSupply = _debtSharesSupply + _shares; 536 | totalSupply = _currentTotalSupply; 537 | // avoid recording accrue interest time if there is no change in total debt i.e. 0 interest 538 | if (lastTotalDebt != _currentTotalDebt) { 539 | lastTotalDebt = _currentTotalDebt; 540 | lastAccrueInterestTime = block.timestamp; 541 | } 542 | lastCollateralRatioMantissa = _currentCollateralRatioMantissa; 543 | emit Borrow(msg.sender, amount); 544 | if(_accruedFeeShares > 0) { 545 | balanceOf[_feeRecipient] += _accruedFeeShares; 546 | emit Transfer(address(0), _feeRecipient, _accruedFeeShares); 547 | } 548 | 549 | // interactions 550 | safeTransfer(LOAN_TOKEN, msg.sender, amount); 551 | 552 | // sync balance 553 | lastLoanTokenBalance = LOAN_TOKEN.balanceOf(address(this)); 554 | } 555 | 556 | /// @notice Repay loan tokens debt 557 | /// @param borrower The address of the borrower to repay on their behalf 558 | /// @param amount The amount of loan tokens to repay 559 | /// @dev If amount is max uint, all debt will be repaid 560 | function repay(address borrower, uint amount) external { 561 | (address _feeRecipient, uint _feeMantissa) = FACTORY.getFee(); 562 | ( 563 | uint _currentTotalSupply, 564 | uint _accruedFeeShares, 565 | uint _currentCollateralRatioMantissa, 566 | uint _currentTotalDebt 567 | ) = getCurrentState( 568 | lastLoanTokenBalance, 569 | _feeMantissa, 570 | lastCollateralRatioMantissa, 571 | totalSupply, 572 | lastAccrueInterestTime, 573 | lastTotalDebt 574 | ); 575 | 576 | uint _debtSharesSupply = debtSharesSupply; 577 | 578 | uint _shares; 579 | if(amount == type(uint).max) { 580 | amount = getDebtOf(debtSharesBalanceOf[borrower], _debtSharesSupply, _currentTotalDebt); 581 | _shares = debtSharesBalanceOf[borrower]; 582 | } else { 583 | _shares = tokenToShares(amount, _currentTotalDebt, _debtSharesSupply, false); 584 | } 585 | _currentTotalDebt -= amount; 586 | 587 | // commit current state 588 | debtSharesBalanceOf[borrower] -= _shares; 589 | debtSharesSupply = _debtSharesSupply - _shares; 590 | totalSupply = _currentTotalSupply; 591 | // avoid recording accrue interest time if there is no change in total debt i.e. 0 interest 592 | if (lastTotalDebt != _currentTotalDebt) { 593 | lastTotalDebt = _currentTotalDebt; 594 | lastAccrueInterestTime = block.timestamp; 595 | } 596 | lastCollateralRatioMantissa = _currentCollateralRatioMantissa; 597 | emit Repay(borrower, msg.sender, amount); 598 | if(_accruedFeeShares > 0) { 599 | balanceOf[_feeRecipient] += _accruedFeeShares; 600 | emit Transfer(address(0), _feeRecipient, _accruedFeeShares); 601 | } 602 | 603 | // interactions 604 | safeTransferFrom(LOAN_TOKEN, msg.sender, address(this), amount); 605 | 606 | // sync balance 607 | lastLoanTokenBalance = LOAN_TOKEN.balanceOf(address(this)); 608 | } 609 | 610 | /// @notice Seize collateral from an underwater borrower in exchange for repaying their debt 611 | /// @param borrower The address of the borrower to liquidate 612 | /// @param amount The amount of debt to repay 613 | /// @dev If amount is max uint, all debt will be liquidated 614 | function liquidate(address borrower, uint amount) external { 615 | (address _feeRecipient, uint _feeMantissa) = FACTORY.getFee(); 616 | ( 617 | uint _currentTotalSupply, 618 | uint _accruedFeeShares, 619 | uint _currentCollateralRatioMantissa, 620 | uint _currentTotalDebt 621 | ) = getCurrentState( 622 | lastLoanTokenBalance, 623 | _feeMantissa, 624 | lastCollateralRatioMantissa, 625 | totalSupply, 626 | lastAccrueInterestTime, 627 | lastTotalDebt 628 | ); 629 | 630 | uint collateralBalance = collateralBalanceOf[borrower]; 631 | uint _debtSharesSupply = debtSharesSupply; 632 | uint userDebt = getDebtOf(debtSharesBalanceOf[borrower], _debtSharesSupply, _currentTotalDebt); 633 | uint userCollateralRatioMantissa = userDebt * 1e36 / collateralBalance; 634 | require(userCollateralRatioMantissa > (_currentCollateralRatioMantissa * 1e18), "Pool: borrower not liquidatable"); 635 | 636 | address _borrower = borrower; // avoid stack too deep 637 | uint _amount = amount; // avoid stack too deep 638 | uint _shares; 639 | uint collateralReward; 640 | if(_amount == type(uint).max || _amount == userDebt) { 641 | collateralReward = collateralBalance; 642 | _shares = debtSharesBalanceOf[_borrower]; 643 | _amount = userDebt; 644 | } else { 645 | collateralReward = _amount * collateralBalance / userDebt; 646 | _shares = tokenToShares(_amount, _currentTotalDebt, _debtSharesSupply, false); 647 | } 648 | 649 | require(_shares != 0, "Pool: zero shares to liquidate"); 650 | _currentTotalDebt -= _amount; 651 | 652 | // commit current state 653 | debtSharesBalanceOf[_borrower] -= _shares; 654 | debtSharesSupply = _debtSharesSupply - _shares; 655 | collateralBalanceOf[_borrower] = collateralBalance - collateralReward; 656 | totalSupply = _currentTotalSupply; 657 | // avoid recording accrue interest time if there is no change in total debt i.e. 0 interest 658 | if (lastTotalDebt != _currentTotalDebt) { 659 | lastTotalDebt = _currentTotalDebt; 660 | lastAccrueInterestTime = block.timestamp; 661 | } 662 | lastCollateralRatioMantissa = _currentCollateralRatioMantissa; 663 | emit Liquidate(_borrower, msg.sender, _amount, collateralReward); 664 | if(_accruedFeeShares > 0) { 665 | address __feeRecipient = _feeRecipient; // avoid stack too deep 666 | balanceOf[__feeRecipient] += _accruedFeeShares; 667 | emit Transfer(address(0), __feeRecipient, _accruedFeeShares); 668 | } 669 | 670 | // interactions 671 | safeTransferFrom(LOAN_TOKEN, msg.sender, address(this), _amount); 672 | safeTransfer(COLLATERAL_TOKEN, msg.sender, collateralReward); 673 | 674 | 675 | // sync balance 676 | lastLoanTokenBalance = LOAN_TOKEN.balanceOf(address(this)); 677 | } 678 | 679 | event Transfer(address indexed from, address indexed to, uint value); 680 | event Approval(address indexed owner, address indexed spender, uint value); 681 | event Deposit(address indexed user, uint amount); 682 | event Withdraw(address indexed user, uint amount); 683 | event Borrow(address indexed user, uint amount); 684 | event Repay(address indexed user, address indexed caller, uint amount); 685 | event Liquidate(address indexed user, address indexed liquidator, uint amount, uint collateralReward); 686 | event AddCollateral(address indexed user, address indexed caller, uint amount); 687 | event RemoveCollateral(address indexed user, uint amount); 688 | } --------------------------------------------------------------------------------