diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index a2ec80d4..3dcad434 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -23,13 +23,16 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // --- Connected contract declarations --- IERC20 public immutable ETH; - ITroveManager public troveManager; + ITroveManager public immutable troveManager; address gasPoolAddress; ICollSurplusPool collSurplusPool; IBoldToken public boldToken; // A doubly linked list of Troves, sorted by their collateral ratios ISortedTroves public sortedTroves; + // Minimum collateral ratio for individual troves + uint256 public immutable MCR; + /* --- Variable container structs --- Used to hold, return and assign variables inside a function, in order to avoid the error: @@ -97,15 +100,21 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe ); event BoldBorrowingFeePaid(uint256 indexed _troveId, uint256 _boldFee); - constructor(address _ETHAddress) { - checkContract(_ETHAddress); - ETH = IERC20(_ETHAddress); + constructor(IERC20 _ETH, ITroveManager _troveManager) { + checkContract(address(_ETH)); + checkContract(address(_troveManager)); + + ETH = _ETH; + troveManager = _troveManager; + + MCR = _troveManager.MCR(); + + emit TroveManagerAddressChanged(address(_troveManager)); } // --- Dependency setters --- function setAddresses( - address _troveManagerAddress, address _activePoolAddress, address _defaultPoolAddress, address _gasPoolAddress, @@ -117,7 +126,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // This makes impossible to open a trove with zero withdrawn Bold assert(MIN_NET_DEBT > 0); - checkContract(_troveManagerAddress); checkContract(_activePoolAddress); checkContract(_defaultPoolAddress); checkContract(_gasPoolAddress); @@ -126,7 +134,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe checkContract(_sortedTrovesAddress); checkContract(_boldTokenAddress); - troveManager = ITroveManager(_troveManagerAddress); activePool = IActivePool(_activePoolAddress); defaultPool = IDefaultPool(_defaultPoolAddress); gasPoolAddress = _gasPoolAddress; @@ -135,7 +142,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe sortedTroves = ISortedTroves(_sortedTrovesAddress); boldToken = IBoldToken(_boldTokenAddress); - emit TroveManagerAddressChanged(_troveManagerAddress); emit ActivePoolAddressChanged(_activePoolAddress); emit DefaultPoolAddressChanged(_defaultPoolAddress); emit GasPoolAddressChanged(_gasPoolAddress); @@ -501,13 +507,11 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } /** - * Claim remaining collateral from a redemption or from a liquidation with ICR > MCR in Recovery Mode + * Claim remaining collateral from a liquidation with ICR exceeding the liquidation penalty */ - function claimCollateral(uint256 _troveId) external override { - _requireIsOwner(_troveId); - + function claimCollateral() external override { // send ETH from CollSurplus Pool to owner - collSurplusPool.claimColl(msg.sender, _troveId); + collSurplusPool.claimColl(msg.sender); } // --- Helper functions --- @@ -726,7 +730,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } } - function _requireICRisAboveMCR(uint256 _newICR) internal pure { + function _requireICRisAboveMCR(uint256 _newICR) internal view { require(_newICR >= MCR, "BorrowerOps: An operation that would result in ICR < MCR is not permitted"); } diff --git a/contracts/src/CollSurplusPool.sol b/contracts/src/CollSurplusPool.sol index 76599a86..8d3c763f 100644 --- a/contracts/src/CollSurplusPool.sol +++ b/contracts/src/CollSurplusPool.sol @@ -6,9 +6,8 @@ import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import "./Interfaces/ICollSurplusPool.sol"; import "./Dependencies/Ownable.sol"; -import "./Dependencies/CheckContract.sol"; -contract CollSurplusPool is Ownable, CheckContract, ICollSurplusPool { +contract CollSurplusPool is Ownable, ICollSurplusPool { using SafeERC20 for IERC20; string public constant NAME = "CollSurplusPool"; @@ -21,7 +20,7 @@ contract CollSurplusPool is Ownable, CheckContract, ICollSurplusPool { // deposited ether tracker uint256 internal ETHBalance; // Collateral surplus claimable by trove owners - mapping(uint256 => uint256) internal balances; + mapping(address => uint256) internal balances; // --- Events --- @@ -29,11 +28,10 @@ contract CollSurplusPool is Ownable, CheckContract, ICollSurplusPool { event TroveManagerAddressChanged(address _newTroveManagerAddress); event ActivePoolAddressChanged(address _newActivePoolAddress); - event CollBalanceUpdated(uint256 indexed _troveId, uint256 _newBalance); + event CollBalanceUpdated(address indexed _account, uint256 _newBalance); event EtherSent(address _to, uint256 _amount); constructor(address _ETHAddress) { - checkContract(_ETHAddress); ETH = IERC20(_ETHAddress); } @@ -44,10 +42,6 @@ contract CollSurplusPool is Ownable, CheckContract, ICollSurplusPool { override onlyOwner { - checkContract(_borrowerOperationsAddress); - checkContract(_troveManagerAddress); - checkContract(_activePoolAddress); - borrowerOperationsAddress = _borrowerOperationsAddress; troveManagerAddress = _troveManagerAddress; activePoolAddress = _activePoolAddress; @@ -65,29 +59,29 @@ contract CollSurplusPool is Ownable, CheckContract, ICollSurplusPool { return ETHBalance; } - function getCollateral(uint256 _troveId) external view override returns (uint256) { - return balances[_troveId]; + function getCollateral(address _account) external view override returns (uint256) { + return balances[_account]; } // --- Pool functionality --- - function accountSurplus(uint256 _troveId, uint256 _amount) external override { + function accountSurplus(address _account, uint256 _amount) external override { _requireCallerIsTroveManager(); - uint256 newAmount = balances[_troveId] + _amount; - balances[_troveId] = newAmount; + uint256 newAmount = balances[_account] + _amount; + balances[_account] = newAmount; ETHBalance = ETHBalance + _amount; - emit CollBalanceUpdated(_troveId, newAmount); + emit CollBalanceUpdated(_account, newAmount); } - function claimColl(address _account, uint256 _troveId) external override { + function claimColl(address _account) external override { _requireCallerIsBorrowerOperations(); - uint256 claimableColl = balances[_troveId]; + uint256 claimableColl = balances[_account]; require(claimableColl > 0, "CollSurplusPool: No collateral available to claim"); - balances[_troveId] = 0; - emit CollBalanceUpdated(_troveId, 0); + balances[_account] = 0; + emit CollBalanceUpdated(_account, 0); ETHBalance = ETHBalance - claimableColl; emit EtherSent(_account, claimableColl); diff --git a/contracts/src/Dependencies/LiquityBase.sol b/contracts/src/Dependencies/LiquityBase.sol index 4e9d9749..9661fe34 100644 --- a/contracts/src/Dependencies/LiquityBase.sol +++ b/contracts/src/Dependencies/LiquityBase.sol @@ -20,9 +20,6 @@ contract LiquityBase is BaseMath, ILiquityBase { uint256 public constant _100pct = 1000000000000000000; // 1e18 == 100% - // Minimum collateral ratio for individual troves - uint256 public constant MCR = 1100000000000000000; // 110% - // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied uint256 public constant CCR = 1500000000000000000; // 150% diff --git a/contracts/src/Dependencies/LiquityMath.sol b/contracts/src/Dependencies/LiquityMath.sol index a7c0718f..d96782eb 100644 --- a/contracts/src/Dependencies/LiquityMath.sol +++ b/contracts/src/Dependencies/LiquityMath.sol @@ -13,6 +13,10 @@ library LiquityMath { return (_a >= _b) ? _a : _b; } + function _sub_min_0(uint256 _a, uint256 _b) internal pure returns (uint256) { + return (_a > _b) ? _a - _b : 0; + } + /* * Multiply two decimal numbers and use normal rounding rules: * -round product up if 19'th mantissa digit >= 5 diff --git a/contracts/src/Interfaces/IBorrowerOperations.sol b/contracts/src/Interfaces/IBorrowerOperations.sol index fd9eba32..b704087d 100644 --- a/contracts/src/Interfaces/IBorrowerOperations.sol +++ b/contracts/src/Interfaces/IBorrowerOperations.sol @@ -13,7 +13,6 @@ interface IBorrowerOperations is ILiquityBase { function sortedTroves() external view returns (ISortedTroves); function setAddresses( - address _troveManagerAddress, address _activePoolAddress, address _defaultPoolAddress, address _gasPoolAddress, @@ -61,7 +60,7 @@ interface IBorrowerOperations is ILiquityBase { uint256 _lowerHint ) external; - function claimCollateral(uint256 _troveId) external; + function claimCollateral() external; function setAddManager(uint256 _troveId, address _manager) external; function setRemoveManager(uint256 _troveId, address _manager) external; diff --git a/contracts/src/Interfaces/ICollSurplusPool.sol b/contracts/src/Interfaces/ICollSurplusPool.sol index bfb2866c..43a0d058 100644 --- a/contracts/src/Interfaces/ICollSurplusPool.sol +++ b/contracts/src/Interfaces/ICollSurplusPool.sol @@ -8,9 +8,9 @@ interface ICollSurplusPool { function getETHBalance() external view returns (uint256); - function getCollateral(uint256 _troveId) external view returns (uint256); + function getCollateral(address _account) external view returns (uint256); - function accountSurplus(uint256 _troveId, uint256 _amount) external; + function accountSurplus(address _account, uint256 _amount) external; - function claimColl(address _account, uint256 _troveId) external; + function claimColl(address _account) external; } diff --git a/contracts/src/Interfaces/ILiquityBase.sol b/contracts/src/Interfaces/ILiquityBase.sol index 81504c2d..086a1f73 100644 --- a/contracts/src/Interfaces/ILiquityBase.sol +++ b/contracts/src/Interfaces/ILiquityBase.sol @@ -12,6 +12,5 @@ interface ILiquityBase { function priceFeed() external view returns (IPriceFeed); function BOLD_GAS_COMPENSATION() external view returns (uint256); function MIN_NET_DEBT() external view returns (uint256); - function MCR() external view returns (uint256); function getEntireSystemDebt() external view returns (uint256); } diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index 3ad4503b..cb8e6fb0 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -11,6 +11,8 @@ import "./ISortedTroves.sol"; // Common interface for the Trove Manager. interface ITroveManager is IERC721, ILiquityBase { + function MCR() external view returns (uint256); + function setAddresses( address _borrowerOperationsAddress, address _activePoolAddress, diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 8e8f39c5..79fe5a16 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -31,6 +31,13 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { // --- Data structures --- + // Minimum collateral ratio for individual troves + uint256 public immutable MCR; + // Liquidation penalty for troves offset to the SP + uint256 public immutable LIQUIDATION_PENALTY_SP; + // Liquidation penalty for troves redistributed + uint256 public immutable LIQUIDATION_PENALTY_REDISTRIBUTION; + uint256 public constant SECONDS_IN_ONE_YEAR = 31536000; // 60 * 60 * 24 * 365, uint256 public constant STALE_TROVE_DURATION = 7776000; // 90 days: 60*60*24*90 = 7776000 @@ -235,7 +242,18 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { redeemCollateral } - constructor() ERC721(NAME, SYMBOL) {} + constructor(uint256 _mcr, uint256 _liquidationPenaltySP, uint256 _liquidationPenaltyRedistribution) + ERC721(NAME, SYMBOL) + { + require(_mcr > 1e18 && _mcr < 2e18, "Invalid MCR"); + require(_liquidationPenaltySP >= 5e16, "SP penalty too low"); + require(_liquidationPenaltySP <= _liquidationPenaltyRedistribution, "SP penalty cannot be > redist"); + require(_liquidationPenaltyRedistribution <= 10e16, "Redistribution penalty too high"); + + MCR = _mcr; + LIQUIDATION_PENALTY_SP = _liquidationPenaltySP; + LIQUIDATION_PENALTY_REDISTRIBUTION = _liquidationPenaltyRedistribution; + } // --- Dependency setter --- @@ -302,10 +320,12 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { // --- Inner single liquidation functions --- // Liquidate one trove, in Normal Mode. - function _liquidate(IDefaultPool _defaultPool, uint256 _troveId, uint256 _boldInStabPool) + function _liquidate(IDefaultPool _defaultPool, uint256 _troveId, uint256 _boldInStabPool, uint256 _price) internal returns (LiquidationValues memory singleLiquidation) { + address owner = ownerOf(_troveId); + LocalVariables_InnerSingleLiquidateFunction memory vars; ( singleLiquidation.entireTroveDebt, @@ -331,10 +351,17 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { singleLiquidation.debtToOffset, singleLiquidation.collToSendToSP, singleLiquidation.debtToRedistribute, - singleLiquidation.collToRedistribute - ) = _getOffsetAndRedistributionVals(singleLiquidation.entireTroveDebt, collToLiquidate, _boldInStabPool); + singleLiquidation.collToRedistribute, + singleLiquidation.collSurplus + ) = _getOffsetAndRedistributionVals(singleLiquidation.entireTroveDebt, collToLiquidate, _boldInStabPool, _price); _closeTrove(_troveId, Status.closedByLiquidation); + + // Differencen between liquidation penalty and liquidation threshold + if (singleLiquidation.collSurplus > 0) { + collSurplusPool.accountSurplus(owner, singleLiquidation.collSurplus); + } + emit TroveLiquidated( _troveId, singleLiquidation.entireTroveDebt, @@ -350,33 +377,69 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { */ function _getOffsetAndRedistributionVals( uint256 _entireTroveDebt, - uint256 _collToLiquidate, - uint256 _boldInStabPool + uint256 _collToLiquidate, // gas compensation is already subtracted + uint256 _boldInStabPool, + uint256 _price ) internal - pure - returns (uint256 debtToOffset, uint256 collToSendToSP, uint256 debtToRedistribute, uint256 collToRedistribute) + view + returns ( + uint256 debtToOffset, + uint256 collToSendToSP, + uint256 debtToRedistribute, + uint256 collToRedistribute, + uint256 collSurplus + ) { + uint256 collSPPortion; + /* + * Offset as much debt & collateral as possible against the Stability Pool, and redistribute the remainder + * between all active troves. + * + * If the trove's debt is larger than the deposited Bold in the Stability Pool: + * + * - Offset an amount of the trove's debt equal to the Bold in the Stability Pool + * - Send a fraction of the trove's collateral to the Stability Pool, equal to the fraction of its offset debt + * + */ if (_boldInStabPool > 0) { - /* - * Offset as much debt & collateral as possible against the Stability Pool, and redistribute the remainder - * between all active troves. - * - * If the trove's debt is larger than the deposited Bold in the Stability Pool: - * - * - Offset an amount of the trove's debt equal to the Bold in the Stability Pool - * - Send a fraction of the trove's collateral to the Stability Pool, equal to the fraction of its offset debt - * - */ debtToOffset = LiquityMath._min(_entireTroveDebt, _boldInStabPool); - collToSendToSP = _collToLiquidate * debtToOffset / _entireTroveDebt; - debtToRedistribute = _entireTroveDebt - debtToOffset; - collToRedistribute = _collToLiquidate - collToSendToSP; + collSPPortion = _collToLiquidate * debtToOffset / _entireTroveDebt; + (collToSendToSP, collSurplus) = + _getCollPenaltyAndSurplus(collSPPortion, debtToOffset, LIQUIDATION_PENALTY_SP, _price); + } + // TODO: this fails if debt in gwei is less than price (rounding coll to zero) + //assert(debtToOffset == 0 || collToSendToSP > 0); + + // Redistribution + debtToRedistribute = _entireTroveDebt - debtToOffset; + if (debtToRedistribute > 0) { + uint256 collRedistributionPortion = _collToLiquidate - collSPPortion; + if (collRedistributionPortion > 0) { + (collToRedistribute, collSurplus) = _getCollPenaltyAndSurplus( + collRedistributionPortion + collSurplus, // Coll surplus from offset can be eaten up by red. penalty + debtToRedistribute, + LIQUIDATION_PENALTY_REDISTRIBUTION, // _penaltyRatio + _price + ); + } + } + assert(_collToLiquidate == collToSendToSP + collToRedistribute + collSurplus); + } + + function _getCollPenaltyAndSurplus( + uint256 _collToLiquidate, + uint256 _debtToLiquidate, + uint256 _penaltyRatio, + uint256 _price + ) internal pure returns (uint256 seizedColl, uint256 collSurplus) { + uint256 maxSeizedColl = _debtToLiquidate * (DECIMAL_PRECISION + _penaltyRatio) / _price; + if (_collToLiquidate > maxSeizedColl) { + seizedColl = maxSeizedColl; + collSurplus = _collToLiquidate - maxSeizedColl; } else { - debtToOffset = 0; - collToSendToSP = 0; - debtToRedistribute = _entireTroveDebt; - collToRedistribute = _collToLiquidate; + seizedColl = _collToLiquidate; + collSurplus = 0; } } @@ -389,11 +452,12 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { uint256 _recordedTroveDebt, uint256 _weightedRecordedTroveDebt, uint256 _price - ) internal pure returns (LiquidationValues memory singleLiquidation) { + ) internal view returns (LiquidationValues memory singleLiquidation) { singleLiquidation.entireTroveDebt = _entireTroveDebt; singleLiquidation.entireTroveColl = _entireTroveColl; singleLiquidation.recordedTroveDebt = _recordedTroveDebt; singleLiquidation.weightedRecordedTroveDebt = _weightedRecordedTroveDebt; + // TODO: We don’t bother updating this because we are removing RM uint256 cappedCollPortion = _entireTroveDebt * MCR / _price; singleLiquidation.collGasCompensation = _getCollGasCompensation(cappedCollPortion); @@ -435,7 +499,10 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { ); // Move liquidated ETH and Bold to the appropriate pools - stabilityPoolCached.offset(totals.totalDebtToOffset, totals.totalCollToSendToSP); + if (totals.totalDebtToOffset > 0 || totals.totalCollToSendToSP > 0) { + stabilityPoolCached.offset(totals.totalDebtToOffset, totals.totalCollToSendToSP); + } + // we check amount is not zero inside _redistributeDebtAndColl( activePoolCached, defaultPoolCached, totals.totalDebtToRedistribute, totals.totalCollToRedistribute ); @@ -482,7 +549,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { vars.ICR = getCurrentICR(vars.troveId, _price); if (vars.ICR < MCR) { - singleLiquidation = _liquidate(_defaultPool, vars.troveId, vars.remainingBoldInStabPool); + singleLiquidation = _liquidate(_defaultPool, vars.troveId, vars.remainingBoldInStabPool, _price); vars.remainingBoldInStabPool = vars.remainingBoldInStabPool - singleLiquidation.debtToOffset; // Add liquidation values to their respective running totals diff --git a/contracts/src/deployment.sol b/contracts/src/deployment.sol index 3c10da0a..5f489e6f 100644 --- a/contracts/src/deployment.sol +++ b/contracts/src/deployment.sol @@ -34,22 +34,38 @@ struct LiquityContracts { IERC20 WETH; } +struct TroveManagerParams { + uint256 MCR; + uint256 LIQUIDATION_PENALTY_SP; + uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; +} + function _deployAndConnectContracts() returns (LiquityContracts memory contracts, ICollateralRegistry collateralRegistry, IBoldToken boldToken) +{ + return _deployAndConnectContracts(TroveManagerParams(110e16, 5e16, 10e16)); +} + +function _deployAndConnectContracts(TroveManagerParams memory troveManagerParams) + returns (LiquityContracts memory contracts, ICollateralRegistry collateralRegistry, IBoldToken boldToken) { LiquityContracts[] memory contractsArray; - (contractsArray, collateralRegistry, boldToken) = _deployAndConnectContracts(1); + TroveManagerParams[] memory troveManagerParamsArray = new TroveManagerParams[](1); + + troveManagerParamsArray[0] = troveManagerParams; + (contractsArray, collateralRegistry, boldToken) = _deployAndConnectContracts(troveManagerParamsArray); contracts = contractsArray[0]; } -function _deployAndConnectContracts(uint256 _numCollaterals) +function _deployAndConnectContracts(TroveManagerParams[] memory troveManagerParamsArray) returns (LiquityContracts[] memory contractsArray, ICollateralRegistry collateralRegistry, IBoldToken boldToken) { + uint256 numCollaterals = troveManagerParamsArray.length; boldToken = new BoldToken(); - contractsArray = new LiquityContracts[](_numCollaterals); - IERC20[] memory collaterals = new IERC20[](_numCollaterals); - ITroveManager[] memory troveManagers = new ITroveManager[](_numCollaterals); + contractsArray = new LiquityContracts[](numCollaterals); + IERC20[] memory collaterals = new IERC20[](numCollaterals); + ITroveManager[] memory troveManagers = new ITroveManager[](numCollaterals); LiquityContracts memory contracts; IERC20 WETH = new ERC20Faucet( @@ -58,20 +74,20 @@ function _deployAndConnectContracts(uint256 _numCollaterals) 100 ether, // _tapAmount 1 days // _tapPeriod ); - contracts = _deployAndConnectCollateralContracts(WETH, boldToken); + contracts = _deployAndConnectCollateralContracts(WETH, boldToken, troveManagerParamsArray[0]); contractsArray[0] = contracts; collaterals[0] = contracts.WETH; troveManagers[0] = contracts.troveManager; // Multicollateral registry - for (uint256 i = 1; i < _numCollaterals; i++) { + for (uint256 i = 1; i < numCollaterals; i++) { IERC20 stETH = new ERC20Faucet( string.concat("Staked ETH", string(abi.encode(i))), // _name string.concat("stETH", string(abi.encode(i))), // _symbol 100 ether, // _tapAmount 1 days // _tapPeriod ); - contracts = _deployAndConnectCollateralContracts(stETH, boldToken); + contracts = _deployAndConnectCollateralContracts(stETH, boldToken, troveManagerParamsArray[i]); collaterals[i] = contracts.WETH; troveManagers[i] = contracts.troveManager; contractsArray[i] = contracts; @@ -80,28 +96,34 @@ function _deployAndConnectContracts(uint256 _numCollaterals) collateralRegistry = new CollateralRegistry(boldToken, collaterals, troveManagers); boldToken.setCollateralRegistry(address(collateralRegistry)); // Set registry in TroveManagers - for (uint256 i = 0; i < _numCollaterals; i++) { + for (uint256 i = 0; i < numCollaterals; i++) { contractsArray[i].troveManager.setCollateralRegistry(address(collateralRegistry)); } } -function _deployAndConnectCollateralContracts(IERC20 _collateralToken, IBoldToken _boldToken) - returns (LiquityContracts memory contracts) -{ +function _deployAndConnectCollateralContracts( + IERC20 _collateralToken, + IBoldToken _boldToken, + TroveManagerParams memory troveManagerParams +) returns (LiquityContracts memory contracts) { // TODO: optimize deployment order & constructor args & connector functions contracts.WETH = _collateralToken; // Deploy all contracts contracts.activePool = new ActivePool(address(_collateralToken)); - contracts.borrowerOperations = new BorrowerOperations(address(_collateralToken)); + contracts.troveManager = new TroveManager( + troveManagerParams.MCR, + troveManagerParams.LIQUIDATION_PENALTY_SP, + troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION + ); + contracts.borrowerOperations = new BorrowerOperations(_collateralToken, contracts.troveManager); contracts.collSurplusPool = new CollSurplusPool(address(_collateralToken)); contracts.defaultPool = new DefaultPool(address(_collateralToken)); contracts.gasPool = new GasPool(); contracts.priceFeed = new PriceFeedTestnet(); contracts.sortedTroves = new SortedTroves(); contracts.stabilityPool = new StabilityPool(address(_collateralToken)); - contracts.troveManager = new TroveManager(); contracts.interestRouter = new MockInterestRouter(); _boldToken.setBranchAddresses( @@ -129,7 +151,6 @@ function _deployAndConnectCollateralContracts(IERC20 _collateralToken, IBoldToke // set contracts in BorrowerOperations contracts.borrowerOperations.setAddresses( - address(contracts.troveManager), address(contracts.activePool), address(contracts.defaultPool), address(contracts.gasPool), diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index db86d9cb..3b8f8c5a 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -35,7 +35,7 @@ contract BaseTest is Test { uint256 public constant MAX_UINT256 = type(uint256).max; uint256 public constant SECONDS_IN_1_YEAR = 31536000; // 60*60*24*365 uint256 _100pct = 100e16; - uint256 MCR = 110e16; + uint256 MCR; uint256 CCR = 150e16; address public constant ZERO_ADDRESS = address(0); diff --git a/contracts/src/test/TestContracts/BorrowerOperationsTester.sol b/contracts/src/test/TestContracts/BorrowerOperationsTester.sol index b3e02581..a9b8b01e 100644 --- a/contracts/src/test/TestContracts/BorrowerOperationsTester.sol +++ b/contracts/src/test/TestContracts/BorrowerOperationsTester.sol @@ -7,7 +7,7 @@ import "../../BorrowerOperations.sol"; /* Tester contract inherits from BorrowerOperations, and provides external functions for testing the parent's internal functions. */ contract BorrowerOperationsTester is BorrowerOperations { - constructor(address _ETHAddress) BorrowerOperations(_ETHAddress) {} + constructor(IERC20 _ETH, ITroveManager _troveManager) BorrowerOperations(_ETH, _troveManager) {} function getNewICRFromTroveChange( uint256 _coll, diff --git a/contracts/src/test/TestContracts/DevTestSetup.sol b/contracts/src/test/TestContracts/DevTestSetup.sol index 83abeaee..e0152bc9 100644 --- a/contracts/src/test/TestContracts/DevTestSetup.sol +++ b/contracts/src/test/TestContracts/DevTestSetup.sol @@ -66,6 +66,8 @@ contract DevTestSetup is BaseTest { troveManager = contracts.troveManager; mockInterestRouter = contracts.interestRouter; + MCR = troveManager.MCR(); + // Give some ETH to test accounts, and approve it to BorrowerOperations uint256 initialETHAmount = 1000_000e18; for (uint256 i = 0; i < 6; i++) { diff --git a/contracts/src/test/TestContracts/TroveManagerTester.sol b/contracts/src/test/TestContracts/TroveManagerTester.sol index ec4388d0..a12ebc28 100644 --- a/contracts/src/test/TestContracts/TroveManagerTester.sol +++ b/contracts/src/test/TestContracts/TroveManagerTester.sol @@ -8,6 +8,10 @@ import "../../TroveManager.sol"; for testing the parent's internal functions. */ contract TroveManagerTester is TroveManager { + constructor(uint256 _mcr, uint256 _liquidationPenaltySP, uint256 _liquidationPenaltyRedistribution) + TroveManager(_mcr, _liquidationPenaltySP, _liquidationPenaltyRedistribution) + {} + function computeICR(uint256 _coll, uint256 _debt, uint256 _price) external pure returns (uint256) { return LiquityMath._computeCR(_coll, _debt, _price); } diff --git a/contracts/src/test/liquidations.t.sol b/contracts/src/test/liquidations.t.sol new file mode 100644 index 00000000..259f6356 --- /dev/null +++ b/contracts/src/test/liquidations.t.sol @@ -0,0 +1,287 @@ +pragma solidity ^0.8.18; + +import "./TestContracts/DevTestSetup.sol"; + +contract LiquidationsTest is DevTestSetup { + function testLiquidationOffsetWithSurplus() public { + uint256 liquidationAmount = 2000e18; + uint256 collAmount = 2e18; + + priceFeed.setPrice(2000e18); + vm.startPrank(A); + uint256 ATroveId = borrowerOperations.openTrove( + A, 0, collAmount, liquidationAmount - troveManager.BOLD_GAS_COMPENSATION(), 0, 0, 0 + ); + vm.stopPrank(); + + vm.startPrank(B); + borrowerOperations.openTrove(B, 0, 2 * collAmount, liquidationAmount, 0, 0, 0); + vm.stopPrank(); + // B deposits to SP + makeSPDepositAndClaim(B, liquidationAmount); + + // Price drops + priceFeed.setPrice(1100e18 - 1); + uint256 price = priceFeed.fetchPrice(); + + uint256 initialSPBoldBalance = stabilityPool.getTotalBoldDeposits(); + uint256 initialSPETHBalance = stabilityPool.getETHBalance(); + uint256 AInitialETHBalance = WETH.balanceOf(A); + + // Check not RM + assertEq(troveManager.checkBelowCriticalThreshold(price), false, "System should not be below CT"); + + // Check CR_A < MCR and TCR > CCR + assertLt(troveManager.getCurrentICR(ATroveId, price), MCR); + assertGt(troveManager.getTCR(price), CCR); + + uint256 trovesCount = troveManager.getTroveIdsCount(); + assertEq(trovesCount, 2); + + troveManager.liquidate(ATroveId); + + // Check Troves count reduced by 1 + trovesCount = troveManager.getTroveIdsCount(); + assertEq(trovesCount, 1); + + // Check SP Bold has decreased + uint256 finalSPBoldBalance = stabilityPool.getTotalBoldDeposits(); + assertEq(initialSPBoldBalance - finalSPBoldBalance, liquidationAmount, "SP Bold balance mismatch"); + // Check SP ETH has increased + uint256 finalSPETHBalance = stabilityPool.getETHBalance(); + // liquidationAmount to ETH + 5% + assertApproxEqAbs( + finalSPETHBalance - initialSPETHBalance, + liquidationAmount * DECIMAL_PRECISION / price * 105 / 100, + 10, + "SP ETH balance mismatch" + ); + + // Check A retains ~4.5% of the collateral (after claiming from CollSurplus) + // collAmount - 0.5% - (liquidationAmount to ETH + 5%) + uint256 collSurplusAmount = collAmount * 995 / 1000 - liquidationAmount * DECIMAL_PRECISION / price * 105 / 100; + assertEq( + WETH.balanceOf(address(collSurplusPool)), + collSurplusAmount, + "CollSurplusPoll should have received collateral" + ); + vm.startPrank(A); + borrowerOperations.claimCollateral(); + vm.stopPrank(); + assertEq(WETH.balanceOf(A) - AInitialETHBalance, collSurplusAmount, "A collateral balance mismatch"); + } + + function testLiquidationOffsetNoSurplus() public { + uint256 liquidationAmount = 10000e18; + uint256 collAmount = 10e18; + + priceFeed.setPrice(2000e18); + vm.startPrank(A); + uint256 ATroveId = borrowerOperations.openTrove( + A, 0, collAmount, liquidationAmount - troveManager.BOLD_GAS_COMPENSATION(), 0, 0, 0 + ); + vm.stopPrank(); + + vm.startPrank(B); + borrowerOperations.openTrove(B, 0, 3 * collAmount, liquidationAmount, 0, 0, 0); + vm.stopPrank(); + // B deposits to SP + makeSPDepositAndClaim(B, liquidationAmount); + + // Price drops + priceFeed.setPrice(1030e18); + uint256 price = priceFeed.fetchPrice(); + + uint256 initialSPBoldBalance = stabilityPool.getTotalBoldDeposits(); + uint256 initialSPETHBalance = stabilityPool.getETHBalance(); + + // Check not RM + assertEq(troveManager.checkBelowCriticalThreshold(price), false, "System should not be below CT"); + + // Check CR_A < MCR and TCR > CCR + assertLt(troveManager.getCurrentICR(ATroveId, price), MCR, "ICR too high"); + assertGe(troveManager.getTCR(price), CCR, "TCR too low"); + + uint256 trovesCount = troveManager.getTroveIdsCount(); + assertEq(trovesCount, 2); + + troveManager.liquidate(ATroveId); + + // Check Troves count reduced by 1 + trovesCount = troveManager.getTroveIdsCount(); + assertEq(trovesCount, 1); + + // Check SP Bold has decreased + uint256 finalSPBoldBalance = stabilityPool.getTotalBoldDeposits(); + assertEq(initialSPBoldBalance - finalSPBoldBalance, liquidationAmount, "SP Bold balance mismatch"); + // Check SP ETH has increased by coll minus coll gas comp + uint256 finalSPETHBalance = stabilityPool.getETHBalance(); + // liquidationAmount to ETH + 5% + assertApproxEqAbs( + finalSPETHBalance - initialSPETHBalance, collAmount * 995 / 1000, 10, "SP ETH balance mismatch" + ); + + // Check there’s no surplus + assertEq(WETH.balanceOf(address(collSurplusPool)), 0, "CollSurplusPoll should be empty"); + + vm.startPrank(A); + vm.expectRevert("CollSurplusPool: No collateral available to claim"); + borrowerOperations.claimCollateral(); + vm.stopPrank(); + } + + function testLiquidationRedistributionNoSurplus() public { + uint256 liquidationAmount = 2000e18; + uint256 collAmount = 2e18; + + priceFeed.setPrice(2000e18); + vm.startPrank(A); + uint256 ATroveId = borrowerOperations.openTrove( + A, 0, collAmount, liquidationAmount - troveManager.BOLD_GAS_COMPENSATION(), 0, 0, 0 + ); + vm.stopPrank(); + + vm.startPrank(B); + uint256 BTroveId = borrowerOperations.openTrove(B, 0, 2 * collAmount, liquidationAmount, 0, 0, 0); + + // Price drops + priceFeed.setPrice(1100e18 - 1); + uint256 price = priceFeed.fetchPrice(); + + uint256 BInitialDebt = troveManager.getTroveEntireDebt(BTroveId); + uint256 BInitialColl = troveManager.getTroveEntireColl(BTroveId); + + // Check not RM + assertEq(troveManager.checkBelowCriticalThreshold(price), false, "System should not be below CT"); + + // Check CR_A < MCR and TCR > CCR + assertLt(troveManager.getCurrentICR(ATroveId, price), MCR); + assertGt(troveManager.getTCR(price), CCR); + + // Check empty SP + assertEq(stabilityPool.getTotalBoldDeposits(), 0, "SP should be empty"); + + uint256 trovesCount = troveManager.getTroveIdsCount(); + assertEq(trovesCount, 2); + + troveManager.liquidate(ATroveId); + + // Check Troves count reduced by 1 + trovesCount = troveManager.getTroveIdsCount(); + assertEq(trovesCount, 1); + + // Check SP stays the same + assertEq(stabilityPool.getTotalBoldDeposits(), 0, "SP should be empty"); + assertEq(stabilityPool.getETHBalance(), 0, "SP should not have ETH rewards"); + + // Check B has received debt + assertEq(troveManager.getTroveEntireDebt(BTroveId) - BInitialDebt, liquidationAmount, "B debt mismatch"); + // Check B has received all coll minus coll gas comp + assertApproxEqAbs( + troveManager.getTroveEntireColl(BTroveId) - BInitialColl, + collAmount * 995 / 1000, // Collateral - coll gas comp + 10, + "B trove coll mismatch" + ); + + assertEq(WETH.balanceOf(address(collSurplusPool)), 0, "CollSurplusPoll should be empty"); + } + + struct InitialValues { + uint256 spBoldBalance; + uint256 spETHBalance; + uint256 AETHBalance; + uint256 BDebt; + uint256 BColl; + } + + // Offset and Redistribution + function testLiquidationMix() public { + uint256 liquidationAmount = 2000e18; + uint256 collAmount = 2e18; + + priceFeed.setPrice(2000e18); + vm.startPrank(A); + uint256 ATroveId = borrowerOperations.openTrove( + A, 0, collAmount, liquidationAmount - troveManager.BOLD_GAS_COMPENSATION(), 0, 0, 0 + ); + vm.stopPrank(); + + vm.startPrank(B); + uint256 BTroveId = borrowerOperations.openTrove(B, 0, 2 * collAmount, liquidationAmount, 0, 0, 0); + vm.stopPrank(); + // B deposits to SP + makeSPDepositAndClaim(B, liquidationAmount / 2); + + // Price drops + priceFeed.setPrice(1100e18 - 1); + uint256 price = priceFeed.fetchPrice(); + + InitialValues memory initialValues; + initialValues.spBoldBalance = stabilityPool.getTotalBoldDeposits(); + initialValues.spETHBalance = stabilityPool.getETHBalance(); + initialValues.AETHBalance = WETH.balanceOf(A); + initialValues.BDebt = troveManager.getTroveEntireDebt(BTroveId); + initialValues.BColl = troveManager.getTroveEntireColl(BTroveId); + + // Check not RM + assertEq(troveManager.checkBelowCriticalThreshold(price), false, "System should not be below CT"); + + // Check CR_A < MCR and TCR > CCR + assertLt(troveManager.getCurrentICR(ATroveId, price), MCR); + assertGt(troveManager.getTCR(price), CCR); + + uint256 trovesCount = troveManager.getTroveIdsCount(); + assertEq(trovesCount, 2); + + troveManager.liquidate(ATroveId); + + // Check Troves count reduced by 1 + trovesCount = troveManager.getTroveIdsCount(); + assertEq(trovesCount, 1); + + // Check SP Bold has decreased + uint256 finalSPBoldBalance = stabilityPool.getTotalBoldDeposits(); + assertEq(initialValues.spBoldBalance - finalSPBoldBalance, liquidationAmount / 2, "SP Bold balance mismatch"); + // Check SP ETH has increased + uint256 finalSPETHBalance = stabilityPool.getETHBalance(); + // liquidationAmount to ETH + 5% + assertApproxEqAbs( + finalSPETHBalance - initialValues.spETHBalance, + liquidationAmount / 2 * DECIMAL_PRECISION / price * 105 / 100, + 10, + "SP ETH balance mismatch" + ); + + // Check B has received debt + assertEq( + troveManager.getTroveEntireDebt(BTroveId) - initialValues.BDebt, liquidationAmount / 2, "B debt mismatch" + ); + // Check B has received coll + assertApproxEqAbs( + troveManager.getTroveEntireColl(BTroveId) - initialValues.BColl, + //collAmount * 995 / 1000 - liquidationAmount / 2 * DECIMAL_PRECISION / price * 105 / 100, + liquidationAmount / 2 * DECIMAL_PRECISION / price * 110 / 100, + 10, + "B trove coll mismatch" + ); + + // Check A retains ~4.5% of the collateral (after claiming from CollSurplus) + // collAmount - 0.5% - (liquidationAmount to ETH + 5%) + uint256 collSurplusAmount = collAmount * 995 / 1000 + - liquidationAmount / 2 * DECIMAL_PRECISION / price * 105 / 100 + - liquidationAmount / 2 * DECIMAL_PRECISION / price * 110 / 100; + assertApproxEqAbs( + WETH.balanceOf(address(collSurplusPool)), + collSurplusAmount, + 10, + "CollSurplusPoll should have received collateral" + ); + vm.startPrank(A); + borrowerOperations.claimCollateral(); + vm.stopPrank(); + assertApproxEqAbs( + WETH.balanceOf(A) - initialValues.AETHBalance, collSurplusAmount, 10, "A collateral balance mismatch" + ); + } +} diff --git a/contracts/src/test/liquidationsLST.t.sol b/contracts/src/test/liquidationsLST.t.sol new file mode 100644 index 00000000..e3900515 --- /dev/null +++ b/contracts/src/test/liquidationsLST.t.sol @@ -0,0 +1,251 @@ +pragma solidity ^0.8.18; + +import "./TestContracts/DevTestSetup.sol"; + +contract LiquidationsLSTTest is DevTestSetup { + function setUp() public override { + // Start tests at a non-zero timestamp + vm.warp(block.timestamp + 600); + + accounts = new Accounts(); + createAccounts(); + + (A, B, C, D, E, F, G) = ( + accountsList[0], + accountsList[1], + accountsList[2], + accountsList[3], + accountsList[4], + accountsList[5], + accountsList[6] + ); + + LiquityContracts memory contracts; + (contracts, collateralRegistry, boldToken) = _deployAndConnectContracts(TroveManagerParams(120e16, 5e16, 10e16)); + WETH = contracts.WETH; + activePool = contracts.activePool; + borrowerOperations = contracts.borrowerOperations; + collSurplusPool = contracts.collSurplusPool; + defaultPool = contracts.defaultPool; + gasPool = contracts.gasPool; + priceFeed = contracts.priceFeed; + sortedTroves = contracts.sortedTroves; + stabilityPool = contracts.stabilityPool; + troveManager = contracts.troveManager; + mockInterestRouter = contracts.interestRouter; + + MCR = troveManager.MCR(); + + // Give some ETH to test accounts, and approve it to BorrowerOperations + uint256 initialETHAmount = 10_000e18; + for (uint256 i = 0; i < 6; i++) { + // A to F + giveAndApproveETH(accountsList[i], initialETHAmount); + } + } + + function testLiquidationRedistributionWithSurplus() public { + uint256 liquidationAmount = 2000e18; + uint256 collAmount = 2e18; + + priceFeed.setPrice(2000e18); + vm.startPrank(A); + uint256 ATroveId = borrowerOperations.openTrove( + A, 0, collAmount, liquidationAmount - troveManager.BOLD_GAS_COMPENSATION(), 0, 0, 0 + ); + vm.stopPrank(); + + vm.startPrank(B); + uint256 BTroveId = borrowerOperations.openTrove(B, 0, 2 * collAmount, liquidationAmount, 0, 0, 0); + + // Price drops + priceFeed.setPrice(1200e18 - 1); + uint256 price = priceFeed.fetchPrice(); + + uint256 BInitialDebt = troveManager.getTroveEntireDebt(BTroveId); + uint256 BInitialColl = troveManager.getTroveEntireColl(BTroveId); + uint256 AInitialETHBalance = WETH.balanceOf(A); + + // Check not RM + assertEq(troveManager.checkBelowCriticalThreshold(price), false, "System should not be below CT"); + + // Check CR_A < MCR and TCR > CCR + assertLt(troveManager.getCurrentICR(ATroveId, price), MCR); + assertGt(troveManager.getTCR(price), CCR); + + uint256 trovesCount = troveManager.getTroveIdsCount(); + assertEq(trovesCount, 2); + + troveManager.liquidate(ATroveId); + + // Check Troves count reduced by 1 + trovesCount = troveManager.getTroveIdsCount(); + assertEq(trovesCount, 1); + + // Check SP stays the same + assertEq(stabilityPool.getTotalBoldDeposits(), 0, "SP should be empty"); + assertEq(stabilityPool.getETHBalance(), 0, "SP should not have ETH rewards"); + + // Check B has received debt + assertEq(troveManager.getTroveEntireDebt(BTroveId) - BInitialDebt, liquidationAmount, "B debt mismatch"); + // Check B has received all coll minus coll gas comp + assertApproxEqAbs( + troveManager.getTroveEntireColl(BTroveId) - BInitialColl, + LiquityMath._min( + collAmount * 995 / 1000, // Collateral - coll gas comp + liquidationAmount * DECIMAL_PRECISION / price * 110 / 100 // debt with penalty + ), + 10, + "B trove coll mismatch" + ); + + // Check A retains ~9.5% of the collateral (after claiming from CollSurplus) + // collAmount - 0.5% - (liquidationAmount to ETH + 10%) + uint256 collSurplusAmount = collAmount * 995 / 1000 - liquidationAmount * DECIMAL_PRECISION / price * 110 / 100; + assertApproxEqAbs( + WETH.balanceOf(address(collSurplusPool)), + collSurplusAmount, + 10, + "CollSurplusPoll should have received collateral" + ); + vm.startPrank(A); + borrowerOperations.claimCollateral(); + vm.stopPrank(); + assertApproxEqAbs( + WETH.balanceOf(A) - AInitialETHBalance, collSurplusAmount, 10, "A collateral balance mismatch" + ); + } + + struct InitialValues { + uint256 spBoldBalance; + uint256 spETHBalance; + uint256 AETHBalance; + uint256 BDebt; + uint256 BColl; + } + + struct FinalValues { + uint256 spBoldBalance; + uint256 spETHBalance; + uint256 collToLiquidate; + uint256 collSPPortion; + uint256 collPenaltySP; + uint256 collToSendToSP; + uint256 collRedistributionPortion; + uint256 collPenaltyRedistribution; + } + + function testLiquidationFuzz(uint256 _finalPrice, uint256 _spAmount) public { + uint256 liquidationAmount = 2000e18; + uint256 collAmount = 2e18; + uint256 initialPrice = 2000e18; + // A initial CR: 200% + + _finalPrice = bound(_finalPrice, 1000e18, 1200e18 - 1); // A final CR in [100%, 120%[ + _spAmount = bound(_spAmount, 0, liquidationAmount); + + priceFeed.setPrice(initialPrice); + vm.startPrank(A); + uint256 ATroveId = borrowerOperations.openTrove( + A, 0, collAmount, liquidationAmount - troveManager.BOLD_GAS_COMPENSATION(), 0, 0, 0 + ); + vm.stopPrank(); + + vm.startPrank(B); + uint256 BTroveId = borrowerOperations.openTrove(B, 0, 3 * collAmount, liquidationAmount, 0, 0, 0); + vm.stopPrank(); + // B deposits to SP + if (_spAmount > 0) { + makeSPDepositAndClaim(B, _spAmount); + } + + // Price drops + priceFeed.setPrice(_finalPrice); + console2.log(_finalPrice, "_finalPrice"); + + InitialValues memory initialValues; + initialValues.spBoldBalance = stabilityPool.getTotalBoldDeposits(); + initialValues.spETHBalance = stabilityPool.getETHBalance(); + initialValues.AETHBalance = WETH.balanceOf(A); + initialValues.BDebt = troveManager.getTroveEntireDebt(BTroveId); + initialValues.BColl = troveManager.getTroveEntireColl(BTroveId); + + // Check not RM + assertEq(troveManager.checkBelowCriticalThreshold(_finalPrice), false, "System should not be below CT"); + + // Check CR_A < MCR and TCR > CCR + assertLt(troveManager.getCurrentICR(ATroveId, _finalPrice), MCR); + assertGt(troveManager.getTCR(_finalPrice), CCR); + + uint256 trovesCount = troveManager.getTroveIdsCount(); + assertEq(trovesCount, 2); + + troveManager.liquidate(ATroveId); + + // Check Troves count reduced by 1 + trovesCount = troveManager.getTroveIdsCount(); + assertEq(trovesCount, 1); + + // Offset part + FinalValues memory finalValues; + finalValues.collToLiquidate = collAmount * 995 / 1000; + // Check SP Bold has decreased + finalValues.spBoldBalance = stabilityPool.getTotalBoldDeposits(); + assertEq(initialValues.spBoldBalance - finalValues.spBoldBalance, _spAmount, "SP Bold balance mismatch"); + // Check SP ETH has increased + finalValues.spETHBalance = stabilityPool.getETHBalance(); + finalValues.collSPPortion = finalValues.collToLiquidate * _spAmount / liquidationAmount; + finalValues.collPenaltySP = _spAmount * DECIMAL_PRECISION / _finalPrice * 105 / 100; + finalValues.collToSendToSP = LiquityMath._min(finalValues.collPenaltySP, finalValues.collSPPortion); + // liquidationAmount to ETH + 5% + assertApproxEqAbs( + finalValues.spETHBalance - initialValues.spETHBalance, + finalValues.collToSendToSP, + 10, + "SP ETH balance mismatch" + ); + + // Redistribution part + finalValues.collRedistributionPortion = finalValues.collToLiquidate - finalValues.collSPPortion; + finalValues.collPenaltyRedistribution = + (liquidationAmount - _spAmount) * DECIMAL_PRECISION / _finalPrice * 110 / 100; + // Check B has received debt + assertApproxEqAbs( + troveManager.getTroveEntireDebt(BTroveId) - initialValues.BDebt, + liquidationAmount - _spAmount, + 10, + "B debt mismatch" + ); + // Check B has received coll + assertApproxEqAbs( + troveManager.getTroveEntireColl(BTroveId) - initialValues.BColl, + LiquityMath._min( + finalValues.collPenaltyRedistribution, + finalValues.collRedistributionPortion + finalValues.collSPPortion - finalValues.collToSendToSP + ), + 10, + "B trove coll mismatch" + ); + + // Surplus + // Check A retains part of the collateral (after claiming from CollSurplus) + // collAmount - 0.5% - (liquidationAmount to ETH + penalty) + uint256 collPenalty = finalValues.collPenaltySP + finalValues.collPenaltyRedistribution; + console2.log(finalValues.collPenaltySP, "finalValues.collPenaltySP"); + console2.log(finalValues.collPenaltyRedistribution, "finalValues.collPenaltyRedistribution"); + console2.log(collPenalty, "collPenalty"); + uint256 collSurplusAmount; + if (collPenalty < finalValues.collToLiquidate) { + collSurplusAmount = finalValues.collToLiquidate - collPenalty; + } + assertApproxEqAbs(WETH.balanceOf(address(collSurplusPool)), collSurplusAmount, 1e9, "CollSurplusPoll mismatch"); + if (collSurplusAmount > 0) { + vm.startPrank(A); + borrowerOperations.claimCollateral(); + vm.stopPrank(); + assertApproxEqAbs( + WETH.balanceOf(A) - initialValues.AETHBalance, collSurplusAmount, 1e9, "A collateral balance mismatch" + ); + } + } +} diff --git a/contracts/src/test/multicollateral.t.sol b/contracts/src/test/multicollateral.t.sol index 9094efe7..06b8c6e7 100644 --- a/contracts/src/test/multicollateral.t.sol +++ b/contracts/src/test/multicollateral.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.18; import "./TestContracts/DevTestSetup.sol"; contract MulticollateralTest is DevTestSetup { - uint256 constant NUM_COLLATERALS = 4; + uint256 NUM_COLLATERALS = 4; LiquityContracts[] public contractsArray; function openMulticollateralTroveNoHints100pctWithIndex( @@ -48,8 +48,14 @@ contract MulticollateralTest is DevTestSetup { accountsList[6] ); + TroveManagerParams[] memory troveManagerParams = new TroveManagerParams[](NUM_COLLATERALS); + troveManagerParams[0] = TroveManagerParams(110e16, 5e16, 10e16); + troveManagerParams[1] = TroveManagerParams(120e16, 5e16, 10e16); + troveManagerParams[2] = TroveManagerParams(120e16, 5e16, 10e16); + troveManagerParams[3] = TroveManagerParams(125e16, 5e16, 10e16); + LiquityContracts[] memory _contractsArray; - (_contractsArray, collateralRegistry, boldToken) = _deployAndConnectContracts(NUM_COLLATERALS); + (_contractsArray, collateralRegistry, boldToken) = _deployAndConnectContracts(troveManagerParams); // Unimplemented feature (...):Copying of type struct LiquityContracts memory[] memory to storage not yet supported. for (uint256 c = 0; c < NUM_COLLATERALS; c++) { contractsArray.push(_contractsArray[c]); diff --git a/contracts/src/test/stabilityPool.t.sol b/contracts/src/test/stabilityPool.t.sol index a45d8e22..ffd24af3 100644 --- a/contracts/src/test/stabilityPool.t.sol +++ b/contracts/src/test/stabilityPool.t.sol @@ -79,7 +79,7 @@ contract SPTest is DevTestSetup { _setupStashedAndCurrentETHGains(); // Check A has both stashed and current gains - uint256 stashedETHGain = stabilityPool.stashedETH(A); + stabilityPool.stashedETH(A); makeSPDepositAndClaim(A, 1e18); @@ -272,8 +272,8 @@ contract SPTest is DevTestSetup { _setupStashedAndCurrentETHGains(); // Check A has both stashed and current gains - uint256 stashedETHGain = stabilityPool.stashedETH(A); - uint256 currentETHGain = stabilityPool.getDepositorETHGain(A); + stabilityPool.stashedETH(A); + stabilityPool.getDepositorETHGain(A); makeSPWithdrawalAndClaim(A, 1e18); @@ -356,8 +356,8 @@ contract SPTest is DevTestSetup { _setupStashedAndCurrentETHGains(); // Check A has both stashed and current gains - uint256 stashedETHGain = stabilityPool.stashedETH(A); - uint256 currentETHGain = stabilityPool.getDepositorETHGain(A); + stabilityPool.stashedETH(A); + stabilityPool.getDepositorETHGain(A); uint256 ETHBal_A = WETH.balanceOf(A); assertGt(ETHBal_A, 0); diff --git a/contracts/test/CollSurplusPool.js b/contracts/test/CollSurplusPool.js index a431eb11..a3bf5952 100644 --- a/contracts/test/CollSurplusPool.js +++ b/contracts/test/CollSurplusPool.js @@ -40,14 +40,14 @@ contract("CollSurplusPool", async (accounts) => { it("CollSurplusPool: claimColl(): Reverts if caller is not Borrower Operations", async () => { await th.assertRevert( - collSurplusPool.claimColl(A, th.addressToTroveId(A), { from: A }), + collSurplusPool.claimColl(A, { from: A }), "CollSurplusPool: Caller is not Borrower Operations", ); }); it("CollSurplusPool: claimColl(): Reverts if nothing to claim", async () => { await th.assertRevert( - borrowerOperations.claimCollateral(th.addressToTroveId(A), { from: A }), + borrowerOperations.claimCollateral({ from: A }), "CollSurplusPool: No collateral available to claim", ); }); diff --git a/contracts/test/GasCompensationTest.js b/contracts/test/GasCompensationTest.js index 5ac5e58c..23cf3065 100644 --- a/contracts/test/GasCompensationTest.js +++ b/contracts/test/GasCompensationTest.js @@ -60,9 +60,9 @@ contract("Gas compensation tests", async (accounts) => { }); before(async () => { - troveManagerTester = await TroveManagerTester.new(); + troveManagerTester = await TroveManagerTester.new(toBN(dec(110, 16)), toBN(dec(10, 16)), toBN(dec(10, 16))); const WETH = await ERC20.new("WETH", "WETH"); - borrowerOperationsTester = await BorrowerOperationsTester.new(WETH.address); + borrowerOperationsTester = await BorrowerOperationsTester.new(WETH.address, troveManagerTester.address); TroveManagerTester.setAsDeployed(troveManagerTester); BorrowerOperationsTester.setAsDeployed(borrowerOperationsTester); diff --git a/contracts/test/OwnershipTest.js b/contracts/test/OwnershipTest.js index 42392060..434f655a 100644 --- a/contracts/test/OwnershipTest.js +++ b/contracts/test/OwnershipTest.js @@ -20,7 +20,7 @@ contract("All Liquity functions with onlyOwner modifier", async (accounts) => { before(async () => { contracts = await deploymentHelper.deployLiquityCore(); - contracts.borrowerOperations = await BorrowerOperationsTester.new(contracts.WETH.address); + contracts.borrowerOperations = await BorrowerOperationsTester.new(contracts.WETH.address, contracts.troveManager.address); contracts = await deploymentHelper.deployBoldToken(contracts); boldToken = contracts.boldToken; @@ -89,7 +89,7 @@ contract("All Liquity functions with onlyOwner modifier", async (accounts) => { describe("BorrowerOperations", async (accounts) => { it("setAddresses(): reverts when called by non-owner, with wrong addresses, or twice", async () => { - await testDeploymentSetter(borrowerOperations, 8); + await testDeploymentSetter(borrowerOperations, 7); }); }); diff --git a/contracts/utils/deploymentHelpers.js b/contracts/utils/deploymentHelpers.js index e826edbf..5c36a067 100644 --- a/contracts/utils/deploymentHelpers.js +++ b/contracts/utils/deploymentHelpers.js @@ -59,7 +59,8 @@ class DeploymentHelper { // Borrowing contracts const activePool = await Contracts.ActivePool.new(WETH.address); - const borrowerOperations = await Contracts.BorrowerOperations.new(WETH.address); + const troveManager = await Contracts.TroveManager.new(web3.utils.toBN("1100000000000000000"), web3.utils.toBN("100000000000000000"), web3.utils.toBN("100000000000000000")); + const borrowerOperations = await Contracts.BorrowerOperations.new(WETH.address, troveManager.address); const collSurplusPool = await Contracts.CollSurplusPool.new(WETH.address); const defaultPool = await Contracts.DefaultPool.new(WETH.address); const gasPool = await Contracts.GasPool.new(); @@ -67,7 +68,6 @@ class DeploymentHelper { const priceFeed = await Contracts.PriceFeedMock.new(); const sortedTroves = await Contracts.SortedTroves.new(); const stabilityPool = await Contracts.StabilityPool.new(WETH.address); - const troveManager = await Contracts.TroveManager.new(); const { boldToken } = await this.deployBoldToken({ troveManager, @@ -167,7 +167,6 @@ class DeploymentHelper { // set contracts in BorrowerOperations await contracts.borrowerOperations.setAddresses( - contracts.troveManager.address, contracts.activePool.address, contracts.defaultPool.address, contracts.gasPool.address,