From 787eb621190db4d976e34d25498754dc164e2ad6 Mon Sep 17 00:00:00 2001 From: 08xmt Date: Mon, 23 Sep 2024 08:24:50 +0200 Subject: [PATCH 1/2] Add marketv2 --- src/MarketV2.sol | 679 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 679 insertions(+) create mode 100644 src/MarketV2.sol diff --git a/src/MarketV2.sol b/src/MarketV2.sol new file mode 100644 index 0000000..0cd827c --- /dev/null +++ b/src/MarketV2.sol @@ -0,0 +1,679 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; +import "src/interfaces/IERC20.sol"; + +// Caution. We assume all failed transfers cause reverts and ignore the returned bool. +interface IOracle { + function getPrice(address,uint) external returns (uint); + function viewPrice(address,uint) external view returns (uint); +} + +interface IEscrow { + function initialize(IERC20 _token, address beneficiary) external; + function onDeposit() external; + function onDepositCallBack() external returns(uint); + function pay(address recipient, uint amount) external; + function balance() external view returns (uint); +} + +interface IDolaBorrowingRights { + function onBorrow(address user, uint additionalDebt) external; + function onRepay(address user, uint repaidDebt) external; + function onForceReplenish(address user, address replenisher, uint amount, uint replenisherReward) external; + function balanceOf(address user) external view returns (uint); + function deficitOf(address user) external view returns (uint); + function replenishmentPriceBps() external view returns (uint); +} + +interface IBorrowController { + function borrowAllowed(address msgSender, address borrower, uint amount) external returns (bool); + function onRepay(uint amount) external; +} + +interface IDebtManager { + function debt(address borrower) external view returns(uint); + function totalDebt() external view returns(uint); + function dbrDeficit(address borrower) external view returns(uint); + function increaseDebt(address borrower, uint amount) external; + function decreaseDebt(address borrower, uint amount) external returns(uint); + function replenish(address borrower, uint amount) external; +} + +contract MarketV2 { + + address public gov; + address public lender; + address public pauseGuardian; + address public defaultEscrowImplementation; + address public defaultDebtManager; + IDolaBorrowingRights public immutable dbr; + IBorrowController public borrowController; + IERC20 public immutable dola = IERC20(0x865377367054516e17014CcdED1e7d814EDC9ce4); + IERC20 public immutable collateral; + IOracle public oracle; + uint public collateralFactorBps; + uint public liquidationIncentiveBps; + uint public liquidationFeeBps; + uint public liquidationFactorBps = 5000; // 50% by default + bool public borrowPaused; + uint256 internal immutable INITIAL_CHAIN_ID; + bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; + mapping (address => IEscrow) public escrows; // user => escrow + mapping(address => uint256) public nonces; // user => nonce + mapping(address => bool) isEscrowImplementation; // escrow implementation => bool + mapping(address => bool) isDebtManager; // debt manager => bool + mapping(address => IDebtManager) debtManagers; // user => debt manager + + constructor ( + address _gov, + address _lender, + address _pauseGuardian, + address _defaultEscrowImplementation, + address _defaultDebtManager, + IDolaBorrowingRights _dbr, + IERC20 _collateral, + IOracle _oracle, + uint _collateralFactorBps, + uint _liquidationIncentiveBps + ) { + require(_collateralFactorBps < 10000, "Invalid collateral factor"); + require(_liquidationIncentiveBps > 0 && _liquidationIncentiveBps < 10000, "Invalid liquidation incentive"); + gov = _gov; + lender = _lender; + pauseGuardian = _pauseGuardian; + defaultEscrowImplementation = _defaultEscrowImplementation; + defaultDebtManager = _defaultDebtManager; + dbr = _dbr; + collateral = _collateral; + oracle = _oracle; + collateralFactorBps = _collateralFactorBps; + liquidationIncentiveBps = _liquidationIncentiveBps; + INITIAL_CHAIN_ID = block.chainid; + INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); + if(collateralFactorBps > 0){ + uint unsafeLiquidationIncentive = (10000 - collateralFactorBps) * (liquidationFeeBps + 10000) / collateralFactorBps; + require(liquidationIncentiveBps < unsafeLiquidationIncentive, "Liquidation param allow profitable self liquidation"); + } + } + + modifier onlyGov { + require(msg.sender == gov, "Only gov can call this function"); + _; + } + + modifier liquidationParamChecker { + _; + if(collateralFactorBps > 0){ + uint unsafeLiquidationIncentive = (10000 - collateralFactorBps) * (liquidationFeeBps + 10000) / collateralFactorBps; + require(liquidationIncentiveBps < unsafeLiquidationIncentive, "New liquidation param allow profitable self liquidation"); + } + } + + function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { + return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); + } + + function computeDomainSeparator() internal view virtual returns (bytes32) { + return + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes("DBR MARKET")), + keccak256("1"), + block.chainid, + address(this) + ) + ); + } + + /** + @notice sets the oracle to a new oracle. Only callable by governance. + @param _oracle The new oracle conforming to the IOracle interface. + */ + function setOracle(IOracle _oracle) public onlyGov { oracle = _oracle; } + + /** + @notice sets the borrow controller to a new borrow controller. Only callable by governance. + @param _borrowController The new borrow controller conforming to the IBorrowController interface. + */ + function setBorrowController(IBorrowController _borrowController) public onlyGov { borrowController = _borrowController; } + + /** + @notice sets the address of governance. Only callable by governance. + @param _gov Address of the new governance. + */ + function setGov(address _gov) public onlyGov { gov = _gov; } + + /** + @notice sets the lender to a new lender. The lender is allowed to recall dola from the contract. Only callable by governance. + @param _lender Address of the new lender. + */ + function setLender(address _lender) public onlyGov { lender = _lender; } + + /** + @notice sets the pause guardian. The pause guardian can pause borrowing. Only callable by governance. + @param _pauseGuardian Address of the new pauseGuardian. + */ + function setPauseGuardian(address _pauseGuardian) public onlyGov { pauseGuardian = _pauseGuardian; } + + /** + @notice sets the Collateral Factor requirement of the market as measured in basis points. 1 = 0.01%. Only callable by governance. + @dev Collateral factor mus be set below 100% + @param _collateralFactorBps The new collateral factor as measured in basis points. + */ + function setCollateralFactorBps(uint _collateralFactorBps) public onlyGov liquidationParamChecker { + require(_collateralFactorBps < 10000, "Invalid collateral factor"); + collateralFactorBps = _collateralFactorBps; + } + + /** + @notice sets the Liquidation Factor of the market as denoted in basis points. + The liquidation Factor denotes the maximum amount of debt that can be liquidated in basis points. + At 5000, 50% of of a borrower's underwater debt can be liquidated. Only callable by governance. + @dev Must be set between 1 and 10000. + @param _liquidationFactorBps The new liquidation factor in basis points. 1 = 0.01%/ + */ + function setLiquidationFactorBps(uint _liquidationFactorBps) public onlyGov { + require(_liquidationFactorBps > 0 && _liquidationFactorBps <= 10000, "Invalid liquidation factor"); + liquidationFactorBps = _liquidationFactorBps; + } + + /** + @notice sets the Liquidation Incentive of the market as denoted in basis points. + The Liquidation Incentive is the percentage paid out to liquidators of a borrower's debt when successfully liquidated. + @dev Must be set between 0 and 10000 - liquidation fee. + @param _liquidationIncentiveBps The new liqudation incentive set in basis points. 1 = 0.01% + */ + function setLiquidationIncentiveBps(uint _liquidationIncentiveBps) public onlyGov liquidationParamChecker { + require(_liquidationIncentiveBps > 0 && _liquidationIncentiveBps + liquidationFeeBps < 10000, "Invalid liquidation incentive"); + liquidationIncentiveBps = _liquidationIncentiveBps; + } + + /** + @notice sets the Liquidation Fee of the market as denoted in basis points. + The Liquidation Fee is the percentage paid out to governance of a borrower's debt when successfully liquidated. + @dev Must be set between 0 and 10000 - liquidation factor. + @param _liquidationFeeBps The new liquidation fee set in basis points. 1 = 0.01% + */ + function setLiquidationFeeBps(uint _liquidationFeeBps) public onlyGov liquidationParamChecker { + require(_liquidationFeeBps + liquidationIncentiveBps < 10000, "Invalid liquidation fee"); + liquidationFeeBps = _liquidationFeeBps; + } + + function setEscrowImplementation(address escrow, bool isAllowed) public onlyGov { + isEscrowImplementation[escrow] = isAllowed; + } + + function setDebtManager(address debtManager, bool isAllowed) public onlyGov { + isDebtManager[debtManager] = isAllowed; + } + + /** + @notice Recalls amount of DOLA to the lender. + @param amount The amount od DOLA to recall to the the lender. + */ + function recall(uint amount) public { + require(msg.sender == lender, "Only lender can recall"); + dola.transfer(msg.sender, amount); + } + + /** + @notice Pauses or unpauses borrowing for the market. Only gov can unpause a market, while gov and pauseGuardian can pause it. + @param _value Boolean representing the state pause state of borrows. true = paused, false = unpaused. + */ + function pauseBorrows(bool _value) public { + if(_value) { + require(msg.sender == pauseGuardian || msg.sender == gov, "Only pause guardian or governance can pause"); + } else { + require(msg.sender == gov, "Only governance can unpause"); + } + borrowPaused = _value; + } + + /** + @notice Internal function for creating an escrow for users to deposit collateral in. + @dev Uses create2 and minimal proxies to create the escrow at a deterministic address + @param user The address of the user to create an escrow for. + */ + function createEscrow(address user, address implementation) internal returns (IEscrow instance) { + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) + mstore(add(ptr, 0x14), shl(0x60, implementation)) + mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) + instance := create2(0, ptr, 0x37, user) + } + require(instance != IEscrow(address(0)), "ERC1167: create2 failed"); + emit CreateEscrow(user, address(instance)); + } + + /** + @notice Internal function for getting the escrow of a user. + @dev If the escrow doesn't exist, an escrow contract is deployed. + @param user The address of the user owning the escrow. + */ + function getEscrow(address user) internal returns (IEscrow) { + if(escrows[user] != IEscrow(address(0))) return escrows[user]; + IEscrow escrow = createEscrow(user, defaultEscrowImplementation); + escrow.initialize(collateral, user); + escrows[user] = escrow; + return escrow; + } + + function switchEscrow(address user, address escrowImplementation) external { + require(msg.sender == user, "Must own escrow or be debt manager"); + require(isEscrowImplementation[escrowImplementation], "Must be allowed escrow implementation"); + IEscrow escrow = getEscrow(user); + IEscrow newEscrow = predictEscrow(user, escrowImplementation); + if(address(newEscrow).code.length == 0) + newEscrow = createEscrow(user, escrowImplementation); + escrow.pay(address(newEscrow), escrow.balance()); + newEscrow.onDeposit(); + escrows[user] = newEscrow; + /*TODO: Consider making sure + 1. Balance remains the same or greater + 2. CreditLimit remains the same or greater + */ + } + + function switchDebtManager(address user, IDebtManager newDebtManager) external { + require(msg.sender == user || msg.sender == address(debtManagers[user]), "Must own escrow or be debt manager"); + require(isDebtManager[address(newDebtManager)], "Must be allowed debt manager"); + IDebtManager debtManager = debtManagers[user]; + if(debtManager.dbrDeficit(user) == 0){ + uint debt = debtManager.debt(user); + debtManager.decreaseDebt(user, debt); + newDebtManager.increaseDebt(user, debt); + debtManagers[user] = newDebtManager; + } + } + + /** + @notice Deposit amount of collateral into escrow + @dev Will deposit the amount into the escrow contract. + @param amount Amount of collateral token to deposit. + */ + function deposit(uint amount) public { + deposit(msg.sender, amount); + } + + /** + @notice Deposit and borrow in a single transaction. + @param amountDeposit Amount of collateral token to deposit into escrow. + @param amountBorrow Amount of DOLA to borrow. + */ + function depositAndBorrow(uint amountDeposit, uint amountBorrow) public { + deposit(amountDeposit); + borrow(amountBorrow); + } + + /** + @notice Deposit amount of collateral into escrow on behalf of msg.sender + @dev Will deposit the amount into the escrow contract. + @param user User to deposit on behalf of. + @param amount Amount of collateral token to deposit. + */ + function deposit(address user, uint amount) public { + IEscrow escrow = getEscrow(user); + collateral.transferFrom(msg.sender, address(escrow), amount); + escrow.onDeposit(); + emit Deposit(user, amount); + } + + /** + @notice View function for predicting the deterministic escrow address of a user. + @dev Only use deposit() function for deposits and NOT the predicted escrow address unless you know what you're doing + @param borrower Address of the user owning the escrow. + */ + function predictEscrow(address borrower, address implementation) public view returns (IEscrow predicted) { + address deployer = address(this); + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) + mstore(add(ptr, 0x14), shl(0x60, implementation)) + mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf3ff00000000000000000000000000000000) + mstore(add(ptr, 0x38), shl(0x60, deployer)) + mstore(add(ptr, 0x4c), borrower) + mstore(add(ptr, 0x6c), keccak256(ptr, 0x37)) + predicted := keccak256(add(ptr, 0x37), 0x55) + } + } + + /** + @notice View function for getting the dollar value of the user's collateral in escrow for the market. + @param user Address of the user. + */ + function getCollateralValue(address user) public view returns (uint) { + IEscrow escrow = escrows[user]; + uint collateralBalance = escrow.balance(); + return collateralBalance * oracle.viewPrice(address(collateral), collateralFactorBps) / 1 ether; + } + + /** + @notice Internal function for getting the dollar value of the user's collateral in escrow for the market. + @dev Updates the lowest price comparisons of the pessimistic oracle + @param user Address of the user. + */ + function getCollateralValueInternal(address user) internal returns (uint) { + IEscrow escrow = escrows[user]; + uint collateralBalance = escrow.balance(); + return collateralBalance * oracle.getPrice(address(collateral), collateralFactorBps) / 1 ether; + } + + /** + @notice View function for getting the credit limit of a user. + @dev To calculate the available credit, subtract user debt from credit limit. + @param user Address of the user. + */ + function getCreditLimit(address user) public view returns (uint) { + uint collateralValue = getCollateralValue(user); + return collateralValue * collateralFactorBps / 10000; + } + + /** + @notice Internal function for getting the credit limit of a user. + @dev To calculate the available credit, subtract user debt from credit limit. Updates the pessimistic oracle. + @param user Address of the user. + */ + function getCreditLimitInternal(address user) internal returns (uint) { + uint collateralValue = getCollateralValueInternal(user); + return collateralValue * collateralFactorBps / 10000; + } + /** + @notice Internal function for getting the withdrawal limit of a user. + The withdrawal limit is how much collateral a user can withdraw before their loan would be underwater. Updates the pessimistic oracle. + @param user Address of the user. + */ + function getWithdrawalLimitInternal(address user) internal returns (uint) { + IEscrow escrow = escrows[user]; + IDebtManager debtManager = debtManagers[user]; + uint collateralBalance = escrow.balance(); + if(collateralBalance == 0) return 0; + uint debt = debtManager.debt(user); + if(debt == 0) return collateralBalance; + if(collateralFactorBps == 0) return 0; + uint minimumCollateral = debt * 1 ether / oracle.getPrice(address(collateral), collateralFactorBps) * 10000 / collateralFactorBps; + if(collateralBalance <= minimumCollateral) return 0; + return collateralBalance - minimumCollateral; + } + + /** + @notice View function for getting the withdrawal limit of a user. + The withdrawal limit is how much collateral a user can withdraw before their loan would be underwater. + @param user Address of the user. + */ + function getWithdrawalLimit(address user) public view returns (uint) { + IEscrow escrow = escrows[user]; + IDebtManager debtManager = debtManagers[user]; + uint collateralBalance = escrow.balance(); + if(collateralBalance == 0) return 0; + uint debt = debtManager.debt(user); + if(debt == 0) return collateralBalance; + if(collateralFactorBps == 0) return 0; + uint minimumCollateral = debt * 1 ether / oracle.viewPrice(address(collateral), collateralFactorBps) * 10000 / collateralFactorBps; + if(collateralBalance <= minimumCollateral) return 0; + return collateralBalance - minimumCollateral; + } + + /** + @notice Internal function for borrowing DOLA against collateral. + @dev This internal function is shared between the borrow and borrowOnBehalf function + @param borrower The address of the borrower that debt will be accrued to. + @param to The address that will receive the borrowed DOLA + @param amount The amount of DOLA to be borrowed + */ + function borrowInternal(address borrower, address to, uint amount) internal { + IDebtManager debtManager = debtManagers[borrower]; + require(!borrowPaused, "Borrowing is paused"); + if(borrowController != IBorrowController(address(0))) { + require(borrowController.borrowAllowed(msg.sender, borrower, amount), "Denied by borrow controller"); + } + uint credit = getCreditLimitInternal(borrower); + debtManager.increaseDebt(borrower, amount); + require(credit >= debtManager.debt(borrower), "Exceeded credit limit"); + dola.transfer(to, amount); + emit Borrow(borrower, amount); + } + + /** + @notice Function for borrowing DOLA. + @dev Will borrow to msg.sender + @param amount The amount of DOLA to be borrowed. + */ + function borrow(uint amount) public { + borrowInternal(msg.sender, msg.sender, amount); + } + + /** + @notice Function for using a signed message to borrow on behalf of an address owning an escrow with collateral. + @dev Signed messaged can be invalidated by incrementing the nonce. Will always borrow to the msg.sender. + @param from The address of the user being borrowed from + @param amount The amount to be borrowed + @param deadline Timestamp after which the signed message will be invalid + @param v The v param of the ECDSA signature + @param r The r param of the ECDSA signature + @param s The s param of the ECDSA signature + */ + function borrowOnBehalf(address from, uint amount, uint deadline, uint8 v, bytes32 r, bytes32 s) public { + require(deadline >= block.timestamp, "DEADLINE_EXPIRED"); + unchecked { + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256( + "BorrowOnBehalf(address caller,address from,uint256 amount,uint256 nonce,uint256 deadline)" + ), + msg.sender, + from, + amount, + nonces[from]++, + deadline + ) + ) + ) + ), + v, + r, + s + ); + require(recoveredAddress != address(0) && recoveredAddress == from, "INVALID_SIGNER"); + borrowInternal(from, msg.sender, amount); + } + } + + /** + @notice Internal function for withdrawing from the escrow + @dev The internal function is shared by the withdraw function and withdrawOnBehalf function + @param from The address owning the escrow to withdraw from. + @param to The address receiving the tokens + @param amount The amount being withdrawn. + */ + function withdrawInternal(address from, address to, uint amount) internal { + uint limit = getWithdrawalLimitInternal(from); + require(limit >= amount, "Insufficient withdrawal limit"); + require(dbr.deficitOf(from) == 0, "Can't withdraw with DBR deficit"); + IEscrow escrow = getEscrow(from); + escrow.pay(to, amount); + emit Withdraw(from, to, amount); + } + + /** + @notice Function for withdrawing to msg.sender. + @param amount Amount to withdraw. + */ + function withdraw(uint amount) public { + withdrawInternal(msg.sender, msg.sender, amount); + } + + /** + @notice Function for withdrawing maximum allowed to msg.sender. + @dev Useful for use with escrows that continously compound tokens, so there won't be dust amounts left + @dev Dangerous to use when the user has any amount of debt! + */ + function withdrawMax() public { + withdrawInternal(msg.sender, msg.sender, getWithdrawalLimitInternal(msg.sender)); + } + + /** + @notice Function for using a signed message to withdraw on behalf of an address owning an escrow with collateral. + @dev Signed messaged can be invalidated by incrementing the nonce. Will always withdraw to the msg.sender. + @param from The address of the user owning the escrow being withdrawn from + @param amount The amount to be withdrawn + @param deadline Timestamp after which the signed message will be invalid + @param v The v param of the ECDSA signature + @param r The r param of the ECDSA signature + @param s The s param of the ECDSA signature + */ + function withdrawOnBehalf(address from, uint amount, uint deadline, uint8 v, bytes32 r, bytes32 s) public { + require(deadline >= block.timestamp, "DEADLINE_EXPIRED"); + unchecked { + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256( + "WithdrawOnBehalf(address caller,address from,uint256 amount,uint256 nonce,uint256 deadline)" + ), + msg.sender, + from, + amount, + nonces[from]++, + deadline + ) + ) + ) + ), + v, + r, + s + ); + require(recoveredAddress != address(0) && recoveredAddress == from, "INVALID_SIGNER"); + withdrawInternal(from, msg.sender, amount); + } + } + + /** + @notice Function for using a signed message to withdraw on behalf of an address owning an escrow with collateral. + @dev Signed messaged can be invalidated by incrementing the nonce. Will always withdraw to the msg.sender. + @dev Useful for use with escrows that continously compound tokens, so there won't be dust amounts left + @dev Dangerous to use when the user has any amount of debt! + @param from The address of the user owning the escrow being withdrawn from + @param deadline Timestamp after which the signed message will be invalid + @param v The v param of the ECDSA signature + @param r The r param of the ECDSA signature + @param s The s param of the ECDSA signature + */ + function withdrawMaxOnBehalf(address from, uint deadline, uint8 v, bytes32 r, bytes32 s) public { + require(deadline >= block.timestamp, "DEADLINE_EXPIRED"); + unchecked { + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256( + "WithdrawMaxOnBehalf(address caller,address from,uint256 nonce,uint256 deadline)" + ), + msg.sender, + from, + nonces[from]++, + deadline + ) + ) + ) + ), + v, + r, + s + ); + require(recoveredAddress != address(0) && recoveredAddress == from, "INVALID_SIGNER"); + withdrawInternal(from, msg.sender, getWithdrawalLimitInternal(from)); + } + } + + /** + @notice Function for incrementing the nonce of the msg.sender, making their latest signed message unusable. + */ + function invalidateNonce() public { + nonces[msg.sender]++; + } + + /** + @notice Function for repaying debt on behalf of user. Debt must be repaid in DOLA. + @dev If the user has a DBR deficit, they risk initial debt being accrued by forced replenishments. + @param borrower Address of the borrower whose debt is being repaid + @param amount DOLA amount to be repaid. If set to max uint debt will be repaid in full. + */ + function repay(address borrower, uint amount) public { + IDebtManager debtManager = debtManagers[borrower]; + amount = debtManager.decreaseDebt(borrower, amount); + + if(address(borrowController) != address(0)){ + borrowController.onRepay(amount); + } + + dola.transferFrom(msg.sender, address(this), amount); + emit Repay(borrower, msg.sender, amount); + } + + /** + @notice Bundles repayment and withdrawal into a single function call. + @param repayAmount Amount of DOLA to be repaid + @param withdrawAmount Amount of underlying to be withdrawn from the escrow + */ + function repayAndWithdraw(uint repayAmount, uint withdrawAmount) public { + repay(msg.sender, repayAmount); + withdraw(withdrawAmount); + } + + /** + @notice Function for liquidating a user's under water debt. Debt is under water when the value of a user's debt is above their collateral factor. + @param borrower The user to be liquidated + @param repaidDebt Th amount of user user debt to liquidate. + */ + function liquidate(address borrower, uint repaidDebt) public { + IDebtManager debtManager = debtManagers[borrower]; + require(repaidDebt > 0, "Must repay positive debt"); + uint debt = debtManager.debt(borrower); + require(getCreditLimitInternal(borrower) < debt, "User debt is healthy"); + repaidDebt = debtManager.decreaseDebt(borrower, repaidDebt); + require(repaidDebt <= debt * liquidationFactorBps / 10000, "Exceeded liquidation factor"); + uint price = oracle.getPrice(address(collateral), collateralFactorBps); + uint liquidatorReward = repaidDebt * 1 ether / price; + liquidatorReward += liquidatorReward * liquidationIncentiveBps / 10000; + if(address(borrowController) != address(0)){ + borrowController.onRepay(repaidDebt); + } + dola.transferFrom(msg.sender, address(this), repaidDebt); + IEscrow escrow = escrows[borrower]; + escrow.pay(msg.sender, liquidatorReward); + if(liquidationFeeBps > 0) { + uint liquidationFee = repaidDebt * 1 ether / price * liquidationFeeBps / 10000; + uint balance = escrow.balance(); + if(balance >= liquidationFee) { + escrow.pay(gov, liquidationFee); + } else if(balance > 0) { + escrow.pay(gov, balance); + } + } + emit Liquidate(borrower, msg.sender, repaidDebt, liquidatorReward); + } + + event Deposit(address indexed account, uint amount); + event Borrow(address indexed account, uint amount); + event Withdraw(address indexed account, address indexed to, uint amount); + event Repay(address indexed account, address indexed repayer, uint amount); + event Liquidate(address indexed account, address indexed liquidator, uint repaidDebt, uint liquidatorReward); + event CreateEscrow(address indexed user, address escrow); +} From 79545c5becb6b626c1d474709dc02c62e1fe3780 Mon Sep 17 00:00:00 2001 From: 0xmt Date: Mon, 11 Nov 2024 11:39:33 +0100 Subject: [PATCH 2/2] Add initial liquidation update (#92) * Liquidation incentive linearly increase as borrower collateral factor decreases * Liquidation fee linearly decreases as borrower collateral factor decreases * Remove collateral factor, instead add max liquidation amount --- src/MarketV2.sol | 431 ++++++++++++++++++++++---------------- src/interfaces/IERC20.sol | 1 + test/MarketV2.t.sol | 118 +++++++++++ 3 files changed, 366 insertions(+), 184 deletions(-) create mode 100644 test/MarketV2.t.sol diff --git a/src/MarketV2.sol b/src/MarketV2.sol index 0cd827c..ab04383 100644 --- a/src/MarketV2.sol +++ b/src/MarketV2.sol @@ -41,6 +41,16 @@ interface IDebtManager { contract MarketV2 { + struct MarketParams { + uint16 collateralFactorBps; + uint16 maxLiquidationIncentiveThresholdBps; + uint16 maxLiquidationIncentiveBps; + uint16 maxLiquidationFeeBps; + uint16 zeroLiquidationFeeThresholdBps; + uint128 maxLiquidationAmount; + bool borrowPaused; + } + address public gov; address public lender; address public pauseGuardian; @@ -51,11 +61,9 @@ contract MarketV2 { IERC20 public immutable dola = IERC20(0x865377367054516e17014CcdED1e7d814EDC9ce4); IERC20 public immutable collateral; IOracle public oracle; - uint public collateralFactorBps; - uint public liquidationIncentiveBps; - uint public liquidationFeeBps; - uint public liquidationFactorBps = 5000; // 50% by default - bool public borrowPaused; + MarketParams marketParameters; + //TODO: Change to decimals factor + uint256 public immutable decimals; uint256 internal immutable INITIAL_CHAIN_ID; bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; mapping (address => IEscrow) public escrows; // user => escrow @@ -73,11 +81,9 @@ contract MarketV2 { IDolaBorrowingRights _dbr, IERC20 _collateral, IOracle _oracle, - uint _collateralFactorBps, - uint _liquidationIncentiveBps + MarketParams memory _marketParameters ) { - require(_collateralFactorBps < 10000, "Invalid collateral factor"); - require(_liquidationIncentiveBps > 0 && _liquidationIncentiveBps < 10000, "Invalid liquidation incentive"); + checkParameters(_marketParameters); gov = _gov; lender = _lender; pauseGuardian = _pauseGuardian; @@ -85,15 +91,26 @@ contract MarketV2 { defaultDebtManager = _defaultDebtManager; dbr = _dbr; collateral = _collateral; + decimals = 10**uint(IERC20(_collateral).decimals()); oracle = _oracle; - collateralFactorBps = _collateralFactorBps; - liquidationIncentiveBps = _liquidationIncentiveBps; + marketParameters = _marketParameters; INITIAL_CHAIN_ID = block.chainid; INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); - if(collateralFactorBps > 0){ - uint unsafeLiquidationIncentive = (10000 - collateralFactorBps) * (liquidationFeeBps + 10000) / collateralFactorBps; - require(liquidationIncentiveBps < unsafeLiquidationIncentive, "Liquidation param allow profitable self liquidation"); - } + } + + function checkParameters(MarketParams memory mp) public pure { + //Collateral factor must be below 100% + require(mp.collateralFactorBps < 10000, "Invalid collateral factor"); + //Max liquidation incentive must be between 0 and 100% + require(mp.maxLiquidationIncentiveBps > 0 && mp.maxLiquidationIncentiveBps < 10000, "Invalid liquidation incentive"); + //The incentive paid out at the max liquidation incentive threshold must never exceed the liquidators ability to liquidate fully + require(mp.maxLiquidationIncentiveThresholdBps + uint(mp.maxLiquidationIncentiveThresholdBps) * mp.maxLiquidationIncentiveBps / 10000 < 10000, "Unsafe max liquidation parameter"); + //The CF threshold for max liquidations should never be below the safe + require(mp.maxLiquidationIncentiveThresholdBps >= mp.collateralFactorBps && mp.maxLiquidationIncentiveThresholdBps <= 10000, "Invalid liquidation incentive"); + //Its fine to let fees exceed 10000 as maximum fee is always borrowers remaining collateral, but lets keep things sensical + require(mp.maxLiquidationFeeBps < 10000, "Invalid liquidation fee"); + //Fees should always be 0 at max liquidation incentive + require(mp.zeroLiquidationFeeThresholdBps <= mp.maxLiquidationIncentiveThresholdBps, "Invalid liquidation fee threshold"); } modifier onlyGov { @@ -101,12 +118,9 @@ contract MarketV2 { _; } - modifier liquidationParamChecker { + modifier marketParamChecker { _; - if(collateralFactorBps > 0){ - uint unsafeLiquidationIncentive = (10000 - collateralFactorBps) * (liquidationFeeBps + 10000) / collateralFactorBps; - require(liquidationIncentiveBps < unsafeLiquidationIncentive, "New liquidation param allow profitable self liquidation"); - } + checkParameters(marketParameters); } function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { @@ -127,77 +141,95 @@ contract MarketV2 { } /** - @notice sets the oracle to a new oracle. Only callable by governance. - @param _oracle The new oracle conforming to the IOracle interface. + * @notice sets the oracle to a new oracle. Only callable by governance. + * @param _oracle The new oracle conforming to the IOracle interface. */ function setOracle(IOracle _oracle) public onlyGov { oracle = _oracle; } /** - @notice sets the borrow controller to a new borrow controller. Only callable by governance. - @param _borrowController The new borrow controller conforming to the IBorrowController interface. + * @notice sets the borrow controller to a new borrow controller. Only callable by governance. + * @param _borrowController The new borrow controller conforming to the IBorrowController interface. */ function setBorrowController(IBorrowController _borrowController) public onlyGov { borrowController = _borrowController; } /** - @notice sets the address of governance. Only callable by governance. - @param _gov Address of the new governance. + * @notice sets the address of governance. Only callable by governance. + * @param _gov Address of the new governance. */ function setGov(address _gov) public onlyGov { gov = _gov; } /** - @notice sets the lender to a new lender. The lender is allowed to recall dola from the contract. Only callable by governance. - @param _lender Address of the new lender. + * @notice sets the lender to a new lender. The lender is allowed to recall dola from the contract. Only callable by governance. + * @param _lender Address of the new lender. */ function setLender(address _lender) public onlyGov { lender = _lender; } /** - @notice sets the pause guardian. The pause guardian can pause borrowing. Only callable by governance. - @param _pauseGuardian Address of the new pauseGuardian. + * @notice sets the pause guardian. The pause guardian can pause borrowing. Only callable by governance. + * @param _pauseGuardian Address of the new pauseGuardian. */ function setPauseGuardian(address _pauseGuardian) public onlyGov { pauseGuardian = _pauseGuardian; } /** - @notice sets the Collateral Factor requirement of the market as measured in basis points. 1 = 0.01%. Only callable by governance. - @dev Collateral factor mus be set below 100% - @param _collateralFactorBps The new collateral factor as measured in basis points. + * @notice sets the Collateral Factor requirement of the market as measured in basis points. 1 = 0.01%. Only callable by governance. + * @dev Collateral factor must be set below 100% + * @param _collateralFactorBps The new collateral factor as measured in basis points. */ - function setCollateralFactorBps(uint _collateralFactorBps) public onlyGov liquidationParamChecker { + function setCollateralFactorBps(uint16 _collateralFactorBps) public onlyGov marketParamChecker { require(_collateralFactorBps < 10000, "Invalid collateral factor"); - collateralFactorBps = _collateralFactorBps; + marketParameters.collateralFactorBps = _collateralFactorBps; } - + /** - @notice sets the Liquidation Factor of the market as denoted in basis points. - The liquidation Factor denotes the maximum amount of debt that can be liquidated in basis points. - At 5000, 50% of of a borrower's underwater debt can be liquidated. Only callable by governance. - @dev Must be set between 1 and 10000. - @param _liquidationFactorBps The new liquidation factor in basis points. 1 = 0.01%/ + * @notice sets the maxLiquidationAmount for the market in DOLA terms. Only callable by governance. + * @param _maxLiquidationAmount The maximum amount of debt that can be liquidated. */ - function setLiquidationFactorBps(uint _liquidationFactorBps) public onlyGov { - require(_liquidationFactorBps > 0 && _liquidationFactorBps <= 10000, "Invalid liquidation factor"); - liquidationFactorBps = _liquidationFactorBps; + function setMaxLiquidationAmount(uint128 _maxLiquidationAmount) public onlyGov marketParamChecker { + marketParameters.maxLiquidationAmount = _maxLiquidationAmount; } /** - @notice sets the Liquidation Incentive of the market as denoted in basis points. + * @notice sets the Liquidation Incentive of the market as denoted in basis points. The Liquidation Incentive is the percentage paid out to liquidators of a borrower's debt when successfully liquidated. - @dev Must be set between 0 and 10000 - liquidation fee. - @param _liquidationIncentiveBps The new liqudation incentive set in basis points. 1 = 0.01% + * @dev Must be set between 0 and 10000 - liquidation fee. + * @param _maxLiquidationIncentiveBps The new liqudation incentive set in basis points. 1 = 0.01% */ - function setLiquidationIncentiveBps(uint _liquidationIncentiveBps) public onlyGov liquidationParamChecker { - require(_liquidationIncentiveBps > 0 && _liquidationIncentiveBps + liquidationFeeBps < 10000, "Invalid liquidation incentive"); - liquidationIncentiveBps = _liquidationIncentiveBps; + function setMaxLiquidationIncentiveBps(uint16 _maxLiquidationIncentiveBps) public onlyGov marketParamChecker { + require(_maxLiquidationIncentiveBps > 0 && _maxLiquidationIncentiveBps <= 10000, "Invalid liquidation incentive"); + marketParameters.maxLiquidationIncentiveBps = _maxLiquidationIncentiveBps; } /** - @notice sets the Liquidation Fee of the market as denoted in basis points. + * @notice sets the Liquidation Incentive of the market as denoted in basis points. + The Liquidation Incentive is the percentage paid out to liquidators of a borrower's debt when successfully liquidated. + * @dev Must be set between 0 and 10000 - liquidation fee. + * @param _maxLiquidationIncentiveThresholdBps The new liqudation incentive set in basis points. 1 = 0.01% + */ + function setMaxLiquidationIncentiveThresholdBps(uint16 _maxLiquidationIncentiveThresholdBps) public onlyGov marketParamChecker { + require(_maxLiquidationIncentiveThresholdBps >= marketParameters.collateralFactorBps && _maxLiquidationIncentiveThresholdBps <= 10000, "Invalid liquidation incentive"); + marketParameters.maxLiquidationIncentiveThresholdBps = _maxLiquidationIncentiveThresholdBps; + } + + /** + * @notice sets the Liquidation Fee of the market as denoted in basis points. The Liquidation Fee is the percentage paid out to governance of a borrower's debt when successfully liquidated. - @dev Must be set between 0 and 10000 - liquidation factor. - @param _liquidationFeeBps The new liquidation fee set in basis points. 1 = 0.01% + * @dev Must be set between 0 and 10000 - liquidation factor. + * @param _maxLiquidationFeeBps The new liquidation fee set in basis points. 1 = 0.01% */ - function setLiquidationFeeBps(uint _liquidationFeeBps) public onlyGov liquidationParamChecker { - require(_liquidationFeeBps + liquidationIncentiveBps < 10000, "Invalid liquidation fee"); - liquidationFeeBps = _liquidationFeeBps; + function setMaxLiquidationFeeBps(uint16 _maxLiquidationFeeBps) public onlyGov marketParamChecker { + require(_maxLiquidationFeeBps < 10000, "Invalid liquidation fee"); + marketParameters.maxLiquidationFeeBps = _maxLiquidationFeeBps; + } + + /** + * @notice sets the Liquidation Fee of the market as denoted in basis points. + The Liquidation Fee is the percentage paid out to governance of a borrower's debt when successfully liquidated. + * @dev Must be set between 0 and 10000 - liquidation factor. + * @param _zeroLiquidationFeeThresholdBps The new liquidation fee set in basis points. 1 = 0.01% + */ + function setZeroLiquidationFeeThresholdBps(uint16 _zeroLiquidationFeeThresholdBps) public onlyGov marketParamChecker { + require(_zeroLiquidationFeeThresholdBps <= marketParameters.maxLiquidationIncentiveThresholdBps, "Invalid liquidation fee threshold"); + marketParameters.zeroLiquidationFeeThresholdBps = _zeroLiquidationFeeThresholdBps; } function setEscrowImplementation(address escrow, bool isAllowed) public onlyGov { @@ -209,8 +241,8 @@ contract MarketV2 { } /** - @notice Recalls amount of DOLA to the lender. - @param amount The amount od DOLA to recall to the the lender. + * @notice Recalls amount of DOLA to the lender. + * @param amount The amount od DOLA to recall to the the lender. */ function recall(uint amount) public { require(msg.sender == lender, "Only lender can recall"); @@ -218,8 +250,8 @@ contract MarketV2 { } /** - @notice Pauses or unpauses borrowing for the market. Only gov can unpause a market, while gov and pauseGuardian can pause it. - @param _value Boolean representing the state pause state of borrows. true = paused, false = unpaused. + * @notice Pauses or unpauses borrowing for the market. Only gov can unpause a market, while gov and pauseGuardian can pause it. + * @param _value Boolean representing the state pause state of borrows. true = paused, false = unpaused. */ function pauseBorrows(bool _value) public { if(_value) { @@ -227,13 +259,13 @@ contract MarketV2 { } else { require(msg.sender == gov, "Only governance can unpause"); } - borrowPaused = _value; + marketParameters.borrowPaused = _value; } /** - @notice Internal function for creating an escrow for users to deposit collateral in. - @dev Uses create2 and minimal proxies to create the escrow at a deterministic address - @param user The address of the user to create an escrow for. + * @notice Internal function for creating an escrow for users to deposit collateral in. + * @dev Uses create2 and minimal proxies to create the escrow at a deterministic address + * @param user The address of the user to create an escrow for. */ function createEscrow(address user, address implementation) internal returns (IEscrow instance) { /// @solidity memory-safe-assembly @@ -249,9 +281,9 @@ contract MarketV2 { } /** - @notice Internal function for getting the escrow of a user. - @dev If the escrow doesn't exist, an escrow contract is deployed. - @param user The address of the user owning the escrow. + * @notice Internal function for getting the escrow of a user. + * @dev If the escrow doesn't exist, an escrow contract is deployed. + * @param user The address of the user owning the escrow. */ function getEscrow(address user) internal returns (IEscrow) { if(escrows[user] != IEscrow(address(0))) return escrows[user]; @@ -290,18 +322,18 @@ contract MarketV2 { } /** - @notice Deposit amount of collateral into escrow - @dev Will deposit the amount into the escrow contract. - @param amount Amount of collateral token to deposit. + * @notice Deposit amount of collateral into escrow + * @dev Will deposit the amount into the escrow contract. + * @param amount Amount of collateral token to deposit. */ function deposit(uint amount) public { deposit(msg.sender, amount); } /** - @notice Deposit and borrow in a single transaction. - @param amountDeposit Amount of collateral token to deposit into escrow. - @param amountBorrow Amount of DOLA to borrow. + * @notice Deposit and borrow in a single transaction. + * @param amountDeposit Amount of collateral token to deposit into escrow. + * @param amountBorrow Amount of DOLA to borrow. */ function depositAndBorrow(uint amountDeposit, uint amountBorrow) public { deposit(amountDeposit); @@ -309,10 +341,10 @@ contract MarketV2 { } /** - @notice Deposit amount of collateral into escrow on behalf of msg.sender - @dev Will deposit the amount into the escrow contract. - @param user User to deposit on behalf of. - @param amount Amount of collateral token to deposit. + * @notice Deposit amount of collateral into escrow on behalf of msg.sender + * @dev Will deposit the amount into the escrow contract. + * @param user User to deposit on behalf of. + * @param amount Amount of collateral token to deposit. */ function deposit(address user, uint amount) public { IEscrow escrow = getEscrow(user); @@ -322,9 +354,9 @@ contract MarketV2 { } /** - @notice View function for predicting the deterministic escrow address of a user. - @dev Only use deposit() function for deposits and NOT the predicted escrow address unless you know what you're doing - @param borrower Address of the user owning the escrow. + * @notice View function for predicting the deterministic escrow address of a user. + * @dev Only use deposit() function for deposits and NOT the predicted escrow address unless you know what you're doing + * @param borrower Address of the user owning the escrow. */ function predictEscrow(address borrower, address implementation) public view returns (IEscrow predicted) { address deployer = address(this); @@ -341,92 +373,118 @@ contract MarketV2 { } } + function getLiquidationIncentiveBps(uint borrowerCollateralFactorBps) public view returns (uint) { + MarketParams memory mp = marketParameters; //Cache marketParameters + return calcLiquidationIncentiveBps(mp, borrowerCollateralFactorBps); + } + + function calcLiquidationIncentiveBps(MarketParams memory mp, uint borrowerCollateralFactorBps) internal pure returns (uint) { + if(borrowerCollateralFactorBps <= mp.collateralFactorBps) return 0; + if(borrowerCollateralFactorBps >= mp.maxLiquidationIncentiveThresholdBps) return mp.maxLiquidationIncentiveBps; + return mp.maxLiquidationIncentiveBps * (borrowerCollateralFactorBps - mp.collateralFactorBps) / (mp.maxLiquidationIncentiveThresholdBps - mp.collateralFactorBps); + } + + function getLiquidationFeeBps(uint borrowerCollateralFactorBps) public view returns (uint) { + MarketParams memory mp = marketParameters; //Cache marketParameters + return calcLiquidationFeeBps(mp, borrowerCollateralFactorBps); + } + + function calcLiquidationFeeBps(MarketParams memory mp, uint borrowerCollateralFactorBps) public pure returns (uint) { + if(mp.maxLiquidationFeeBps == 0) return 0; + if(borrowerCollateralFactorBps < mp.collateralFactorBps) return 0; + if(borrowerCollateralFactorBps >= mp.zeroLiquidationFeeThresholdBps) return 0; + uint distBps = 10000 * (borrowerCollateralFactorBps - mp.collateralFactorBps) / (mp.zeroLiquidationFeeThresholdBps - mp.collateralFactorBps); + return mp.maxLiquidationFeeBps * (10000 - distBps) / 10000; + } + /** - @notice View function for getting the dollar value of the user's collateral in escrow for the market. - @param user Address of the user. + * @notice View function for getting the dollar value of the user's collateral in escrow for the market. + * @param user Address of the user. */ function getCollateralValue(address user) public view returns (uint) { - IEscrow escrow = escrows[user]; - uint collateralBalance = escrow.balance(); - return collateralBalance * oracle.viewPrice(address(collateral), collateralFactorBps) / 1 ether; + uint collateralBalance = escrows[user].balance(); + return calcCollateralValue(collateralBalance, oracle.viewPrice(address(collateral), marketParameters.collateralFactorBps)); } /** - @notice Internal function for getting the dollar value of the user's collateral in escrow for the market. - @dev Updates the lowest price comparisons of the pessimistic oracle - @param user Address of the user. + * @notice Internal function for getting the dollar value of the user's collateral in escrow for the market. + * @dev Updates the lowest price comparisons of the pessimistic oracle + * @param user Address of the user. */ function getCollateralValueInternal(address user) internal returns (uint) { - IEscrow escrow = escrows[user]; - uint collateralBalance = escrow.balance(); - return collateralBalance * oracle.getPrice(address(collateral), collateralFactorBps) / 1 ether; + uint collateralBalance = escrows[user].balance(); + return calcCollateralValue(collateralBalance, oracle.getPrice(address(collateral), marketParameters.collateralFactorBps)); + } + + function calcCollateralValue(uint collateralBalance, uint price) internal pure returns (uint) { + return collateralBalance * price / 1 ether; } /** - @notice View function for getting the credit limit of a user. - @dev To calculate the available credit, subtract user debt from credit limit. - @param user Address of the user. + * @notice View function for getting the credit limit of a user. + * @dev To calculate the available credit, subtract user debt from credit limit. + * @param user Address of the user. */ function getCreditLimit(address user) public view returns (uint) { uint collateralValue = getCollateralValue(user); - return collateralValue * collateralFactorBps / 10000; + return calcCreditLimit(collateralValue, marketParameters.collateralFactorBps); } /** - @notice Internal function for getting the credit limit of a user. - @dev To calculate the available credit, subtract user debt from credit limit. Updates the pessimistic oracle. - @param user Address of the user. + * @notice Internal function for getting the credit limit of a user. + * @dev To calculate the available credit, subtract user debt from credit limit. Updates the pessimistic oracle. + * @param user Address of the user. */ function getCreditLimitInternal(address user) internal returns (uint) { uint collateralValue = getCollateralValueInternal(user); + return calcCreditLimit(collateralValue, marketParameters.collateralFactorBps); + } + + function calcCreditLimit(uint collateralValue, uint collateralFactorBps) internal pure returns (uint) { return collateralValue * collateralFactorBps / 10000; } + /** - @notice Internal function for getting the withdrawal limit of a user. + * @notice Internal function for getting the withdrawal limit of a user. + * @dev Updates oracle state The withdrawal limit is how much collateral a user can withdraw before their loan would be underwater. Updates the pessimistic oracle. - @param user Address of the user. + * @param user Address of the user. */ function getWithdrawalLimitInternal(address user) internal returns (uint) { - IEscrow escrow = escrows[user]; - IDebtManager debtManager = debtManagers[user]; - uint collateralBalance = escrow.balance(); - if(collateralBalance == 0) return 0; - uint debt = debtManager.debt(user); - if(debt == 0) return collateralBalance; - if(collateralFactorBps == 0) return 0; - uint minimumCollateral = debt * 1 ether / oracle.getPrice(address(collateral), collateralFactorBps) * 10000 / collateralFactorBps; - if(collateralBalance <= minimumCollateral) return 0; - return collateralBalance - minimumCollateral; + return _withdrawalLimit(user, oracle.getPrice(address(collateral), marketParameters.collateralFactorBps)); } /** - @notice View function for getting the withdrawal limit of a user. + * @notice View function for getting the withdrawal limit of a user. The withdrawal limit is how much collateral a user can withdraw before their loan would be underwater. - @param user Address of the user. + * @param user Address of the user. */ function getWithdrawalLimit(address user) public view returns (uint) { - IEscrow escrow = escrows[user]; - IDebtManager debtManager = debtManagers[user]; - uint collateralBalance = escrow.balance(); + return _withdrawalLimit(user, oracle.viewPrice(address(collateral), marketParameters.collateralFactorBps)); + } + + function _withdrawalLimit(address user, uint price) internal view returns (uint) { + uint collateralBalance = escrows[user].balance(); if(collateralBalance == 0) return 0; - uint debt = debtManager.debt(user); - if(debt == 0) return collateralBalance; + uint collateralFactorBps = marketParameters.collateralFactorBps; if(collateralFactorBps == 0) return 0; - uint minimumCollateral = debt * 1 ether / oracle.viewPrice(address(collateral), collateralFactorBps) * 10000 / collateralFactorBps; + uint debt = debtManagers[user].debt(user); + if(debt == 0) return collateralBalance; + uint minimumCollateral = debt * 1 ether / price * 10000 / collateralFactorBps; if(collateralBalance <= minimumCollateral) return 0; return collateralBalance - minimumCollateral; } /** - @notice Internal function for borrowing DOLA against collateral. - @dev This internal function is shared between the borrow and borrowOnBehalf function - @param borrower The address of the borrower that debt will be accrued to. - @param to The address that will receive the borrowed DOLA - @param amount The amount of DOLA to be borrowed + * @notice Internal function for borrowing DOLA against collateral. + * @dev This internal function is shared between the borrow and borrowOnBehalf function + * @param borrower The address of the borrower that debt will be accrued to. + * @param to The address that will receive the borrowed DOLA + * @param amount The amount of DOLA to be borrowed */ function borrowInternal(address borrower, address to, uint amount) internal { IDebtManager debtManager = debtManagers[borrower]; - require(!borrowPaused, "Borrowing is paused"); + require(!marketParameters.borrowPaused, "Borrowing is paused"); if(borrowController != IBorrowController(address(0))) { require(borrowController.borrowAllowed(msg.sender, borrower, amount), "Denied by borrow controller"); } @@ -438,23 +496,23 @@ contract MarketV2 { } /** - @notice Function for borrowing DOLA. - @dev Will borrow to msg.sender - @param amount The amount of DOLA to be borrowed. + * @notice Function for borrowing DOLA. + * @dev Will borrow to msg.sender + * @param amount The amount of DOLA to be borrowed. */ function borrow(uint amount) public { borrowInternal(msg.sender, msg.sender, amount); } /** - @notice Function for using a signed message to borrow on behalf of an address owning an escrow with collateral. - @dev Signed messaged can be invalidated by incrementing the nonce. Will always borrow to the msg.sender. - @param from The address of the user being borrowed from - @param amount The amount to be borrowed - @param deadline Timestamp after which the signed message will be invalid - @param v The v param of the ECDSA signature - @param r The r param of the ECDSA signature - @param s The s param of the ECDSA signature + * @notice Function for using a signed message to borrow on behalf of an address owning an escrow with collateral. + * @dev Signed messaged can be invalidated by incrementing the nonce. Will always borrow to the msg.sender. + * @param from The address of the user being borrowed from + * @param amount The amount to be borrowed + * @param deadline Timestamp after which the signed message will be invalid + * @param v The v param of the ECDSA signature + * @param r The r param of the ECDSA signature + * @param s The s param of the ECDSA signature */ function borrowOnBehalf(address from, uint amount, uint deadline, uint8 v, bytes32 r, bytes32 s) public { require(deadline >= block.timestamp, "DEADLINE_EXPIRED"); @@ -488,11 +546,11 @@ contract MarketV2 { } /** - @notice Internal function for withdrawing from the escrow - @dev The internal function is shared by the withdraw function and withdrawOnBehalf function - @param from The address owning the escrow to withdraw from. - @param to The address receiving the tokens - @param amount The amount being withdrawn. + * @notice Internal function for withdrawing from the escrow + * @dev The internal function is shared by the withdraw function and withdrawOnBehalf function + * @param from The address owning the escrow to withdraw from. + * @param to The address receiving the tokens + * @param amount The amount being withdrawn. */ function withdrawInternal(address from, address to, uint amount) internal { uint limit = getWithdrawalLimitInternal(from); @@ -504,31 +562,31 @@ contract MarketV2 { } /** - @notice Function for withdrawing to msg.sender. - @param amount Amount to withdraw. + * @notice Function for withdrawing to msg.sender. + * @param amount Amount to withdraw. */ function withdraw(uint amount) public { withdrawInternal(msg.sender, msg.sender, amount); } /** - @notice Function for withdrawing maximum allowed to msg.sender. - @dev Useful for use with escrows that continously compound tokens, so there won't be dust amounts left - @dev Dangerous to use when the user has any amount of debt! + * @notice Function for withdrawing maximum allowed to msg.sender. + * @dev Useful for use with escrows that continously compound tokens, so there won't be dust amounts left + * @dev Dangerous to use when the user has any amount of debt! */ function withdrawMax() public { withdrawInternal(msg.sender, msg.sender, getWithdrawalLimitInternal(msg.sender)); } /** - @notice Function for using a signed message to withdraw on behalf of an address owning an escrow with collateral. - @dev Signed messaged can be invalidated by incrementing the nonce. Will always withdraw to the msg.sender. - @param from The address of the user owning the escrow being withdrawn from - @param amount The amount to be withdrawn - @param deadline Timestamp after which the signed message will be invalid - @param v The v param of the ECDSA signature - @param r The r param of the ECDSA signature - @param s The s param of the ECDSA signature + * @notice Function for using a signed message to withdraw on behalf of an address owning an escrow with collateral. + * @dev Signed messaged can be invalidated by incrementing the nonce. Will always withdraw to the msg.sender. + * @param from The address of the user owning the escrow being withdrawn from + * @param amount The amount to be withdrawn + * @param deadline Timestamp after which the signed message will be invalid + * @param v The v param of the ECDSA signature + * @param r The r param of the ECDSA signature + * @param s The s param of the ECDSA signature */ function withdrawOnBehalf(address from, uint amount, uint deadline, uint8 v, bytes32 r, bytes32 s) public { require(deadline >= block.timestamp, "DEADLINE_EXPIRED"); @@ -562,15 +620,15 @@ contract MarketV2 { } /** - @notice Function for using a signed message to withdraw on behalf of an address owning an escrow with collateral. - @dev Signed messaged can be invalidated by incrementing the nonce. Will always withdraw to the msg.sender. - @dev Useful for use with escrows that continously compound tokens, so there won't be dust amounts left - @dev Dangerous to use when the user has any amount of debt! - @param from The address of the user owning the escrow being withdrawn from - @param deadline Timestamp after which the signed message will be invalid - @param v The v param of the ECDSA signature - @param r The r param of the ECDSA signature - @param s The s param of the ECDSA signature + * @notice Function for using a signed message to withdraw on behalf of an address owning an escrow with collateral. + * @dev Signed messaged can be invalidated by incrementing the nonce. Will always withdraw to the msg.sender. + * @dev Useful for use with escrows that continously compound tokens, so there won't be dust amounts left + * @dev Dangerous to use when the user has any amount of debt! + * @param from The address of the user owning the escrow being withdrawn from + * @param deadline Timestamp after which the signed message will be invalid + * @param v The v param of the ECDSA signature + * @param r The r param of the ECDSA signature + * @param s The s param of the ECDSA signature */ function withdrawMaxOnBehalf(address from, uint deadline, uint8 v, bytes32 r, bytes32 s) public { require(deadline >= block.timestamp, "DEADLINE_EXPIRED"); @@ -603,17 +661,17 @@ contract MarketV2 { } /** - @notice Function for incrementing the nonce of the msg.sender, making their latest signed message unusable. + * @notice Function for incrementing the nonce of the msg.sender, making their latest signed message unusable. */ function invalidateNonce() public { nonces[msg.sender]++; } /** - @notice Function for repaying debt on behalf of user. Debt must be repaid in DOLA. - @dev If the user has a DBR deficit, they risk initial debt being accrued by forced replenishments. - @param borrower Address of the borrower whose debt is being repaid - @param amount DOLA amount to be repaid. If set to max uint debt will be repaid in full. + * @notice Function for repaying debt on behalf of user. Debt must be repaid in DOLA. + * @dev If the user has a DBR deficit, they risk initial debt being accrued by forced replenishments. + * @param borrower Address of the borrower whose debt is being repaid + * @param amount DOLA amount to be repaid. If set to max uint debt will be repaid in full. */ function repay(address borrower, uint amount) public { IDebtManager debtManager = debtManagers[borrower]; @@ -628,9 +686,9 @@ contract MarketV2 { } /** - @notice Bundles repayment and withdrawal into a single function call. - @param repayAmount Amount of DOLA to be repaid - @param withdrawAmount Amount of underlying to be withdrawn from the escrow + * @notice Bundles repayment and withdrawal into a single function call. + * @param repayAmount Amount of DOLA to be repaid + * @param withdrawAmount Amount of underlying to be withdrawn from the escrow */ function repayAndWithdraw(uint repayAmount, uint withdrawAmount) public { repay(msg.sender, repayAmount); @@ -638,33 +696,38 @@ contract MarketV2 { } /** - @notice Function for liquidating a user's under water debt. Debt is under water when the value of a user's debt is above their collateral factor. - @param borrower The user to be liquidated - @param repaidDebt Th amount of user user debt to liquidate. + * @notice Function for liquidating a user's under water debt. Debt is under water when the value of a user's debt is above their collateral factor. + * @param borrower The user to be liquidated + * @param repaidDebt Th amount of user user debt to liquidate. */ function liquidate(address borrower, uint repaidDebt) public { + MarketParams memory mp = marketParameters; //cache MarketParameters + uint price = oracle.getPrice(address(collateral), mp.collateralFactorBps); + IEscrow escrow = escrows[borrower]; + uint balance = escrow.balance(); + uint collateralValue = calcCollateralValue(balance, price); IDebtManager debtManager = debtManagers[borrower]; require(repaidDebt > 0, "Must repay positive debt"); uint debt = debtManager.debt(borrower); - require(getCreditLimitInternal(borrower) < debt, "User debt is healthy"); + uint borrowerCollateralFactorBps = 10000 * debt / collateralValue; + require(calcCreditLimit(collateralValue, mp.collateralFactorBps) < debt, "User debt is healthy"); + uint maxLiquidationAmount = mp.maxLiquidationAmount < debt ? mp.maxLiquidationAmount : debt; repaidDebt = debtManager.decreaseDebt(borrower, repaidDebt); - require(repaidDebt <= debt * liquidationFactorBps / 10000, "Exceeded liquidation factor"); - uint price = oracle.getPrice(address(collateral), collateralFactorBps); + require(repaidDebt < maxLiquidationAmount, "Repaid debt exceeds max liquidation amount"); uint liquidatorReward = repaidDebt * 1 ether / price; - liquidatorReward += liquidatorReward * liquidationIncentiveBps / 10000; + liquidatorReward += liquidatorReward * calcLiquidationIncentiveBps(mp, borrowerCollateralFactorBps) / 10000; if(address(borrowController) != address(0)){ borrowController.onRepay(repaidDebt); } dola.transferFrom(msg.sender, address(this), repaidDebt); - IEscrow escrow = escrows[borrower]; escrow.pay(msg.sender, liquidatorReward); - if(liquidationFeeBps > 0) { - uint liquidationFee = repaidDebt * 1 ether / price * liquidationFeeBps / 10000; - uint balance = escrow.balance(); - if(balance >= liquidationFee) { + if(calcLiquidationFeeBps(mp, borrowerCollateralFactorBps) > 0) { + uint liquidationFee = repaidDebt * 1 ether / price * calcLiquidationFeeBps(mp, borrowerCollateralFactorBps) / 10000; + uint remainingBalance = balance - liquidatorReward; + if(remainingBalance >= liquidationFee) { escrow.pay(gov, liquidationFee); - } else if(balance > 0) { - escrow.pay(gov, balance); + } else if(remainingBalance > 0) { + escrow.pay(gov, remainingBalance); } } emit Liquidate(borrower, msg.sender, repaidDebt, liquidatorReward); diff --git a/src/interfaces/IERC20.sol b/src/interfaces/IERC20.sol index 3e0798c..f13aa6e 100644 --- a/src/interfaces/IERC20.sol +++ b/src/interfaces/IERC20.sol @@ -6,6 +6,7 @@ interface IERC20 { function transferFrom(address,address,uint) external returns (bool); function balanceOf(address) external view returns (uint); function allowance(address from, address to) external view returns (uint); + function decimals() external view returns (uint8); } interface IMintable is IERC20 { diff --git a/test/MarketV2.t.sol b/test/MarketV2.t.sol new file mode 100644 index 0000000..03c5d5a --- /dev/null +++ b/test/MarketV2.t.sol @@ -0,0 +1,118 @@ +pragma solidity ^0.8.20; +import {MarketV2, IDolaBorrowingRights, IERC20, IOracle} from "src/MarketV2.sol"; +import {ERC20} from "test/mocks/ERC20.sol"; +import "forge-std/Test.sol"; + +contract MarketV2Test is Test { + + MarketV2 market; + address gov = address(1); + address lender; + address pauseGuardian = address(2); + address escrowImplementation; + address fixedDebtManager; + IDolaBorrowingRights dbr; + IERC20 collateral; + IOracle oracle; + MarketV2.MarketParams marketParams; + + function setUp() external{ + marketParams = MarketV2.MarketParams( + 8000, //cf + 9000, //max liquidation incentive threshold + 1000, //max liquidation incentive + 1000, //max liquidation fee + 9000, //zero liquidaiton fee threshold + 10_000 * 1e18, + false + ); + collateral = IERC20(address(new ERC20("Mock Collateral", "MOCK", 18))); + market = new MarketV2( + gov, + lender, + pauseGuardian, + escrowImplementation, + fixedDebtManager, + dbr, + collateral, + oracle, + marketParams + ); + } + + function testGetLiquidationIncentiveBps() external { + uint collateralFactor = marketParams.collateralFactorBps - 1; + uint liquidationIncentive = market.getLiquidationIncentiveBps(collateralFactor); + uint maxLiquidationIncentive = marketParams.maxLiquidationIncentiveBps; + uint thresholdDiff = marketParams.maxLiquidationIncentiveThresholdBps - marketParams.collateralFactorBps; + assertEq(0, liquidationIncentive, "Liquidation Incentive not 0% when CF below liquidation levels"); + + collateralFactor = marketParams.collateralFactorBps; + liquidationIncentive = market.getLiquidationIncentiveBps(collateralFactor); + assertEq(0, liquidationIncentive, "Liquidation Incentive not 0% when CF at liquidation threshold"); + + collateralFactor = marketParams.maxLiquidationIncentiveThresholdBps; + liquidationIncentive = market.getLiquidationIncentiveBps(collateralFactor); + assertEq(maxLiquidationIncentive, liquidationIncentive, "Liquidation Incentive not max when CF at max liquidation threshold"); + + collateralFactor = marketParams.maxLiquidationIncentiveThresholdBps + 1; + liquidationIncentive = market.getLiquidationIncentiveBps(collateralFactor); + assertEq(maxLiquidationIncentive, liquidationIncentive, "Liquidation Incentive not max when CF above max liquidation threshold"); + + collateralFactor = marketParams.collateralFactorBps + thresholdDiff / 10; + liquidationIncentive = market.getLiquidationIncentiveBps(collateralFactor); + assertEq(100, liquidationIncentive, "Liquidation Incentive not 1% when 10% to liquidation threshold"); + + collateralFactor = marketParams.collateralFactorBps + thresholdDiff / 4; + liquidationIncentive = market.getLiquidationIncentiveBps(collateralFactor); + assertEq(250, liquidationIncentive, "Liquidation Incentive not 2.5% when 25% to liquidation threshold"); + + collateralFactor = marketParams.collateralFactorBps + thresholdDiff / 2; + liquidationIncentive = market.getLiquidationIncentiveBps(collateralFactor); + assertEq(500, liquidationIncentive, "Liquidation Incentive not 5% when 50% to liquidation threshold"); + + collateralFactor = marketParams.collateralFactorBps + thresholdDiff * 3 / 4; + liquidationIncentive = market.getLiquidationIncentiveBps(collateralFactor); + assertEq(750, liquidationIncentive, "Liquidation Incentive not 7.5% when 75% to liquidation threshold"); + } + + function testGetLiquidationFeeBps() external { + uint collateralFactor = marketParams.collateralFactorBps - 1; + uint thresholdDiff = marketParams.zeroLiquidationFeeThresholdBps - marketParams.collateralFactorBps; + uint liquidationFee = market.getLiquidationFeeBps(collateralFactor); + uint maxLiquidationFee = marketParams.maxLiquidationFeeBps; + assertEq(0, liquidationFee, "Liquidation fee not 0% when CF below liquidation levels"); + + collateralFactor = marketParams.collateralFactorBps; + liquidationFee = market.getLiquidationFeeBps(collateralFactor); + assertEq(maxLiquidationFee, liquidationFee, "Liquidation fee not 10% when CF at liquidation levels"); + + collateralFactor = marketParams.zeroLiquidationFeeThresholdBps; + liquidationFee = market.getLiquidationFeeBps(collateralFactor); + assertEq(0, liquidationFee, "Liquidation fee not 0% when CF at zero fee threshold"); + + collateralFactor = marketParams.zeroLiquidationFeeThresholdBps + 1; + liquidationFee = market.getLiquidationFeeBps(collateralFactor); + assertEq(0, liquidationFee, "Liquidation fee not 0% when CF above zero fee threshold"); + + collateralFactor = marketParams.collateralFactorBps + thresholdDiff/10; + liquidationFee = market.getLiquidationFeeBps(collateralFactor); + assertEq(900, liquidationFee, "Liquidation fee not 9% when 10% towards zero fee threshold"); + + collateralFactor = marketParams.collateralFactorBps + thresholdDiff/4; + liquidationFee = market.getLiquidationFeeBps(collateralFactor); + assertEq(750, liquidationFee, "Liquidation fee not 7.5% when 25% towards zero fee threshold"); + + collateralFactor = marketParams.collateralFactorBps + thresholdDiff/2; + liquidationFee = market.getLiquidationFeeBps(collateralFactor); + assertEq(500, liquidationFee, "Liquidation fee not 5% when 50% towards zero fee threshold"); + + collateralFactor = marketParams.collateralFactorBps + thresholdDiff*3/4; + liquidationFee = market.getLiquidationFeeBps(collateralFactor); + assertEq(250, liquidationFee, "Liquidation fee not 2.5% when 75% towards zero fee threshold"); + + collateralFactor = marketParams.collateralFactorBps + thresholdDiff*99/100; + liquidationFee = market.getLiquidationFeeBps(collateralFactor); + assertEq(10, liquidationFee, "Liquidation fee not 0.1% when 99% towards zero fee threshold"); + } +}