From ffbda7c8968de390cd5ecd67c9873ac3809dab9d Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Tue, 22 Aug 2023 08:55:21 +0200 Subject: [PATCH 01/68] chore: naive impl --- .../contracts/contracts/CdpManagerStorage.sol | 38 +++++++++++++++++++ .../contracts/LiquidationLibrary.sol | 17 +++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index a26f76921..4e381f526 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -18,6 +18,44 @@ import "./Dependencies/AuthNoOwner.sol"; @dev Shared functions were also added here to de-dup code */ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, AuthNoOwner { + + // TODO: IMPROVE + // NOTE: No packing cause it's the last var, no need for u64 + uint128 constant UNSET_TIMESTAMP_FLAG = type(uint128).max; + + // TODO: IMPROVE THIS!!! + uint128 public lastRecoveryModeTimestamp = UNSET_TIMESTAMP_FLAG; // use max to signify + uint128 public waitTimeFromRMTriggerToLiquidations = 10 minutes; + + // TODO: Pitfal is fee split + + /// @dev Checks that the system is in RM + function beginRMLiquidationCooldown() external { + // Require we're in RM + uint256 price = priceFeed.fetchPrice(); + bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); + require(isRecoveryMode); + + // Arm the countdown + if(lastRecoveryModeTimestamp == UNSET_TIMESTAMP_FLAG) { + lastRecoveryModeTimestamp = uint128(block.timestamp); + } + } + + /// @dev Checks that the system is not in RM + function stopRMLiquidationCooldown() external { + // Require we're in RM + uint256 price = priceFeed.fetchPrice(); + bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); + require(!isRecoveryMode); + + // Disarm the countdown + if(lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG) { + lastRecoveryModeTimestamp = UNSET_TIMESTAMP_FLAG; + } + } + + string public constant NAME = "CdpManager"; // --- Connected contract declarations --- diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 6587301ff..2517ffb08 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -66,7 +66,7 @@ contract LiquidationLibrary is CdpManagerStorage { (uint _TCR, uint systemColl, uint systemDebt) = _getTCRWithTotalCollAndDebt(_price); require( - _ICR < MCR || (_TCR < CCR && _ICR < _TCR), + _ICR < MCR || (_TCR < CCR && canLiquidateRecoveryMode(_ICR, _TCR)), "CdpManager: ICR is not below liquidation threshold in current mode" ); @@ -419,7 +419,7 @@ contract LiquidationLibrary is CdpManagerStorage { uint _cnt; for (uint i = 0; i < _n && _cdpId != _first; ++i) { uint _icr = getCurrentICR(_cdpId, _price); - bool _liquidatable = _recovery ? (_icr < MCR || _icr < _TCR) : _icr < MCR; + bool _liquidatable = _recovery ? (_icr < MCR || canLiquidateRecoveryMode(_icr, _TCR)) : _icr < MCR; if (_liquidatable && Cdps[_cdpId].status == Status.active) { _cnt += 1; } @@ -432,7 +432,7 @@ contract LiquidationLibrary is CdpManagerStorage { uint _j; for (uint i = 0; i < _n && _cdpId != _first; ++i) { uint _icr = getCurrentICR(_cdpId, _price); - bool _liquidatable = _recovery ? (_icr < MCR || _icr < _TCR) : _icr < MCR; + bool _liquidatable = _recovery ? (_icr < MCR || canLiquidateRecoveryMode(_icr, _TCR)) : _icr < MCR; if (_liquidatable && Cdps[_cdpId].status == Status.active) { _array[_cnt - _j - 1] = _cdpId; _j += 1; @@ -756,7 +756,7 @@ contract LiquidationLibrary is CdpManagerStorage { if (vars.cdpId != bytes32(0) && Cdps[vars.cdpId].status == Status.active) { vars.ICR = getCurrentICR(vars.cdpId, _price); - if (!vars.backToNormalMode && (vars.ICR < MCR || vars.ICR < _TCR)) { + if (!vars.backToNormalMode && (vars.ICR < MCR || canLiquidateRecoveryMode(vars.ICR, _TCR))) { vars.price = _price; _applyAccumulatedFeeSplit(vars.cdpId); _getLiquidationValuesRecoveryMode( @@ -974,4 +974,13 @@ contract LiquidationLibrary is CdpManagerStorage { "LiquidationLibrary: Coll remaining in partially liquidated CDP must be >= minimum" ); } + + // Can liquidate in RM if ICR < TCR AND Enough time has passed + function canLiquidateRecoveryMode(uint256 icr, uint256 tcr) public view returns (bool) { + // ICR < TCR and we have waited enough + return + icr < tcr && + lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG && + block.timestamp > lastRecoveryModeTimestamp + waitTimeFromRMTriggerToLiquidations; + } } From 4f6cbaeef21082205af761492ef2800d42cf42a7 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Tue, 22 Aug 2023 09:02:48 +0200 Subject: [PATCH 02/68] feat: sketch of RM checker that is synched with Liquidations and can be publicly disarmed --- packages/contracts/contracts/CdpManager.sol | 4 ++++ .../contracts/contracts/CdpManagerStorage.sol | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/packages/contracts/contracts/CdpManager.sol b/packages/contracts/contracts/CdpManager.sol index 914ab0f2f..39c843b74 100644 --- a/packages/contracts/contracts/CdpManager.sol +++ b/packages/contracts/contracts/CdpManager.sol @@ -111,6 +111,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { function liquidate(bytes32 _cdpId) external override { _delegate(liquidationLibrary); + _checkLiquidateCoolDownAndReset(); // TODO: Make this better } /// @notice Partially liquidate a single CDP. @@ -126,6 +127,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { bytes32 _lowerPartialHint ) external override { _delegate(liquidationLibrary); + _checkLiquidateCoolDownAndReset(); // TODO: Make this better } // --- Batch/Sequence liquidation functions --- @@ -137,6 +139,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { /// @param _n Maximum number of CDPs to liquidate. function liquidateCdps(uint _n) external override { _delegate(liquidationLibrary); + _checkLiquidateCoolDownAndReset(); // TODO: Make this better } /// @notice Attempt to liquidate a custom list of CDPs provided by the caller @@ -145,6 +148,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { /// @param _cdpArray Array of CDPs to liquidate. function batchLiquidateCdps(bytes32[] memory _cdpArray) external override { _delegate(liquidationLibrary); + _checkLiquidateCoolDownAndReset(); // TODO: Make this better } // --- Redemption functions --- diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 4e381f526..1d553b706 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -36,10 +36,16 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); require(isRecoveryMode); + _beginRMLiquidationCooldown(); + } + + function _beginRMLiquidationCooldown() internal { // Arm the countdown if(lastRecoveryModeTimestamp == UNSET_TIMESTAMP_FLAG) { lastRecoveryModeTimestamp = uint128(block.timestamp); } + + // TODO: EVENT } /// @dev Checks that the system is not in RM @@ -49,10 +55,28 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); require(!isRecoveryMode); + _stopRMLiquidationCooldown(); + } + + function _stopRMLiquidationCooldown() internal { // Disarm the countdown if(lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG) { lastRecoveryModeTimestamp = UNSET_TIMESTAMP_FLAG; } + + // TODO: Event + } + + /// TODO: obv optimizations + function _checkLiquidateCoolDownAndReset() internal { + uint256 price = priceFeed.fetchPrice(); + bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); + + if(isRecoveryMode) { + _beginRMLiquidationCooldown(); + } else { + _stopRMLiquidationCooldown(); + } } From f121c3ade59558cf1a7573f3bdb8096fd6b5caa3 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Tue, 22 Aug 2023 09:18:33 +0200 Subject: [PATCH 03/68] chore: made function public --- packages/contracts/contracts/CdpManager.sol | 8 ++++---- packages/contracts/contracts/CdpManagerStorage.sol | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/contracts/contracts/CdpManager.sol b/packages/contracts/contracts/CdpManager.sol index 39c843b74..ad04279a7 100644 --- a/packages/contracts/contracts/CdpManager.sol +++ b/packages/contracts/contracts/CdpManager.sol @@ -111,7 +111,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { function liquidate(bytes32 _cdpId) external override { _delegate(liquidationLibrary); - _checkLiquidateCoolDownAndReset(); // TODO: Make this better + checkLiquidateCoolDownAndReset(); // TODO: Make this better } /// @notice Partially liquidate a single CDP. @@ -127,7 +127,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { bytes32 _lowerPartialHint ) external override { _delegate(liquidationLibrary); - _checkLiquidateCoolDownAndReset(); // TODO: Make this better + checkLiquidateCoolDownAndReset(); // TODO: Make this better } // --- Batch/Sequence liquidation functions --- @@ -139,7 +139,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { /// @param _n Maximum number of CDPs to liquidate. function liquidateCdps(uint _n) external override { _delegate(liquidationLibrary); - _checkLiquidateCoolDownAndReset(); // TODO: Make this better + checkLiquidateCoolDownAndReset(); // TODO: Make this better } /// @notice Attempt to liquidate a custom list of CDPs provided by the caller @@ -148,7 +148,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { /// @param _cdpArray Array of CDPs to liquidate. function batchLiquidateCdps(bytes32[] memory _cdpArray) external override { _delegate(liquidationLibrary); - _checkLiquidateCoolDownAndReset(); // TODO: Make this better + checkLiquidateCoolDownAndReset(); // TODO: Make this better } // --- Redemption functions --- diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 1d553b706..382ff3071 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -68,7 +68,7 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut } /// TODO: obv optimizations - function _checkLiquidateCoolDownAndReset() internal { + function checkLiquidateCoolDownAndReset() public { uint256 price = priceFeed.fetchPrice(); bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); From b7b3bd3890330a98e72eb6b157caf6824b67ab04 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Tue, 22 Aug 2023 09:19:22 +0200 Subject: [PATCH 04/68] feat: call cdpManager on external interactions --- packages/contracts/contracts/BorrowerOperations.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/contracts/contracts/BorrowerOperations.sol b/packages/contracts/contracts/BorrowerOperations.sol index 9822ac90e..6a9257c83 100644 --- a/packages/contracts/contracts/BorrowerOperations.sol +++ b/packages/contracts/contracts/BorrowerOperations.sol @@ -266,6 +266,7 @@ contract BorrowerOperations is vars.price = priceFeed.fetchPrice(); bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(vars.price)); + cdpManager.checkLiquidateCoolDownAndReset(); // TODO: Check this if (_isDebtIncrease) { _requireNonZeroDebtChange(_EBTCChange); @@ -373,6 +374,7 @@ contract BorrowerOperations is vars.price = priceFeed.fetchPrice(); bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(vars.price)); + cdpManager.checkLiquidateCoolDownAndReset(); // TODO: Check this vars.debt = _EBTCAmount; @@ -447,6 +449,7 @@ contract BorrowerOperations is uint price = priceFeed.fetchPrice(); _requireNotInRecoveryMode(_getTCR(price)); + cdpManager.checkLiquidateCoolDownAndReset(); // TODO: Check this uint coll = cdpManager.getCdpColl(_cdpId); uint debt = cdpManager.getCdpDebt(_cdpId); From 42e42d04ebd74e68840e8f6a6ef741df4fa98ad8 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Tue, 22 Aug 2023 09:27:37 +0200 Subject: [PATCH 05/68] feat: RECHECK - break CEI but update RM status appropriately --- packages/contracts/contracts/BorrowerOperations.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/contracts/contracts/BorrowerOperations.sol b/packages/contracts/contracts/BorrowerOperations.sol index 6a9257c83..2b955f297 100644 --- a/packages/contracts/contracts/BorrowerOperations.sol +++ b/packages/contracts/contracts/BorrowerOperations.sol @@ -266,7 +266,6 @@ contract BorrowerOperations is vars.price = priceFeed.fetchPrice(); bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(vars.price)); - cdpManager.checkLiquidateCoolDownAndReset(); // TODO: Check this if (_isDebtIncrease) { _requireNonZeroDebtChange(_EBTCChange); @@ -350,6 +349,8 @@ contract BorrowerOperations is ); _processTokenMovesFromAdjustment(_varMvTokens); } + + cdpManager.checkLiquidateCoolDownAndReset(); // TODO: Check this } function _openCdp( @@ -374,7 +375,6 @@ contract BorrowerOperations is vars.price = priceFeed.fetchPrice(); bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(vars.price)); - cdpManager.checkLiquidateCoolDownAndReset(); // TODO: Check this vars.debt = _EBTCAmount; @@ -435,6 +435,8 @@ contract BorrowerOperations is "BorrowerOperations: deposited collateral mismatch!" ); + cdpManager.checkLiquidateCoolDownAndReset(); // TODO: Check this + return _cdpId; } @@ -449,7 +451,6 @@ contract BorrowerOperations is uint price = priceFeed.fetchPrice(); _requireNotInRecoveryMode(_getTCR(price)); - cdpManager.checkLiquidateCoolDownAndReset(); // TODO: Check this uint coll = cdpManager.getCdpColl(_cdpId); uint debt = cdpManager.getCdpDebt(_cdpId); @@ -476,6 +477,8 @@ contract BorrowerOperations is // CEI: Send the collateral and liquidator reward shares back to the user activePool.sendStEthCollAndLiquidatorReward(msg.sender, coll, liquidatorRewardShares); + + cdpManager.checkLiquidateCoolDownAndReset(); // TODO: Check this } /** From 8291b065effcfc19fbc1dc5969b42d09ce657880 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Tue, 22 Aug 2023 09:28:18 +0200 Subject: [PATCH 06/68] chore: prettier --- packages/contracts/contracts/CdpManagerStorage.sol | 8 +++----- packages/contracts/contracts/LiquidationLibrary.sol | 13 ++++++++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 382ff3071..5f489878f 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -18,7 +18,6 @@ import "./Dependencies/AuthNoOwner.sol"; @dev Shared functions were also added here to de-dup code */ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, AuthNoOwner { - // TODO: IMPROVE // NOTE: No packing cause it's the last var, no need for u64 uint128 constant UNSET_TIMESTAMP_FLAG = type(uint128).max; @@ -41,7 +40,7 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut function _beginRMLiquidationCooldown() internal { // Arm the countdown - if(lastRecoveryModeTimestamp == UNSET_TIMESTAMP_FLAG) { + if (lastRecoveryModeTimestamp == UNSET_TIMESTAMP_FLAG) { lastRecoveryModeTimestamp = uint128(block.timestamp); } @@ -60,7 +59,7 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut function _stopRMLiquidationCooldown() internal { // Disarm the countdown - if(lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG) { + if (lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG) { lastRecoveryModeTimestamp = UNSET_TIMESTAMP_FLAG; } @@ -72,14 +71,13 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut uint256 price = priceFeed.fetchPrice(); bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); - if(isRecoveryMode) { + if (isRecoveryMode) { _beginRMLiquidationCooldown(); } else { _stopRMLiquidationCooldown(); } } - string public constant NAME = "CdpManager"; // --- Connected contract declarations --- diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 2517ffb08..365662b68 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -419,7 +419,9 @@ contract LiquidationLibrary is CdpManagerStorage { uint _cnt; for (uint i = 0; i < _n && _cdpId != _first; ++i) { uint _icr = getCurrentICR(_cdpId, _price); - bool _liquidatable = _recovery ? (_icr < MCR || canLiquidateRecoveryMode(_icr, _TCR)) : _icr < MCR; + bool _liquidatable = _recovery + ? (_icr < MCR || canLiquidateRecoveryMode(_icr, _TCR)) + : _icr < MCR; if (_liquidatable && Cdps[_cdpId].status == Status.active) { _cnt += 1; } @@ -432,7 +434,9 @@ contract LiquidationLibrary is CdpManagerStorage { uint _j; for (uint i = 0; i < _n && _cdpId != _first; ++i) { uint _icr = getCurrentICR(_cdpId, _price); - bool _liquidatable = _recovery ? (_icr < MCR || canLiquidateRecoveryMode(_icr, _TCR)) : _icr < MCR; + bool _liquidatable = _recovery + ? (_icr < MCR || canLiquidateRecoveryMode(_icr, _TCR)) + : _icr < MCR; if (_liquidatable && Cdps[_cdpId].status == Status.active) { _array[_cnt - _j - 1] = _cdpId; _j += 1; @@ -756,7 +760,10 @@ contract LiquidationLibrary is CdpManagerStorage { if (vars.cdpId != bytes32(0) && Cdps[vars.cdpId].status == Status.active) { vars.ICR = getCurrentICR(vars.cdpId, _price); - if (!vars.backToNormalMode && (vars.ICR < MCR || canLiquidateRecoveryMode(vars.ICR, _TCR))) { + if ( + !vars.backToNormalMode && + (vars.ICR < MCR || canLiquidateRecoveryMode(vars.ICR, _TCR)) + ) { vars.price = _price; _applyAccumulatedFeeSplit(vars.cdpId); _getLiquidationValuesRecoveryMode( From 05e6306019e80dd4b2e790a9029689826ebe3dbf Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Tue, 22 Aug 2023 09:38:04 +0200 Subject: [PATCH 07/68] fix: compilation and interfaces --- packages/contracts/contracts/BorrowerOperations.sol | 7 ++++--- .../contracts/Interfaces/IRmLiquidationsChecker.sol | 8 ++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol diff --git a/packages/contracts/contracts/BorrowerOperations.sol b/packages/contracts/contracts/BorrowerOperations.sol index 2b955f297..b5250947d 100644 --- a/packages/contracts/contracts/BorrowerOperations.sol +++ b/packages/contracts/contracts/BorrowerOperations.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.17; import "./Interfaces/IBorrowerOperations.sol"; +import "./Interfaces/IRmLiquidationsChecker.sol"; import "./Interfaces/ICdpManager.sol"; import "./Interfaces/ICdpManagerData.sol"; import "./Interfaces/IEBTCToken.sol"; @@ -350,7 +351,7 @@ contract BorrowerOperations is _processTokenMovesFromAdjustment(_varMvTokens); } - cdpManager.checkLiquidateCoolDownAndReset(); // TODO: Check this + IRmLiquidationsChecker(address(cdpManager)).checkLiquidateCoolDownAndReset(); // TODO: Check this } function _openCdp( @@ -435,7 +436,7 @@ contract BorrowerOperations is "BorrowerOperations: deposited collateral mismatch!" ); - cdpManager.checkLiquidateCoolDownAndReset(); // TODO: Check this + IRmLiquidationsChecker(address(cdpManager)).checkLiquidateCoolDownAndReset(); // TODO: Check this return _cdpId; } @@ -478,7 +479,7 @@ contract BorrowerOperations is // CEI: Send the collateral and liquidator reward shares back to the user activePool.sendStEthCollAndLiquidatorReward(msg.sender, coll, liquidatorRewardShares); - cdpManager.checkLiquidateCoolDownAndReset(); // TODO: Check this + IRmLiquidationsChecker(address(cdpManager)).checkLiquidateCoolDownAndReset(); // TODO: Check this } /** diff --git a/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol b/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol new file mode 100644 index 000000000..76cce729a --- /dev/null +++ b/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + + +// Interface for State Updates that can trigger RM Liquidations +interface IRmLiquidationsChecker { + function checkLiquidateCoolDownAndReset() external; +} \ No newline at end of file From e91406969ce974561b697ec77b57060306eb53e2 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Tue, 22 Aug 2023 09:38:17 +0200 Subject: [PATCH 08/68] chore: prettier --- .../contracts/Interfaces/IRmLiquidationsChecker.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol b/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol index 76cce729a..da37bf548 100644 --- a/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol +++ b/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol @@ -1,8 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.17; - // Interface for State Updates that can trigger RM Liquidations interface IRmLiquidationsChecker { - function checkLiquidateCoolDownAndReset() external; -} \ No newline at end of file + function checkLiquidateCoolDownAndReset() external; +} From 9c1accb5a327a35d382179743163faebb87c8e87 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Tue, 22 Aug 2023 14:12:14 +0200 Subject: [PATCH 09/68] chore: comment --- packages/contracts/contracts/CdpManagerStorage.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 5f489878f..888390592 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -26,7 +26,7 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut uint128 public lastRecoveryModeTimestamp = UNSET_TIMESTAMP_FLAG; // use max to signify uint128 public waitTimeFromRMTriggerToLiquidations = 10 minutes; - // TODO: Pitfal is fee split + // TODO: Pitfal is fee split // NOTE: Solved by calling `checkLiquidateCoolDownAndReset` on external operations from BO /// @dev Checks that the system is in RM function beginRMLiquidationCooldown() external { From d3dc31cbf31f7fc6d7b65a325d27bb4aa99a3b6e Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 09:14:15 +0200 Subject: [PATCH 10/68] feat: refactor towards CEI conformity --- .../contracts/BorrowerOperations.sol | 61 +++++++++++++++---- packages/contracts/contracts/CdpManager.sol | 7 --- .../contracts/contracts/CdpManagerStorage.sol | 25 ++++++++ .../Interfaces/IRmLiquidationsChecker.sol | 2 + 4 files changed, 76 insertions(+), 19 deletions(-) diff --git a/packages/contracts/contracts/BorrowerOperations.sol b/packages/contracts/contracts/BorrowerOperations.sol index b5250947d..9d3c16d17 100644 --- a/packages/contracts/contracts/BorrowerOperations.sol +++ b/packages/contracts/contracts/BorrowerOperations.sol @@ -398,12 +398,28 @@ contract BorrowerOperations is In normal mode, ICR must be greater thatn MCR Additionally, the new system TCR after the CDPs addition must be >CCR */ + uint newTCR = _getNewTCRFromCdpChange(vars.netColl, true, vars.debt, true, vars.price); if (isRecoveryMode) { _requireICRisAboveCCR(vars.ICR); + + // == Grace Period == // + // We are in RM, Edge case is Depositing Coll could exit RM + // We check with newTCR + if(newTCR < CCR) { + // Notify RM + IRmLiquidationsChecker(address(cdpManager)).notifyBeginRM(); + } else { + // Notify Back to Normal Mode + IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); + } } else { _requireICRisAboveMCR(vars.ICR); - uint newTCR = _getNewTCRFromCdpChange(vars.netColl, true, vars.debt, true, vars.price); // bools: coll increase, debt increase _requireNewTCRisAboveCCR(newTCR); + + // == Grace Period == // + // We are not in RM, no edge case, we always stay above RM + // Always Notify Back to Normal Mode + IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); } // Set the cdp struct's properties @@ -436,8 +452,6 @@ contract BorrowerOperations is "BorrowerOperations: deposited collateral mismatch!" ); - IRmLiquidationsChecker(address(cdpManager)).checkLiquidateCoolDownAndReset(); // TODO: Check this - return _cdpId; } @@ -468,6 +482,10 @@ contract BorrowerOperations is ); _requireNewTCRisAboveCCR(newTCR); + // == Grace Period == // + // By definition we are not in RM, notify CDPManager to ensure "Glass is on" + IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); + cdpManager.removeStake(_cdpId); // We already verified msg.sender is the borrower @@ -479,7 +497,7 @@ contract BorrowerOperations is // CEI: Send the collateral and liquidator reward shares back to the user activePool.sendStEthCollAndLiquidatorReward(msg.sender, coll, liquidatorRewardShares); - IRmLiquidationsChecker(address(cdpManager)).checkLiquidateCoolDownAndReset(); // TODO: Check this + } /** @@ -614,7 +632,7 @@ contract BorrowerOperations is uint _collWithdrawal, bool _isDebtIncrease, LocalVariables_adjustCdp memory _vars - ) internal view { + ) internal { /* *In Recovery Mode, only allow: * @@ -629,23 +647,42 @@ contract BorrowerOperations is * - The new ICR is above MCR * - The adjustment won't pull the TCR below CCR */ + + _vars.newTCR = _getNewTCRFromCdpChange( + collateral.getPooledEthByShares(_vars.collChange), + _vars.isCollIncrease, + _vars.netDebtChange, + _isDebtIncrease, + _vars.price + ); + if (_isRecoveryMode) { _requireNoCollWithdrawal(_collWithdrawal); if (_isDebtIncrease) { _requireICRisAboveCCR(_vars.newICR); _requireNewICRisAboveOldICR(_vars.newICR, _vars.oldICR); } + + // == Grace Period == // + // We are in RM, Edge case is Depositing Coll could exit RM + // We check with newTCR + if(_vars.newTCR < CCR) { + // Notify RM + IRmLiquidationsChecker(address(cdpManager)).notifyBeginRM(); + } else { + // Notify Back to Normal Mode + IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); + } + } else { // if Normal Mode _requireICRisAboveMCR(_vars.newICR); - _vars.newTCR = _getNewTCRFromCdpChange( - collateral.getPooledEthByShares(_vars.collChange), - _vars.isCollIncrease, - _vars.netDebtChange, - _isDebtIncrease, - _vars.price - ); _requireNewTCRisAboveCCR(_vars.newTCR); + + // == Grace Period == // + // We are not in RM, no edge case, we always stay above RM + // Always Notify Back to Normal Mode + IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); } } diff --git a/packages/contracts/contracts/CdpManager.sol b/packages/contracts/contracts/CdpManager.sol index ad04279a7..778b844a8 100644 --- a/packages/contracts/contracts/CdpManager.sol +++ b/packages/contracts/contracts/CdpManager.sol @@ -693,13 +693,6 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { // --- 'require' wrapper functions --- - function _requireCallerIsBorrowerOperations() internal view { - require( - msg.sender == borrowerOperationsAddress, - "CdpManager: Caller is not the BorrowerOperations contract" - ); - } - function _requireEBTCBalanceCoversRedemptionAndWithinSupply( IEBTCToken _ebtcToken, address _redeemer, diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 888390592..74b386303 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -28,6 +28,24 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut // TODO: Pitfal is fee split // NOTE: Solved by calling `checkLiquidateCoolDownAndReset` on external operations from BO + /// @dev Trusted Function from BO + /// @dev BO accrues totals before adjusting them + /// To maintain CEI compliance we use this trusted function + function notifyBeginRM() external { + _requireCallerIsBorrowerOperations(); + + _beginRMLiquidationCooldown(); + } + + /// @dev Trusted Function from BO + /// @dev BO accrues totals before adjusting them + /// To maintain CEI compliance we use this trusted function + function notifyEndRM() external { + _requireCallerIsBorrowerOperations(); + + _stopRMLiquidationCooldown(); + } + /// @dev Checks that the system is in RM function beginRMLiquidationCooldown() external { // Require we're in RM @@ -564,6 +582,13 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut ); } + function _requireCallerIsBorrowerOperations() internal view { + require( + msg.sender == borrowerOperationsAddress, + "CdpManager: Caller is not the BorrowerOperations contract" + ); + } + // --- Helper functions --- // Return the nominal collateral ratio (ICR) of a given Cdp, without the price. diff --git a/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol b/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol index da37bf548..3890d242e 100644 --- a/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol +++ b/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol @@ -4,4 +4,6 @@ pragma solidity 0.8.17; // Interface for State Updates that can trigger RM Liquidations interface IRmLiquidationsChecker { function checkLiquidateCoolDownAndReset() external; + function notifyBeginRM() external; + function notifyEndRM() external; } From 0dddcf1baabe0f2791c1830a0065f97f21e22c19 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 09:29:41 +0200 Subject: [PATCH 11/68] feat: 15 minute delay for RM liquidations --- packages/contracts/contracts/CdpManagerStorage.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 74b386303..e457667e3 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -24,7 +24,7 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut // TODO: IMPROVE THIS!!! uint128 public lastRecoveryModeTimestamp = UNSET_TIMESTAMP_FLAG; // use max to signify - uint128 public waitTimeFromRMTriggerToLiquidations = 10 minutes; + uint128 public waitTimeFromRMTriggerToLiquidations = 15 minutes; // TODO: Pitfal is fee split // NOTE: Solved by calling `checkLiquidateCoolDownAndReset` on external operations from BO From 0284fa9321fc9c8a1e3ee7932b94e42ce010fc69 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 09:32:50 +0200 Subject: [PATCH 12/68] chore: prettier --- .../contracts/contracts/BorrowerOperations.sol | 15 ++++++--------- .../Interfaces/IRmLiquidationsChecker.sol | 2 ++ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/contracts/contracts/BorrowerOperations.sol b/packages/contracts/contracts/BorrowerOperations.sol index 9d3c16d17..af03b6853 100644 --- a/packages/contracts/contracts/BorrowerOperations.sol +++ b/packages/contracts/contracts/BorrowerOperations.sol @@ -405,12 +405,12 @@ contract BorrowerOperations is // == Grace Period == // // We are in RM, Edge case is Depositing Coll could exit RM // We check with newTCR - if(newTCR < CCR) { + if (newTCR < CCR) { // Notify RM IRmLiquidationsChecker(address(cdpManager)).notifyBeginRM(); } else { // Notify Back to Normal Mode - IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); + IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); } } else { _requireICRisAboveMCR(vars.ICR); @@ -496,8 +496,6 @@ contract BorrowerOperations is // CEI: Send the collateral and liquidator reward shares back to the user activePool.sendStEthCollAndLiquidatorReward(msg.sender, coll, liquidatorRewardShares); - - } /** @@ -647,7 +645,7 @@ contract BorrowerOperations is * - The new ICR is above MCR * - The adjustment won't pull the TCR below CCR */ - + _vars.newTCR = _getNewTCRFromCdpChange( collateral.getPooledEthByShares(_vars.collChange), _vars.isCollIncrease, @@ -662,18 +660,17 @@ contract BorrowerOperations is _requireICRisAboveCCR(_vars.newICR); _requireNewICRisAboveOldICR(_vars.newICR, _vars.oldICR); } - + // == Grace Period == // // We are in RM, Edge case is Depositing Coll could exit RM // We check with newTCR - if(_vars.newTCR < CCR) { + if (_vars.newTCR < CCR) { // Notify RM IRmLiquidationsChecker(address(cdpManager)).notifyBeginRM(); } else { // Notify Back to Normal Mode - IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); + IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); } - } else { // if Normal Mode _requireICRisAboveMCR(_vars.newICR); diff --git a/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol b/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol index 3890d242e..0bca7281b 100644 --- a/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol +++ b/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.17; // Interface for State Updates that can trigger RM Liquidations interface IRmLiquidationsChecker { function checkLiquidateCoolDownAndReset() external; + function notifyBeginRM() external; + function notifyEndRM() external; } From 6ad4d60519dba34c376349cb367f897d9bebcda3 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 09:46:42 +0200 Subject: [PATCH 13/68] fix: liquidation test waits for grace period --- .../contracts/foundry_test/CdpManager.Liquidation.t.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol b/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol index 267346e5b..2be9ec4a0 100644 --- a/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol +++ b/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol @@ -113,6 +113,12 @@ contract CdpManagerLiquidationTest is eBTCBaseInvariants { deal(address(eBTCToken), users[0], _cdpState.debt); // sugardaddy liquidator uint _debtLiquidatorBefore = eBTCToken.balanceOf(users[0]); uint _debtSystemBefore = cdpManager.getEntireSystemDebt(); + + // Grace Period + // Check never reverts so it's safe to use + cdpManager.checkLiquidateCoolDownAndReset(); + vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + vm.prank(users[0]); cdpManager.liquidate(cdpId1); uint _debtLiquidatorAfter = eBTCToken.balanceOf(users[0]); From 9a25da57cb53bc4039494de4d403d9efc6446d19 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 09:51:04 +0200 Subject: [PATCH 14/68] chore: dry --- .../contracts/contracts/LiquidationLibrary.sol | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 365662b68..0be1886a5 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -419,9 +419,7 @@ contract LiquidationLibrary is CdpManagerStorage { uint _cnt; for (uint i = 0; i < _n && _cdpId != _first; ++i) { uint _icr = getCurrentICR(_cdpId, _price); - bool _liquidatable = _recovery - ? (_icr < MCR || canLiquidateRecoveryMode(_icr, _TCR)) - : _icr < MCR; + bool _liquidatable = _canLiquidateInCurrentMode(_recovery, _icr, _TCR); if (_liquidatable && Cdps[_cdpId].status == Status.active) { _cnt += 1; } @@ -434,9 +432,7 @@ contract LiquidationLibrary is CdpManagerStorage { uint _j; for (uint i = 0; i < _n && _cdpId != _first; ++i) { uint _icr = getCurrentICR(_cdpId, _price); - bool _liquidatable = _recovery - ? (_icr < MCR || canLiquidateRecoveryMode(_icr, _TCR)) - : _icr < MCR; + bool _liquidatable = _canLiquidateInCurrentMode(_recovery, _icr, _TCR); if (_liquidatable && Cdps[_cdpId].status == Status.active) { _array[_cnt - _j - 1] = _cdpId; _j += 1; @@ -990,4 +986,14 @@ contract LiquidationLibrary is CdpManagerStorage { lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG && block.timestamp > lastRecoveryModeTimestamp + waitTimeFromRMTriggerToLiquidations; } + + function _canLiquidateInCurrentMode( + bool _recovery, + uint256 _icr, + uint256 _TCR + ) internal pure returns (bool) { + bool _liquidatable = _recovery + ? (_icr < MCR || canLiquidateRecoveryMode(_icr, _TCR)) + : _icr < MCR; + } } From c99a3fb7a5eafb26df913539a1cbb46640241523 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 09:51:40 +0200 Subject: [PATCH 15/68] fix: view --- packages/contracts/contracts/LiquidationLibrary.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 0be1886a5..2518229eb 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -991,7 +991,7 @@ contract LiquidationLibrary is CdpManagerStorage { bool _recovery, uint256 _icr, uint256 _TCR - ) internal pure returns (bool) { + ) internal view returns (bool) { bool _liquidatable = _recovery ? (_icr < MCR || canLiquidateRecoveryMode(_icr, _TCR)) : _icr < MCR; From 44e3b8f4c0bc45a887365cb181d5041c5ea821a9 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Wed, 23 Aug 2023 15:56:18 +0800 Subject: [PATCH 16/68] fix failing tests by adding RM cooldown check --- packages/contracts/test/CdpManagerTest.js | 7 +- .../test/CdpManager_RecoveryModeTest.js | 91 ++++++++++++++++--- ...ager_RecoveryMode_Batch_Liqudation_Test.js | 37 +++++++- .../test/CdpManager_SimpleLiquidation_Test.js | 7 +- .../test/CdpManager_StakingSplitFee_Test.js | 7 +- 5 files changed, 128 insertions(+), 21 deletions(-) diff --git a/packages/contracts/test/CdpManagerTest.js b/packages/contracts/test/CdpManagerTest.js index 19fd308dc..6abfd97e2 100644 --- a/packages/contracts/test/CdpManagerTest.js +++ b/packages/contracts/test/CdpManagerTest.js @@ -1113,7 +1113,12 @@ contract('CdpManager', async accounts => { assert.isTrue(await th.checkRecoveryMode(contracts)) await priceFeed.setPrice(dec(2500, 13)) - await borrowerOperations.addColl(_eCdpId, _eCdpId, _eCdpId, dec(10, 'ether'), { from: E }) + await borrowerOperations.addColl(_eCdpId, _eCdpId, _eCdpId, dec(10, 'ether'), { from: E }) + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); // Try to liquidate C again. await debtToken.transfer(owner, toBN((await debtToken.balanceOf(D)).toString()), {from: D}); diff --git a/packages/contracts/test/CdpManager_RecoveryModeTest.js b/packages/contracts/test/CdpManager_RecoveryModeTest.js index 8752fe3d9..9ff2c484c 100644 --- a/packages/contracts/test/CdpManager_RecoveryModeTest.js +++ b/packages/contracts/test/CdpManager_RecoveryModeTest.js @@ -2837,7 +2837,12 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_A.gt(mv._MCR) && ICR_A.lt(TCR)) assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR)) - assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) + assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); /* Liquidate cdps. Cdps are ordered by ICR, from low to high: A, B, C. With 253 in the SP, Alice (102 debt) and Bob (101 debt) should be entirely liquidated. @@ -2906,7 +2911,12 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR)) assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) assert.isTrue(ICR_D.gt(mv._MCR) && ICR_D.lt(TCR)) - assert.isTrue(ICR_E.gt(mv._MCR) && ICR_E.lt(TCR)) + assert.isTrue(ICR_E.gt(mv._MCR) && ICR_E.lt(TCR)) + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); /* Liquidate cdps. Cdps are ordered by ICR, from low to high: A, B, C, D, E. With 300 in the SP, Alice (102 debt) and Bob (101 debt) should be entirely liquidated. @@ -2969,7 +2979,12 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR)) assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) assert.isTrue(ICR_D.gt(mv._MCR) && ICR_D.lt(TCR)) - assert.isTrue(ICR_E.gt(mv._MCR) && ICR_E.lt(TCR)) + assert.isTrue(ICR_E.gt(mv._MCR) && ICR_E.lt(TCR)) + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); /* Liquidate cdps. Cdps are ordered by ICR, from low to high: A, B, C, D, E. With 301 in the SP, Alice (102 debt) and Bob (101 debt) should be entirely liquidated. @@ -3030,7 +3045,12 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) const entireSystemCollBefore = await cdpManager.getEntireSystemColl() - const entireSystemDebtBefore = await cdpManager.getEntireSystemDebt() + const entireSystemDebtBefore = await cdpManager.getEntireSystemDebt() + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); /* Liquidate cdps. Cdps are ordered by ICR, from low to high: A, B, C, D, E. With 253 in the SP, Alice (102 debt) and Bob (101 debt) should be entirely liquidated. @@ -3142,7 +3162,12 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_A.gt(mv._MCR) && ICR_A.lt(TCR)) assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR)) - assert.isTrue(ICR_C_Before.gt(mv._MCR) && ICR_C_Before.lt(TCR)) + assert.isTrue(ICR_C_Before.gt(mv._MCR) && ICR_C_Before.lt(TCR)) + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); /* Liquidate cdps. Cdps are ordered by ICR, from low to high: A, B, C, D, E. With 253 in the SP, Alice (102 debt) and Bob (101 debt) should be entirely liquidated. @@ -3508,7 +3533,12 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_A.gt(mv._MCR) && ICR_A.lt(TCR)) assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR)) - assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) + assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); const cdpsToLiquidate = [_aliceCdpId, _bobCdpId, _carolCdpId] await debtToken.transfer(owner, toBN((await debtToken.balanceOf(alice)).toString()), {from: alice}); @@ -3555,7 +3585,12 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_A.gt(mv._MCR) && ICR_A.lt(TCR)) assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR)) - assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) + assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); const cdpsToLiquidate = [_aliceCdpId, _bobCdpId, _carolCdpId] await debtToken.transfer(owner, toBN((await debtToken.balanceOf(alice)).toString()), {from: alice}); @@ -3620,7 +3655,12 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR)) assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) assert.isTrue(ICR_D.gt(mv._MCR) && ICR_D.lt(TCR)) - assert.isTrue(ICR_E.gt(mv._MCR) && ICR_E.lt(TCR)) + assert.isTrue(ICR_E.gt(mv._MCR) && ICR_E.lt(TCR)) + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); /* With 300 in the SP, Alice (102 debt) and Bob (101 debt) should be entirely liquidated. That leaves 97 EBTC in the Pool that won’t be enough to absorb Carol, @@ -3683,7 +3723,12 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR)) assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) assert.isTrue(ICR_D.gt(mv._MCR) && ICR_D.lt(TCR)) - assert.isTrue(ICR_E.gt(mv._MCR) && ICR_E.lt(TCR)) + assert.isTrue(ICR_E.gt(mv._MCR) && ICR_E.lt(TCR)) + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); /* With 301 in the SP, Alice (102 debt) and Bob (101 debt) should be entirely liquidated. That leaves 97 EBTC in the Pool that won’t be enough to absorb Carol, @@ -3744,7 +3789,12 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) const entireSystemCollBefore = await cdpManager.getEntireSystemColl() - const entireSystemDebtBefore = await cdpManager.getEntireSystemDebt() + const entireSystemDebtBefore = await cdpManager.getEntireSystemDebt() + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); const cdpsToLiquidate = [_aliceCdpId, _bobCdpId, _carolCdpId] await debtToken.transfer(owner, toBN((await debtToken.balanceOf(alice)).toString()), {from: alice}); @@ -3794,7 +3844,12 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_A.gt(mv._MCR) && ICR_A.lt(TCR)) assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR)) - assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) + assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); const cdpsToLiquidate = [_aliceCdpId, _bobCdpId, _carolCdpId] await debtToken.transfer(owner, toBN((await debtToken.balanceOf(alice)).toString()), {from: alice}); @@ -3861,7 +3916,12 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_A.gt(mv._MCR) && ICR_A.lt(TCR)) assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR)) - assert.isTrue(ICR_C_Before.gt(mv._MCR) && ICR_C_Before.lt(TCR)) + assert.isTrue(ICR_C_Before.gt(mv._MCR) && ICR_C_Before.lt(TCR)) + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); const cdpsToLiquidate = [_aliceCdpId, _bobCdpId, _carolCdpId] await debtToken.transfer(owner, toBN((await debtToken.balanceOf(alice)).toString()), {from: alice}); @@ -4133,7 +4193,12 @@ contract('CdpManager - in Recovery Mode', async accounts => { // B and E are still in range 110-TCR. // Attempt to liquidate B, G, H, I, E. // Expected liquidator to fully absorb B (92 EBTC + 10 virtual debt), - // but not E as there are not enough funds in liquidator + // but not E as there are not enough funds in liquidator + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); const dEbtBefore = (await cdpManager.Cdps(_eCdpId))[0] diff --git a/packages/contracts/test/CdpManager_RecoveryMode_Batch_Liqudation_Test.js b/packages/contracts/test/CdpManager_RecoveryMode_Batch_Liqudation_Test.js index 546d7cb5f..49af06f24 100644 --- a/packages/contracts/test/CdpManager_RecoveryMode_Batch_Liqudation_Test.js +++ b/packages/contracts/test/CdpManager_RecoveryMode_Batch_Liqudation_Test.js @@ -74,7 +74,13 @@ contract('CdpManager - in Recovery Mode - back to normal mode in 1 tx', async ac } it('First cdp only doesn’t get out of Recovery Mode', async () => { - await setup() + await setup() + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); + let _aliceCdpId = await sortedCdps.cdpOfOwnerByIndex(alice, 0); await debtToken.transfer(owner, toBN((await debtToken.balanceOf(alice)).toString()), {from: alice}); await debtToken.transfer(owner, toBN((await debtToken.balanceOf(bob)).toString()), {from: bob}); @@ -88,7 +94,12 @@ contract('CdpManager - in Recovery Mode - back to normal mode in 1 tx', async ac await setup() let _aliceCdpId = await sortedCdps.cdpOfOwnerByIndex(alice, 0); let _bobCdpId = await sortedCdps.cdpOfOwnerByIndex(bob, 0); - let _carolCdpId = await sortedCdps.cdpOfOwnerByIndex(carol, 0); + let _carolCdpId = await sortedCdps.cdpOfOwnerByIndex(carol, 0); + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); await debtToken.transfer(owner, toBN((await debtToken.balanceOf(alice)).toString()), {from: alice}); await debtToken.transfer(owner, toBN((await debtToken.balanceOf(bob)).toString()), {from: bob}); @@ -140,7 +151,12 @@ contract('CdpManager - in Recovery Mode - back to normal mode in 1 tx', async ac assert.isTrue(ICR_A.gt(TCR)) assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR)) - assert.isTrue(ICR_C.lt(mv._ICR100)) + assert.isTrue(ICR_C.lt(mv._ICR100)) + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); await debtToken.transfer(owner, toBN((await debtToken.balanceOf(alice)).toString()), {from: alice}); await debtToken.transfer(owner, toBN((await debtToken.balanceOf(bob)).toString()), {from: bob}); @@ -198,7 +214,13 @@ contract('CdpManager - in Recovery Mode - back to normal mode in 1 tx', async ac } it('First cdp only doesn’t get out of Recovery Mode', async () => { - await setup() + await setup() + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); + await debtToken.transfer(owner, toBN((await debtToken.balanceOf(alice)).toString()), {from: alice}); await debtToken.transfer(owner, toBN((await debtToken.balanceOf(bob)).toString()), {from: bob}); const tx = await cdpManager.liquidateCdps(1) @@ -210,7 +232,12 @@ contract('CdpManager - in Recovery Mode - back to normal mode in 1 tx', async ac it('Two cdps over MCR are liquidated', async () => { await setup() let _aliceCdpId = await sortedCdps.cdpOfOwnerByIndex(alice, 0); - let _bobCdpId = await sortedCdps.cdpOfOwnerByIndex(bob, 0); + let _bobCdpId = await sortedCdps.cdpOfOwnerByIndex(bob, 0); + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); await debtToken.transfer(owner, toBN((await debtToken.balanceOf(alice)).toString()), {from: alice}); await debtToken.transfer(owner, toBN((await debtToken.balanceOf(bob)).toString()), {from: bob}); diff --git a/packages/contracts/test/CdpManager_SimpleLiquidation_Test.js b/packages/contracts/test/CdpManager_SimpleLiquidation_Test.js index 815d09757..954b497c7 100644 --- a/packages/contracts/test/CdpManager_SimpleLiquidation_Test.js +++ b/packages/contracts/test/CdpManager_SimpleLiquidation_Test.js @@ -391,7 +391,12 @@ contract('CdpManager - Simple Liquidation with external liquidators', async acco let aliceDebt = await cdpManager.getCdpDebt(aliceCdpId); let aliceColl = await cdpManager.getCdpColl(aliceCdpId); let prevDebtOfOwner = await debtToken.balanceOf(owner); - assert.isTrue(toBN(prevDebtOfOwner.toString()).gt(toBN(aliceDebt.toString()))); + assert.isTrue(toBN(prevDebtOfOwner.toString()).gt(toBN(aliceDebt.toString()))); + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); // liquidate alice in recovery mode let prevETHOfOwner = await ethers.provider.getBalance(owner); diff --git a/packages/contracts/test/CdpManager_StakingSplitFee_Test.js b/packages/contracts/test/CdpManager_StakingSplitFee_Test.js index e67aa71de..cc5c60851 100644 --- a/packages/contracts/test/CdpManager_StakingSplitFee_Test.js +++ b/packages/contracts/test/CdpManager_StakingSplitFee_Test.js @@ -281,7 +281,12 @@ contract('CdpManager - Simple Liquidation with external liquidators', async acco let _partialDebtRepaid = minDebt; let _expectedSeizedColl = toBN(_partialDebtRepaid.toString()).mul(_MCR).div(toBN(_newPrice)); let _expectedLiquidatedColl = _expectedSeizedColl.mul(mv._1e18BN).div(_newIndex); - let _collBeforeLiquidator = await collToken.balanceOf(owner); + let _collBeforeLiquidator = await collToken.balanceOf(owner); + + // trigger cooldown and pass the liq wait + await cdpManager.checkLiquidateCoolDownAndReset(); + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); await cdpManager.partiallyLiquidate(_aliceCdpId, _partialDebtRepaid, _aliceCdpId, _aliceCdpId, {from: owner}); let _collAfterLiquidator = await collToken.balanceOf(owner); From 126dfe6fc6fecae1d6906b6dad40304ab657f491 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 10:11:53 +0200 Subject: [PATCH 17/68] fix: dork --- packages/contracts/contracts/LiquidationLibrary.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 2518229eb..5cbe2bb4e 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -995,5 +995,7 @@ contract LiquidationLibrary is CdpManagerStorage { bool _liquidatable = _recovery ? (_icr < MCR || canLiquidateRecoveryMode(_icr, _TCR)) : _icr < MCR; + + return _liquidatable; } } From 5805c1b5491ed2ac9649fb48f8ad3453f40c5b7a Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 11:17:46 +0200 Subject: [PATCH 18/68] feat: error messages based on glass --- packages/contracts/contracts/LiquidationLibrary.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 5cbe2bb4e..16523a0db 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -65,10 +65,14 @@ contract LiquidationLibrary is CdpManagerStorage { uint256 _ICR = getCurrentICR(_cdpId, _price); (uint _TCR, uint systemColl, uint systemDebt) = _getTCRWithTotalCollAndDebt(_price); - require( - _ICR < MCR || (_TCR < CCR && canLiquidateRecoveryMode(_ICR, _TCR)), - "CdpManager: ICR is not below liquidation threshold in current mode" - ); + if(_ICR >= MCR) { + // We must be in RM + require(_TCR < CCR, "CdpManager: ICR is not below liquidation threshold in current mode"); + + // Grace period + require(lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG, "Grace period not started, call `notifyBeginRM`"); + require(block.timestamp > lastRecoveryModeTimestamp + waitTimeFromRMTriggerToLiquidations, "Grace period yet to finish"); + } // Implies ICR < MRC, meaning the CDP is liquidatable bool _recoveryModeAtStart = _TCR < CCR ? true : false; LocalVar_InternalLiquidate memory _liqState = LocalVar_InternalLiquidate( From 66498fda44eb6bcd703d081f3aa8030fa818b65e Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 11:17:54 +0200 Subject: [PATCH 19/68] fix: test reverts as intended (fixed) --- packages/contracts/foundry_test/SandwhichSniper.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/foundry_test/SandwhichSniper.t.sol b/packages/contracts/foundry_test/SandwhichSniper.t.sol index 411fe850d..aa15b910d 100644 --- a/packages/contracts/foundry_test/SandwhichSniper.t.sol +++ b/packages/contracts/foundry_test/SandwhichSniper.t.sol @@ -97,10 +97,10 @@ contract SandWhichSniperTest is eBTCBaseFixture { // We can now liquidate victim /** SANDWHICH 3 */ vm.startPrank(users[0]); + vm.expectRevert("Grace period not started, call `notifyBeginRM`"); cdpManager.liquidate(cdpIdVictim); uint256 tcrEnd = cdpManager.getTCR(_newPrice); console.log("tcrEnd liquidation", tcrEnd); assertEq(cdpManager.getCdpStatus(cdpIdVictim), 3); //closedByLiquidation - //assertGt(tcrEnd, 1250000000000000000); } } From 353bb8ebb53a6c7b908b92f738126f3b425afdac Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 11:18:12 +0200 Subject: [PATCH 20/68] chore: prettier --- .../contracts/contracts/LiquidationLibrary.sol | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 16523a0db..2ea653b65 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -65,13 +65,22 @@ contract LiquidationLibrary is CdpManagerStorage { uint256 _ICR = getCurrentICR(_cdpId, _price); (uint _TCR, uint systemColl, uint systemDebt) = _getTCRWithTotalCollAndDebt(_price); - if(_ICR >= MCR) { + if (_ICR >= MCR) { // We must be in RM - require(_TCR < CCR, "CdpManager: ICR is not below liquidation threshold in current mode"); + require( + _TCR < CCR, + "CdpManager: ICR is not below liquidation threshold in current mode" + ); // Grace period - require(lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG, "Grace period not started, call `notifyBeginRM`"); - require(block.timestamp > lastRecoveryModeTimestamp + waitTimeFromRMTriggerToLiquidations, "Grace period yet to finish"); + require( + lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG, + "Grace period not started, call `notifyBeginRM`" + ); + require( + block.timestamp > lastRecoveryModeTimestamp + waitTimeFromRMTriggerToLiquidations, + "Grace period yet to finish" + ); } // Implies ICR < MRC, meaning the CDP is liquidatable bool _recoveryModeAtStart = _TCR < CCR ? true : false; From b490484bd4a24f6fafec3f07525adca254270d1c Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 11:22:31 +0200 Subject: [PATCH 21/68] fix: assertion --- packages/contracts/foundry_test/SandwhichSniper.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/foundry_test/SandwhichSniper.t.sol b/packages/contracts/foundry_test/SandwhichSniper.t.sol index aa15b910d..4bfa1788c 100644 --- a/packages/contracts/foundry_test/SandwhichSniper.t.sol +++ b/packages/contracts/foundry_test/SandwhichSniper.t.sol @@ -101,6 +101,6 @@ contract SandWhichSniperTest is eBTCBaseFixture { cdpManager.liquidate(cdpIdVictim); uint256 tcrEnd = cdpManager.getTCR(_newPrice); console.log("tcrEnd liquidation", tcrEnd); - assertEq(cdpManager.getCdpStatus(cdpIdVictim), 3); //closedByLiquidation + assertEq(cdpManager.getCdpStatus(cdpIdVictim), 1); //Still Open (And safe until end of Grace Period) } } From 6b0d171d41340a5ed36a01a9015f245e46fa2bb6 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 11:23:28 +0200 Subject: [PATCH 22/68] fix: CEI restored in open, adjust and close --- packages/contracts/contracts/BorrowerOperations.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/contracts/contracts/BorrowerOperations.sol b/packages/contracts/contracts/BorrowerOperations.sol index af03b6853..50dcf843b 100644 --- a/packages/contracts/contracts/BorrowerOperations.sol +++ b/packages/contracts/contracts/BorrowerOperations.sol @@ -350,8 +350,6 @@ contract BorrowerOperations is ); _processTokenMovesFromAdjustment(_varMvTokens); } - - IRmLiquidationsChecker(address(cdpManager)).checkLiquidateCoolDownAndReset(); // TODO: Check this } function _openCdp( From 7fa37da4370632e6efdcfb04c3cc59ef3e34e65f Mon Sep 17 00:00:00 2001 From: rayeaster Date: Wed, 23 Aug 2023 18:11:54 +0800 Subject: [PATCH 23/68] add _waitUntilRMColldown() in BaseFixture.sol --- packages/contracts/foundry_test/BaseFixture.sol | 6 ++++++ .../foundry_test/CdpManager.Liquidation.t.sol | 10 ++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/contracts/foundry_test/BaseFixture.sol b/packages/contracts/foundry_test/BaseFixture.sol index 892011203..8f96cab82 100644 --- a/packages/contracts/foundry_test/BaseFixture.sol +++ b/packages/contracts/foundry_test/BaseFixture.sol @@ -457,4 +457,10 @@ contract eBTCBaseFixture is Test, BytecodeReader { _currentCdpId = sortedCdps.getPrev(_currentCdpId); } } + + // Grace Period, check never reverts so it's safe to use + function _waitUntilRMColldown() internal { + cdpManager.checkLiquidateCoolDownAndReset(); + vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + } } diff --git a/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol b/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol index 2be9ec4a0..6f7c7c802 100644 --- a/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol +++ b/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol @@ -114,10 +114,7 @@ contract CdpManagerLiquidationTest is eBTCBaseInvariants { uint _debtLiquidatorBefore = eBTCToken.balanceOf(users[0]); uint _debtSystemBefore = cdpManager.getEntireSystemDebt(); - // Grace Period - // Check never reverts so it's safe to use - cdpManager.checkLiquidateCoolDownAndReset(); - vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + _waitUntilRMColldown(); vm.prank(users[0]); cdpManager.liquidate(cdpId1); @@ -217,6 +214,9 @@ contract CdpManagerLiquidationTest is eBTCBaseInvariants { uint _debtSystemBefore = cdpManager.getEntireSystemDebt(); uint _collSystemBefore = cdpManager.getEntireSystemColl(); vm.prank(users[0]); + + _waitUntilRMColldown(); + cdpManager.partiallyLiquidate(cdpId1, _partialLiq._repaidDebt, cdpId1, cdpId1); uint _debtLiquidatorAfter = eBTCToken.balanceOf(users[0]); uint _debtSystemAfter = cdpManager.getEntireSystemDebt(); @@ -270,6 +270,8 @@ contract CdpManagerLiquidationTest is eBTCBaseInvariants { deal(address(eBTCToken), _liquidator, _debtSystemBefore); // sugardaddy liquidator uint _debtLiquidatorBefore = eBTCToken.balanceOf(_liquidator); + _waitUntilRMColldown(); + vm.prank(_liquidator); if (_n > 0) { cdpManager.liquidateCdps(_n); From 921ecab53ccd3bab8dbbc3002ae1e1c63ffa3deb Mon Sep 17 00:00:00 2001 From: rayeaster Date: Wed, 23 Aug 2023 18:30:02 +0800 Subject: [PATCH 24/68] fix prank order --- packages/contracts/foundry_test/CdpManager.Liquidation.t.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol b/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol index 6f7c7c802..334449e21 100644 --- a/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol +++ b/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol @@ -213,9 +213,8 @@ contract CdpManagerLiquidationTest is eBTCBaseInvariants { uint _debtLiquidatorBefore = eBTCToken.balanceOf(users[0]); uint _debtSystemBefore = cdpManager.getEntireSystemDebt(); uint _collSystemBefore = cdpManager.getEntireSystemColl(); - vm.prank(users[0]); - _waitUntilRMColldown(); + vm.prank(users[0]); cdpManager.partiallyLiquidate(cdpId1, _partialLiq._repaidDebt, cdpId1, cdpId1); uint _debtLiquidatorAfter = eBTCToken.balanceOf(users[0]); From db9e68dd9f7ba219229b93b0afd809a2ec899f76 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Wed, 23 Aug 2023 20:10:51 +0800 Subject: [PATCH 25/68] add CdpManager_RecoveryMode_Cooldown_Test.js --- .../CdpManager_RecoveryMode_Cooldown_Test.js | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js diff --git a/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js b/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js new file mode 100644 index 000000000..569ca30b9 --- /dev/null +++ b/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js @@ -0,0 +1,107 @@ +const deploymentHelper = require("../utils/deploymentHelpers.js") +const { TestHelper: th, MoneyValues: mv } = require("../utils/testHelpers.js") +const { toBN, dec, ZERO_ADDRESS } = th + +const CdpManagerTester = artifacts.require("./CdpManagerTester") +const EBTCToken = artifacts.require("./EBTCToken.sol") +const GovernorTester = artifacts.require("./GovernorTester.sol"); + +const assertRevert = th.assertRevert + +contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure delay on all external liquidations (exclusively for CDPs that can be liquidated in RM)', async accounts => { + const [bountyAddress, lpRewardsAddress, multisig] = accounts.slice(accounts.length - 3, accounts.length) + const [ + owner, + alice, bob, carol, dennis, erin, freddy, greta, harry, ida, + whale, defaulter_1, defaulter_2, defaulter_3, defaulter_4, + A, B, C, D, E, F, G, H, I + ] = accounts; + + let contracts + let cdpManager + let priceFeed + let sortedCdps + let collSurplusPool; + let _MCR; + let _CCR; + let _coolDownWait; + let collToken; + let splitFeeRecipient; + let authority; + + const openCdp = async (params) => th.openCdp(contracts, params) + + beforeEach(async () => { + await deploymentHelper.setDeployGasPrice(1000000000) + contracts = await deploymentHelper.deployTesterContractsHardhat() + let LQTYContracts = {} + LQTYContracts.feeRecipient = contracts.feeRecipient; + + cdpManager = contracts.cdpManager + priceFeed = contracts.priceFeedTestnet + sortedCdps = contracts.sortedCdps + debtToken = contracts.ebtcToken; + activePool = contracts.activePool; + defaultPool = contracts.defaultPool; + feeSplit = await contracts.cdpManager.stakingRewardSplit(); + liq_stipend = await contracts.cdpManager.LIQUIDATOR_REWARD(); + minDebt = await contracts.borrowerOperations.MIN_NET_COLL(); + _MCR = await cdpManager.MCR(); + _CCR = await cdpManager.CCR(); + LICR = await cdpManager.LICR(); + _coolDownWait = await cdpManager.waitTimeFromRMTriggerToLiquidations(); + borrowerOperations = contracts.borrowerOperations; + collSurplusPool = contracts.collSurplusPool; + collToken = contracts.collateral; + hintHelpers = contracts.hintHelpers; + authority = contracts.authority; + + await deploymentHelper.connectCoreContracts(contracts, LQTYContracts) + + splitFeeRecipient = await LQTYContracts.feeRecipient; + }) + + it("Happy case: 2 CDPs with 1st Safe and 2nd Unsafe and ensure you must wait cooldown", async() => { + + await openCdp({ ICR: toBN(dec(149, 16)), extraEBTCAmount: toBN(minDebt.toString()).mul(toBN("10")), extraParams: { from: alice } }) + await openCdp({ ICR: toBN(dec(139, 16)), extraParams: { from: bob } }) + await openCdp({ ICR: toBN(dec(129, 16)), extraParams: { from: carol } }) + + let _aliceCdpId = await sortedCdps.cdpOfOwnerByIndex(alice, 0); + let _bobCdpId = await sortedCdps.cdpOfOwnerByIndex(bob, 0); + let _carolCdpId = await sortedCdps.cdpOfOwnerByIndex(carol, 0); + await debtToken.transfer(owner, (await debtToken.balanceOf(alice)), {from : alice}); + await debtToken.transfer(owner, (await debtToken.balanceOf(bob)), {from : bob}); + await debtToken.transfer(owner, (await debtToken.balanceOf(carol)), {from : carol}); + + // price drops to trigger RM + let _newPrice = dec(6000, 13); + await priceFeed.setPrice(_newPrice); + let _aliceICRBefore = await cdpManager.getCurrentICR(_aliceCdpId, _newPrice); + let _bobICRBefore = await cdpManager.getCurrentICR(_bobCdpId, _newPrice); + let _carolICRBefore = await cdpManager.getCurrentICR(_carolCdpId, _newPrice); + let _tcrBefore = await cdpManager.getTCR(_newPrice); + console.log('_aliceICRBefore=' + _aliceICRBefore + ', _bobICRBefore=' + _bobICRBefore + ', _carolICRBefore=' + _carolICRBefore + ', _tcrBefore=' + _tcrBefore); + assert.isTrue(toBN(_tcrBefore.toString()).lt(_CCR)); + assert.isTrue(toBN(_aliceICRBefore.toString()).gt(_tcrBefore)); + assert.isTrue(toBN(_bobICRBefore.toString()).lt(_tcrBefore)); + assert.isTrue(toBN(_bobICRBefore.toString()).gt(_MCR)); + assert.isTrue(toBN(_carolICRBefore.toString()).lt(_MCR)); + + // trigger RM cooldown + await cdpManager.checkLiquidateCoolDownAndReset(); + await assertRevert(cdpManager.liquidate(_bobCdpId, {from: owner}), "Grace period yet to finish"); + + // cooldown only apply those [> MCR & < TCR] + await cdpManager.liquidate(_carolCdpId, {from: owner}); + assert.isFalse((await sortedCdps.contains(_carolCdpId))); + + // pass the cooldown wait + await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_mine"); + await cdpManager.liquidate(_bobCdpId, {from: owner}); + assert.isFalse((await sortedCdps.contains(_bobCdpId))); + }) + + +}) \ No newline at end of file From 698c87f7b89fb9914c9df8667fd80070bb734819 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 15:38:38 +0200 Subject: [PATCH 26/68] fix: nice error message when nothing to liquidate --- packages/contracts/contracts/LiquidationLibrary.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 6587301ff..d07a4ab80 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -578,6 +578,7 @@ contract LiquidationLibrary is CdpManagerStorage { bytes32[] memory _batchedCdps; if (vars.recoveryModeAtStart) { _batchedCdps = _sequenceLiqToBatchLiq(_n, true, vars.price); + require(_batchedCdps.length > 0, "LiquidationLibrary: nothing to liquidate"); totals = _getTotalFromBatchLiquidate_RecoveryMode( vars.price, systemColl, @@ -588,6 +589,7 @@ contract LiquidationLibrary is CdpManagerStorage { } else { // if !vars.recoveryModeAtStart _batchedCdps = _sequenceLiqToBatchLiq(_n, false, vars.price); + require(_batchedCdps.length > 0, "LiquidationLibrary: nothing to liquidate"); totals = _getTotalsFromBatchLiquidate_NormalMode(vars.price, _TCR, _batchedCdps, true); } From d440d66b2379657268caeaf2952b0bf4b420a4d8 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 18:45:24 +0200 Subject: [PATCH 27/68] feat: incomplete tests for grace period spec --- .../contracts/foundry_test/GracePeriod.t.sol | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 packages/contracts/foundry_test/GracePeriod.t.sol diff --git a/packages/contracts/foundry_test/GracePeriod.t.sol b/packages/contracts/foundry_test/GracePeriod.t.sol new file mode 100644 index 000000000..52b76bd98 --- /dev/null +++ b/packages/contracts/foundry_test/GracePeriod.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; + +import "forge-std/Test.sol"; +import "../contracts/Dependencies/LiquityMath.sol"; +import {eBTCBaseFixture} from "./BaseFixture.sol"; + +/* + Tests around GracePeriod + */ +contract GracePeriodBaseTests is eBTCBaseFixture { + function setUp() public override { + eBTCBaseFixture.setUp(); + eBTCBaseFixture.connectCoreContracts(); + eBTCBaseFixture.connectLQTYContractsToCore(); + } + + address liquidator; + + + // == DELAY TEST == // + // Delay of 15 minutes is enforced to liquidate CDPs in RM that are not below MCR - DONE + // No Delay for the Portion of CDPs which is below MCR - TODO + + // RM Triggered via Price - DONE + // RM Triggered via Split - DONE + + // RM Triggered via User Operation + // All operations where the system is in RM should trigger the countdown + + // RM untriggered via Price + // RM untriggered via Split + + // RM untriggered via User Operations + // All operations where the system goes off of RM should cancel the countdown + + /// @dev Setup function to ensure we liquidate the correct amount of CDPs + function triggerRmBasic(uint256 numberOfCdpsAtRisk) internal returns (bytes32[] memory) { + address payable[] memory users; + users = _utils.createUsers(2); + + bytes32[] memory cdps = new bytes32[](numberOfCdpsAtRisk + 1); + + // Deposit a big CDP, not at risk + uint _curPrice = priceFeedMock.getPrice(); + uint256 debt1 = 1000e18; + uint256 coll1 = _utils.calculateCollAmount(debt1, _curPrice, 1.30e18); // Comfy unliquidatable + + cdps[0] = _openTestCDP(users[0], coll1, debt1); + liquidator = users[0]; + + // At risk CDPs (small CDPs) + for(uint256 i; i < numberOfCdpsAtRisk; i++) { + uint256 debt2 = 2e18; + uint256 coll2 = _utils.calculateCollAmount(debt2, _curPrice, 1.15e18); // Fairly risky + cdps[1 + i] = _openTestCDP(users[1], coll2, debt2); + } + + // Deposit a bunch at risk + + return cdps; + } + + function testTheBasicGracePeriodViaPrice() public { + bytes32[] memory cdps = triggerRmBasic(5); + assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) + + + // 2% Downward Price will trigger + priceFeedMock.setPrice(priceFeedMock.getPrice() * 96 / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM + uint256 reducedPrice = priceFeedMock.getPrice(); + + // Check if we are in RM + uint256 TCR = cdpManager.getTCR(reducedPrice); + assertLt(TCR, 1.25e18, "!RM"); + + // Check the stuff after RM + // Do it as a function and reuse it + // checkPropertiesOfTriggerinRM(); + } + + function testTheBasicGracePeriodViaSplit() public { + bytes32[] memory cdps = triggerRmBasic(5); + assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) + + + // 2% Downward Price will trigger + collateral.setEthPerShare(collateral.getSharesByPooledEth(1e18) * 96 / 100);// 4% downturn, 5% should be enough to liquidate in-spite of RM + uint256 price = priceFeedMock.getPrice(); + + // Check if we are in RM + uint256 TCR = cdpManager.getTCR(price); + assertLt(TCR, 1.25e18, "!RM"); + + // Check the stuff after RM + // Do it as a function and reuse it + + // Glass is not broken all liquidations revert (since those are not risky CDPs) + assertRevertOnAllLiquidations(cdps); + + // Glass is broken + cdpManager.beginRMLiquidationCooldown(); + // Liquidations still revert + assertRevertOnAllLiquidations(cdps); + + // Grace Period Ended, liquidations work + vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + assertAllLiquidationSuccess(cdps); + } + + function assertRevertOnAllLiquidations(bytes32[] memory cdps) internal { + // Try liquidating a cdp + vm.expectRevert(); + cdpManager.liquidate(cdps[1]); + + // Try liquidating a cdp partially + vm.expectRevert(); + cdpManager.partiallyLiquidate(cdps[1], 1e18, cdps[1], cdps[1]); + + // Try liquidating a cdp via the list (1) + vm.expectRevert(); + cdpManager.liquidateCdps(1); + + // Try liquidating a cdp via the list (2) + bytes32[] memory cdpsToLiquidateBatch = new bytes32[](1); + cdpsToLiquidateBatch[0] = cdps[1]; + vm.expectRevert(); + cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); + } + + function assertAllLiquidationSuccess(bytes32[] memory cdps) internal { + vm.startPrank(liquidator); + uint256 snapshotId = vm.snapshot(); + + // Try liquidating a cdp + cdpManager.liquidate(cdps[1]); + + vm.revertTo(snapshotId); + + // Try liquidating a cdp partially + cdpManager.partiallyLiquidate(cdps[1], 1e18, cdps[1], cdps[1]); + + vm.revertTo(snapshotId); + + // Try liquidating a cdp via the list (1) + cdpManager.liquidateCdps(1); + + vm.revertTo(snapshotId); + + // Try liquidating a cdp via the list (2) + bytes32[] memory cdpsToLiquidateBatch = new bytes32[](1); + cdpsToLiquidateBatch[0] = cdps[1]; + cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); + + vm.revertTo(snapshotId); + } +} \ No newline at end of file From cdb19f174d942608555266d65489cfcff907980d Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 23 Aug 2023 18:45:30 +0200 Subject: [PATCH 28/68] chore: prettier --- .../contracts/foundry_test/GracePeriod.t.sol | 143 +++++++++--------- 1 file changed, 70 insertions(+), 73 deletions(-) diff --git a/packages/contracts/foundry_test/GracePeriod.t.sol b/packages/contracts/foundry_test/GracePeriod.t.sol index 52b76bd98..aeb5ccbda 100644 --- a/packages/contracts/foundry_test/GracePeriod.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.t.sol @@ -17,14 +17,13 @@ contract GracePeriodBaseTests is eBTCBaseFixture { address liquidator; - // == DELAY TEST == // // Delay of 15 minutes is enforced to liquidate CDPs in RM that are not below MCR - DONE // No Delay for the Portion of CDPs which is below MCR - TODO // RM Triggered via Price - DONE // RM Triggered via Split - DONE - + // RM Triggered via User Operation // All operations where the system is in RM should trigger the countdown @@ -41,7 +40,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { bytes32[] memory cdps = new bytes32[](numberOfCdpsAtRisk + 1); - // Deposit a big CDP, not at risk + // Deposit a big CDP, not at risk uint _curPrice = priceFeedMock.getPrice(); uint256 debt1 = 1000e18; uint256 coll1 = _utils.calculateCollAmount(debt1, _curPrice, 1.30e18); // Comfy unliquidatable @@ -50,108 +49,106 @@ contract GracePeriodBaseTests is eBTCBaseFixture { liquidator = users[0]; // At risk CDPs (small CDPs) - for(uint256 i; i < numberOfCdpsAtRisk; i++) { + for (uint256 i; i < numberOfCdpsAtRisk; i++) { uint256 debt2 = 2e18; uint256 coll2 = _utils.calculateCollAmount(debt2, _curPrice, 1.15e18); // Fairly risky cdps[1 + i] = _openTestCDP(users[1], coll2, debt2); } - // Deposit a bunch at risk + // Deposit a bunch at risk - return cdps; + return cdps; } - + function testTheBasicGracePeriodViaPrice() public { - bytes32[] memory cdps = triggerRmBasic(5); - assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) - + bytes32[] memory cdps = triggerRmBasic(5); + assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) - // 2% Downward Price will trigger - priceFeedMock.setPrice(priceFeedMock.getPrice() * 96 / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM - uint256 reducedPrice = priceFeedMock.getPrice(); + // 2% Downward Price will trigger + priceFeedMock.setPrice((priceFeedMock.getPrice() * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM + uint256 reducedPrice = priceFeedMock.getPrice(); - // Check if we are in RM - uint256 TCR = cdpManager.getTCR(reducedPrice); - assertLt(TCR, 1.25e18, "!RM"); + // Check if we are in RM + uint256 TCR = cdpManager.getTCR(reducedPrice); + assertLt(TCR, 1.25e18, "!RM"); - // Check the stuff after RM - // Do it as a function and reuse it - // checkPropertiesOfTriggerinRM(); + // Check the stuff after RM + // Do it as a function and reuse it + // checkPropertiesOfTriggerinRM(); } function testTheBasicGracePeriodViaSplit() public { - bytes32[] memory cdps = triggerRmBasic(5); - assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) - + bytes32[] memory cdps = triggerRmBasic(5); + assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) - // 2% Downward Price will trigger - collateral.setEthPerShare(collateral.getSharesByPooledEth(1e18) * 96 / 100);// 4% downturn, 5% should be enough to liquidate in-spite of RM - uint256 price = priceFeedMock.getPrice(); + // 2% Downward Price will trigger + collateral.setEthPerShare((collateral.getSharesByPooledEth(1e18) * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM + uint256 price = priceFeedMock.getPrice(); - // Check if we are in RM - uint256 TCR = cdpManager.getTCR(price); - assertLt(TCR, 1.25e18, "!RM"); + // Check if we are in RM + uint256 TCR = cdpManager.getTCR(price); + assertLt(TCR, 1.25e18, "!RM"); - // Check the stuff after RM - // Do it as a function and reuse it + // Check the stuff after RM + // Do it as a function and reuse it - // Glass is not broken all liquidations revert (since those are not risky CDPs) - assertRevertOnAllLiquidations(cdps); + // Glass is not broken all liquidations revert (since those are not risky CDPs) + assertRevertOnAllLiquidations(cdps); - // Glass is broken - cdpManager.beginRMLiquidationCooldown(); - // Liquidations still revert - assertRevertOnAllLiquidations(cdps); + // Glass is broken + cdpManager.beginRMLiquidationCooldown(); + // Liquidations still revert + assertRevertOnAllLiquidations(cdps); - // Grace Period Ended, liquidations work - vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); - assertAllLiquidationSuccess(cdps); + // Grace Period Ended, liquidations work + vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + assertAllLiquidationSuccess(cdps); } function assertRevertOnAllLiquidations(bytes32[] memory cdps) internal { - // Try liquidating a cdp - vm.expectRevert(); - cdpManager.liquidate(cdps[1]); - - // Try liquidating a cdp partially - vm.expectRevert(); - cdpManager.partiallyLiquidate(cdps[1], 1e18, cdps[1], cdps[1]); - - // Try liquidating a cdp via the list (1) - vm.expectRevert(); - cdpManager.liquidateCdps(1); - - // Try liquidating a cdp via the list (2) - bytes32[] memory cdpsToLiquidateBatch = new bytes32[](1); - cdpsToLiquidateBatch[0] = cdps[1]; - vm.expectRevert(); - cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); + // Try liquidating a cdp + vm.expectRevert(); + cdpManager.liquidate(cdps[1]); + + // Try liquidating a cdp partially + vm.expectRevert(); + cdpManager.partiallyLiquidate(cdps[1], 1e18, cdps[1], cdps[1]); + + // Try liquidating a cdp via the list (1) + vm.expectRevert(); + cdpManager.liquidateCdps(1); + + // Try liquidating a cdp via the list (2) + bytes32[] memory cdpsToLiquidateBatch = new bytes32[](1); + cdpsToLiquidateBatch[0] = cdps[1]; + vm.expectRevert(); + cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); } function assertAllLiquidationSuccess(bytes32[] memory cdps) internal { - vm.startPrank(liquidator); - uint256 snapshotId = vm.snapshot(); + vm.startPrank(liquidator); + uint256 snapshotId = vm.snapshot(); - // Try liquidating a cdp - cdpManager.liquidate(cdps[1]); + // Try liquidating a cdp + cdpManager.liquidate(cdps[1]); - vm.revertTo(snapshotId); + vm.revertTo(snapshotId); - // Try liquidating a cdp partially - cdpManager.partiallyLiquidate(cdps[1], 1e18, cdps[1], cdps[1]); + // Try liquidating a cdp partially + cdpManager.partiallyLiquidate(cdps[1], 1e18, cdps[1], cdps[1]); - vm.revertTo(snapshotId); + vm.revertTo(snapshotId); - // Try liquidating a cdp via the list (1) - cdpManager.liquidateCdps(1); + // Try liquidating a cdp via the list (1) + cdpManager.liquidateCdps(1); - vm.revertTo(snapshotId); + vm.revertTo(snapshotId); - // Try liquidating a cdp via the list (2) - bytes32[] memory cdpsToLiquidateBatch = new bytes32[](1); - cdpsToLiquidateBatch[0] = cdps[1]; - cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); + // Try liquidating a cdp via the list (2) + bytes32[] memory cdpsToLiquidateBatch = new bytes32[](1); + cdpsToLiquidateBatch[0] = cdps[1]; + cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); - vm.revertTo(snapshotId); + vm.revertTo(snapshotId); } -} \ No newline at end of file +} From d5e6e5bb85cb8b11cf6481c2b9a557dcbe9e379b Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Wed, 23 Aug 2023 20:49:41 -0400 Subject: [PATCH 29/68] valid action test draft --- .../contracts/foundry_test/GracePeriod.t.sol | 109 +++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/packages/contracts/foundry_test/GracePeriod.t.sol b/packages/contracts/foundry_test/GracePeriod.t.sol index aeb5ccbda..963aa3d51 100644 --- a/packages/contracts/foundry_test/GracePeriod.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.t.sol @@ -54,9 +54,11 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint256 coll2 = _utils.calculateCollAmount(debt2, _curPrice, 1.15e18); // Fairly risky cdps[1 + i] = _openTestCDP(users[1], coll2, debt2); } + + // Move past bootstrap phase to allow redemptions + vm.warp(cdpManager.getDeploymentStartTime() + cdpManager.BOOTSTRAP_PERIOD()); // Deposit a bunch at risk - return cdps; } @@ -74,9 +76,17 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Check the stuff after RM // Do it as a function and reuse it - // checkPropertiesOfTriggerinRM(); + // _checkPropertiesOfTriggerinRM(); + + _preValidActionLiquidationChecks(cdps); + + // Action: Glass is broken + cdpManager.beginRMLiquidationCooldown(); + + _postValidActionLiquidationChecks(cdps); } + function testTheBasicGracePeriodViaSplit() public { bytes32[] memory cdps = triggerRmBasic(5); assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) @@ -105,6 +115,101 @@ contract GracePeriodBaseTests is eBTCBaseFixture { assertAllLiquidationSuccess(cdps); } + /** + @dev Test ways the grace period could be set in RM by expected exteral calls + @dev "Valid" actions are actions that can trigger grace period and also keep the system in recovery mode + + PriceDecreaseAction: + - setPrice + - setEthPerShare + + Action: + - openCdp + - adjustCdp + - redemptions + */ + function test_GracePeriodViaValidAction(uint8 priceDecreaseAction, uint8 action) public { + vm.assume(priceDecreaseAction <= 1); + vm.assume(action <= 2); + + // setup: create Cdps, enter RM via price change or rebase + bytes32[] memory cdps = triggerRmBasic(5); + assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) + + _execPriceDecreaseAction(priceDecreaseAction); + uint256 price = priceFeedMock.getPrice(); + + // Check if we are in RM + uint256 TCR = cdpManager.getTCR(price); + assertLt(TCR, 1.25e18, "!RM"); + + _preValidActionLiquidationChecks(cdps); + + _execValidRMAction(cdps, action); + + _postValidActionLiquidationChecks(cdps); + } + + /// @dev Enumerate variants of ways the grace period could be reset + /// @dev "Valid" actions are actions that can trigger grace period and also keep the system in recovery mode + function test_GracePeriodResetWhenRecoveryModeExitedViaAction(uint8 priceDecreaseAction, uint8 validA) public { + // setup: create Cdps, enter RM via price change or rebase + } + + function _execValidRMAction(bytes32[] memory cdps, uint action) internal { + address borrower = sortedCdps.getOwnerAddress(cdps[0]); + uint price = priceFeedMock.fetchPrice(); + if (action == 0) { // openCdp + uint debt = 2e18; + uint256 coll = _utils.calculateCollAmount(debt, price, 1.3 ether); + + dealCollateral(borrower, coll); + + vm.prank(borrower); + borrowerOperations.openCdp(coll, bytes32(0), bytes32(0), debt); + + uint256 TCR = cdpManager.getTCR(price); + assertLt(TCR, 1.25e18, "!RM"); + } else if (action == 1) { // adjustCdp: addColl + dealCollateral(borrower, 1); + + vm.prank(borrower); + borrowerOperations.addColl(cdps[0], bytes32(0), bytes32(0), 1); + + uint256 TCR = cdpManager.getTCR(price); + assertLt(TCR, 1.25e18, "!RM"); + } else if (action == 2) { //adjustCdp: repayEBTC + vm.prank(borrower); + borrowerOperations.repayEBTC(cdps[0], 1, bytes32(0), bytes32(0)); + + uint256 TCR = cdpManager.getTCR(price); + assertLt(TCR, 1.25e18, "!RM"); + } + } + + function _execPriceDecreaseAction(uint8 priceDecreaseAction) internal { + // 2% Downward Price will trigger + if (priceDecreaseAction == 0) { + priceFeedMock.setPrice((priceFeedMock.getPrice() * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM + } else { + collateral.setEthPerShare((collateral.getSharesByPooledEth(1e18) * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM + } + } + + function _preValidActionLiquidationChecks(bytes32[] memory cdps) internal { + // Glass is not broken all liquidations revert (since those are not risky CDPs) + assertRevertOnAllLiquidations(cdps); + } + + function _postValidActionLiquidationChecks(bytes32[] memory cdps) internal { + // Liquidations still revert + assertRevertOnAllLiquidations(cdps); + + // Grace Period Ended, liquidations work + vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + assertAllLiquidationSuccess(cdps); + } + function assertRevertOnAllLiquidations(bytes32[] memory cdps) internal { // Try liquidating a cdp vm.expectRevert(); From 0c3864d8b0e1aa978d755ea415fe6c2a6b169d51 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Thu, 24 Aug 2023 11:52:23 +0200 Subject: [PATCH 30/68] feat: FSM with regards to liquidations and Grace Period --- .../contracts/foundry_test/GracePeriod.t.sol | 217 ++++++++++++++---- 1 file changed, 170 insertions(+), 47 deletions(-) diff --git a/packages/contracts/foundry_test/GracePeriod.t.sol b/packages/contracts/foundry_test/GracePeriod.t.sol index 963aa3d51..d24d83d37 100644 --- a/packages/contracts/foundry_test/GracePeriod.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.17; import "forge-std/Test.sol"; +import "forge-std/console2.sol"; import "../contracts/Dependencies/LiquityMath.sol"; import {eBTCBaseFixture} from "./BaseFixture.sol"; @@ -34,14 +35,14 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // All operations where the system goes off of RM should cancel the countdown /// @dev Setup function to ensure we liquidate the correct amount of CDPs - function triggerRmBasic(uint256 numberOfCdpsAtRisk) internal returns (bytes32[] memory) { + function _openCdps(uint256 numberOfCdpsAtRisk) internal returns (bytes32[] memory) { address payable[] memory users; users = _utils.createUsers(2); bytes32[] memory cdps = new bytes32[](numberOfCdpsAtRisk + 1); // Deposit a big CDP, not at risk - uint _curPrice = priceFeedMock.getPrice(); + uint256 _curPrice = priceFeedMock.getPrice(); uint256 debt1 = 1000e18; uint256 coll1 = _utils.calculateCollAmount(debt1, _curPrice, 1.30e18); // Comfy unliquidatable @@ -58,61 +59,157 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Move past bootstrap phase to allow redemptions vm.warp(cdpManager.getDeploymentStartTime() + cdpManager.BOOTSTRAP_PERIOD()); - // Deposit a bunch at risk return cdps; } + function _openDegen() internal returns (bytes32) { + address payable[] memory users; + users = _utils.createUsers(1); + + uint256 _curPrice = priceFeedMock.getPrice(); + uint256 debt2 = 2e18; + uint256 coll2 = _utils.calculateCollAmount(debt2, _curPrice, 1.105e18); // Extremely Risky + bytes32 cdp = _openTestCDP(users[0], coll2, debt2); + + // Move past bootstrap phase to allow redemptions + vm.warp(cdpManager.getDeploymentStartTime() + cdpManager.BOOTSTRAP_PERIOD()); + + return cdp; + } + + /// @dev Verifies that the Grace Period Works when triggered by a Price Dump function testTheBasicGracePeriodViaPrice() public { - bytes32[] memory cdps = triggerRmBasic(5); + bytes32[] memory cdps = _openCdps(5); assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) - // 2% Downward Price will trigger - priceFeedMock.setPrice((priceFeedMock.getPrice() * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM - uint256 reducedPrice = priceFeedMock.getPrice(); + _triggerRMViaPrice(); - // Check if we are in RM - uint256 TCR = cdpManager.getTCR(reducedPrice); - assertLt(TCR, 1.25e18, "!RM"); + _checkLiquidationsFSMForRmCdps(cdps); + } - // Check the stuff after RM - // Do it as a function and reuse it - // _checkPropertiesOfTriggerinRM(); + function testTheBasicGracePeriodViaPriceWithDegenGettingLiquidated() public { + // Open Safe and RM Risky + bytes32[] memory rmLiquidatableCdps = _openCdps(5); - _preValidActionLiquidationChecks(cdps); + // Open Degen + bytes32 degen = _openDegen(); - // Action: Glass is broken - cdpManager.beginRMLiquidationCooldown(); + // Trigger RM + _triggerRMViaPrice(); - _postValidActionLiquidationChecks(cdps); + uint256 degenSnapshot = vm.snapshot(); + // Do extra checks for Degen getting liquidated Etc.. + _checkLiquidationsForDegen(degen); + vm.revertTo(degenSnapshot); + + vm.startPrank(liquidator); + // Liquidate Degen + cdpManager.liquidate(degen); // Liquidate them "for real" + vm.stopPrank(); + + // Then do the same checks for Grace Period + _checkLiquidationsFSMForRmCdps(rmLiquidatableCdps); // Verify rest of behaviour is consistent with Grace Period } + function _triggerRMViaPrice() internal { + // 4% Downward Price will trigger RM but not Liquidations + priceFeedMock.setPrice((priceFeedMock.getPrice() * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM + uint256 reducedPrice = priceFeedMock.getPrice(); + // Check if we are in RM + uint256 TCR = cdpManager.getTCR(reducedPrice); + assertLt(TCR, 1.25e18, "!RM"); + } + + /// @dev Verifies that the Grace Period Works when triggered by a Slashing function testTheBasicGracePeriodViaSplit() public { - bytes32[] memory cdps = triggerRmBasic(5); - assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) + bytes32[] memory cdps = _setupAndTriggerRMViaSplit(); + + _checkLiquidationsFSMForRmCdps(cdps); + } - // 2% Downward Price will trigger + function _triggerRMViaSplit() internal { + // 4% Downward Price will trigger RM but not Liquidations collateral.setEthPerShare((collateral.getSharesByPooledEth(1e18) * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM uint256 price = priceFeedMock.getPrice(); // Check if we are in RM uint256 TCR = cdpManager.getTCR(price); assertLt(TCR, 1.25e18, "!RM"); + } + + function _setupAndTriggerRMViaSplit() internal returns (bytes32[] memory) { + bytes32[] memory cdps = _openCdps(5); + assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) + + _triggerRMViaSplit(); + + return cdps; + } + + function testTheBasicGracePeriodViaSplitWithDegenGettingLiquidated() public { + // Open Safe and RM Risky + bytes32[] memory rmLiquidatableCdps = _openCdps(5); + + // Open Degen + bytes32 degen = _openDegen(); + + // Trigger RM + _triggerRMViaSplit(); + + uint256 degenSnapshot = vm.snapshot(); + // Do extra checks for Degen getting liquidated Etc.. + _checkLiquidationsForDegen(degen); + vm.revertTo(degenSnapshot); + + vm.startPrank(liquidator); + // Liquidate Degen + cdpManager.liquidate(degen); // Liquidate them "for real" + vm.stopPrank(); + + // Then do the same checks for Grace Period + _checkLiquidationsFSMForRmCdps(rmLiquidatableCdps); // Verify rest of behaviour is consistent with Grace Period + } + - // Check the stuff after RM - // Do it as a function and reuse it + /// Verify that if the Grace Period is not started, true liquidations still happen + /// Verify that if the Grace Period is started, true liquidations still happen + /// Verify that if the Grace Period is finished, true liquidations still happen - // Glass is not broken all liquidations revert (since those are not risky CDPs) - assertRevertOnAllLiquidations(cdps); - // Glass is broken + + /// Verify Grace Period Synching applies to all external functions + + + /// Claim Fee Split prob doesn't + + + /// @dev Verifies liquidations wrt Grace Period and Cdps that can be always be liquidated + function _checkLiquidationsForDegen(bytes32 cdp) internal { + // Grace Period not started, expect reverts on liquidations + _assertSuccessOnAllLiquidationsDegen(cdp); + cdpManager.beginRMLiquidationCooldown(); - // Liquidations still revert - assertRevertOnAllLiquidations(cdps); + // 15 mins not elapsed, prove these cdps still revert + _assertSuccessOnAllLiquidationsDegen(cdp); + + // Grace Period Ended, liquidations work + vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + _assertSuccessOnAllLiquidationsDegen(cdp); + } + + /// @dev Verifies liquidations wrt Grace Period and Cdps that can be liquidated only during RM + function _checkLiquidationsFSMForRmCdps(bytes32[] memory cdps) internal { + // Grace Period not started, expect reverts on liquidations + _assertRevertOnAllLiquidations(cdps); + + cdpManager.beginRMLiquidationCooldown(); + // 15 mins not elapsed, prove these cdps still revert + _assertRevertOnAllLiquidations(cdps); // Grace Period Ended, liquidations work vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); - assertAllLiquidationSuccess(cdps); + _assertAllLiquidationSuccess(cdps); } /** @@ -133,7 +230,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { vm.assume(action <= 2); // setup: create Cdps, enter RM via price change or rebase - bytes32[] memory cdps = triggerRmBasic(5); + bytes32[] memory cdps = _openCdps(5); assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) _execPriceDecreaseAction(priceDecreaseAction); @@ -143,7 +240,8 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint256 TCR = cdpManager.getTCR(price); assertLt(TCR, 1.25e18, "!RM"); - _preValidActionLiquidationChecks(cdps); + // Pre valid + _assertRevertOnAllLiquidations(cdps); _execValidRMAction(cdps, action); @@ -156,11 +254,11 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // setup: create Cdps, enter RM via price change or rebase } - function _execValidRMAction(bytes32[] memory cdps, uint action) internal { + function _execValidRMAction(bytes32[] memory cdps, uint256 action) internal { address borrower = sortedCdps.getOwnerAddress(cdps[0]); - uint price = priceFeedMock.fetchPrice(); + uint256 price = priceFeedMock.fetchPrice(); if (action == 0) { // openCdp - uint debt = 2e18; + uint256 debt = 2e18; uint256 coll = _utils.calculateCollAmount(debt, price, 1.3 ether); dealCollateral(borrower, coll); @@ -188,7 +286,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { } function _execPriceDecreaseAction(uint8 priceDecreaseAction) internal { - // 2% Downward Price will trigger + // 4% Downward Price will trigger if (priceDecreaseAction == 0) { priceFeedMock.setPrice((priceFeedMock.getPrice() * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM } else { @@ -196,21 +294,16 @@ contract GracePeriodBaseTests is eBTCBaseFixture { } } - function _preValidActionLiquidationChecks(bytes32[] memory cdps) internal { - // Glass is not broken all liquidations revert (since those are not risky CDPs) - assertRevertOnAllLiquidations(cdps); - } - function _postValidActionLiquidationChecks(bytes32[] memory cdps) internal { // Liquidations still revert - assertRevertOnAllLiquidations(cdps); + _assertRevertOnAllLiquidations(cdps); // Grace Period Ended, liquidations work vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); - assertAllLiquidationSuccess(cdps); + _assertAllLiquidationSuccess(cdps); } - function assertRevertOnAllLiquidations(bytes32[] memory cdps) internal { + function _assertRevertOnAllLiquidations(bytes32[] memory cdps) internal { // Try liquidating a cdp vm.expectRevert(); cdpManager.liquidate(cdps[1]); @@ -230,30 +323,60 @@ contract GracePeriodBaseTests is eBTCBaseFixture { cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); } - function assertAllLiquidationSuccess(bytes32[] memory cdps) internal { + function _assertSuccessOnAllLiquidationsDegen(bytes32 cdp) internal { vm.startPrank(liquidator); uint256 snapshotId = vm.snapshot(); // Try liquidating a cdp - cdpManager.liquidate(cdps[1]); - + cdpManager.liquidate(cdp); vm.revertTo(snapshotId); // Try liquidating a cdp partially - cdpManager.partiallyLiquidate(cdps[1], 1e18, cdps[1], cdps[1]); + cdpManager.partiallyLiquidate(cdp, 1e18, cdp, cdp); + vm.revertTo(snapshotId); + // Try liquidating a cdp via the list (2) + bytes32[] memory cdpsToLiquidateBatch = new bytes32[](1); + cdpsToLiquidateBatch[0] = cdp; + cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); vm.revertTo(snapshotId); + // Try liquidating a cdp via the list (1) cdpManager.liquidateCdps(1); + vm.revertTo(snapshotId); + + console2.log("About to batchLiquidateCdps", uint256(cdp)); + + + console2.log("This log if batchLiquidateCdps didn't revert"); + + + vm.stopPrank(); + } + + function _assertAllLiquidationSuccess(bytes32[] memory cdps) internal { + vm.startPrank(liquidator); + uint256 snapshotId = vm.snapshot(); + + // Try liquidating a cdp + cdpManager.liquidate(cdps[1]); + vm.revertTo(snapshotId); + // Try liquidating a cdp partially + cdpManager.partiallyLiquidate(cdps[1], 1e18, cdps[1], cdps[1]); + vm.revertTo(snapshotId); + + // Try liquidating a cdp via the list (1) + cdpManager.liquidateCdps(1); vm.revertTo(snapshotId); // Try liquidating a cdp via the list (2) bytes32[] memory cdpsToLiquidateBatch = new bytes32[](1); cdpsToLiquidateBatch[0] = cdps[1]; cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); - vm.revertTo(snapshotId); + + vm.stopPrank(); } } From cededd46bc8cf2700df8568480c54f280fe095d9 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Thu, 24 Aug 2023 12:54:17 +0200 Subject: [PATCH 31/68] chore: comment --- packages/contracts/contracts/CdpManagerStorage.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index e457667e3..75d648c14 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -19,7 +19,6 @@ import "./Dependencies/AuthNoOwner.sol"; */ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, AuthNoOwner { // TODO: IMPROVE - // NOTE: No packing cause it's the last var, no need for u64 uint128 constant UNSET_TIMESTAMP_FLAG = type(uint128).max; // TODO: IMPROVE THIS!!! From 0ccdd137d75b57bbf78afe0788a0cc024d1a777c Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Thu, 24 Aug 2023 13:35:35 +0200 Subject: [PATCH 32/68] chore: comment on logic --- packages/contracts/contracts/LiquidationLibrary.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 2ea653b65..cd0d8fd23 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -65,6 +65,7 @@ contract LiquidationLibrary is CdpManagerStorage { uint256 _ICR = getCurrentICR(_cdpId, _price); (uint _TCR, uint systemColl, uint systemDebt) = _getTCRWithTotalCollAndDebt(_price); + // If CDP is above MCR if (_ICR >= MCR) { // We must be in RM require( @@ -72,7 +73,7 @@ contract LiquidationLibrary is CdpManagerStorage { "CdpManager: ICR is not below liquidation threshold in current mode" ); - // Grace period + // == Grace Period == // require( lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG, "Grace period not started, call `notifyBeginRM`" @@ -81,7 +82,7 @@ contract LiquidationLibrary is CdpManagerStorage { block.timestamp > lastRecoveryModeTimestamp + waitTimeFromRMTriggerToLiquidations, "Grace period yet to finish" ); - } // Implies ICR < MRC, meaning the CDP is liquidatable + } // Implicit Else Case, Implies ICR < MRC, meaning the CDP is liquidatable bool _recoveryModeAtStart = _TCR < CCR ? true : false; LocalVar_InternalLiquidate memory _liqState = LocalVar_InternalLiquidate( From 7123cb3e3eb86342595b2532cadbe7c766480cba Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Thu, 24 Aug 2023 13:54:05 +0200 Subject: [PATCH 33/68] feat: all external functions (except claiming fee split sync Grace Period) --- packages/contracts/contracts/CdpManager.sol | 6 ++++++ packages/contracts/contracts/LiquidationLibrary.sol | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/packages/contracts/contracts/CdpManager.sol b/packages/contracts/contracts/CdpManager.sol index 778b844a8..90bc155df 100644 --- a/packages/contracts/contracts/CdpManager.sol +++ b/packages/contracts/contracts/CdpManager.sol @@ -448,6 +448,11 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { totals.ETHToSendToRedeemer = totals.totalETHDrawn - totals.ETHFee; + // TODO: E part of code is here for new TCR and notification + // New coll = eth - totalETHDrawn, + // New debt = debt - totalEBTCToRedeem + // Compute TCR and then notify self + emit Redemption(_EBTCamount, totals.totalEBTCToRedeem, totals.totalETHDrawn, totals.ETHFee); // Burn the total eBTC that is redeemed @@ -464,6 +469,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { // TODO: an alternative is we could track a variable on the activePool and avoid the transfer, for claim at-will be feeRecipient // Then we can avoid the whole feeRecipient contract in every other contract. It can then be governable and switched out. ActivePool can handle sending any extra metadata to the recipient + checkLiquidateCoolDownAndReset(); // TODO: Else you would do it here in the I part since all totals have accrued now } // --- Helper functions --- diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index cd0d8fd23..a06ab5ed6 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -518,6 +518,11 @@ contract LiquidationLibrary is CdpManagerStorage { emit Liquidation(totalDebtToBurn, totalColToSend, totalColReward); + // E: Total Debt = Debt - TotalDebtToBurn + // E: Total Coll = Coll - totalColToSend + // From here we can determine if we're in RM or not + + // redistribute debt if any if (totalDebtToRedistribute > 0) { _redistributeDebt(totalDebtToRedistribute); @@ -531,6 +536,8 @@ contract LiquidationLibrary is CdpManagerStorage { // CEI: ensure sending back collateral to liquidator is last thing to do activePool.sendStEthCollAndLiquidatorReward(msg.sender, totalColToSend, totalColReward); + + // checkLiquidateCoolDownAndReset(); NOTE: If you place this here, you may as well place it in the external functions in CdpManager } // Function that calculates the amount of collateral to send to liquidator (plus incentive) and the amount of collateral surplus From d7c69c510eaae83942ce1dfdc2ecb3baf8ab687d Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Thu, 24 Aug 2023 14:25:29 +0200 Subject: [PATCH 34/68] feat: `syncPendingGlobalState` externally --- packages/contracts/contracts/ActivePool.sol | 10 +++++----- packages/contracts/contracts/CdpManager.sol | 12 ++++++------ .../contracts/contracts/CdpManagerStorage.sol | 17 +++++++++++++++-- .../contracts/Interfaces/ICdpManagerData.sol | 3 ++- .../contracts/contracts/LiquidationLibrary.sol | 4 ++-- .../CdpManager.StakingSplitFee.t.sol | 2 +- .../contracts/foundry_test/WhaleSniper.t.sol | 8 ++++---- .../test/CdpManager_StakingSplitFee_Test.js | 12 ++++++------ 8 files changed, 41 insertions(+), 27 deletions(-) diff --git a/packages/contracts/contracts/ActivePool.sol b/packages/contracts/contracts/ActivePool.sol index 8e600d470..bdb55c215 100644 --- a/packages/contracts/contracts/ActivePool.sol +++ b/packages/contracts/contracts/ActivePool.sol @@ -346,7 +346,7 @@ contract ActivePool is IActivePool, ERC3156FlashLender, ReentrancyGuard, BaseMat uint256 _FeeRecipientColl = FeeRecipientColl; require(_FeeRecipientColl >= _shares, "ActivePool: Insufficient fee recipient coll"); - ICdpManagerData(cdpManagerAddress).applyPendingGlobalState(); + ICdpManagerData(cdpManagerAddress).syncPendingGlobalState(); unchecked { _FeeRecipientColl -= _shares; @@ -368,7 +368,7 @@ contract ActivePool is IActivePool, ERC3156FlashLender, ReentrancyGuard, BaseMat uint256 balance = IERC20(token).balanceOf(address(this)); require(amount <= balance, "ActivePool: Attempt to sweep more than balance"); - ICdpManagerData(cdpManagerAddress).applyPendingGlobalState(); + ICdpManagerData(cdpManagerAddress).syncPendingGlobalState(); address cachedFeeRecipientAddress = feeRecipientAddress; // Saves an SLOAD @@ -383,7 +383,7 @@ contract ActivePool is IActivePool, ERC3156FlashLender, ReentrancyGuard, BaseMat "ActivePool: Cannot set fee recipient to zero address" ); - ICdpManagerData(cdpManagerAddress).applyPendingGlobalState(); + ICdpManagerData(cdpManagerAddress).syncPendingGlobalState(); feeRecipientAddress = _feeRecipientAddress; emit FeeRecipientAddressChanged(_feeRecipientAddress); @@ -392,7 +392,7 @@ contract ActivePool is IActivePool, ERC3156FlashLender, ReentrancyGuard, BaseMat function setFeeBps(uint _newFee) external requiresAuth { require(_newFee <= MAX_FEE_BPS, "ERC3156FlashLender: _newFee should <= MAX_FEE_BPS"); - ICdpManagerData(cdpManagerAddress).applyPendingGlobalState(); + ICdpManagerData(cdpManagerAddress).syncPendingGlobalState(); // set new flash fee uint _oldFee = feeBps; @@ -401,7 +401,7 @@ contract ActivePool is IActivePool, ERC3156FlashLender, ReentrancyGuard, BaseMat } function setFlashLoansPaused(bool _paused) external requiresAuth { - ICdpManagerData(cdpManagerAddress).applyPendingGlobalState(); + ICdpManagerData(cdpManagerAddress).syncPendingGlobalState(); flashLoansPaused = _paused; emit FlashLoansPaused(msg.sender, _paused); diff --git a/packages/contracts/contracts/CdpManager.sol b/packages/contracts/contracts/CdpManager.sol index 90bc155df..266319d2e 100644 --- a/packages/contracts/contracts/CdpManager.sol +++ b/packages/contracts/contracts/CdpManager.sol @@ -342,7 +342,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { _requireValidMaxFeePercentage(_maxFeePercentage); _requireAfterBootstrapPeriod(); - applyPendingGlobalState(); + _applyPendingGlobalState(); // Apply state, we will checkLiquidateCoolDownAndReset at end of function totals.price = priceFeed.fetchPrice(); _requireTCRoverMCR(totals.price); @@ -747,7 +747,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { "CDPManager: new staking reward split exceeds max" ); - applyPendingGlobalState(); + syncPendingGlobalState(); stakingRewardSplit = _stakingRewardSplit; emit StakingRewardSplitSet(_stakingRewardSplit); @@ -763,7 +763,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { "CDPManager: new redemption fee floor is higher than maximum" ); - applyPendingGlobalState(); + syncPendingGlobalState(); redemptionFeeFloor = _redemptionFeeFloor; emit RedemptionFeeFloorSet(_redemptionFeeFloor); @@ -779,7 +779,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { "CDPManager: new minute decay factor out of range" ); - applyPendingGlobalState(); + syncPendingGlobalState(); // decay first according to previous factor _decayBaseRate(); @@ -790,7 +790,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { } function setBeta(uint _beta) external requiresAuth { - applyPendingGlobalState(); + syncPendingGlobalState(); _decayBaseRate(); @@ -799,7 +799,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { } function setRedemptionsPaused(bool _paused) external requiresAuth { - applyPendingGlobalState(); + syncPendingGlobalState(); _decayBaseRate(); redemptionsPaused = _paused; diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 75d648c14..530cf8100 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -438,7 +438,13 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut // Claim split fee if there is staking-reward coming // and update global index & fee-per-unit variables - function applyPendingGlobalState() public { + /// @dev BO can call this without trigggering a + function applyPendingGlobalState() external { + _requireCallerIsBorrowerOperations(); + _applyPendingGlobalState(); + } + + function _applyPendingGlobalState() internal { (uint _oldIndex, uint _newIndex) = _syncIndex(); if (_newIndex > _oldIndex && totalStakes > 0) { (uint _feeTaken, uint _deltaFeePerUnit, uint _perUnitError) = calcFeeUponStakingReward( @@ -450,6 +456,13 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut } } + /// @notice Claim Fee Split, toggles Grace Period accordingly + /// @notice Call this if you want to accrue feeSplit + function syncPendingGlobalState() public { + _applyPendingGlobalState(); // Apply // Could trigger RM + checkLiquidateCoolDownAndReset(); // Synch Grace Period + } + // Update the global index via collateral token function _syncIndex() internal returns (uint, uint) { uint _oldIndex = stFPPSg; @@ -511,7 +524,7 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut // whenever there is a CDP modification operation, // such as opening, closing, adding collateral, repaying debt, or liquidating // OR Should we utilize some bot-keeper to work the routine job at fixed interval? - applyPendingGlobalState(); + _applyPendingGlobalState(); uint _oldPerUnitCdp = stFeePerUnitcdp[_cdpId]; uint _stFeePerUnitg = stFeePerUnitg; diff --git a/packages/contracts/contracts/Interfaces/ICdpManagerData.sol b/packages/contracts/contracts/Interfaces/ICdpManagerData.sol index a326f2a41..29e457f9f 100644 --- a/packages/contracts/contracts/Interfaces/ICdpManagerData.sol +++ b/packages/contracts/contracts/Interfaces/ICdpManagerData.sol @@ -223,7 +223,8 @@ interface ICdpManagerData { uint256 _prevIndex ) external view returns (uint256, uint256, uint256); - function applyPendingGlobalState() external; + function applyPendingGlobalState() external; // Accrues StEthFeeSplit without influencing Grace Period + function syncPendingGlobalState() external; // Accrues StEthFeeSplit and influences Grace Period function getAccumulatedFeeSplitApplied( bytes32 _cdpId, diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index a06ab5ed6..be4f85885 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -589,7 +589,7 @@ contract LiquidationLibrary is CdpManagerStorage { LiquidationTotals memory totals; // taking fee to avoid accounted for the calculation of the TCR - applyPendingGlobalState(); + _applyPendingGlobalState(); vars.price = priceFeed.fetchPrice(); (uint _TCR, uint systemColl, uint systemDebt) = _getTCRWithTotalCollAndDebt(vars.price); @@ -710,7 +710,7 @@ contract LiquidationLibrary is CdpManagerStorage { LiquidationTotals memory totals; // taking fee to avoid accounted for the calculation of the TCR - applyPendingGlobalState(); + _applyPendingGlobalState(); vars.price = priceFeed.fetchPrice(); (uint _TCR, uint systemColl, uint systemDebt) = _getTCRWithTotalCollAndDebt(vars.price); diff --git a/packages/contracts/foundry_test/CdpManager.StakingSplitFee.t.sol b/packages/contracts/foundry_test/CdpManager.StakingSplitFee.t.sol index 7e85e31a2..8b3ee0faf 100644 --- a/packages/contracts/foundry_test/CdpManager.StakingSplitFee.t.sol +++ b/packages/contracts/foundry_test/CdpManager.StakingSplitFee.t.sol @@ -115,7 +115,7 @@ contract CdpManagerLiquidationTest is eBTCBaseInvariants { uint _feeBalBefore = collateral.balanceOf(splitFeeRecipient); uint _feeInternalAccountingBefore = activePool.getFeeRecipientClaimableColl(); - cdpManager.applyPendingGlobalState(); + cdpManager.syncPendingGlobalState(); uint _totalCollAfter = cdpManager.getEntireSystemColl(); uint _collateralTokensInActivePoolAfter = collateral.balanceOf(address(activePool)); diff --git a/packages/contracts/foundry_test/WhaleSniper.t.sol b/packages/contracts/foundry_test/WhaleSniper.t.sol index f34434f51..0fd72ed74 100644 --- a/packages/contracts/foundry_test/WhaleSniper.t.sol +++ b/packages/contracts/foundry_test/WhaleSniper.t.sol @@ -56,7 +56,7 @@ contract WhaleSniperPOCTest is eBTCBaseFixture { console.log("tcr b4", tcr); // And show that the TCR goes down once you claim - cdpManager.applyPendingGlobalState(); + cdpManager.syncPendingGlobalState(); uint256 tcrAfter = cdpManager.getTCR(_curPrice); console.log("tcrAfter", tcrAfter); @@ -135,7 +135,7 @@ contract WhaleSniperPOCTest is eBTCBaseFixture { // hack manipulation to sync global index in attacker's benefit uint _oldIdx = _newIndex - _requiredDeltaIdxTriggeRM - 1234567890; collateral.setEthPerShare(_oldIdx); - cdpManager.applyPendingGlobalState(); + cdpManager.syncPendingGlobalState(); console.log("_oldIndex:", cdpManager.stFPPSg()); assertEq(_oldIdx, cdpManager.stFPPSg()); assertLt(_oldIdx, _curIndex); @@ -156,7 +156,7 @@ contract WhaleSniperPOCTest is eBTCBaseFixture { } // Now we take the split - cdpManager.applyPendingGlobalState(); + cdpManager.syncPendingGlobalState(); uint256 tcrAfter = cdpManager.getTCR(_curPrice); console.log("tcrAfter claim", tcrAfter); @@ -213,7 +213,7 @@ contract WhaleSniperPOCTest is eBTCBaseFixture { console.log("tcrAfterOpen Attacker", cdpManager.getTCR(_curPrice)); // Now we take the split - cdpManager.applyPendingGlobalState(); + cdpManager.syncPendingGlobalState(); uint256 tcrAfter = cdpManager.getTCR(_curPrice); console.log("tcrAfter claim", tcrAfter); diff --git a/packages/contracts/test/CdpManager_StakingSplitFee_Test.js b/packages/contracts/test/CdpManager_StakingSplitFee_Test.js index cc5c60851..13c9081ec 100644 --- a/packages/contracts/test/CdpManager_StakingSplitFee_Test.js +++ b/packages/contracts/test/CdpManager_StakingSplitFee_Test.js @@ -110,7 +110,7 @@ contract('CdpManager - Simple Liquidation with external liquidators', async acco let _expectedFee = _fees[0].mul(_newIndex).div(mv._1e18BN); let _feeBalBefore = await activePool.getFeeRecipientClaimableColl(); - await cdpManager.applyPendingGlobalState(); + await cdpManager.syncPendingGlobalState(); let _feeBalAfter = await activePool.getFeeRecipientClaimableColl(); th.assertIsApproximatelyEqual(_feeBalAfter.sub(_feeBalBefore), _fees[0]); @@ -133,13 +133,13 @@ contract('CdpManager - Simple Liquidation with external liquidators', async acco assert.isTrue(toBN(_icrAfter.toString()).gt(toBN(_icrBefore.toString()))); assert.isTrue(toBN(_tcrAfter.toString()).gt(toBN(_tcrBefore.toString()))); - // ensure applyPendingGlobalState() could be called any time + // ensure syncPendingGlobalState() could be called any time let _loop = 10; for(let i = 0;i < _loop;i++){ _newIndex = _newIndex.add(_deltaIndex.div(toBN("10"))); await collToken.setEthPerShare(_newIndex); let _newBalClaimable = await activePool.getFeeRecipientClaimableColl(); - await cdpManager.applyPendingGlobalState(); + await cdpManager.syncPendingGlobalState(); assert.isTrue(_newBalClaimable.lt(await activePool.getFeeRecipientClaimableColl())); assert.isTrue(_newIndex.eq(await cdpManager.stFPPSg())); } @@ -199,7 +199,7 @@ contract('CdpManager - Simple Liquidation with external liquidators', async acco let _expectedFeeShare = _fees[0]; let _expectedFee = _expectedFeeShare.mul(_newIndex).div(mv._1e18BN); let _feeBalBefore = await activePool.getFeeRecipientClaimableColl(); - await cdpManager.applyPendingGlobalState(); + await cdpManager.syncPendingGlobalState(); let _feeBalAfter = await activePool.getFeeRecipientClaimableColl(); let _actualFee = _feeBalAfter.sub(_feeBalBefore); @@ -337,7 +337,7 @@ contract('CdpManager - Simple Liquidation with external liquidators', async acco let _expectedFee = _expectedFeeShare.mul(_newIndex).div(mv._1e18BN); let _feeBalBefore = await activePool.getFeeRecipientClaimableColl(); - await cdpManager.applyPendingGlobalState(); + await cdpManager.syncPendingGlobalState(); let _feeBalAfter = await activePool.getFeeRecipientClaimableColl(); let _stFeePerUnitg = await cdpManager.stFeePerUnitg(); @@ -425,7 +425,7 @@ contract('CdpManager - Simple Liquidation with external liquidators', async acco await collToken.setEthPerShare(_newIndex); // claim fee - await cdpManager.applyPendingGlobalState(); + await cdpManager.syncPendingGlobalState(); // final check _cdpDebtColl = await cdpManager.getEntireDebtAndColl(_cdpId); From bb3f7514194be73e47c6f13bc76db722e536874d Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Thu, 24 Aug 2023 14:27:28 +0200 Subject: [PATCH 35/68] chore: prettier --- packages/contracts/contracts/CdpManager.sol | 2 +- .../contracts/contracts/CdpManagerStorage.sol | 4 +-- .../contracts/Interfaces/ICdpManagerData.sol | 1 + .../contracts/LiquidationLibrary.sol | 1 - .../contracts/foundry_test/GracePeriod.t.sol | 30 +++++++++---------- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/contracts/contracts/CdpManager.sol b/packages/contracts/contracts/CdpManager.sol index 266319d2e..2118b65f3 100644 --- a/packages/contracts/contracts/CdpManager.sol +++ b/packages/contracts/contracts/CdpManager.sol @@ -449,7 +449,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { totals.ETHToSendToRedeemer = totals.totalETHDrawn - totals.ETHFee; // TODO: E part of code is here for new TCR and notification - // New coll = eth - totalETHDrawn, + // New coll = eth - totalETHDrawn, // New debt = debt - totalEBTCToRedeem // Compute TCR and then notify self diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 530cf8100..804e114b4 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -438,7 +438,7 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut // Claim split fee if there is staking-reward coming // and update global index & fee-per-unit variables - /// @dev BO can call this without trigggering a + /// @dev BO can call this without trigggering a function applyPendingGlobalState() external { _requireCallerIsBorrowerOperations(); _applyPendingGlobalState(); @@ -460,7 +460,7 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut /// @notice Call this if you want to accrue feeSplit function syncPendingGlobalState() public { _applyPendingGlobalState(); // Apply // Could trigger RM - checkLiquidateCoolDownAndReset(); // Synch Grace Period + checkLiquidateCoolDownAndReset(); // Synch Grace Period } // Update the global index via collateral token diff --git a/packages/contracts/contracts/Interfaces/ICdpManagerData.sol b/packages/contracts/contracts/Interfaces/ICdpManagerData.sol index 29e457f9f..3da89ae8e 100644 --- a/packages/contracts/contracts/Interfaces/ICdpManagerData.sol +++ b/packages/contracts/contracts/Interfaces/ICdpManagerData.sol @@ -224,6 +224,7 @@ interface ICdpManagerData { ) external view returns (uint256, uint256, uint256); function applyPendingGlobalState() external; // Accrues StEthFeeSplit without influencing Grace Period + function syncPendingGlobalState() external; // Accrues StEthFeeSplit and influences Grace Period function getAccumulatedFeeSplitApplied( diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index be4f85885..789dd8d4e 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -522,7 +522,6 @@ contract LiquidationLibrary is CdpManagerStorage { // E: Total Coll = Coll - totalColToSend // From here we can determine if we're in RM or not - // redistribute debt if any if (totalDebtToRedistribute > 0) { _redistributeDebt(totalDebtToRedistribute); diff --git a/packages/contracts/foundry_test/GracePeriod.t.sol b/packages/contracts/foundry_test/GracePeriod.t.sol index d24d83d37..994cb9222 100644 --- a/packages/contracts/foundry_test/GracePeriod.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.t.sol @@ -55,7 +55,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint256 coll2 = _utils.calculateCollAmount(debt2, _curPrice, 1.15e18); // Fairly risky cdps[1 + i] = _openTestCDP(users[1], coll2, debt2); } - + // Move past bootstrap phase to allow redemptions vm.warp(cdpManager.getDeploymentStartTime() + cdpManager.BOOTSTRAP_PERIOD()); @@ -70,7 +70,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint256 debt2 = 2e18; uint256 coll2 = _utils.calculateCollAmount(debt2, _curPrice, 1.105e18); // Extremely Risky bytes32 cdp = _openTestCDP(users[0], coll2, debt2); - + // Move past bootstrap phase to allow redemptions vm.warp(cdpManager.getDeploymentStartTime() + cdpManager.BOOTSTRAP_PERIOD()); @@ -171,18 +171,13 @@ contract GracePeriodBaseTests is eBTCBaseFixture { _checkLiquidationsFSMForRmCdps(rmLiquidatableCdps); // Verify rest of behaviour is consistent with Grace Period } - /// Verify that if the Grace Period is not started, true liquidations still happen /// Verify that if the Grace Period is started, true liquidations still happen /// Verify that if the Grace Period is finished, true liquidations still happen - - /// Verify Grace Period Synching applies to all external functions - - - /// Claim Fee Split prob doesn't + /// Claim Fee Split prob doesn't /// @dev Verifies liquidations wrt Grace Period and Cdps that can be always be liquidated function _checkLiquidationsForDegen(bytes32 cdp) internal { @@ -250,14 +245,18 @@ contract GracePeriodBaseTests is eBTCBaseFixture { /// @dev Enumerate variants of ways the grace period could be reset /// @dev "Valid" actions are actions that can trigger grace period and also keep the system in recovery mode - function test_GracePeriodResetWhenRecoveryModeExitedViaAction(uint8 priceDecreaseAction, uint8 validA) public { + function test_GracePeriodResetWhenRecoveryModeExitedViaAction( + uint8 priceDecreaseAction, + uint8 validA + ) public { // setup: create Cdps, enter RM via price change or rebase } function _execValidRMAction(bytes32[] memory cdps, uint256 action) internal { address borrower = sortedCdps.getOwnerAddress(cdps[0]); uint256 price = priceFeedMock.fetchPrice(); - if (action == 0) { // openCdp + if (action == 0) { + // openCdp uint256 debt = 2e18; uint256 coll = _utils.calculateCollAmount(debt, price, 1.3 ether); @@ -268,7 +267,8 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint256 TCR = cdpManager.getTCR(price); assertLt(TCR, 1.25e18, "!RM"); - } else if (action == 1) { // adjustCdp: addColl + } else if (action == 1) { + // adjustCdp: addColl dealCollateral(borrower, 1); vm.prank(borrower); @@ -276,7 +276,8 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint256 TCR = cdpManager.getTCR(price); assertLt(TCR, 1.25e18, "!RM"); - } else if (action == 2) { //adjustCdp: repayEBTC + } else if (action == 2) { + //adjustCdp: repayEBTC vm.prank(borrower); borrowerOperations.repayEBTC(cdps[0], 1, bytes32(0), bytes32(0)); @@ -341,17 +342,14 @@ contract GracePeriodBaseTests is eBTCBaseFixture { cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); vm.revertTo(snapshotId); - // Try liquidating a cdp via the list (1) cdpManager.liquidateCdps(1); vm.revertTo(snapshotId); console2.log("About to batchLiquidateCdps", uint256(cdp)); - console2.log("This log if batchLiquidateCdps didn't revert"); - vm.stopPrank(); } @@ -376,7 +374,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { cdpsToLiquidateBatch[0] = cdps[1]; cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); vm.revertTo(snapshotId); - + vm.stopPrank(); } } From 10e8c73658607837f0e60f060b78f072ffca4eef Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Thu, 24 Aug 2023 08:44:42 -0400 Subject: [PATCH 36/68] action tests draft --- .../contracts/contracts/CdpManagerStorage.sol | 3 +- .../contracts/foundry_test/BaseFixture.sol | 41 +++++- .../contracts/foundry_test/GracePeriod.t.sol | 136 +++++++++++++++--- .../contracts/foundry_test/utils/LogUtils.sol | 5 + .../contracts/foundry_test/utils/Strings.sol | 12 ++ 5 files changed, 175 insertions(+), 22 deletions(-) diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 804e114b4..aab3c3aa3 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -19,7 +19,8 @@ import "./Dependencies/AuthNoOwner.sol"; */ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, AuthNoOwner { // TODO: IMPROVE - uint128 constant UNSET_TIMESTAMP_FLAG = type(uint128).max; + // NOTE: No packing cause it's the last var, no need for u64 + uint128 constant public UNSET_TIMESTAMP_FLAG = type(uint128).max; // TODO: IMPROVE THIS!!! uint128 public lastRecoveryModeTimestamp = UNSET_TIMESTAMP_FLAG; // use max to signify diff --git a/packages/contracts/foundry_test/BaseFixture.sol b/packages/contracts/foundry_test/BaseFixture.sol index 8f96cab82..5f9dd91cd 100644 --- a/packages/contracts/foundry_test/BaseFixture.sol +++ b/packages/contracts/foundry_test/BaseFixture.sol @@ -18,10 +18,11 @@ import {CollateralTokenTester} from "../contracts/TestContracts/CollateralTokenT import {Governor} from "../contracts/Governor.sol"; import {EBTCDeployer} from "../contracts/EBTCDeployer.sol"; import {Utilities} from "./utils/Utilities.sol"; +import {LogUtils} from "./utils/LogUtils.sol"; import {BytecodeReader} from "./utils/BytecodeReader.sol"; import {IERC3156FlashLender} from "../contracts/Interfaces/IERC3156FlashLender.sol"; -contract eBTCBaseFixture is Test, BytecodeReader { +contract eBTCBaseFixture is Test, BytecodeReader, LogUtils { uint internal constant FEE = 5e15; // 0.5% uint256 internal constant MINIMAL_COLLATERAL_RATIO = 110e16; // MCR: 110% uint public constant CCR = 125e16; // 125% @@ -32,6 +33,7 @@ contract eBTCBaseFixture is Test, BytecodeReader { uint internal constant AMOUNT_OF_USERS = 100; uint internal constant AMOUNT_OF_CDPS = 3; uint internal DECIMAL_PRECISION = 1e18; + bytes32 constant public ZERO_ID = bytes32(0); uint internal constant MAX_BPS = 10000; @@ -442,6 +444,43 @@ contract eBTCBaseFixture is Test, BytecodeReader { assertTrue(cdpManager.rewardSnapshots(cdpId) == 0); assertTrue(cdpManager.stFeePerUnitcdp(cdpId) == 0); } + + function _printSystemState() internal { + uint price = priceFeedMock.fetchPrice(); + console.log("== Core State =="); + console.log("systemCollShares :", activePool.getStEthColl()); + console.log("systemStEthBalance :", collateral.getPooledEthByShares(activePool.getStEthColl())); + console.log("systemDebt :", activePool.getEBTCDebt()); + console.log("TCR :", cdpManager.getTCR(price)); + console.log("stEthLiveIndex :", collateral.getPooledEthByShares(DECIMAL_PRECISION)); + console.log("stEthGlobalIndex :", cdpManager.stFPPSg()); + console.log("price :", price); + } + + function _getICR(bytes32 cdpId) internal returns (uint) { + uint price = priceFeedMock.fetchPrice(); + return cdpManager.getCurrentICR(cdpId, price); + } + + function _printAllCdps() internal { + uint price = priceFeedMock.fetchPrice(); + uint numCdps = sortedCdps.getSize(); + bytes32 node = sortedCdps.getLast(); + address borrower = sortedCdps.getOwnerAddress(node); + + while (borrower != address(0)) { + console.log("=== ", bytes32ToString(node)); + console.log("debt (realized) :", cdpManager.getCdpDebt(node)); + console.log("collShares (realized) :", cdpManager.getCdpColl(node)); + console.log("ICR :", cdpManager.getCurrentICR(node, price)); + console.log("Percent of System :", cdpManager.getCdpColl(node) * DECIMAL_PRECISION / activePool.getStEthColl()); + console.log(""); + + node = sortedCdps.getPrev(node); + borrower = sortedCdps.getOwnerAddress(node); + } + } + /// @dev Ensure a given CdpId is not in the Sorted Cdps LL. /// @dev a Cdp should only be present in the LL when it is active. diff --git a/packages/contracts/foundry_test/GracePeriod.t.sol b/packages/contracts/foundry_test/GracePeriod.t.sol index 994cb9222..2e21a9980 100644 --- a/packages/contracts/foundry_test/GracePeriod.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.t.sol @@ -34,7 +34,15 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // RM untriggered via User Operations // All operations where the system goes off of RM should cancel the countdown - /// @dev Setup function to ensure we liquidate the correct amount of CDPs + /** + @dev Setup function to ensure we liquidate the correct amount of CDPs + + Current use: + - 1 healthy Cdps at 130% (1000 scale) + - 5 unhealthy Cdps at 115% (2 scale each) + + + */ function _openCdps(uint256 numberOfCdpsAtRisk) internal returns (bytes32[] memory) { address payable[] memory users; users = _utils.createUsers(2); @@ -56,6 +64,9 @@ contract GracePeriodBaseTests is eBTCBaseFixture { cdps[1 + i] = _openTestCDP(users[1], coll2, debt2); } + uint TCR = cdpManager.getTCR(_curPrice); + assertGt(TCR, CCR); + // Move past bootstrap phase to allow redemptions vm.warp(cdpManager.getDeploymentStartTime() + cdpManager.BOOTSTRAP_PERIOD()); @@ -116,6 +127,8 @@ contract GracePeriodBaseTests is eBTCBaseFixture { priceFeedMock.setPrice((priceFeedMock.getPrice() * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM uint256 reducedPrice = priceFeedMock.getPrice(); + _printSystemState(); + // Check if we are in RM uint256 TCR = cdpManager.getTCR(reducedPrice); assertLt(TCR, 1.25e18, "!RM"); @@ -133,6 +146,8 @@ contract GracePeriodBaseTests is eBTCBaseFixture { collateral.setEthPerShare((collateral.getSharesByPooledEth(1e18) * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM uint256 price = priceFeedMock.getPrice(); + _printSystemState(); + // Check if we are in RM uint256 TCR = cdpManager.getTCR(price); assertLt(TCR, 1.25e18, "!RM"); @@ -222,7 +237,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { */ function test_GracePeriodViaValidAction(uint8 priceDecreaseAction, uint8 action) public { vm.assume(priceDecreaseAction <= 1); - vm.assume(action <= 2); + vm.assume(action <= 3); // setup: create Cdps, enter RM via price change or rebase bytes32[] memory cdps = _openCdps(5); @@ -245,11 +260,27 @@ contract GracePeriodBaseTests is eBTCBaseFixture { /// @dev Enumerate variants of ways the grace period could be reset /// @dev "Valid" actions are actions that can trigger grace period and also keep the system in recovery mode - function test_GracePeriodResetWhenRecoveryModeExitedViaAction( - uint8 priceDecreaseAction, - uint8 validA - ) public { + function test_GracePeriodResetWhenRecoveryModeExitedViaAction(uint8 priceDecreaseAction, uint8 action) public { + // setup: create Cdps, enter RM via price change or rebase + vm.assume(priceDecreaseAction <= 1); + vm.assume(action <= 3); + // setup: create Cdps, enter RM via price change or rebase + bytes32[] memory cdps = _openCdps(5); + assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) + + _execPriceDecreaseAction(priceDecreaseAction); + uint256 price = priceFeedMock.getPrice(); + + // Check if we are in RM + uint256 TCR = cdpManager.getTCR(price); + assertLt(TCR, 1.25e18, "!RM"); + + _assertRevertOnAllLiquidations(cdps); + + _execExitRMAction(cdps, action); + + _postExitRMLiquidationChecks(cdps); } function _execValidRMAction(bytes32[] memory cdps, uint256 action) internal { @@ -263,27 +294,65 @@ contract GracePeriodBaseTests is eBTCBaseFixture { dealCollateral(borrower, coll); vm.prank(borrower); - borrowerOperations.openCdp(coll, bytes32(0), bytes32(0), debt); + borrowerOperations.openCdp(debt, ZERO_ID, ZERO_ID, coll); - uint256 TCR = cdpManager.getTCR(price); - assertLt(TCR, 1.25e18, "!RM"); - } else if (action == 1) { - // adjustCdp: addColl + } else if (action == 1) { // adjustCdp: addColl dealCollateral(borrower, 1); vm.prank(borrower); - borrowerOperations.addColl(cdps[0], bytes32(0), bytes32(0), 1); + borrowerOperations.addColl(cdps[0], ZERO_ID, ZERO_ID, 1); - uint256 TCR = cdpManager.getTCR(price); - assertLt(TCR, 1.25e18, "!RM"); - } else if (action == 2) { - //adjustCdp: repayEBTC + } else if (action == 2) { //adjustCdp: repayEBTC vm.prank(borrower); - borrowerOperations.repayEBTC(cdps[0], 1, bytes32(0), bytes32(0)); + borrowerOperations.repayEBTC(cdps[0], 1, ZERO_ID, ZERO_ID); - uint256 TCR = cdpManager.getTCR(price); - assertLt(TCR, 1.25e18, "!RM"); + } else if (action == 3) { //redemption + vm.prank(borrower); + cdpManager.redeemCollateral(1e18, ZERO_ID, ZERO_ID, ZERO_ID, 1e18, 0, 1e18); } + + uint256 TCR = cdpManager.getTCR(price); + assertLt(TCR, 1.25e18, "!RM"); + } + + function _execExitRMAction(bytes32[] memory cdps, uint256 action) internal { + address borrower = sortedCdps.getOwnerAddress(cdps[0]); + uint256 price = priceFeedMock.fetchPrice(); + + // Debt and coll values to push us out of RM + uint256 debt = 1e18; + uint256 coll = _utils.calculateCollAmount(debt, price, 1.3 ether) * 100000; + if (action == 0) { // openCdp + dealCollateral(borrower, coll); + + vm.prank(borrower); + borrowerOperations.openCdp(debt, ZERO_ID, ZERO_ID, coll); + + } else if (action == 1) { // adjustCdp: addColl (increase coll) + dealCollateral(borrower, coll); + + vm.prank(borrower); + borrowerOperations.addColl(cdps[0], ZERO_ID, ZERO_ID, coll); + + } else if (action == 2) { //adjustCdp: withdrawEBTC (reduce debt) + debt = cdpManager.getCdpDebt(cdps[0]); + console.log(debt); + + vm.prank(borrower); + borrowerOperations.repayEBTC(cdps[0], debt - 1, ZERO_ID, ZERO_ID); + + } else if (action == 3) { //adjustCdp: adjustCdpWithColl (reduce debt + increase coll) + debt = cdpManager.getCdpDebt(cdps[0]); + dealCollateral(borrower, coll); + + vm.prank(borrower); + borrowerOperations.adjustCdpWithColl(cdps[0], 0, debt - 1, false, ZERO_ID, ZERO_ID, coll); + } + + uint256 TCR = cdpManager.getTCR(price); + console.log(TCR); + console.log(1.25e18); + assertGt(TCR, 1.25e18, "!RM"); } function _execPriceDecreaseAction(uint8 priceDecreaseAction) internal { @@ -295,14 +364,41 @@ contract GracePeriodBaseTests is eBTCBaseFixture { } } + /// @dev Run these checks immediately after action that sets grace period function _postValidActionLiquidationChecks(bytes32[] memory cdps) internal { + // Grace period timestamp is now + uint recoveryModeSetTimestamp = block.timestamp; + assertEq(cdpManager.lastRecoveryModeTimestamp(), block.timestamp, "lastRecoveryModeTimestamp set time"); + // Liquidations still revert _assertRevertOnAllLiquidations(cdps); - // Grace Period Ended, liquidations work + // Grace Period Ended vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + + // Grace period timestamp hasn't changed + assertEq(cdpManager.lastRecoveryModeTimestamp(), recoveryModeSetTimestamp, "lastRecoveryModeTimestamp set time"); + + // Liquidations work _assertAllLiquidationSuccess(cdps); } + + function _postExitRMLiquidationChecks(bytes32[] memory cdps) internal { + // Grace period timestamp is now + assertEq(cdpManager.lastRecoveryModeTimestamp(), cdpManager.UNSET_TIMESTAMP_FLAG(), "lastRecoveryModeTimestamp unset"); + + // Liquidations still revert + _assertRevertOnAllLiquidations(cdps); + + // Grace Period Ended + vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + + // Grace period timestamp hasn't changed + assertEq(cdpManager.lastRecoveryModeTimestamp(), cdpManager.UNSET_TIMESTAMP_FLAG(), "lastRecoveryModeTimestamp unset"); + + // Only liquidations valid under normal work + _assertRevertOnAllLiquidations(cdps); + } function _assertRevertOnAllLiquidations(bytes32[] memory cdps) internal { // Try liquidating a cdp diff --git a/packages/contracts/foundry_test/utils/LogUtils.sol b/packages/contracts/foundry_test/utils/LogUtils.sol index c377c66e3..c05395479 100644 --- a/packages/contracts/foundry_test/utils/LogUtils.sol +++ b/packages/contracts/foundry_test/utils/LogUtils.sol @@ -4,6 +4,7 @@ import "./Strings.sol"; contract LogUtils { using Strings for uint256; + using Strings for bytes32; enum GlueType { None, @@ -47,4 +48,8 @@ contract LogUtils { return result; } + + function bytes32ToString(bytes32 _bytes32) public pure returns (string memory) { + return _bytes32.bytes32ToString(); + } } diff --git a/packages/contracts/foundry_test/utils/Strings.sol b/packages/contracts/foundry_test/utils/Strings.sol index a260c2647..4f1520693 100644 --- a/packages/contracts/foundry_test/utils/Strings.sol +++ b/packages/contracts/foundry_test/utils/Strings.sol @@ -31,4 +31,16 @@ library Strings { } return string(buffer); } + + function bytes32ToString(bytes32 _bytes) public pure returns (string memory) { + bytes memory charset = "0123456789abcdef"; + bytes memory result = new bytes(64); // as each byte will be represented by 2 chars in hex + + for (uint256 i = 0; i < 32; i++) { + result[i*2] = charset[uint8(_bytes[i] >> 4)]; + result[i*2 + 1] = charset[uint8(_bytes[i] & 0x0F)]; + } + + return string(result); + } } From 159e8aaf39e159ca32a1aa8d519894e8c0332507 Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Thu, 24 Aug 2023 08:52:23 -0400 Subject: [PATCH 37/68] Actions that exit RM and reset flag after flag set --- .../contracts/contracts/CdpManagerStorage.sol | 2 +- .../contracts/foundry_test/BaseFixture.sol | 17 ++- .../contracts/foundry_test/GracePeriod.t.sol | 105 ++++++++++++++---- .../contracts/foundry_test/utils/Strings.sol | 8 +- 4 files changed, 97 insertions(+), 35 deletions(-) diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index aab3c3aa3..85b5f19f6 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -20,7 +20,7 @@ import "./Dependencies/AuthNoOwner.sol"; contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, AuthNoOwner { // TODO: IMPROVE // NOTE: No packing cause it's the last var, no need for u64 - uint128 constant public UNSET_TIMESTAMP_FLAG = type(uint128).max; + uint128 public constant UNSET_TIMESTAMP_FLAG = type(uint128).max; // TODO: IMPROVE THIS!!! uint128 public lastRecoveryModeTimestamp = UNSET_TIMESTAMP_FLAG; // use max to signify diff --git a/packages/contracts/foundry_test/BaseFixture.sol b/packages/contracts/foundry_test/BaseFixture.sol index 5f9dd91cd..bc5857bf0 100644 --- a/packages/contracts/foundry_test/BaseFixture.sol +++ b/packages/contracts/foundry_test/BaseFixture.sol @@ -33,7 +33,7 @@ contract eBTCBaseFixture is Test, BytecodeReader, LogUtils { uint internal constant AMOUNT_OF_USERS = 100; uint internal constant AMOUNT_OF_CDPS = 3; uint internal DECIMAL_PRECISION = 1e18; - bytes32 constant public ZERO_ID = bytes32(0); + bytes32 public constant ZERO_ID = bytes32(0); uint internal constant MAX_BPS = 10000; @@ -444,12 +444,15 @@ contract eBTCBaseFixture is Test, BytecodeReader, LogUtils { assertTrue(cdpManager.rewardSnapshots(cdpId) == 0); assertTrue(cdpManager.stFeePerUnitcdp(cdpId) == 0); } - + function _printSystemState() internal { uint price = priceFeedMock.fetchPrice(); console.log("== Core State =="); console.log("systemCollShares :", activePool.getStEthColl()); - console.log("systemStEthBalance :", collateral.getPooledEthByShares(activePool.getStEthColl())); + console.log( + "systemStEthBalance :", + collateral.getPooledEthByShares(activePool.getStEthColl()) + ); console.log("systemDebt :", activePool.getEBTCDebt()); console.log("TCR :", cdpManager.getTCR(price)); console.log("stEthLiveIndex :", collateral.getPooledEthByShares(DECIMAL_PRECISION)); @@ -468,12 +471,15 @@ contract eBTCBaseFixture is Test, BytecodeReader, LogUtils { bytes32 node = sortedCdps.getLast(); address borrower = sortedCdps.getOwnerAddress(node); - while (borrower != address(0)) { + while (borrower != address(0)) { console.log("=== ", bytes32ToString(node)); console.log("debt (realized) :", cdpManager.getCdpDebt(node)); console.log("collShares (realized) :", cdpManager.getCdpColl(node)); console.log("ICR :", cdpManager.getCurrentICR(node, price)); - console.log("Percent of System :", cdpManager.getCdpColl(node) * DECIMAL_PRECISION / activePool.getStEthColl()); + console.log( + "Percent of System :", + (cdpManager.getCdpColl(node) * DECIMAL_PRECISION) / activePool.getStEthColl() + ); console.log(""); node = sortedCdps.getPrev(node); @@ -481,7 +487,6 @@ contract eBTCBaseFixture is Test, BytecodeReader, LogUtils { } } - /// @dev Ensure a given CdpId is not in the Sorted Cdps LL. /// @dev a Cdp should only be present in the LL when it is active. function _assertCdpNotInSortedCdps(bytes32 cdpId) internal { diff --git a/packages/contracts/foundry_test/GracePeriod.t.sol b/packages/contracts/foundry_test/GracePeriod.t.sol index 2e21a9980..9fb9e765f 100644 --- a/packages/contracts/foundry_test/GracePeriod.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.t.sol @@ -66,7 +66,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint TCR = cdpManager.getTCR(_curPrice); assertGt(TCR, CCR); - + // Move past bootstrap phase to allow redemptions vm.warp(cdpManager.getDeploymentStartTime() + cdpManager.BOOTSTRAP_PERIOD()); @@ -260,7 +260,10 @@ contract GracePeriodBaseTests is eBTCBaseFixture { /// @dev Enumerate variants of ways the grace period could be reset /// @dev "Valid" actions are actions that can trigger grace period and also keep the system in recovery mode - function test_GracePeriodResetWhenRecoveryModeExitedViaAction(uint8 priceDecreaseAction, uint8 action) public { + function test_GracePeriodResetWhenRecoveryModeExitedViaAction_WithoutGracePeriodSet( + uint8 priceDecreaseAction, + uint8 action + ) public { // setup: create Cdps, enter RM via price change or rebase vm.assume(priceDecreaseAction <= 1); vm.assume(action <= 3); @@ -275,7 +278,36 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Check if we are in RM uint256 TCR = cdpManager.getTCR(price); assertLt(TCR, 1.25e18, "!RM"); - + + _assertRevertOnAllLiquidations(cdps); + + _execExitRMAction(cdps, action); + + _postExitRMLiquidationChecks(cdps); + } + + function test_GracePeriodResetWhenRecoveryModeExitedViaAction_WithGracePeriodSet( + uint8 priceDecreaseAction, + uint8 action + ) public { + // setup: create Cdps, enter RM via price change or rebase + vm.assume(priceDecreaseAction <= 1); + vm.assume(action <= 3); + + // setup: create Cdps, enter RM via price change or rebase + bytes32[] memory cdps = _openCdps(5); + assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) + + _execPriceDecreaseAction(priceDecreaseAction); + uint256 price = priceFeedMock.getPrice(); + + // Check if we are in RM + uint256 TCR = cdpManager.getTCR(price); + assertLt(TCR, 1.25e18, "!RM"); + + // Set grace period before action which exits RM + cdpManager.beginRMLiquidationCooldown(); + _assertRevertOnAllLiquidations(cdps); _execExitRMAction(cdps, action); @@ -295,18 +327,18 @@ contract GracePeriodBaseTests is eBTCBaseFixture { vm.prank(borrower); borrowerOperations.openCdp(debt, ZERO_ID, ZERO_ID, coll); - - } else if (action == 1) { // adjustCdp: addColl + } else if (action == 1) { + // adjustCdp: addColl dealCollateral(borrower, 1); vm.prank(borrower); borrowerOperations.addColl(cdps[0], ZERO_ID, ZERO_ID, 1); - - } else if (action == 2) { //adjustCdp: repayEBTC + } else if (action == 2) { + //adjustCdp: repayEBTC vm.prank(borrower); borrowerOperations.repayEBTC(cdps[0], 1, ZERO_ID, ZERO_ID); - - } else if (action == 3) { //redemption + } else if (action == 3) { + //redemption vm.prank(borrower); cdpManager.redeemCollateral(1e18, ZERO_ID, ZERO_ID, ZERO_ID, 1e18, 0, 1e18); } @@ -322,31 +354,40 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Debt and coll values to push us out of RM uint256 debt = 1e18; uint256 coll = _utils.calculateCollAmount(debt, price, 1.3 ether) * 100000; - if (action == 0) { // openCdp + if (action == 0) { + // openCdp dealCollateral(borrower, coll); vm.prank(borrower); borrowerOperations.openCdp(debt, ZERO_ID, ZERO_ID, coll); - - } else if (action == 1) { // adjustCdp: addColl (increase coll) + } else if (action == 1) { + // adjustCdp: addColl (increase coll) dealCollateral(borrower, coll); vm.prank(borrower); borrowerOperations.addColl(cdps[0], ZERO_ID, ZERO_ID, coll); - - } else if (action == 2) { //adjustCdp: withdrawEBTC (reduce debt) + } else if (action == 2) { + //adjustCdp: withdrawEBTC (reduce debt) debt = cdpManager.getCdpDebt(cdps[0]); console.log(debt); vm.prank(borrower); borrowerOperations.repayEBTC(cdps[0], debt - 1, ZERO_ID, ZERO_ID); - - } else if (action == 3) { //adjustCdp: adjustCdpWithColl (reduce debt + increase coll) + } else if (action == 3) { + //adjustCdp: adjustCdpWithColl (reduce debt + increase coll) debt = cdpManager.getCdpDebt(cdps[0]); dealCollateral(borrower, coll); vm.prank(borrower); - borrowerOperations.adjustCdpWithColl(cdps[0], 0, debt - 1, false, ZERO_ID, ZERO_ID, coll); + borrowerOperations.adjustCdpWithColl( + cdps[0], + 0, + debt - 1, + false, + ZERO_ID, + ZERO_ID, + coll + ); } uint256 TCR = cdpManager.getTCR(price); @@ -368,33 +409,49 @@ contract GracePeriodBaseTests is eBTCBaseFixture { function _postValidActionLiquidationChecks(bytes32[] memory cdps) internal { // Grace period timestamp is now uint recoveryModeSetTimestamp = block.timestamp; - assertEq(cdpManager.lastRecoveryModeTimestamp(), block.timestamp, "lastRecoveryModeTimestamp set time"); + assertEq( + cdpManager.lastRecoveryModeTimestamp(), + block.timestamp, + "lastRecoveryModeTimestamp set time" + ); // Liquidations still revert _assertRevertOnAllLiquidations(cdps); // Grace Period Ended vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); - + // Grace period timestamp hasn't changed - assertEq(cdpManager.lastRecoveryModeTimestamp(), recoveryModeSetTimestamp, "lastRecoveryModeTimestamp set time"); + assertEq( + cdpManager.lastRecoveryModeTimestamp(), + recoveryModeSetTimestamp, + "lastRecoveryModeTimestamp set time" + ); // Liquidations work _assertAllLiquidationSuccess(cdps); } - + function _postExitRMLiquidationChecks(bytes32[] memory cdps) internal { // Grace period timestamp is now - assertEq(cdpManager.lastRecoveryModeTimestamp(), cdpManager.UNSET_TIMESTAMP_FLAG(), "lastRecoveryModeTimestamp unset"); + assertEq( + cdpManager.lastRecoveryModeTimestamp(), + cdpManager.UNSET_TIMESTAMP_FLAG(), + "lastRecoveryModeTimestamp unset" + ); // Liquidations still revert _assertRevertOnAllLiquidations(cdps); // Grace Period Ended vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); - + // Grace period timestamp hasn't changed - assertEq(cdpManager.lastRecoveryModeTimestamp(), cdpManager.UNSET_TIMESTAMP_FLAG(), "lastRecoveryModeTimestamp unset"); + assertEq( + cdpManager.lastRecoveryModeTimestamp(), + cdpManager.UNSET_TIMESTAMP_FLAG(), + "lastRecoveryModeTimestamp unset" + ); // Only liquidations valid under normal work _assertRevertOnAllLiquidations(cdps); diff --git a/packages/contracts/foundry_test/utils/Strings.sol b/packages/contracts/foundry_test/utils/Strings.sol index 4f1520693..63358625f 100644 --- a/packages/contracts/foundry_test/utils/Strings.sol +++ b/packages/contracts/foundry_test/utils/Strings.sol @@ -32,13 +32,13 @@ library Strings { return string(buffer); } - function bytes32ToString(bytes32 _bytes) public pure returns (string memory) { + function bytes32ToString(bytes32 _bytes) public pure returns (string memory) { bytes memory charset = "0123456789abcdef"; - bytes memory result = new bytes(64); // as each byte will be represented by 2 chars in hex + bytes memory result = new bytes(64); // as each byte will be represented by 2 chars in hex for (uint256 i = 0; i < 32; i++) { - result[i*2] = charset[uint8(_bytes[i] >> 4)]; - result[i*2 + 1] = charset[uint8(_bytes[i] & 0x0F)]; + result[i * 2] = charset[uint8(_bytes[i] >> 4)]; + result[i * 2 + 1] = charset[uint8(_bytes[i] & 0x0F)]; } return string(result); From 4e4617f8e947f6f8e72684918b61bef876de9440 Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Thu, 24 Aug 2023 10:29:26 -0400 Subject: [PATCH 38/68] IRmLiquidationsChecker cleanup --- .../contracts/contracts/BorrowerOperations.sol | 15 +++++++-------- .../contracts/Interfaces/ICdpManagerData.sol | 3 ++- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/contracts/contracts/BorrowerOperations.sol b/packages/contracts/contracts/BorrowerOperations.sol index 50dcf843b..a62e0464f 100644 --- a/packages/contracts/contracts/BorrowerOperations.sol +++ b/packages/contracts/contracts/BorrowerOperations.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.17; import "./Interfaces/IBorrowerOperations.sol"; -import "./Interfaces/IRmLiquidationsChecker.sol"; import "./Interfaces/ICdpManager.sol"; import "./Interfaces/ICdpManagerData.sol"; import "./Interfaces/IEBTCToken.sol"; @@ -405,10 +404,10 @@ contract BorrowerOperations is // We check with newTCR if (newTCR < CCR) { // Notify RM - IRmLiquidationsChecker(address(cdpManager)).notifyBeginRM(); + cdpManager.notifyBeginRM(); } else { // Notify Back to Normal Mode - IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); + cdpManager.notifyEndRM(); } } else { _requireICRisAboveMCR(vars.ICR); @@ -417,7 +416,7 @@ contract BorrowerOperations is // == Grace Period == // // We are not in RM, no edge case, we always stay above RM // Always Notify Back to Normal Mode - IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); + cdpManager.notifyEndRM(); } // Set the cdp struct's properties @@ -482,7 +481,7 @@ contract BorrowerOperations is // == Grace Period == // // By definition we are not in RM, notify CDPManager to ensure "Glass is on" - IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); + cdpManager.notifyEndRM(); cdpManager.removeStake(_cdpId); @@ -664,10 +663,10 @@ contract BorrowerOperations is // We check with newTCR if (_vars.newTCR < CCR) { // Notify RM - IRmLiquidationsChecker(address(cdpManager)).notifyBeginRM(); + cdpManager.notifyBeginRM(); } else { // Notify Back to Normal Mode - IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); + cdpManager.notifyEndRM(); } } else { // if Normal Mode @@ -677,7 +676,7 @@ contract BorrowerOperations is // == Grace Period == // // We are not in RM, no edge case, we always stay above RM // Always Notify Back to Normal Mode - IRmLiquidationsChecker(address(cdpManager)).notifyEndRM(); + cdpManager.notifyEndRM(); } } diff --git a/packages/contracts/contracts/Interfaces/ICdpManagerData.sol b/packages/contracts/contracts/Interfaces/ICdpManagerData.sol index 3da89ae8e..725ad3030 100644 --- a/packages/contracts/contracts/Interfaces/ICdpManagerData.sol +++ b/packages/contracts/contracts/Interfaces/ICdpManagerData.sol @@ -6,10 +6,11 @@ import "./ICollSurplusPool.sol"; import "./IEBTCToken.sol"; import "./ISortedCdps.sol"; import "./IActivePool.sol"; +import "./IRmLiquidationsChecker.sol"; import "../Dependencies/ICollateralTokenOracle.sol"; // Common interface for the Cdp Manager. -interface ICdpManagerData { +interface ICdpManagerData is IRmLiquidationsChecker { // --- Events --- event LiquidationLibraryAddressChanged(address _liquidationLibraryAddress); From 680d8b7c511f6244053905e5d7b120e42b547c33 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Fri, 25 Aug 2023 00:57:24 +0800 Subject: [PATCH 39/68] add dedicated tests for RM cooldown switch --- .../CdpManager_RecoveryMode_Cooldown_Test.js | 121 +++++++++++++++++- 1 file changed, 119 insertions(+), 2 deletions(-) diff --git a/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js b/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js index 569ca30b9..aae7cf237 100644 --- a/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js +++ b/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js @@ -61,7 +61,7 @@ contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure d splitFeeRecipient = await LQTYContracts.feeRecipient; }) - it("Happy case: 2 CDPs with 1st Safe and 2nd Unsafe and ensure you must wait cooldown", async() => { + it("Happy case: 3 CDPs with 1st Safe and 2 other Unsafe and ensure you must wait cooldown for one of them", async() => { await openCdp({ ICR: toBN(dec(149, 16)), extraEBTCAmount: toBN(minDebt.toString()).mul(toBN("10")), extraParams: { from: alice } }) await openCdp({ ICR: toBN(dec(139, 16)), extraParams: { from: bob } }) @@ -97,11 +97,128 @@ contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure d assert.isFalse((await sortedCdps.contains(_carolCdpId))); // pass the cooldown wait - await ethers.provider.send("evm_increaseTime", [901]); + await ethers.provider.send("evm_increaseTime", [_coolDownWait.toNumber() + 1]); await ethers.provider.send("evm_mine"); await cdpManager.liquidate(_bobCdpId, {from: owner}); assert.isFalse((await sortedCdps.contains(_bobCdpId))); }) + it("openCDP() in RM: should notifyBeginRM() if RM persist or notifyEndRM() if RM exit", async() => { + + await openCdp({ ICR: toBN(dec(149, 16)), extraEBTCAmount: toBN(minDebt.toString()).mul(toBN("10")), extraParams: { from: alice } }) + let _initVal = await cdpManager.lastRecoveryModeTimestamp(); + assert.isTrue(_initVal.gt(toBN('0'))); + await openCdp({ ICR: toBN(dec(129, 16)), extraParams: { from: carol } }) + + // price drops to trigger RM + let _newPrice = dec(6000, 13); + await priceFeed.setPrice(_newPrice); + let _tcrBefore = await cdpManager.getTCR(_newPrice); + assert.isTrue(toBN(_tcrBefore.toString()).lt(_CCR)); + let _stillInitVal = await cdpManager.lastRecoveryModeTimestamp(); + assert.isTrue(_stillInitVal.eq(_initVal)); + + // trigger RM cooldown by open a new CDP + await openCdp({ ICR: _CCR.add(toBN('1234567890123456789')), extraParams: { from: bob } }) + let _bobCdpId = await sortedCdps.cdpOfOwnerByIndex(bob, 0); + let _bobICRBefore = await cdpManager.getCurrentICR(_bobCdpId, _newPrice); + let _tcrInMiddle = await cdpManager.getTCR(_newPrice); + console.log('_tcrInMiddle=' + _tcrInMiddle); + assert.isTrue(toBN(_tcrInMiddle.toString()).lt(_CCR)); + let _rmTriggerTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + assert.isTrue(_rmTriggerTimestamp.gt(toBN('0'))); + assert.isTrue(_rmTriggerTimestamp.lt(_initVal)); + + // trigger RM exit by open another new CDP + await openCdp({ ICR: toBN(dec(349, 16)), extraEBTCAmount: toBN(minDebt.toString()).mul(toBN("100")), extraParams: { from: owner } }) + let _tcrFinal = await cdpManager.getTCR(_newPrice); + console.log('_tcrFinal=' + _tcrFinal); + assert.isTrue(toBN(_tcrFinal.toString()).gt(_CCR)); + let _rmExitTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + assert.isTrue(_rmExitTimestamp.eq(_initVal)); + }) + + it("closeCDP(): should always notifyEndRM() since TCR after close is aboce CCR", async() => { + + await openCdp({ ICR: toBN(dec(155, 16)), extraEBTCAmount: toBN(minDebt.toString()).mul(toBN("10")), extraParams: { from: alice } }) + let _initVal = await cdpManager.lastRecoveryModeTimestamp(); + assert.isTrue(_initVal.gt(toBN('0'))); + await openCdp({ ICR: toBN(dec(126, 16)), extraParams: { from: carol } }) + + let _aliceCdpId = await sortedCdps.cdpOfOwnerByIndex(alice, 0); + let _carolCdpId = await sortedCdps.cdpOfOwnerByIndex(carol, 0); + + // price drops to trigger RM & cooldown + let _originalPrice = await priceFeed.getPrice(); + let _newPrice = dec(5992, 13); + await priceFeed.setPrice(_newPrice); + let _tcrBefore = await cdpManager.getTCR(_newPrice); + let _aliceICRBefore = await cdpManager.getCurrentICR(_aliceCdpId, _newPrice); + console.log('_tcrBefore=' + _tcrBefore + ',_aliceICRBefore=' + _aliceICRBefore); + assert.isTrue(toBN(_tcrBefore.toString()).lt(_CCR)); + await cdpManager.checkLiquidateCoolDownAndReset(); + let _rmTriggerTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + assert.isTrue(_rmTriggerTimestamp.gt(toBN('0'))); + assert.isTrue(_rmTriggerTimestamp.lt(_initVal)); + + // reset RM cooldown by close a CDP + await priceFeed.setPrice(_originalPrice); + await borrowerOperations.closeCdp(_carolCdpId, { from: carol } ); + assert.isFalse((await sortedCdps.contains(_carolCdpId))); + let _tcrAfter = await cdpManager.getTCR(_originalPrice); + assert.isTrue(toBN(_tcrAfter.toString()).gt(_CCR)); + let _rmExitTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + assert.isTrue(_rmExitTimestamp.eq(_initVal)); + }) + + it("adjustCDP(): should notifyBeginRM() if RM persist or notifyEndRM() if RM exit", async() => { + let _dustVal = 123456789; + await openCdp({ ICR: toBN(dec(155, 16)), extraEBTCAmount: toBN(minDebt.toString()).mul(toBN("10")), extraParams: { from: alice } }) + let _initVal = await cdpManager.lastRecoveryModeTimestamp(); + assert.isTrue(_initVal.gt(toBN('0'))); + await openCdp({ ICR: toBN(dec(126, 16)), extraParams: { from: carol } }) + + let _aliceCdpId = await sortedCdps.cdpOfOwnerByIndex(alice, 0); + let _carolCdpId = await sortedCdps.cdpOfOwnerByIndex(carol, 0); + let _carolDebt = await cdpManager.getCdpDebt(_carolCdpId); + + // price drops to trigger RM & cooldown + let _originalPrice = await priceFeed.getPrice(); + let _newPrice = dec(5992, 13); + await priceFeed.setPrice(_newPrice); + let _tcrBefore = await cdpManager.getTCR(_newPrice); + let _aliceICRBefore = await cdpManager.getCurrentICR(_aliceCdpId, _newPrice); + assert.isTrue(toBN(_tcrBefore.toString()).lt(_CCR)); + await cdpManager.checkLiquidateCoolDownAndReset(); + let _rmTriggerTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + assert.isTrue(_rmTriggerTimestamp.gt(toBN('0'))); + assert.isTrue(_rmTriggerTimestamp.lt(_initVal)); + + // reset RM cooldown by adjust a CDP in Normal Mode (withdraw more debt) + await priceFeed.setPrice(_originalPrice); + let _tcrAfter = await cdpManager.getTCR(_originalPrice); + assert.isTrue(toBN(_tcrAfter.toString()).gt(_CCR)); + await borrowerOperations.withdrawEBTC(_carolCdpId, _dustVal, _carolCdpId, _carolCdpId, { from: carol } ); + let _rmExitTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + assert.isTrue(_rmExitTimestamp.eq(_initVal)); + + // price drops to trigger RM & cooldown again by adjust CDP (add more collateral) + await priceFeed.setPrice(_newPrice); + await collToken.deposit({from : carol, value: _dustVal}); + await borrowerOperations.addColl(_carolCdpId, _carolCdpId, _carolCdpId, _dustVal, { from: carol } ); + let _adjustRmTriggerTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + assert.isTrue(_adjustRmTriggerTimestamp.gt(_rmTriggerTimestamp)); + assert.isTrue(_adjustRmTriggerTimestamp.lt(_initVal)); + + // end RM cooldown by adjust a CDP in RM (repayment) + await debtToken.approve(borrowerOperations.address, _carolDebt, {from: carol}); + await borrowerOperations.repayEBTC(_carolCdpId, _carolDebt, _carolCdpId, _carolCdpId, { from: carol } ); + let _tcrFinal = await cdpManager.getTCR(_newPrice); + assert.isTrue(toBN(_tcrFinal.toString()).gt(_CCR)); + let _rmExitFinal = await cdpManager.lastRecoveryModeTimestamp(); + assert.isTrue(_rmExitFinal.eq(_initVal)); + + }) + }) \ No newline at end of file From dc717aff3d8ef91cddc0ca26bbaf6bb598399540 Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Thu, 24 Aug 2023 17:11:13 -0400 Subject: [PATCH 40/68] fix redemption action --- .../contracts/foundry_test/GracePeriod.t.sol | 80 ++++++++++++++++--- 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/packages/contracts/foundry_test/GracePeriod.t.sol b/packages/contracts/foundry_test/GracePeriod.t.sol index 9fb9e765f..c57c6ba6e 100644 --- a/packages/contracts/foundry_test/GracePeriod.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.t.sol @@ -28,8 +28,8 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // RM Triggered via User Operation // All operations where the system is in RM should trigger the countdown - // RM untriggered via Price - // RM untriggered via Split + // RM untriggered via Price - DONE + // RM untriggered via Split - DONE // RM untriggered via User Operations // All operations where the system goes off of RM should cancel the countdown @@ -41,6 +41,11 @@ contract GracePeriodBaseTests is eBTCBaseFixture { - 1 healthy Cdps at 130% (1000 scale) - 5 unhealthy Cdps at 115% (2 scale each) + TCR somewhat above RM thresold ~130% + + - price drop via market price or rebase + + TCR just below RM threshold ~124.5% */ function _openCdps(uint256 numberOfCdpsAtRisk) internal returns (bytes32[] memory) { @@ -127,8 +132,6 @@ contract GracePeriodBaseTests is eBTCBaseFixture { priceFeedMock.setPrice((priceFeedMock.getPrice() * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM uint256 reducedPrice = priceFeedMock.getPrice(); - _printSystemState(); - // Check if we are in RM uint256 TCR = cdpManager.getTCR(reducedPrice); assertLt(TCR, 1.25e18, "!RM"); @@ -146,8 +149,6 @@ contract GracePeriodBaseTests is eBTCBaseFixture { collateral.setEthPerShare((collateral.getSharesByPooledEth(1e18) * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM uint256 price = priceFeedMock.getPrice(); - _printSystemState(); - // Check if we are in RM uint256 TCR = cdpManager.getTCR(price); assertLt(TCR, 1.25e18, "!RM"); @@ -286,6 +287,38 @@ contract GracePeriodBaseTests is eBTCBaseFixture { _postExitRMLiquidationChecks(cdps); } + /// @dev Recovery mode is "virtually" entered and subsequently exited via price movement + /// @dev When no action is taken during RM, actions after RM naturally exited via price should behave as expected from NM + function test_GracePeriodResetWhenRecoveryModeExited_WithoutAction_WithoutGracePeriodSet( + uint8 priceDecreaseAction, + uint8 priceIncreaseAction + ) public { + // setup: create Cdps, enter RM via price change or rebase + vm.assume(priceDecreaseAction <= 1); + vm.assume(priceIncreaseAction <= 1); + + // setup: create Cdps, enter RM via price change or rebase + bytes32[] memory cdps = _openCdps(5); + assertTrue(cdps.length == 5 + 1, "length"); // 5 created, 1 safe (first) + + _execPriceDecreaseAction(priceDecreaseAction); + uint256 price = priceFeedMock.getPrice(); + + // Check if we are in RM + uint256 TCR = cdpManager.getTCR(price); + assertLt(TCR, 1.25e18, "!RM"); + + _assertRevertOnAllLiquidations(cdps); + + _execPriceIncreaseAction(priceIncreaseAction); + + // Confirm no longer in RM + TCR = cdpManager.getTCR(priceFeedMock.getPrice()); + assertGt(TCR, 1.25e18, "still in RM"); + + _assertRevertOnAllLiquidations(cdps); + } + function test_GracePeriodResetWhenRecoveryModeExitedViaAction_WithGracePeriodSet( uint8 priceDecreaseAction, uint8 action @@ -338,9 +371,25 @@ contract GracePeriodBaseTests is eBTCBaseFixture { vm.prank(borrower); borrowerOperations.repayEBTC(cdps[0], 1, ZERO_ID, ZERO_ID); } else if (action == 3) { + uint toRedeem = 5e17; //redemption + ( + bytes32 firstRedemptionHint, + uint partialRedemptionHintNICR, + uint truncatedEBTCamount, + uint partialRedemptionNewColl + ) = hintHelpers.getRedemptionHints(toRedeem, price, 0); + vm.prank(borrower); - cdpManager.redeemCollateral(1e18, ZERO_ID, ZERO_ID, ZERO_ID, 1e18, 0, 1e18); + cdpManager.redeemCollateral( + toRedeem, + firstRedemptionHint, + ZERO_ID, + ZERO_ID, + partialRedemptionHintNICR, + 0, + 1e18 + ); } uint256 TCR = cdpManager.getTCR(price); @@ -396,15 +445,26 @@ contract GracePeriodBaseTests is eBTCBaseFixture { assertGt(TCR, 1.25e18, "!RM"); } - function _execPriceDecreaseAction(uint8 priceDecreaseAction) internal { - // 4% Downward Price will trigger - if (priceDecreaseAction == 0) { + /// @dev Trigger recovery mode via a dependency action that decreases price + function _execPriceDecreaseAction(uint8 action) internal { + // 4% Downward Price will trigger RM + if (action == 0) { priceFeedMock.setPrice((priceFeedMock.getPrice() * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM } else { collateral.setEthPerShare((collateral.getSharesByPooledEth(1e18) * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM } } + /// @dev Trigger exit of recovery mode via a dependency action that decreases price + function _execPriceIncreaseAction(uint8 action) internal { + // Upward Price will leave RM + if (action == 0) { + priceFeedMock.setPrice((priceFeedMock.getPrice() * 105) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM + } else { + collateral.setEthPerShare((collateral.getSharesByPooledEth(1e18) * 105) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM + } + } + /// @dev Run these checks immediately after action that sets grace period function _postValidActionLiquidationChecks(bytes32[] memory cdps) internal { // Grace period timestamp is now From 266c079fc48a39d011212942302ff7fee9dab010 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 25 Aug 2023 11:08:32 +0200 Subject: [PATCH 41/68] feat: event with TCR --- .../contracts/BorrowerOperations.sol | 14 ++++++------- .../contracts/contracts/CdpManagerStorage.sol | 21 ++++++++++++------- .../Interfaces/IRmLiquidationsChecker.sol | 12 +++++++++-- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/packages/contracts/contracts/BorrowerOperations.sol b/packages/contracts/contracts/BorrowerOperations.sol index a62e0464f..7f39a17ec 100644 --- a/packages/contracts/contracts/BorrowerOperations.sol +++ b/packages/contracts/contracts/BorrowerOperations.sol @@ -404,10 +404,10 @@ contract BorrowerOperations is // We check with newTCR if (newTCR < CCR) { // Notify RM - cdpManager.notifyBeginRM(); + cdpManager.notifyBeginRM(newTCR); } else { // Notify Back to Normal Mode - cdpManager.notifyEndRM(); + cdpManager.notifyEndRM(newTCR); } } else { _requireICRisAboveMCR(vars.ICR); @@ -416,7 +416,7 @@ contract BorrowerOperations is // == Grace Period == // // We are not in RM, no edge case, we always stay above RM // Always Notify Back to Normal Mode - cdpManager.notifyEndRM(); + cdpManager.notifyEndRM(newTCR); } // Set the cdp struct's properties @@ -481,7 +481,7 @@ contract BorrowerOperations is // == Grace Period == // // By definition we are not in RM, notify CDPManager to ensure "Glass is on" - cdpManager.notifyEndRM(); + cdpManager.notifyEndRM(newTCR); cdpManager.removeStake(_cdpId); @@ -663,10 +663,10 @@ contract BorrowerOperations is // We check with newTCR if (_vars.newTCR < CCR) { // Notify RM - cdpManager.notifyBeginRM(); + cdpManager.notifyBeginRM(_vars.newTCR); } else { // Notify Back to Normal Mode - cdpManager.notifyEndRM(); + cdpManager.notifyEndRM(_vars.newTCR); } } else { // if Normal Mode @@ -676,7 +676,7 @@ contract BorrowerOperations is // == Grace Period == // // We are not in RM, no edge case, we always stay above RM // Always Notify Back to Normal Mode - cdpManager.notifyEndRM(); + cdpManager.notifyEndRM(_vars.newTCR); } } diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 85b5f19f6..17f14733e 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -31,18 +31,22 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut /// @dev Trusted Function from BO /// @dev BO accrues totals before adjusting them /// To maintain CEI compliance we use this trusted function - function notifyBeginRM() external { + function notifyBeginRM(uint256 tcr) external { _requireCallerIsBorrowerOperations(); + emit TCRNotified(tcr); + _beginRMLiquidationCooldown(); } /// @dev Trusted Function from BO /// @dev BO accrues totals before adjusting them /// To maintain CEI compliance we use this trusted function - function notifyEndRM() external { + function notifyEndRM(uint256 tcr) external { _requireCallerIsBorrowerOperations(); + emit TCRNotified(tcr); + _stopRMLiquidationCooldown(); } @@ -60,9 +64,9 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut // Arm the countdown if (lastRecoveryModeTimestamp == UNSET_TIMESTAMP_FLAG) { lastRecoveryModeTimestamp = uint128(block.timestamp); - } - // TODO: EVENT + emit GracePeriodStart(); + } } /// @dev Checks that the system is not in RM @@ -79,15 +83,18 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut // Disarm the countdown if (lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG) { lastRecoveryModeTimestamp = UNSET_TIMESTAMP_FLAG; - } - // TODO: Event + emit GracePeriodEnd(); + } } /// TODO: obv optimizations function checkLiquidateCoolDownAndReset() public { uint256 price = priceFeed.fetchPrice(); - bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); + uint256 tcr = _getTCR(price); + bool isRecoveryMode = _checkRecoveryModeForTCR(tcr); + + emit TCRNotified(tcr); if (isRecoveryMode) { _beginRMLiquidationCooldown(); diff --git a/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol b/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol index 0bca7281b..c4c903ceb 100644 --- a/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol +++ b/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol @@ -3,9 +3,17 @@ pragma solidity 0.8.17; // Interface for State Updates that can trigger RM Liquidations interface IRmLiquidationsChecker { + + + event TCRNotified(uint TCR); /// NOTE: Mostly for debugging to ensure synch + + // NOTE: Ts is implicit in events (it's added by GETH) + event GracePeriodStart(); + event GracePeriodEnd(); + function checkLiquidateCoolDownAndReset() external; - function notifyBeginRM() external; + function notifyBeginRM(uint256 tcr) external; - function notifyEndRM() external; + function notifyEndRM(uint256 tcr) external; } From b761868327d0323f19713bda2dce61d673d7a3d3 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 25 Aug 2023 11:27:06 +0200 Subject: [PATCH 42/68] feat: redemptions are CEI compliant --- packages/contracts/contracts/CdpManager.sol | 22 +++++++++++++++++-- .../contracts/contracts/CdpManagerStorage.sol | 10 +++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/contracts/contracts/CdpManager.sol b/packages/contracts/contracts/CdpManager.sol index 2118b65f3..94681bf33 100644 --- a/packages/contracts/contracts/CdpManager.sol +++ b/packages/contracts/contracts/CdpManager.sol @@ -448,10 +448,29 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { totals.ETHToSendToRedeemer = totals.totalETHDrawn - totals.ETHFee; + // == GRACE PERIOD == // // TODO: E part of code is here for new TCR and notification // New coll = eth - totalETHDrawn, - // New debt = debt - totalEBTCToRedeem + // New debt = debt - totalEBTCToRedeem totalEBTCSupplyAtStart // Compute TCR and then notify self + { + // TODO: RE-CHECK!! // TODO: Ideally bring up || Can do by adding to struct + uint256 totalCollAtStart = collateral.getPooledEthByShares(getEntireSystemColl()); + + uint256 newTotalColl = totalCollAtStart - totals.totalETHDrawn; + uint256 newTotalDebt = totals.totalEBTCSupplyAtStart - totals.totalEBTCToRedeem; + // Compute new TCR with these + uint newTCR = LiquityMath._computeCR(newTotalColl, newTotalDebt, totals.price); + + if(newTCR < CCR) { + // Notify RM + _notifyBeginRM(newTCR); + } else { + // Notify outside RM + _notifyEndRM(newTCR); + } + } + emit Redemption(_EBTCamount, totals.totalEBTCToRedeem, totals.totalETHDrawn, totals.ETHFee); @@ -469,7 +488,6 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { // TODO: an alternative is we could track a variable on the activePool and avoid the transfer, for claim at-will be feeRecipient // Then we can avoid the whole feeRecipient contract in every other contract. It can then be governable and switched out. ActivePool can handle sending any extra metadata to the recipient - checkLiquidateCoolDownAndReset(); // TODO: Else you would do it here in the I part since all totals have accrued now } // --- Helper functions --- diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 17f14733e..aeaefe5cc 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -34,6 +34,11 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut function notifyBeginRM(uint256 tcr) external { _requireCallerIsBorrowerOperations(); + _notifyBeginRM(tcr); + } + + /// @dev Internal notify called by Redemptions and Liquidations + function _notifyBeginRM(uint256 tcr) internal { emit TCRNotified(tcr); _beginRMLiquidationCooldown(); @@ -45,6 +50,11 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut function notifyEndRM(uint256 tcr) external { _requireCallerIsBorrowerOperations(); + _notifyEndRM(tcr); + } + + /// @dev Internal notify called by Redemptions and Liquidations + function _notifyEndRM(uint256 tcr) internal { emit TCRNotified(tcr); _stopRMLiquidationCooldown(); From 05090a37ebacd33d5926ad6e065123b4bec6b46b Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 25 Aug 2023 11:36:44 +0200 Subject: [PATCH 43/68] feat: bodged initial fix for CEI --- .../contracts/LiquidationLibrary.sol | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 789dd8d4e..fb48a1890 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -522,6 +522,38 @@ contract LiquidationLibrary is CdpManagerStorage { // E: Total Coll = Coll - totalColToSend // From here we can determine if we're in RM or not + // We need the totalDebt + // We need the totalColl + // Then compute + // TODO: WE CALL THIS TWICE FOR NOW, FIX AFTER TESTED + { + uint256 price = priceFeed.fetchPrice(); // We 100% Had this + // Most likely we also had the rest of the stuff + + // TODO: RE-CHECK!! // TODO: Ideally bring up || Can do by adding to struct + // TODO: Virtual Accounting here requires: CollNew = CollAtStart - Surplus - TotalColToSend + // TODO: Verify + + uint256 totalCollAtStart = collateral.getPooledEthByShares(getEntireSystemColl()); // NOTE: This is getting the updated coll after ColSurplus + // Changing this value requires being cognizant of the changes from Surplus, not just to AP + + uint256 totalEBTCSupplyAtStart = _getEntireSystemDebt(); + + uint256 newTotalColl = totalCollAtStart - totalColToSend; + uint256 newTotalDebt = totalEBTCSupplyAtStart - totalDebtToBurn; + // Compute new TCR with these + uint newTCR = LiquityMath._computeCR(newTotalColl, newTotalDebt, price); + + if(newTCR < CCR) { + // Notify RM + _notifyBeginRM(newTCR); + } else { + // Notify outside RM + _notifyEndRM(newTCR); + } + } + + // redistribute debt if any if (totalDebtToRedistribute > 0) { _redistributeDebt(totalDebtToRedistribute); @@ -535,8 +567,6 @@ contract LiquidationLibrary is CdpManagerStorage { // CEI: ensure sending back collateral to liquidator is last thing to do activePool.sendStEthCollAndLiquidatorReward(msg.sender, totalColToSend, totalColReward); - - // checkLiquidateCoolDownAndReset(); NOTE: If you place this here, you may as well place it in the external functions in CdpManager } // Function that calculates the amount of collateral to send to liquidator (plus incentive) and the amount of collateral surplus From ea06caf4cc6a1b1701471d7cc8a9f12ebd6ba610 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 25 Aug 2023 11:41:59 +0200 Subject: [PATCH 44/68] feat: remove "training wheels" from liquidate library --- packages/contracts/contracts/CdpManager.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/contracts/contracts/CdpManager.sol b/packages/contracts/contracts/CdpManager.sol index 94681bf33..c54b72169 100644 --- a/packages/contracts/contracts/CdpManager.sol +++ b/packages/contracts/contracts/CdpManager.sol @@ -111,7 +111,6 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { function liquidate(bytes32 _cdpId) external override { _delegate(liquidationLibrary); - checkLiquidateCoolDownAndReset(); // TODO: Make this better } /// @notice Partially liquidate a single CDP. @@ -127,7 +126,6 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { bytes32 _lowerPartialHint ) external override { _delegate(liquidationLibrary); - checkLiquidateCoolDownAndReset(); // TODO: Make this better } // --- Batch/Sequence liquidation functions --- @@ -139,7 +137,6 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { /// @param _n Maximum number of CDPs to liquidate. function liquidateCdps(uint _n) external override { _delegate(liquidationLibrary); - checkLiquidateCoolDownAndReset(); // TODO: Make this better } /// @notice Attempt to liquidate a custom list of CDPs provided by the caller @@ -148,7 +145,6 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { /// @param _cdpArray Array of CDPs to liquidate. function batchLiquidateCdps(bytes32[] memory _cdpArray) external override { _delegate(liquidationLibrary); - checkLiquidateCoolDownAndReset(); // TODO: Make this better } // --- Redemption functions --- @@ -456,6 +452,9 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { { // TODO: RE-CHECK!! // TODO: Ideally bring up || Can do by adding to struct uint256 totalCollAtStart = collateral.getPooledEthByShares(getEntireSystemColl()); + // TODO: Desynch issue with this one as well + // TODO: CollSurplus is handled early so it may not be tracked unless we ask the AP for it's current balance + // TODO: INVESTIGATE BALANCES FULLY uint256 newTotalColl = totalCollAtStart - totals.totalETHDrawn; uint256 newTotalDebt = totals.totalEBTCSupplyAtStart - totals.totalEBTCToRedeem; From 64bd47da5b9acdcb71436aa6c1ff8e7be9e6708c Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 25 Aug 2023 11:59:28 +0200 Subject: [PATCH 45/68] chore: refactor assume to % to avoid assertion cap --- .../contracts/foundry_test/GracePeriod.t.sol | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/contracts/foundry_test/GracePeriod.t.sol b/packages/contracts/foundry_test/GracePeriod.t.sol index c57c6ba6e..3ec92c96e 100644 --- a/packages/contracts/foundry_test/GracePeriod.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.t.sol @@ -237,8 +237,10 @@ contract GracePeriodBaseTests is eBTCBaseFixture { - redemptions */ function test_GracePeriodViaValidAction(uint8 priceDecreaseAction, uint8 action) public { - vm.assume(priceDecreaseAction <= 1); - vm.assume(action <= 3); + // vm.assume(priceDecreaseAction <= 1); + // vm.assume(action <= 3); + priceDecreaseAction = priceDecreaseAction % 2; + action = action % 4; // setup: create Cdps, enter RM via price change or rebase bytes32[] memory cdps = _openCdps(5); @@ -266,8 +268,10 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint8 action ) public { // setup: create Cdps, enter RM via price change or rebase - vm.assume(priceDecreaseAction <= 1); - vm.assume(action <= 3); + // vm.assume(priceDecreaseAction <= 1); + // vm.assume(action <= 3); + priceDecreaseAction = priceDecreaseAction % 2; + action = action % 4; // setup: create Cdps, enter RM via price change or rebase bytes32[] memory cdps = _openCdps(5); @@ -294,8 +298,10 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint8 priceIncreaseAction ) public { // setup: create Cdps, enter RM via price change or rebase - vm.assume(priceDecreaseAction <= 1); - vm.assume(priceIncreaseAction <= 1); + // vm.assume(priceDecreaseAction <= 1); + // vm.assume(priceIncreaseAction <= 1); + priceDecreaseAction = priceDecreaseAction % 2; + priceIncreaseAction = priceIncreaseAction % 2; // setup: create Cdps, enter RM via price change or rebase bytes32[] memory cdps = _openCdps(5); @@ -324,8 +330,10 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint8 action ) public { // setup: create Cdps, enter RM via price change or rebase - vm.assume(priceDecreaseAction <= 1); - vm.assume(action <= 3); + // vm.assume(priceDecreaseAction <= 1); + // vm.assume(action <= 3); + priceDecreaseAction = priceDecreaseAction % 2; + action = action % 2; // setup: create Cdps, enter RM via price change or rebase bytes32[] memory cdps = _openCdps(5); @@ -459,9 +467,9 @@ contract GracePeriodBaseTests is eBTCBaseFixture { function _execPriceIncreaseAction(uint8 action) internal { // Upward Price will leave RM if (action == 0) { - priceFeedMock.setPrice((priceFeedMock.getPrice() * 105) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM + priceFeedMock.setPrice((priceFeedMock.getPrice() * 105) / 100); // 5% appreciation, sufficient to exit RM after 4% drawdown } else { - collateral.setEthPerShare((collateral.getSharesByPooledEth(1e18) * 105) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM + collateral.setEthPerShare((collateral.getSharesByPooledEth(1e18) * 105) / 100); // 5% appreciation, sufficient to exit RM after 4% drawdown } } From 022af5d9d55e29f9761300b4fad4d75a1d028f4c Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 25 Aug 2023 12:13:12 +0200 Subject: [PATCH 46/68] feat: initial progress on ensuring TCR is synched --- .../foundry_test/GracePeriod.Sync.t.sol | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 packages/contracts/foundry_test/GracePeriod.Sync.t.sol diff --git a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol new file mode 100644 index 000000000..ab00b6146 --- /dev/null +++ b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; + +import "forge-std/Test.sol"; +import "forge-std/console2.sol"; +import "../contracts/Dependencies/LiquityMath.sol"; +import {eBTCBaseFixture} from "./BaseFixture.sol"; + +/* + Tests around GracePeriod + */ +contract GracePeriodBaseTests is eBTCBaseFixture { + + event TCRNotified(uint TCR); /// NOTE: Mostly for debugging to ensure synch + + function setUp() public override { + eBTCBaseFixture.setUp(); + eBTCBaseFixture.connectCoreContracts(); + eBTCBaseFixture.connectLQTYContractsToCore(); + } + + address liquidator; + address safeUser; + address degen; + address risky; + + function testBasicSynchOnEachOperation() public { + // SKIPPED CAUSE BORING AF + // Open + _openSafeCdp(); + // Adjust + // Close + + _openRiskyCdps(1); + + + // Adjust and close one cdp + // TODO + + // Redeem + // Get TCR after Redeem + // Snapshot back + // Then expect it to work + + uint256 biggerSnap = vm.snapshot(); + uint256 price = priceFeedMock.fetchPrice(); + vm.startPrank(safeUser); + _partialRedemption(1e17, price); + // Get TCR here + uint256 EXPECTED_TCR = cdpManager.getTCR(price); + vm.revertTo(biggerSnap); + + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_TCR); + _partialRedemption(1e17, price); + + + // Liquidate 4x + } + + function _partialRedemption(uint256 toRedeem, uint256 price) internal { + //redemption + ( + bytes32 firstRedemptionHint, + uint partialRedemptionHintNICR, + uint truncatedEBTCamount, + uint partialRedemptionNewColl + ) = hintHelpers.getRedemptionHints(toRedeem, price, 0); + cdpManager.redeemCollateral( + toRedeem, + firstRedemptionHint, + ZERO_ID, + ZERO_ID, + partialRedemptionHintNICR, + 0, + 1e18 + ); + } + + function _openSafeCdp() internal returns (bytes32) { + address payable[] memory users; + users = _utils.createUsers(1); + safeUser = users[0]; + + // Deposit a big CDP, not at risk + uint256 _curPrice = priceFeedMock.getPrice(); + uint256 debt1 = 1000e18; + uint256 coll1 = _utils.calculateCollAmount(debt1, _curPrice, 1.30e18); // Comfy unliquidatable + + + // TODO: Add a check here for TCR being computed correctly and sent properly + + return _openTestCDP(safeUser, coll1, debt1); + } + + + function _openRiskyCdps(uint256 numberOfCdpsAtRisk) internal returns (bytes32[] memory) { + address payable[] memory users; + users = _utils.createUsers(1); + + uint256 _curPrice = priceFeedMock.getPrice(); + + bytes32[] memory cdps = new bytes32[](numberOfCdpsAtRisk + 1); + + // At risk CDPs (small CDPs) + for (uint256 i; i < numberOfCdpsAtRisk; i++) { + uint256 debt2 = 2e18; + uint256 coll2 = _utils.calculateCollAmount(debt2, _curPrice, 1.15e18); // Fairly risky + cdps[1 + i] = _openTestCDP(users[0], coll2, debt2); + } + + uint TCR = cdpManager.getTCR(_curPrice); + assertGt(TCR, CCR); + + // Move past bootstrap phase to allow redemptions + vm.warp(cdpManager.getDeploymentStartTime() + cdpManager.BOOTSTRAP_PERIOD()); + + return cdps; + } +} From 47d56d40775edaf2cc12feceb8d12ece11290812 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 25 Aug 2023 13:14:11 +0200 Subject: [PATCH 47/68] feat: test that works (see commented code) --- .../contracts/LiquidationLibrary.sol | 44 ++++----- .../foundry_test/GracePeriod.Sync.t.sol | 91 ++++++++++++++++++- 2 files changed, 111 insertions(+), 24 deletions(-) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index fb48a1890..ca3b6fcae 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -526,32 +526,32 @@ contract LiquidationLibrary is CdpManagerStorage { // We need the totalColl // Then compute // TODO: WE CALL THIS TWICE FOR NOW, FIX AFTER TESTED - { - uint256 price = priceFeed.fetchPrice(); // We 100% Had this - // Most likely we also had the rest of the stuff + // { + // uint256 price = priceFeed.fetchPrice(); // We 100% Had this + // // Most likely we also had the rest of the stuff - // TODO: RE-CHECK!! // TODO: Ideally bring up || Can do by adding to struct - // TODO: Virtual Accounting here requires: CollNew = CollAtStart - Surplus - TotalColToSend - // TODO: Verify + // // TODO: RE-CHECK!! // TODO: Ideally bring up || Can do by adding to struct + // // TODO: Virtual Accounting here requires: CollNew = CollAtStart - Surplus - TotalColToSend + // // TODO: Verify - uint256 totalCollAtStart = collateral.getPooledEthByShares(getEntireSystemColl()); // NOTE: This is getting the updated coll after ColSurplus - // Changing this value requires being cognizant of the changes from Surplus, not just to AP + // uint256 totalCollAtStart = collateral.getPooledEthByShares(getEntireSystemColl()); // NOTE: This is getting the updated coll after ColSurplus + // // Changing this value requires being cognizant of the changes from Surplus, not just to AP - uint256 totalEBTCSupplyAtStart = _getEntireSystemDebt(); + // uint256 totalEBTCSupplyAtStart = _getEntireSystemDebt(); - uint256 newTotalColl = totalCollAtStart - totalColToSend; - uint256 newTotalDebt = totalEBTCSupplyAtStart - totalDebtToBurn; - // Compute new TCR with these - uint newTCR = LiquityMath._computeCR(newTotalColl, newTotalDebt, price); + // uint256 newTotalColl = totalCollAtStart - totalColToSend; + // uint256 newTotalDebt = totalEBTCSupplyAtStart - totalDebtToBurn; + // // Compute new TCR with these + // uint newTCR = LiquityMath._computeCR(newTotalColl, newTotalDebt, price); - if(newTCR < CCR) { - // Notify RM - _notifyBeginRM(newTCR); - } else { - // Notify outside RM - _notifyEndRM(newTCR); - } - } + // if(newTCR < CCR) { + // // Notify RM + // _notifyBeginRM(newTCR); + // } else { + // // Notify outside RM + // _notifyEndRM(newTCR); + // } + // } // redistribute debt if any @@ -567,6 +567,8 @@ contract LiquidationLibrary is CdpManagerStorage { // CEI: ensure sending back collateral to liquidator is last thing to do activePool.sendStEthCollAndLiquidatorReward(msg.sender, totalColToSend, totalColReward); + + checkLiquidateCoolDownAndReset(); } // Function that calculates the amount of collateral to send to liquidator (plus incentive) and the amount of collateral surplus diff --git a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol index ab00b6146..c2989c811 100644 --- a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol @@ -31,7 +31,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Adjust // Close - _openRiskyCdps(1); + bytes32[] memory cdps = _openRiskyCdps(1); // Adjust and close one cdp @@ -54,8 +54,83 @@ contract GracePeriodBaseTests is eBTCBaseFixture { emit TCRNotified(EXPECTED_TCR); _partialRedemption(1e17, price); + // Trigger Liquidations via Split (so price is constant) + _triggerRMViaSplit(); + cdpManager.beginRMLiquidationCooldown(); + vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); // Liquidate 4x + vm.startPrank(safeUser); + uint256 liquidationSnapshotId = vm.snapshot(); // New snap for liquidations + + // == Liquidation 1 == // + console.log("Liq 1"); + + // Try liquidating a cdp + cdpManager.liquidate(cdps[0]); + // Get TCR after Liquidation + uint256 EXPECTED_TCR_FIRST_LIQ = cdpManager.getTCR(price); + // Revert so we can verify Event + vm.revertTo(liquidationSnapshotId); + + // Verify it worked + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_TCR_FIRST_LIQ); + cdpManager.liquidate(cdps[0]); + + + // == Liquidate 2 == // + console.log("Liq 2"); + + // Re-revert for next Op + vm.revertTo(liquidationSnapshotId); + + // Try liquidating a cdp partially + cdpManager.partiallyLiquidate(cdps[0], 1e18, cdps[0], cdps[0]); + uint256 EXPECTED_TCR_SECOND_LIQ = cdpManager.getTCR(price); + vm.revertTo(liquidationSnapshotId); + + // Verify it worked + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_TCR_SECOND_LIQ); + cdpManager.partiallyLiquidate(cdps[0], 1e18, cdps[0], cdps[0]); + + + // == Liquidate 3 == // + console.log("Liq 3"); + + // Re-revert for next Op + vm.revertTo(liquidationSnapshotId); + + // Try liquidating a cdp via the list (1) + cdpManager.liquidateCdps(1); + uint256 EXPECTED_TCR_THIRD_LIQ = cdpManager.getTCR(price); + vm.revertTo(liquidationSnapshotId); + + // Verify it worked + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_TCR_THIRD_LIQ); + cdpManager.liquidateCdps(1); + + // == Liquidate 4 == // + console.log("Liq 4"); + + // Re-revert for next Op + vm.revertTo(liquidationSnapshotId); + + // Try liquidating a cdp via the list (2) + bytes32[] memory cdpsToLiquidateBatch = new bytes32[](1); + cdpsToLiquidateBatch[0] = cdps[0]; + cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); + uint256 EXPECTED_TCR_FOURTH_LIQ = cdpManager.getTCR(price); + vm.revertTo(liquidationSnapshotId); + + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_TCR_FOURTH_LIQ); + cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); + vm.revertTo(liquidationSnapshotId); + + vm.stopPrank(); } function _partialRedemption(uint256 toRedeem, uint256 price) internal { @@ -100,13 +175,13 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint256 _curPrice = priceFeedMock.getPrice(); - bytes32[] memory cdps = new bytes32[](numberOfCdpsAtRisk + 1); + bytes32[] memory cdps = new bytes32[](numberOfCdpsAtRisk); // At risk CDPs (small CDPs) for (uint256 i; i < numberOfCdpsAtRisk; i++) { uint256 debt2 = 2e18; uint256 coll2 = _utils.calculateCollAmount(debt2, _curPrice, 1.15e18); // Fairly risky - cdps[1 + i] = _openTestCDP(users[0], coll2, debt2); + cdps[i] = _openTestCDP(users[0], coll2, debt2); } uint TCR = cdpManager.getTCR(_curPrice); @@ -117,4 +192,14 @@ contract GracePeriodBaseTests is eBTCBaseFixture { return cdps; } + + function _triggerRMViaSplit() internal { + // 4% Downward Price will trigger RM but not Liquidations + collateral.setEthPerShare((collateral.getSharesByPooledEth(1e18) * 96) / 100); // 4% downturn, 5% should be enough to liquidate in-spite of RM + uint256 price = priceFeedMock.getPrice(); + + // Check if we are in RM + uint256 TCR = cdpManager.getTCR(price); + assertLt(TCR, 1.25e18, "!RM"); + } } From 228ac0a52aaf23f474a9fd94eb79d001bafa15f2 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 25 Aug 2023 13:26:45 +0200 Subject: [PATCH 48/68] fix: liquidation math LG --- .../contracts/LiquidationLibrary.sol | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index ca3b6fcae..2af2bb82b 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -526,32 +526,32 @@ contract LiquidationLibrary is CdpManagerStorage { // We need the totalColl // Then compute // TODO: WE CALL THIS TWICE FOR NOW, FIX AFTER TESTED - // { - // uint256 price = priceFeed.fetchPrice(); // We 100% Had this - // // Most likely we also had the rest of the stuff + { + uint256 price = priceFeed.fetchPrice(); // We 100% Had this + // Most likely we also had the rest of the stuff - // // TODO: RE-CHECK!! // TODO: Ideally bring up || Can do by adding to struct - // // TODO: Virtual Accounting here requires: CollNew = CollAtStart - Surplus - TotalColToSend - // // TODO: Verify + // TODO: RE-CHECK!! // TODO: Ideally bring up || Can do by adding to struct + // TODO: Virtual Accounting here requires: CollNew = CollAtStart - Surplus - TotalColToSend + // TODO: Verify - // uint256 totalCollAtStart = collateral.getPooledEthByShares(getEntireSystemColl()); // NOTE: This is getting the updated coll after ColSurplus - // // Changing this value requires being cognizant of the changes from Surplus, not just to AP + uint256 totalSharesAtStart = getEntireSystemColl(); // NOTE: This is getting the updated coll after ColSurplus + // Changing this value requires being cognizant of the changes from Surplus, not just to AP - // uint256 totalEBTCSupplyAtStart = _getEntireSystemDebt(); + uint256 totalEBTCSupplyAtStart = _getEntireSystemDebt(); - // uint256 newTotalColl = totalCollAtStart - totalColToSend; - // uint256 newTotalDebt = totalEBTCSupplyAtStart - totalDebtToBurn; - // // Compute new TCR with these - // uint newTCR = LiquityMath._computeCR(newTotalColl, newTotalDebt, price); + uint256 newTotalShares = totalSharesAtStart - totalColToSend; + uint256 newTotalDebt = totalEBTCSupplyAtStart - totalDebtToBurn; + // Compute new TCR with these + uint newTCR = LiquityMath._computeCR(collateral.getPooledEthByShares(newTotalShares), newTotalDebt, price); - // if(newTCR < CCR) { - // // Notify RM - // _notifyBeginRM(newTCR); - // } else { - // // Notify outside RM - // _notifyEndRM(newTCR); - // } - // } + if(newTCR < CCR) { + // Notify RM + _notifyBeginRM(newTCR); + } else { + // Notify outside RM + _notifyEndRM(newTCR); + } + } // redistribute debt if any @@ -568,7 +568,7 @@ contract LiquidationLibrary is CdpManagerStorage { // CEI: ensure sending back collateral to liquidator is last thing to do activePool.sendStEthCollAndLiquidatorReward(msg.sender, totalColToSend, totalColReward); - checkLiquidateCoolDownAndReset(); + // checkLiquidateCoolDownAndReset(); // TODO: This works correctly, use this to compare vs new impl } // Function that calculates the amount of collateral to send to liquidator (plus incentive) and the amount of collateral surplus From 4ad913efbd153b1186643a04b59706cd6f85c2d0 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 25 Aug 2023 13:27:02 +0200 Subject: [PATCH 49/68] chore: prettier --- packages/contracts/contracts/CdpManager.sol | 5 ++--- .../contracts/Interfaces/IRmLiquidationsChecker.sol | 2 -- packages/contracts/contracts/LiquidationLibrary.sol | 13 ++++++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/contracts/contracts/CdpManager.sol b/packages/contracts/contracts/CdpManager.sol index c54b72169..c11404c70 100644 --- a/packages/contracts/contracts/CdpManager.sol +++ b/packages/contracts/contracts/CdpManager.sol @@ -460,8 +460,8 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { uint256 newTotalDebt = totals.totalEBTCSupplyAtStart - totals.totalEBTCToRedeem; // Compute new TCR with these uint newTCR = LiquityMath._computeCR(newTotalColl, newTotalDebt, totals.price); - - if(newTCR < CCR) { + + if (newTCR < CCR) { // Notify RM _notifyBeginRM(newTCR); } else { @@ -470,7 +470,6 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { } } - emit Redemption(_EBTCamount, totals.totalEBTCToRedeem, totals.totalETHDrawn, totals.ETHFee); // Burn the total eBTC that is redeemed diff --git a/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol b/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol index c4c903ceb..f622eba2e 100644 --- a/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol +++ b/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol @@ -3,8 +3,6 @@ pragma solidity 0.8.17; // Interface for State Updates that can trigger RM Liquidations interface IRmLiquidationsChecker { - - event TCRNotified(uint TCR); /// NOTE: Mostly for debugging to ensure synch // NOTE: Ts is implicit in events (it's added by GETH) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 2af2bb82b..33e54bded 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -533,7 +533,7 @@ contract LiquidationLibrary is CdpManagerStorage { // TODO: RE-CHECK!! // TODO: Ideally bring up || Can do by adding to struct // TODO: Virtual Accounting here requires: CollNew = CollAtStart - Surplus - TotalColToSend // TODO: Verify - + uint256 totalSharesAtStart = getEntireSystemColl(); // NOTE: This is getting the updated coll after ColSurplus // Changing this value requires being cognizant of the changes from Surplus, not just to AP @@ -542,9 +542,13 @@ contract LiquidationLibrary is CdpManagerStorage { uint256 newTotalShares = totalSharesAtStart - totalColToSend; uint256 newTotalDebt = totalEBTCSupplyAtStart - totalDebtToBurn; // Compute new TCR with these - uint newTCR = LiquityMath._computeCR(collateral.getPooledEthByShares(newTotalShares), newTotalDebt, price); - - if(newTCR < CCR) { + uint newTCR = LiquityMath._computeCR( + collateral.getPooledEthByShares(newTotalShares), + newTotalDebt, + price + ); + + if (newTCR < CCR) { // Notify RM _notifyBeginRM(newTCR); } else { @@ -553,7 +557,6 @@ contract LiquidationLibrary is CdpManagerStorage { } } - // redistribute debt if any if (totalDebtToRedistribute > 0) { _redistributeDebt(totalDebtToRedistribute); From ef6df3746ad4e5ecd7cddcec544c0dffddfa5ecc Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 25 Aug 2023 13:27:07 +0200 Subject: [PATCH 50/68] chore: prettier --- .../contracts/foundry_test/GracePeriod.Sync.t.sol | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol index c2989c811..aaac12dcc 100644 --- a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol @@ -10,7 +10,6 @@ import {eBTCBaseFixture} from "./BaseFixture.sol"; Tests around GracePeriod */ contract GracePeriodBaseTests is eBTCBaseFixture { - event TCRNotified(uint TCR); /// NOTE: Mostly for debugging to ensure synch function setUp() public override { @@ -33,7 +32,6 @@ contract GracePeriodBaseTests is eBTCBaseFixture { bytes32[] memory cdps = _openRiskyCdps(1); - // Adjust and close one cdp // TODO @@ -41,7 +39,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Get TCR after Redeem // Snapshot back // Then expect it to work - + uint256 biggerSnap = vm.snapshot(); uint256 price = priceFeedMock.fetchPrice(); vm.startPrank(safeUser); @@ -71,13 +69,12 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Get TCR after Liquidation uint256 EXPECTED_TCR_FIRST_LIQ = cdpManager.getTCR(price); // Revert so we can verify Event - vm.revertTo(liquidationSnapshotId); - + vm.revertTo(liquidationSnapshotId); + // Verify it worked vm.expectEmit(false, false, false, true); emit TCRNotified(EXPECTED_TCR_FIRST_LIQ); cdpManager.liquidate(cdps[0]); - // == Liquidate 2 == // console.log("Liq 2"); @@ -95,7 +92,6 @@ contract GracePeriodBaseTests is eBTCBaseFixture { emit TCRNotified(EXPECTED_TCR_SECOND_LIQ); cdpManager.partiallyLiquidate(cdps[0], 1e18, cdps[0], cdps[0]); - // == Liquidate 3 == // console.log("Liq 3"); @@ -162,13 +158,11 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint256 debt1 = 1000e18; uint256 coll1 = _utils.calculateCollAmount(debt1, _curPrice, 1.30e18); // Comfy unliquidatable - // TODO: Add a check here for TCR being computed correctly and sent properly return _openTestCDP(safeUser, coll1, debt1); } - function _openRiskyCdps(uint256 numberOfCdpsAtRisk) internal returns (bytes32[] memory) { address payable[] memory users; users = _utils.createUsers(1); From 6f37b9d84f4c286e8885c6195256a32e3093d75e Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Sun, 27 Aug 2023 19:41:19 -0400 Subject: [PATCH 51/68] liquidation data dedup read draft --- .../contracts/LiquidationLibrary.sol | 80 ++++++++++++++++--- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 33e54bded..47e81723b 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.17; - +import "forge-std/Test.sol"; import "./Interfaces/ICdpManagerData.sol"; import "./Interfaces/ICollSurplusPool.sol"; import "./Interfaces/IEBTCToken.sol"; @@ -129,13 +129,15 @@ contract LiquidationLibrary is CdpManagerStorage { uint256 totalColToSend; uint256 totalDebtToRedistribute; uint256 totalColReward; + uint256 totalColSurplus; if (_liqState._partialAmount == 0) { ( totalDebtToBurn, totalColToSend, totalDebtToRedistribute, - totalColReward + totalColReward, + totalColSurplus ) = _liquidateCDPByExternalLiquidator(_liqState, _recoveryState); } else { (totalDebtToBurn, totalColToSend) = _liquidateCDPPartially(_liqState); @@ -145,7 +147,8 @@ contract LiquidationLibrary is CdpManagerStorage { totalDebtToBurn, totalColToSend, totalDebtToRedistribute, - totalColReward + totalColReward, + totalColSurplus ) = _liquidateCDPByExternalLiquidator(_liqState, _recoveryState); } } @@ -154,7 +157,11 @@ contract LiquidationLibrary is CdpManagerStorage { totalDebtToBurn, totalColToSend, totalDebtToRedistribute, - totalColReward + totalColReward, + totalColSurplus, + _recoveryState.entireSystemDebt, + _recoveryState.entireSystemColl, + _liqState._price ); } @@ -163,7 +170,7 @@ contract LiquidationLibrary is CdpManagerStorage { function _liquidateCDPByExternalLiquidator( LocalVar_InternalLiquidate memory _liqState, LocalVar_RecoveryLiquidate memory _recoveryState - ) private returns (uint256, uint256, uint256, uint256) { + ) private returns (uint256, uint256, uint256, uint256, uint256) { if (_liqState._recoveryModeAtStart) { LocalVar_RecoveryLiquidate memory _outputState = _liquidateSingleCDPInRecoveryMode( _recoveryState @@ -178,7 +185,8 @@ contract LiquidationLibrary is CdpManagerStorage { _outputState.totalDebtToBurn, _outputState.totalColToSend, _outputState.totalDebtToRedistribute, - _outputState.totalColReward + _outputState.totalColReward, + _outputState.totalColSurplus ); } else { LocalVar_InternalLiquidate memory _outputState = _liquidateSingleCDPInNormalMode( @@ -188,7 +196,8 @@ contract LiquidationLibrary is CdpManagerStorage { _outputState.totalDebtToBurn, _outputState.totalColToSend, _outputState.totalDebtToRedistribute, - _outputState.totalColReward + _outputState.totalColReward, + _outputState.totalColSurplus ); } } @@ -511,12 +520,23 @@ contract LiquidationLibrary is CdpManagerStorage { uint256 totalDebtToBurn, uint256 totalColToSend, uint256 totalDebtToRedistribute, - uint256 totalColReward + uint256 totalColReward, + uint256 totalColSurplus, + uint256 systemInitialCollShares, + uint256 systemInitialDebt, + uint256 price ) internal { // update the staking and collateral snapshots _updateSystemSnapshots_excludeCollRemainder(totalColToSend); emit Liquidation(totalDebtToBurn, totalColToSend, totalColReward); + console.log("totalDebtToBurn :", totalDebtToBurn); + console.log("totalColToSend :", totalColToSend); + console.log("totalDebtToRedistribute :", totalDebtToRedistribute); + console.log("totalColReward :", totalColReward); + console.log("totalColSurplus :", totalColSurplus); + console.log("systemInitialCollShares :", systemInitialCollShares); + console.log("systemInitialDebt :", systemInitialDebt); // E: Total Debt = Debt - TotalDebtToBurn // E: Total Coll = Coll - totalColToSend @@ -527,7 +547,6 @@ contract LiquidationLibrary is CdpManagerStorage { // Then compute // TODO: WE CALL THIS TWICE FOR NOW, FIX AFTER TESTED { - uint256 price = priceFeed.fetchPrice(); // We 100% Had this // Most likely we also had the rest of the stuff // TODO: RE-CHECK!! // TODO: Ideally bring up || Can do by adding to struct @@ -539,8 +558,33 @@ contract LiquidationLibrary is CdpManagerStorage { uint256 totalEBTCSupplyAtStart = _getEntireSystemDebt(); - uint256 newTotalShares = totalSharesAtStart - totalColToSend; - uint256 newTotalDebt = totalEBTCSupplyAtStart - totalDebtToBurn; + console.log("totalSharesAtStart", systemInitialCollShares); + console.log("totalEBTCSupplyAtStart", systemInitialCollShares); + + uint256 newTotalShares2 = totalSharesAtStart - totalColToSend; + uint256 newTotalDebt2 = totalEBTCSupplyAtStart - totalDebtToBurn; + + console.log("newTotalShares with true supply", newTotalShares2); + console.log("newTotalDebt with true supply", newTotalDebt2); + + console.log("systemInitialCollShares", systemInitialCollShares); + console.log( + "systemInitialCollShares - totalColToSend", + systemInitialCollShares - totalColToSend + ); + console.log( + "systemInitialCollShares - totalColSurplus", + systemInitialCollShares - totalColSurplus + ); + console.log( + "systemInitialCollShares - totalColToSend - totalColSurplus", + systemInitialCollShares - totalColToSend - totalColSurplus + ); + uint256 newTotalShares = systemInitialCollShares - totalColToSend - totalColSurplus; + + console.log("systemInitialDebt", systemInitialDebt); + console.log("systemInitialDebt - totalDebtToBurn", systemInitialDebt - totalDebtToBurn); + uint256 newTotalDebt = systemInitialDebt - totalDebtToBurn; // Compute new TCR with these uint newTCR = LiquityMath._computeCR( collateral.getPooledEthByShares(newTotalShares), @@ -548,6 +592,8 @@ contract LiquidationLibrary is CdpManagerStorage { price ); + console.log("newTCR", newTCR); + if (newTCR < CCR) { // Notify RM _notifyBeginRM(newTCR); @@ -657,7 +703,11 @@ contract LiquidationLibrary is CdpManagerStorage { totals.totalDebtToOffset, totals.totalCollToSendToLiquidator, totals.totalDebtToRedistribute, - totals.totalCollReward + totals.totalCollReward, + totals.totalCollSurplus, + systemColl, + systemDebt, + vars.price ); } @@ -775,7 +825,11 @@ contract LiquidationLibrary is CdpManagerStorage { totals.totalDebtToOffset, totals.totalCollToSendToLiquidator, totals.totalDebtToRedistribute, - totals.totalCollReward + totals.totalCollReward, + totals.totalCollSurplus, + systemColl, + systemDebt, + vars.price ); } From b3a569641d0c6dbe7d3d898c5e62f2ec461b9b91 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Mon, 28 Aug 2023 08:50:54 +0200 Subject: [PATCH 52/68] fix: test works for open adjust close --- .../foundry_test/GracePeriod.Sync.t.sol | 99 ++++++++++++++++--- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol index aaac12dcc..c262b7cef 100644 --- a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol @@ -24,11 +24,79 @@ contract GracePeriodBaseTests is eBTCBaseFixture { address risky; function testBasicSynchOnEachOperation() public { + uint256 price = priceFeedMock.fetchPrice(); + // SKIPPED CAUSE BORING AF - // Open + // == Open == // + console2.log("Open"); + uint256 openSnap = vm.snapshot(); + + { _openSafeCdp(); + uint256 EXPECTED_OPEN_TCR = cdpManager.getTCR(price); + vm.revertTo(openSnap); + + // vm.expectEmit(false, false, false, true); + // emit TCRNotified(EXPECTED_OPEN_TCR); + bytes32 safeId = _openSafeCdp(); + // TODO: Open CDP differenty because event catches something else + + // Adjust + console2.log("Adjust"); + + dealCollateral(safeUser, 12345); + uint256 adjustSnap = vm.snapshot(); + + vm.startPrank(safeUser); + borrowerOperations.addColl(safeId, ZERO_ID, ZERO_ID, 123); + uint256 EXPECTED_ADJUST_TCR = cdpManager.getTCR(price); + vm.revertTo(adjustSnap); + + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_ADJUST_TCR); + borrowerOperations.addColl(safeId, ZERO_ID, ZERO_ID, 123); + vm.stopPrank(); + vm.revertTo(adjustSnap); + } + + + + + + // Close + { + console2.log("Close"); + uint256 closeSnapshot = vm.snapshot(); + // Open another so we can close it + bytes32 safeIdSecond = _openSafeCdp(); + + + vm.startPrank(safeUser); + borrowerOperations.closeCdp(safeIdSecond); + uint256 EXPECTED_CLOSE_TCR = cdpManager.getTCR(price); + vm.revertTo(closeSnapshot); + vm.stopPrank(); + + // Open another so we can close it + safeIdSecond = _openSafeCdp(); + + vm.startPrank(safeUser); + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_CLOSE_TCR); + borrowerOperations.closeCdp(safeIdSecond); + vm.stopPrank(); + } + + + // Revert back to here + vm.revertTo(openSnap); + + + // Do the rest (Redemptions and liquidations) + _openSafeCdp(); + bytes32[] memory cdps = _openRiskyCdps(1); @@ -41,15 +109,14 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Then expect it to work uint256 biggerSnap = vm.snapshot(); - uint256 price = priceFeedMock.fetchPrice(); vm.startPrank(safeUser); _partialRedemption(1e17, price); // Get TCR here - uint256 EXPECTED_TCR = cdpManager.getTCR(price); + uint256 EXPECTED_REDEMPTION_TCR = cdpManager.getTCR(price); vm.revertTo(biggerSnap); vm.expectEmit(false, false, false, true); - emit TCRNotified(EXPECTED_TCR); + emit TCRNotified(EXPECTED_REDEMPTION_TCR); _partialRedemption(1e17, price); // Trigger Liquidations via Split (so price is constant) @@ -62,21 +129,24 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint256 liquidationSnapshotId = vm.snapshot(); // New snap for liquidations // == Liquidation 1 == // + { console.log("Liq 1"); // Try liquidating a cdp cdpManager.liquidate(cdps[0]); // Get TCR after Liquidation - uint256 EXPECTED_TCR_FIRST_LIQ = cdpManager.getTCR(price); + uint256 EXPECTED_TCR_FIRST_LIQ_TCR = cdpManager.getTCR(price); // Revert so we can verify Event vm.revertTo(liquidationSnapshotId); // Verify it worked vm.expectEmit(false, false, false, true); - emit TCRNotified(EXPECTED_TCR_FIRST_LIQ); + emit TCRNotified(EXPECTED_TCR_FIRST_LIQ_TCR); cdpManager.liquidate(cdps[0]); + } // == Liquidate 2 == // + { console.log("Liq 2"); // Re-revert for next Op @@ -84,15 +154,17 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Try liquidating a cdp partially cdpManager.partiallyLiquidate(cdps[0], 1e18, cdps[0], cdps[0]); - uint256 EXPECTED_TCR_SECOND_LIQ = cdpManager.getTCR(price); + uint256 EXPECTED_TCR_SECOND_LIQ_TCR = cdpManager.getTCR(price); vm.revertTo(liquidationSnapshotId); // Verify it worked vm.expectEmit(false, false, false, true); - emit TCRNotified(EXPECTED_TCR_SECOND_LIQ); + emit TCRNotified(EXPECTED_TCR_SECOND_LIQ_TCR); cdpManager.partiallyLiquidate(cdps[0], 1e18, cdps[0], cdps[0]); + } // == Liquidate 3 == // + { console.log("Liq 3"); // Re-revert for next Op @@ -100,15 +172,17 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Try liquidating a cdp via the list (1) cdpManager.liquidateCdps(1); - uint256 EXPECTED_TCR_THIRD_LIQ = cdpManager.getTCR(price); + uint256 EXPECTED_TCR_THIRD_LIQ_TCR = cdpManager.getTCR(price); vm.revertTo(liquidationSnapshotId); // Verify it worked vm.expectEmit(false, false, false, true); - emit TCRNotified(EXPECTED_TCR_THIRD_LIQ); + emit TCRNotified(EXPECTED_TCR_THIRD_LIQ_TCR); cdpManager.liquidateCdps(1); + } // == Liquidate 4 == // + { console.log("Liq 4"); // Re-revert for next Op @@ -118,13 +192,14 @@ contract GracePeriodBaseTests is eBTCBaseFixture { bytes32[] memory cdpsToLiquidateBatch = new bytes32[](1); cdpsToLiquidateBatch[0] = cdps[0]; cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); - uint256 EXPECTED_TCR_FOURTH_LIQ = cdpManager.getTCR(price); + uint256 EXPECTED_TCR_FOURTH_LIQ_TCR = cdpManager.getTCR(price); vm.revertTo(liquidationSnapshotId); vm.expectEmit(false, false, false, true); - emit TCRNotified(EXPECTED_TCR_FOURTH_LIQ); + emit TCRNotified(EXPECTED_TCR_FOURTH_LIQ_TCR); cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); vm.revertTo(liquidationSnapshotId); + } vm.stopPrank(); } From f612d0208c331c0e7a17801ed17a5233d7a76f66 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Mon, 28 Aug 2023 08:55:46 +0200 Subject: [PATCH 53/68] fix: open CDP also tracked --- .../foundry_test/GracePeriod.Sync.t.sol | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol index c262b7cef..05b1f953f 100644 --- a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol @@ -36,10 +36,22 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint256 EXPECTED_OPEN_TCR = cdpManager.getTCR(price); vm.revertTo(openSnap); - // vm.expectEmit(false, false, false, true); - // emit TCRNotified(EXPECTED_OPEN_TCR); - bytes32 safeId = _openSafeCdp(); - // TODO: Open CDP differenty because event catches something else + + // NOTE: Ported the same code of open because foundry doesn't find the event + address payable[] memory users; + users = _utils.createUsers(1); + safeUser = users[0]; + + uint256 debt1 = 1000e18; + uint256 coll1 = _utils.calculateCollAmount(debt1, price, 1.30e18); // Comfy unliquidatable + dealCollateral(safeUser, coll1); + vm.startPrank(safeUser); + collateral.approve(address(borrowerOperations), type(uint256).max); + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_OPEN_TCR); + bytes32 safeId = borrowerOperations.openCdp(debt1, bytes32(0), bytes32(0), coll1); + vm.stopPrank(); + // Adjust From 61891389af912ef0c509792eed262fbe7c85b701 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Mon, 28 Aug 2023 15:44:20 +0200 Subject: [PATCH 54/68] fix: error messages --- packages/contracts/contracts/CdpManagerStorage.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index aeaefe5cc..73b1d9487 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -65,7 +65,7 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut // Require we're in RM uint256 price = priceFeed.fetchPrice(); bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); - require(isRecoveryMode); + require(isRecoveryMode, "Grace Period can only start in RM"); _beginRMLiquidationCooldown(); } @@ -84,7 +84,7 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut // Require we're in RM uint256 price = priceFeed.fetchPrice(); bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); - require(!isRecoveryMode); + require(!isRecoveryMode, "Grace Period can end only outside RM"); _stopRMLiquidationCooldown(); } From 5e043c9411287c124bf46f628087fb2189db91ef Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Mon, 28 Aug 2023 15:45:00 +0200 Subject: [PATCH 55/68] fix: convention for error messages --- packages/contracts/contracts/CdpManagerStorage.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 73b1d9487..1cc9055ef 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -65,7 +65,7 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut // Require we're in RM uint256 price = priceFeed.fetchPrice(); bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); - require(isRecoveryMode, "Grace Period can only start in RM"); + require(isRecoveryMode, "CdpManagerStorage: Not in RM"); _beginRMLiquidationCooldown(); } @@ -84,7 +84,7 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut // Require we're in RM uint256 price = priceFeed.fetchPrice(); bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); - require(!isRecoveryMode, "Grace Period can end only outside RM"); + require(!isRecoveryMode, "CdpManagerStorage: RM still ongoing"); _stopRMLiquidationCooldown(); } From 5a06b35594e889d11c97f0db549873913646ef4d Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Mon, 28 Aug 2023 10:28:37 -0400 Subject: [PATCH 56/68] fix single liq path optimization --- .../contracts/LiquidationLibrary.sol | 90 ++++++------------- 1 file changed, 29 insertions(+), 61 deletions(-) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 47e81723b..7202dec4f 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -125,42 +125,47 @@ contract LiquidationLibrary is CdpManagerStorage { LocalVar_InternalLiquidate memory _liqState, LocalVar_RecoveryLiquidate memory _recoveryState ) internal { - uint256 totalDebtToBurn; - uint256 totalColToSend; - uint256 totalDebtToRedistribute; - uint256 totalColReward; - uint256 totalColSurplus; + LiquidationValues memory liquidationValues; + + uint256 startingSystemDebt = _recoveryState.entireSystemDebt; + uint256 startingSystemColl = _recoveryState.entireSystemColl; if (_liqState._partialAmount == 0) { ( - totalDebtToBurn, - totalColToSend, - totalDebtToRedistribute, - totalColReward, - totalColSurplus + liquidationValues.debtToOffset, + liquidationValues.totalCollToSendToLiquidator, + liquidationValues.debtToRedistribute, + liquidationValues.collReward, + liquidationValues.collSurplus ) = _liquidateCDPByExternalLiquidator(_liqState, _recoveryState); } else { - (totalDebtToBurn, totalColToSend) = _liquidateCDPPartially(_liqState); - if (totalColToSend == 0 && totalDebtToBurn == 0) { + ( + liquidationValues.debtToOffset, + liquidationValues.totalCollToSendToLiquidator + ) = _liquidateCDPPartially(_liqState); + if ( + liquidationValues.totalCollToSendToLiquidator == 0 && + liquidationValues.debtToOffset == 0 + ) { // retry with fully liquidation ( - totalDebtToBurn, - totalColToSend, - totalDebtToRedistribute, - totalColReward, - totalColSurplus + liquidationValues.debtToOffset, + liquidationValues.totalCollToSendToLiquidator, + liquidationValues.debtToRedistribute, + liquidationValues.collReward, + liquidationValues.collSurplus ) = _liquidateCDPByExternalLiquidator(_liqState, _recoveryState); } } _finalizeExternalLiquidation( - totalDebtToBurn, - totalColToSend, - totalDebtToRedistribute, - totalColReward, - totalColSurplus, - _recoveryState.entireSystemDebt, - _recoveryState.entireSystemColl, + liquidationValues.debtToOffset, + liquidationValues.totalCollToSendToLiquidator, + liquidationValues.debtToRedistribute, + liquidationValues.collReward, + liquidationValues.collSurplus, + startingSystemColl, + startingSystemDebt, _liqState._price ); } @@ -530,13 +535,6 @@ contract LiquidationLibrary is CdpManagerStorage { _updateSystemSnapshots_excludeCollRemainder(totalColToSend); emit Liquidation(totalDebtToBurn, totalColToSend, totalColReward); - console.log("totalDebtToBurn :", totalDebtToBurn); - console.log("totalColToSend :", totalColToSend); - console.log("totalDebtToRedistribute :", totalDebtToRedistribute); - console.log("totalColReward :", totalColReward); - console.log("totalColSurplus :", totalColSurplus); - console.log("systemInitialCollShares :", systemInitialCollShares); - console.log("systemInitialDebt :", systemInitialDebt); // E: Total Debt = Debt - TotalDebtToBurn // E: Total Coll = Coll - totalColToSend @@ -553,37 +551,7 @@ contract LiquidationLibrary is CdpManagerStorage { // TODO: Virtual Accounting here requires: CollNew = CollAtStart - Surplus - TotalColToSend // TODO: Verify - uint256 totalSharesAtStart = getEntireSystemColl(); // NOTE: This is getting the updated coll after ColSurplus - // Changing this value requires being cognizant of the changes from Surplus, not just to AP - - uint256 totalEBTCSupplyAtStart = _getEntireSystemDebt(); - - console.log("totalSharesAtStart", systemInitialCollShares); - console.log("totalEBTCSupplyAtStart", systemInitialCollShares); - - uint256 newTotalShares2 = totalSharesAtStart - totalColToSend; - uint256 newTotalDebt2 = totalEBTCSupplyAtStart - totalDebtToBurn; - - console.log("newTotalShares with true supply", newTotalShares2); - console.log("newTotalDebt with true supply", newTotalDebt2); - - console.log("systemInitialCollShares", systemInitialCollShares); - console.log( - "systemInitialCollShares - totalColToSend", - systemInitialCollShares - totalColToSend - ); - console.log( - "systemInitialCollShares - totalColSurplus", - systemInitialCollShares - totalColSurplus - ); - console.log( - "systemInitialCollShares - totalColToSend - totalColSurplus", - systemInitialCollShares - totalColToSend - totalColSurplus - ); uint256 newTotalShares = systemInitialCollShares - totalColToSend - totalColSurplus; - - console.log("systemInitialDebt", systemInitialDebt); - console.log("systemInitialDebt - totalDebtToBurn", systemInitialDebt - totalDebtToBurn); uint256 newTotalDebt = systemInitialDebt - totalDebtToBurn; // Compute new TCR with these uint newTCR = LiquityMath._computeCR( From e36e608e3937f22e593727631cd4703c850aad68 Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Mon, 28 Aug 2023 10:28:43 -0400 Subject: [PATCH 57/68] lint --- .../foundry_test/GracePeriod.Sync.t.sol | 222 +++++++++--------- 1 file changed, 105 insertions(+), 117 deletions(-) diff --git a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol index 05b1f953f..216e2be8e 100644 --- a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol @@ -32,84 +32,72 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint256 openSnap = vm.snapshot(); { - _openSafeCdp(); - uint256 EXPECTED_OPEN_TCR = cdpManager.getTCR(price); - vm.revertTo(openSnap); - - - // NOTE: Ported the same code of open because foundry doesn't find the event - address payable[] memory users; - users = _utils.createUsers(1); - safeUser = users[0]; - - uint256 debt1 = 1000e18; - uint256 coll1 = _utils.calculateCollAmount(debt1, price, 1.30e18); // Comfy unliquidatable - dealCollateral(safeUser, coll1); - vm.startPrank(safeUser); - collateral.approve(address(borrowerOperations), type(uint256).max); - vm.expectEmit(false, false, false, true); - emit TCRNotified(EXPECTED_OPEN_TCR); - bytes32 safeId = borrowerOperations.openCdp(debt1, bytes32(0), bytes32(0), coll1); - vm.stopPrank(); - - - - // Adjust - console2.log("Adjust"); - - dealCollateral(safeUser, 12345); - uint256 adjustSnap = vm.snapshot(); - - vm.startPrank(safeUser); - borrowerOperations.addColl(safeId, ZERO_ID, ZERO_ID, 123); - uint256 EXPECTED_ADJUST_TCR = cdpManager.getTCR(price); - vm.revertTo(adjustSnap); - - vm.expectEmit(false, false, false, true); - emit TCRNotified(EXPECTED_ADJUST_TCR); - borrowerOperations.addColl(safeId, ZERO_ID, ZERO_ID, 123); - vm.stopPrank(); - vm.revertTo(adjustSnap); + _openSafeCdp(); + uint256 EXPECTED_OPEN_TCR = cdpManager.getTCR(price); + vm.revertTo(openSnap); + + // NOTE: Ported the same code of open because foundry doesn't find the event + address payable[] memory users; + users = _utils.createUsers(1); + safeUser = users[0]; + + uint256 debt1 = 1000e18; + uint256 coll1 = _utils.calculateCollAmount(debt1, price, 1.30e18); // Comfy unliquidatable + dealCollateral(safeUser, coll1); + vm.startPrank(safeUser); + collateral.approve(address(borrowerOperations), type(uint256).max); + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_OPEN_TCR); + bytes32 safeId = borrowerOperations.openCdp(debt1, bytes32(0), bytes32(0), coll1); + vm.stopPrank(); + + // Adjust + console2.log("Adjust"); + + dealCollateral(safeUser, 12345); + uint256 adjustSnap = vm.snapshot(); + + vm.startPrank(safeUser); + borrowerOperations.addColl(safeId, ZERO_ID, ZERO_ID, 123); + uint256 EXPECTED_ADJUST_TCR = cdpManager.getTCR(price); + vm.revertTo(adjustSnap); + + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_ADJUST_TCR); + borrowerOperations.addColl(safeId, ZERO_ID, ZERO_ID, 123); + vm.stopPrank(); + vm.revertTo(adjustSnap); } - - - - - // Close { - console2.log("Close"); - uint256 closeSnapshot = vm.snapshot(); - // Open another so we can close it - bytes32 safeIdSecond = _openSafeCdp(); - - - vm.startPrank(safeUser); - borrowerOperations.closeCdp(safeIdSecond); - uint256 EXPECTED_CLOSE_TCR = cdpManager.getTCR(price); - vm.revertTo(closeSnapshot); - vm.stopPrank(); - - // Open another so we can close it - safeIdSecond = _openSafeCdp(); - - vm.startPrank(safeUser); - vm.expectEmit(false, false, false, true); - emit TCRNotified(EXPECTED_CLOSE_TCR); - borrowerOperations.closeCdp(safeIdSecond); - vm.stopPrank(); + console2.log("Close"); + uint256 closeSnapshot = vm.snapshot(); + // Open another so we can close it + bytes32 safeIdSecond = _openSafeCdp(); + + vm.startPrank(safeUser); + borrowerOperations.closeCdp(safeIdSecond); + uint256 EXPECTED_CLOSE_TCR = cdpManager.getTCR(price); + vm.revertTo(closeSnapshot); + vm.stopPrank(); + + // Open another so we can close it + safeIdSecond = _openSafeCdp(); + + vm.startPrank(safeUser); + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_CLOSE_TCR); + borrowerOperations.closeCdp(safeIdSecond); + vm.stopPrank(); } - // Revert back to here vm.revertTo(openSnap); - // Do the rest (Redemptions and liquidations) _openSafeCdp(); - bytes32[] memory cdps = _openRiskyCdps(1); // Adjust and close one cdp @@ -142,75 +130,75 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // == Liquidation 1 == // { - console.log("Liq 1"); - - // Try liquidating a cdp - cdpManager.liquidate(cdps[0]); - // Get TCR after Liquidation - uint256 EXPECTED_TCR_FIRST_LIQ_TCR = cdpManager.getTCR(price); - // Revert so we can verify Event - vm.revertTo(liquidationSnapshotId); - - // Verify it worked - vm.expectEmit(false, false, false, true); - emit TCRNotified(EXPECTED_TCR_FIRST_LIQ_TCR); - cdpManager.liquidate(cdps[0]); + console.log("Liq 1"); + + // Try liquidating a cdp + cdpManager.liquidate(cdps[0]); + // Get TCR after Liquidation + uint256 EXPECTED_TCR_FIRST_LIQ_TCR = cdpManager.getTCR(price); + // Revert so we can verify Event + vm.revertTo(liquidationSnapshotId); + + // Verify it worked + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_TCR_FIRST_LIQ_TCR); + cdpManager.liquidate(cdps[0]); } // == Liquidate 2 == // { - console.log("Liq 2"); + console.log("Liq 2"); - // Re-revert for next Op - vm.revertTo(liquidationSnapshotId); + // Re-revert for next Op + vm.revertTo(liquidationSnapshotId); - // Try liquidating a cdp partially - cdpManager.partiallyLiquidate(cdps[0], 1e18, cdps[0], cdps[0]); - uint256 EXPECTED_TCR_SECOND_LIQ_TCR = cdpManager.getTCR(price); - vm.revertTo(liquidationSnapshotId); + // Try liquidating a cdp partially + cdpManager.partiallyLiquidate(cdps[0], 1e18, cdps[0], cdps[0]); + uint256 EXPECTED_TCR_SECOND_LIQ_TCR = cdpManager.getTCR(price); + vm.revertTo(liquidationSnapshotId); - // Verify it worked - vm.expectEmit(false, false, false, true); - emit TCRNotified(EXPECTED_TCR_SECOND_LIQ_TCR); - cdpManager.partiallyLiquidate(cdps[0], 1e18, cdps[0], cdps[0]); + // Verify it worked + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_TCR_SECOND_LIQ_TCR); + cdpManager.partiallyLiquidate(cdps[0], 1e18, cdps[0], cdps[0]); } // == Liquidate 3 == // { - console.log("Liq 3"); + console.log("Liq 3"); - // Re-revert for next Op - vm.revertTo(liquidationSnapshotId); + // Re-revert for next Op + vm.revertTo(liquidationSnapshotId); - // Try liquidating a cdp via the list (1) - cdpManager.liquidateCdps(1); - uint256 EXPECTED_TCR_THIRD_LIQ_TCR = cdpManager.getTCR(price); - vm.revertTo(liquidationSnapshotId); + // Try liquidating a cdp via the list (1) + cdpManager.liquidateCdps(1); + uint256 EXPECTED_TCR_THIRD_LIQ_TCR = cdpManager.getTCR(price); + vm.revertTo(liquidationSnapshotId); - // Verify it worked - vm.expectEmit(false, false, false, true); - emit TCRNotified(EXPECTED_TCR_THIRD_LIQ_TCR); - cdpManager.liquidateCdps(1); + // Verify it worked + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_TCR_THIRD_LIQ_TCR); + cdpManager.liquidateCdps(1); } // == Liquidate 4 == // { - console.log("Liq 4"); - - // Re-revert for next Op - vm.revertTo(liquidationSnapshotId); - - // Try liquidating a cdp via the list (2) - bytes32[] memory cdpsToLiquidateBatch = new bytes32[](1); - cdpsToLiquidateBatch[0] = cdps[0]; - cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); - uint256 EXPECTED_TCR_FOURTH_LIQ_TCR = cdpManager.getTCR(price); - vm.revertTo(liquidationSnapshotId); - - vm.expectEmit(false, false, false, true); - emit TCRNotified(EXPECTED_TCR_FOURTH_LIQ_TCR); - cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); - vm.revertTo(liquidationSnapshotId); + console.log("Liq 4"); + + // Re-revert for next Op + vm.revertTo(liquidationSnapshotId); + + // Try liquidating a cdp via the list (2) + bytes32[] memory cdpsToLiquidateBatch = new bytes32[](1); + cdpsToLiquidateBatch[0] = cdps[0]; + cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); + uint256 EXPECTED_TCR_FOURTH_LIQ_TCR = cdpManager.getTCR(price); + vm.revertTo(liquidationSnapshotId); + + vm.expectEmit(false, false, false, true); + emit TCRNotified(EXPECTED_TCR_FOURTH_LIQ_TCR); + cdpManager.batchLiquidateCdps(cdpsToLiquidateBatch); + vm.revertTo(liquidationSnapshotId); } vm.stopPrank(); From 57ee143bf454e28bc01b7d93b689ff6e3b5c3d61 Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Mon, 28 Aug 2023 16:41:26 -0400 Subject: [PATCH 58/68] redemptions grace period opt draft --- packages/contracts/contracts/CdpManager.sol | 148 ++++++++++++------ .../contracts/Interfaces/ICdpManagerData.sol | 5 + 2 files changed, 106 insertions(+), 47 deletions(-) diff --git a/packages/contracts/contracts/CdpManager.sol b/packages/contracts/contracts/CdpManager.sol index c11404c70..2d77f2b26 100644 --- a/packages/contracts/contracts/CdpManager.sol +++ b/packages/contracts/contracts/CdpManager.sol @@ -181,21 +181,33 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { if (newDebt == 0) { // No debt remains, close CDP // No debt left in the Cdp, therefore the cdp gets closed + { + address _borrower = sortedCdps.getOwnerAddress(_redeemColFromCdp._cdpId); + uint _liquidatorRewardShares = Cdps[_redeemColFromCdp._cdpId].liquidatorRewardShares; + + singleRedemption.collSurplus = newColl; // Collateral surplus processed on full redemption + singleRedemption.liquidatorRewardShares = _liquidatorRewardShares; + singleRedemption.fullRedemption = true; + + _closeCdpByRedemption( + _redeemColFromCdp._cdpId, + 0, + newColl, + _liquidatorRewardShares, + _borrower + ); - address _borrower = sortedCdps.getOwnerAddress(_redeemColFromCdp._cdpId); - _redeemCloseCdp(_redeemColFromCdp._cdpId, 0, newColl, _borrower); - singleRedemption.fullRedemption = true; - - emit CdpUpdated( - _redeemColFromCdp._cdpId, - _borrower, - _oldDebtAndColl.entireDebt, - _oldDebtAndColl.entireColl, - 0, - 0, - 0, - CdpOperation.redeemCollateral - ); + emit CdpUpdated( + _redeemColFromCdp._cdpId, + _borrower, + _oldDebtAndColl.entireDebt, + _oldDebtAndColl.entireColl, + 0, + 0, + 0, + CdpOperation.redeemCollateral + ); + } } else { // Debt remains, reinsert CDP uint newNICR = LiquityMath._computeNominalCR(newColl, newDebt); @@ -249,14 +261,13 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { * The debt recorded on the cdp's struct is zero'd elswhere, in _closeCdp. * Any surplus stETH left in the cdp, is sent to the Coll surplus pool, and can be later claimed by the borrower. */ - function _redeemCloseCdp( + function _closeCdpByRedemption( bytes32 _cdpId, // TODO: Remove? uint _EBTC, - uint _stEth, + uint _collSurplus, + uint _liquidatorRewardShares, address _borrower ) internal { - uint _liquidatorRewardShares = Cdps[_cdpId].liquidatorRewardShares; - _removeStake(_cdpId); _closeCdpWithoutRemovingSortedCdps(_cdpId, Status.closedByRedemption); @@ -264,31 +275,35 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { activePool.decreaseEBTCDebt(_EBTC); // Register stETH surplus from upcoming transfers of stETH collateral and liquidator reward shares - collSurplusPool.accountSurplus(_borrower, _stEth + _liquidatorRewardShares); + collSurplusPool.accountSurplus(_borrower, _collSurplus + _liquidatorRewardShares); // CEI: send stETH coll and liquidator reward shares from Active Pool to CollSurplus Pool activePool.sendStEthCollAndLiquidatorReward( address(collSurplusPool), - _stEth, + _collSurplus, _liquidatorRewardShares ); } + /// @notice Returns true if the CdpId specified is the lowest-ICR Cdp in the linked list that still has MCR > ICR + /// @dev Returns false if the specified CdpId hint is blank + /// @dev Returns false if the specified CdpId hint doesn't exist in the list + /// @dev Returns false if the ICR of the specified CdpId is < MCR + /// @dev Returns true if the specified CdpId is not blank, exists in the list, has an ICR > MCR, and the next lower Cdp in the list is either blank or has an ICR < MCR. function _isValidFirstRedemptionHint( - ISortedCdps _sortedCdps, bytes32 _firstRedemptionHint, uint _price ) internal view returns (bool) { if ( - _firstRedemptionHint == _sortedCdps.nonExistId() || - !_sortedCdps.contains(_firstRedemptionHint) || + _firstRedemptionHint == sortedCdps.nonExistId() || + !sortedCdps.contains(_firstRedemptionHint) || getCurrentICR(_firstRedemptionHint, _price) < MCR ) { return false; } - bytes32 nextCdp = _sortedCdps.getNext(_firstRedemptionHint); - return nextCdp == _sortedCdps.nonExistId() || getCurrentICR(nextCdp, _price) < MCR; + bytes32 nextCdp = sortedCdps.getNext(_firstRedemptionHint); + return nextCdp == sortedCdps.nonExistId() || getCurrentICR(nextCdp, _price) < MCR; } /** @@ -341,12 +356,22 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { _applyPendingGlobalState(); // Apply state, we will checkLiquidateCoolDownAndReset at end of function totals.price = priceFeed.fetchPrice(); - _requireTCRoverMCR(totals.price); + { + ( + uint tcrAtStart, + uint totalCollSharesAtStart, + uint totalEBTCSupplyAtStart + ) = _getTCRWithTotalCollAndDebt(totals.price); + totals.tcrAtStart = tcrAtStart; + totals.totalCollSharesAtStart = totalCollSharesAtStart; + totals.totalEBTCSupplyAtStart = totalEBTCSupplyAtStart; + } + + _requireTCRoverMCR(totals.price, totals.tcrAtStart); _requireAmountGreaterThanZero(_EBTCamount); require(redemptionsPaused == false, "CdpManager: Redemptions Paused"); - totals.totalEBTCSupplyAtStart = _getEntireSystemDebt(); _requireEBTCBalanceCoversRedemptionAndWithinSupply( ebtcToken, msg.sender, @@ -358,7 +383,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { address currentBorrower; bytes32 _cId = _firstRedemptionHint; - if (_isValidFirstRedemptionHint(sortedCdps, _firstRedemptionHint, totals.price)) { + if (_isValidFirstRedemptionHint(_firstRedemptionHint, totals.price)) { currentBorrower = sortedCdps.existCdpOwners(_firstRedemptionHint); } else { _cId = sortedCdps.getLast(); @@ -375,16 +400,17 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { if (_maxIterations == 0) { _maxIterations = type(uint256).max; } + bytes32 _firstRedeemed = _cId; bytes32 _lastRedeemed = _cId; - uint _fullRedeemed; + uint _numCdpsFullyRedeemed; + + /** + Core Redemption Loop + */ while (currentBorrower != address(0) && totals.remainingEBTC > 0 && _maxIterations > 0) { - _maxIterations--; // Save the address of the Cdp preceding the current one, before potentially modifying the list { - bytes32 _nextId = sortedCdps.getPrev(_cId); - address nextUserToCheck = sortedCdps.getOwnerAddress(_nextId); - _applyPendingState(_cId); LocalVariables_RedeemCollateralFromCdp @@ -405,25 +431,32 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { totals.totalEBTCToRedeem = totals.totalEBTCToRedeem + singleRedemption.eBtcToRedeem; totals.totalETHDrawn = totals.totalETHDrawn + singleRedemption.stEthToRecieve; - totals.remainingEBTC = totals.remainingEBTC - singleRedemption.eBtcToRedeem; - currentBorrower = nextUserToCheck; + totals.totalCollSharesSurplus = + totals.totalCollSharesSurplus + + singleRedemption.collSurplus; + if (singleRedemption.fullRedemption) { _lastRedeemed = _cId; - _fullRedeemed = _fullRedeemed + 1; + _numCdpsFullyRedeemed = _numCdpsFullyRedeemed + 1; } + + bytes32 _nextId = sortedCdps.getPrev(_cId); + address nextUserToCheck = sortedCdps.getOwnerAddress(_nextId); + currentBorrower = nextUserToCheck; _cId = _nextId; } + _maxIterations--; } require(totals.totalETHDrawn > 0, "CdpManager: Unable to redeem any amount"); // remove from sortedCdps - if (_fullRedeemed == 1) { + if (_numCdpsFullyRedeemed == 1) { sortedCdps.remove(_firstRedeemed); - } else if (_fullRedeemed > 1) { + } else if (_numCdpsFullyRedeemed > 1) { bytes32[] memory _toRemoveIds = _getCdpIdsToRemove( _lastRedeemed, - _fullRedeemed, + _numCdpsFullyRedeemed, _firstRedeemed ); sortedCdps.batchRemove(_toRemoveIds); @@ -450,16 +483,20 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { // New debt = debt - totalEBTCToRedeem totalEBTCSupplyAtStart // Compute TCR and then notify self { - // TODO: RE-CHECK!! // TODO: Ideally bring up || Can do by adding to struct - uint256 totalCollAtStart = collateral.getPooledEthByShares(getEntireSystemColl()); // TODO: Desynch issue with this one as well // TODO: CollSurplus is handled early so it may not be tracked unless we ask the AP for it's current balance // TODO: INVESTIGATE BALANCES FULLY - uint256 newTotalColl = totalCollAtStart - totals.totalETHDrawn; + uint256 newTotalColl = totals.totalCollSharesAtStart - + totals.totalETHDrawn - + totals.totalCollSharesSurplus; + + uint newTotalStEthBalance = collateral.getPooledEthByShares(newTotalColl); + uint256 newTotalDebt = totals.totalEBTCSupplyAtStart - totals.totalEBTCToRedeem; + // Compute new TCR with these - uint newTCR = LiquityMath._computeCR(newTotalColl, newTotalDebt, totals.price); + uint newTCR = LiquityMath._computeCR(newTotalStEthBalance, newTotalDebt, totals.price); if (newTCR < CCR) { // Notify RM @@ -484,8 +521,25 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { // CEI: Send the stETH drawn to the redeemer activePool.sendStEthColl(msg.sender, totals.ETHToSendToRedeemer); - // TODO: an alternative is we could track a variable on the activePool and avoid the transfer, for claim at-will be feeRecipient - // Then we can avoid the whole feeRecipient contract in every other contract. It can then be governable and switched out. ActivePool can handle sending any extra metadata to the recipient + // TODO: Debug checks, to remove + require( + activePool.getStEthColl() == + totals.totalCollSharesAtStart - totals.totalETHDrawn - totals.totalCollSharesSurplus, + "live getStEthColl after transfers does not match Grace Period accounting" + ); + require( + activePool.getEBTCDebt() == totals.totalEBTCSupplyAtStart - totals.totalEBTCToRedeem, + "lives getEBTCDebt after transfers does not match Grace Period accounting" + ); + require( + getEntireSystemColl() == + totals.totalCollSharesAtStart - totals.totalETHDrawn - totals.totalCollSharesSurplus, + "live getEntireSystemColl after transfers does not match Grace Period accounting" + ); + require( + _getEntireSystemDebt() == totals.totalEBTCSupplyAtStart - totals.totalEBTCToRedeem, + "lives _getEntireSystemDebt after transfers does not match Grace Period accounting" + ); } // --- Helper functions --- @@ -736,8 +790,8 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { require(_amount > 0, "CdpManager: Amount must be greater than zero"); } - function _requireTCRoverMCR(uint _price) internal view { - require(_getTCR(_price) >= MCR, "CdpManager: Cannot redeem when TCR < MCR"); + function _requireTCRoverMCR(uint _price, uint _TCR) internal view { + require(_TCR >= MCR, "CdpManager: Cannot redeem when TCR < MCR"); } function _requireAfterBootstrapPeriod() internal view { diff --git a/packages/contracts/contracts/Interfaces/ICdpManagerData.sol b/packages/contracts/contracts/Interfaces/ICdpManagerData.sol index 725ad3030..4e1ee8291 100644 --- a/packages/contracts/contracts/Interfaces/ICdpManagerData.sol +++ b/packages/contracts/contracts/Interfaces/ICdpManagerData.sol @@ -195,16 +195,21 @@ interface ICdpManagerData is IRmLiquidationsChecker { uint remainingEBTC; uint totalEBTCToRedeem; uint totalETHDrawn; + uint totalCollSharesSurplus; uint ETHFee; uint ETHToSendToRedeemer; uint decayedBaseRate; uint price; uint totalEBTCSupplyAtStart; + uint totalCollSharesAtStart; + uint tcrAtStart; } struct SingleRedemptionValues { uint eBtcToRedeem; uint stEthToRecieve; + uint collSurplus; + uint liquidatorRewardShares; bool cancelledPartial; bool fullRedemption; } From 2de76b2c40900423841d11b3420e40bb991e2aad Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Mon, 28 Aug 2023 17:51:32 -0400 Subject: [PATCH 59/68] redemption collSurplus check test shell --- .../foundry_test/CDPManager.redemptions.t.sol | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/contracts/foundry_test/CDPManager.redemptions.t.sol b/packages/contracts/foundry_test/CDPManager.redemptions.t.sol index baac5b5fc..afc3899f9 100644 --- a/packages/contracts/foundry_test/CDPManager.redemptions.t.sol +++ b/packages/contracts/foundry_test/CDPManager.redemptions.t.sol @@ -255,6 +255,29 @@ contract CDPManagerRedemptionsTest is eBTCBaseInvariants { vm.stopPrank(); } + function test_SingleRedemptionCollSurplus() public { + // setup healthy whale Cdp + // set 1 Cdp that is valid to redeem + // calculate expected collSurplus from redemption of Cdp + // calculate expected system debt after valid redemption + // calculate expected system coll after valid redemption + // fully redeem single Cdp + // borrower of Redeemed Cdp should have expected collSurplus available + // confirm expected system debt and coll + } + + function test_MultipleRedemptionCollSurplus() public { + // setup healthy whale Cdp + // set 3 Cdps that are valid to redeem at same ICR, different borrowers + // calculate expected collSurplus from full redemption of Cdps + // calculate expected system debt after all valid redemptions + // calculate expected system coll after all valid redemptions + // fully redeem 2 Cdps, partially redeem the third + // borrowers of full Redeemed Cdps should have expected collSurplus available + // borrowers of partially redeemed Cdp should have no collSurplus available + // confirm expected system debt and coll + } + function _singleCdpRedemptionSetup() internal returns (address user, bytes32 userCdpId) { uint debt = 2e17; user = _utils.getNextUserAddress(); From 2f9bac14171d3c57f99d3ed623bfc3ef87483188 Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Mon, 28 Aug 2023 18:19:50 -0400 Subject: [PATCH 60/68] clean up optimization code --- packages/contracts/contracts/CdpManager.sol | 34 +++------------- .../contracts/contracts/CdpManagerStorage.sol | 22 +++++++++++ .../contracts/LiquidationLibrary.sol | 39 +++---------------- 3 files changed, 32 insertions(+), 63 deletions(-) diff --git a/packages/contracts/contracts/CdpManager.sol b/packages/contracts/contracts/CdpManager.sol index 2d77f2b26..c252a3722 100644 --- a/packages/contracts/contracts/CdpManager.sol +++ b/packages/contracts/contracts/CdpManager.sol @@ -477,35 +477,11 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { totals.ETHToSendToRedeemer = totals.totalETHDrawn - totals.ETHFee; - // == GRACE PERIOD == // - // TODO: E part of code is here for new TCR and notification - // New coll = eth - totalETHDrawn, - // New debt = debt - totalEBTCToRedeem totalEBTCSupplyAtStart - // Compute TCR and then notify self - { - // TODO: Desynch issue with this one as well - // TODO: CollSurplus is handled early so it may not be tracked unless we ask the AP for it's current balance - // TODO: INVESTIGATE BALANCES FULLY - - uint256 newTotalColl = totals.totalCollSharesAtStart - - totals.totalETHDrawn - - totals.totalCollSharesSurplus; - - uint newTotalStEthBalance = collateral.getPooledEthByShares(newTotalColl); - - uint256 newTotalDebt = totals.totalEBTCSupplyAtStart - totals.totalEBTCToRedeem; - - // Compute new TCR with these - uint newTCR = LiquityMath._computeCR(newTotalStEthBalance, newTotalDebt, totals.price); - - if (newTCR < CCR) { - // Notify RM - _notifyBeginRM(newTCR); - } else { - // Notify outside RM - _notifyEndRM(newTCR); - } - } + _syncRecoveryModeGracePeriod( + totals.totalCollSharesAtStart - totals.totalETHDrawn - totals.totalCollSharesSurplus, + totals.totalEBTCSupplyAtStart - totals.totalEBTCToRedeem, + totals.price + ); emit Redemption(_EBTCamount, totals.totalEBTCToRedeem, totals.totalETHDrawn, totals.ETHFee); diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 1cc9055ef..728a308fb 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -113,6 +113,28 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut } } + /// @dev Sync grace period status given system collShares and debt amounts + function _syncRecoveryModeGracePeriod( + uint systemCollShares, + uint systemDebt, + uint price + ) internal { + // Compute new TCR with these + uint newTCR = LiquityMath._computeCR( + collateral.getPooledEthByShares(systemCollShares), + systemDebt, + price + ); + + if (newTCR < CCR) { + // Notify RM + _notifyBeginRM(newTCR); + } else { + // Notify outside RM + _notifyEndRM(newTCR); + } + } + string public constant NAME = "CdpManager"; // --- Connected contract declarations --- diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 7202dec4f..901c3c4d8 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -536,40 +536,11 @@ contract LiquidationLibrary is CdpManagerStorage { emit Liquidation(totalDebtToBurn, totalColToSend, totalColReward); - // E: Total Debt = Debt - TotalDebtToBurn - // E: Total Coll = Coll - totalColToSend - // From here we can determine if we're in RM or not - - // We need the totalDebt - // We need the totalColl - // Then compute - // TODO: WE CALL THIS TWICE FOR NOW, FIX AFTER TESTED - { - // Most likely we also had the rest of the stuff - - // TODO: RE-CHECK!! // TODO: Ideally bring up || Can do by adding to struct - // TODO: Virtual Accounting here requires: CollNew = CollAtStart - Surplus - TotalColToSend - // TODO: Verify - - uint256 newTotalShares = systemInitialCollShares - totalColToSend - totalColSurplus; - uint256 newTotalDebt = systemInitialDebt - totalDebtToBurn; - // Compute new TCR with these - uint newTCR = LiquityMath._computeCR( - collateral.getPooledEthByShares(newTotalShares), - newTotalDebt, - price - ); - - console.log("newTCR", newTCR); - - if (newTCR < CCR) { - // Notify RM - _notifyBeginRM(newTCR); - } else { - // Notify outside RM - _notifyEndRM(newTCR); - } - } + _syncRecoveryModeGracePeriod( + systemInitialCollShares - totalColToSend - totalColSurplus, + systemInitialDebt - totalDebtToBurn, + price + ); // redistribute debt if any if (totalDebtToRedistribute > 0) { From d6d601ef24bbe74d3fca5bb0a3959773f95aea99 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Tue, 29 Aug 2023 11:38:24 +0800 Subject: [PATCH 61/68] add tests for redemption coll suplus --- .../contracts/LiquidationLibrary.sol | 1 - .../foundry_test/CDPManager.redemptions.t.sol | 139 +++++++++++++++++- 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 901c3c4d8..ee9b043c4 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.17; -import "forge-std/Test.sol"; import "./Interfaces/ICdpManagerData.sol"; import "./Interfaces/ICollSurplusPool.sol"; import "./Interfaces/IEBTCToken.sol"; diff --git a/packages/contracts/foundry_test/CDPManager.redemptions.t.sol b/packages/contracts/foundry_test/CDPManager.redemptions.t.sol index afc3899f9..1d9063a55 100644 --- a/packages/contracts/foundry_test/CDPManager.redemptions.t.sol +++ b/packages/contracts/foundry_test/CDPManager.redemptions.t.sol @@ -9,6 +9,8 @@ contract CDPManagerRedemptionsTest is eBTCBaseInvariants { // Storage array of cdpIDs when impossible to calculate array size bytes32[] cdpIds; uint public mintAmount = 1e18; + uint private ICR_COMPARE_TOLERANCE = 1000000; //in the scale of 1e18 + address payable[] users; function setUp() public override { super.setUp(); @@ -255,7 +257,7 @@ contract CDPManagerRedemptionsTest is eBTCBaseInvariants { vm.stopPrank(); } - function test_SingleRedemptionCollSurplus() public { + function test_SingleRedemptionCollSurplus(uint _toRedeemICR) public { // setup healthy whale Cdp // set 1 Cdp that is valid to redeem // calculate expected collSurplus from redemption of Cdp @@ -264,9 +266,42 @@ contract CDPManagerRedemptionsTest is eBTCBaseInvariants { // fully redeem single Cdp // borrower of Redeemed Cdp should have expected collSurplus available // confirm expected system debt and coll + address user = _utils.getNextUserAddress(); + + // ensure redemption ICR falls in reasonable range + vm.assume(_toRedeemICR > cdpManager.MCR()); + vm.assume(_toRedeemICR <= cdpManager.CCR()); + + uint _originalPrice = priceFeedMock.fetchPrice(); + + // ensure there is more than one CDP + _singleCdpSetupWithICR(user, 200e16); + (, bytes32 userCdpid) = _singleCdpSetupWithICR(user, _toRedeemICR); + uint _totalCollBefore = cdpManager.getEntireSystemColl(); + uint _totalDebtBefore = cdpManager.getEntireSystemDebt(); + uint _redeemedDebt = cdpManager.getCdpDebt(userCdpid); + uint _cdpColl = cdpManager.getCdpColl(userCdpid); + uint _cdpLiqReward = cdpManager.getCdpLiquidatorRewardShares(userCdpid); + + // perform redemption + _performRedemption(user, _redeemedDebt, userCdpid, userCdpid); + + { + _checkFullyRedeemedCdp(userCdpid, user, _cdpColl, _redeemedDebt); + _utils.assertApproximateEq( + _totalCollBefore - _cdpColl, + cdpManager.getEntireSystemColl(), + ICR_COMPARE_TOLERANCE + ); + assertEq( + _totalDebtBefore - _redeemedDebt, + cdpManager.getEntireSystemDebt(), + "total debt mismatch after redemption!!!" + ); + } } - function test_MultipleRedemptionCollSurplus() public { + function test_MultipleRedemptionCollSurplus(uint _toRedeemICR) public { // setup healthy whale Cdp // set 3 Cdps that are valid to redeem at same ICR, different borrowers // calculate expected collSurplus from full redemption of Cdps @@ -276,6 +311,50 @@ contract CDPManagerRedemptionsTest is eBTCBaseInvariants { // borrowers of full Redeemed Cdps should have expected collSurplus available // borrowers of partially redeemed Cdp should have no collSurplus available // confirm expected system debt and coll + users = _utils.createUsers(3); + + // ensure redemption ICR falls in reasonable range + vm.assume(_toRedeemICR > cdpManager.MCR()); + vm.assume(_toRedeemICR <= cdpManager.CCR()); + + uint _originalPrice = priceFeedMock.fetchPrice(); + + // ensure there is more than one CDP + _singleCdpSetupWithICR(users[0], 200e16); + (, bytes32 userCdpid1) = _singleCdpSetupWithICR(users[0], _toRedeemICR); + (, bytes32 userCdpid2) = _singleCdpSetupWithICR(users[1], _toRedeemICR + 2e16); + (, bytes32 userCdpid3) = _singleCdpSetupWithICR(users[2], _toRedeemICR + 4e16); + uint _totalCollBefore = cdpManager.getEntireSystemColl(); + uint _totalDebtBefore = cdpManager.getEntireSystemDebt(); + uint _cdpDebt1 = cdpManager.getCdpDebt(userCdpid1); + uint _cdpDebt2 = cdpManager.getCdpDebt(userCdpid2); + uint _cdpDebt3 = cdpManager.getCdpDebt(userCdpid3); + uint _cdpColl1 = cdpManager.getCdpColl(userCdpid1); + uint _cdpColl2 = cdpManager.getCdpColl(userCdpid2); + uint _redeemedDebt = _cdpDebt1 + _cdpDebt2 + (_cdpDebt3 / 2); + deal(address(eBTCToken), users[0], _redeemedDebt); // sugardaddy redeemer + + // perform redemption + _performRedemption(users[0], _redeemedDebt, userCdpid1, userCdpid1); + + { + _checkFullyRedeemedCdp(userCdpid1, users[0], _cdpColl1, _cdpDebt1); + _checkFullyRedeemedCdp(userCdpid2, users[1], _cdpColl2, _cdpDebt2); + _checkPartiallyRedeemedCdp(userCdpid3, users[2]); + _utils.assertApproximateEq( + _totalCollBefore - + _cdpColl1 - + _cdpColl2 - + (((_cdpDebt3 * 1e18) / 2) / _originalPrice), + cdpManager.getEntireSystemColl(), + ICR_COMPARE_TOLERANCE + ); + assertEq( + _totalDebtBefore - _redeemedDebt, + cdpManager.getEntireSystemDebt(), + "total debt mismatch after redemption!!!" + ); + } } function _singleCdpRedemptionSetup() internal returns (address user, bytes32 userCdpId) { @@ -287,4 +366,60 @@ contract CDPManagerRedemptionsTest is eBTCBaseInvariants { eBTCToken.approve(address(cdpManager), type(uint256).max); vm.stopPrank(); } + + function _singleCdpSetupWithICR(address _usr, uint _icr) internal returns (address, bytes32) { + uint _price = priceFeedMock.fetchPrice(); + uint _coll = cdpManager.MIN_NET_COLL() * 2; + uint _debt = (_coll * _price) / _icr; + bytes32 _cdpId = _openTestCDP(_usr, _coll + cdpManager.LIQUIDATOR_REWARD(), _debt); + uint _cdpICR = cdpManager.getCurrentICR(_cdpId, _price); + _utils.assertApproximateEq(_icr, _cdpICR, ICR_COMPARE_TOLERANCE); // in the scale of 1e18 + return (_usr, _cdpId); + } + + function _performRedemption( + address _redeemer, + uint _redeemedDebt, + bytes32 _upperPartialRedemptionHint, + bytes32 _lowerPartialRedemptionHint + ) internal { + (bytes32 firstRedemptionHint, uint partialRedemptionHintNICR, , ) = hintHelpers + .getRedemptionHints(_redeemedDebt, priceFeedMock.fetchPrice(), 0); + vm.prank(_redeemer); + cdpManager.redeemCollateral( + _redeemedDebt, + firstRedemptionHint, + _upperPartialRedemptionHint, + _lowerPartialRedemptionHint, + partialRedemptionHintNICR, + 0, + 1e18 + ); + } + + function _checkFullyRedeemedCdp( + bytes32 _cdpId, + address _cdpOwner, + uint _cdpColl, + uint _cdpDebt + ) internal { + uint _expectedCollSurplus = _cdpColl + + cdpManager.LIQUIDATOR_REWARD() - + ((_cdpDebt * 1e18) / priceFeedMock.fetchPrice()); + assertTrue(sortedCdps.contains(_cdpId) == false); + assertEq( + _expectedCollSurplus, + collSurplusPool.getCollateral(_cdpOwner), + "coll surplus balance mismatch after full redemption!!!" + ); + } + + function _checkPartiallyRedeemedCdp(bytes32 _cdpId, address _cdpOwner) internal { + assertTrue(sortedCdps.contains(_cdpId) == true); + assertEq( + 0, + collSurplusPool.getCollateral(_cdpOwner), + "coll surplus not zero after partial redemption!!!" + ); + } } From 04371f2b4e4b96323ccc5b4bb6574af4f9763a71 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Tue, 29 Aug 2023 13:10:42 +0800 Subject: [PATCH 62/68] fix test_LiqPremiumWithCdpOvercollateralized_AboveMaxPremium() --- packages/contracts/foundry_test/CdpManager.Liquidation.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol b/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol index bde7eaf0a..6f8c23423 100644 --- a/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol +++ b/packages/contracts/foundry_test/CdpManager.Liquidation.t.sol @@ -426,7 +426,7 @@ contract CdpManagerLiquidationTest is eBTCBaseInvariants { // prepare sequence liquidation address _liquidator = users[users.length - 1]; deal(address(eBTCToken), _liquidator, cdpManager.getEntireSystemDebt()); // sugardaddy liquidator - // FIXME _waitUntilRMColldown(); + _waitUntilRMColldown(); uint _liquidatorBalBefore = collateral.balanceOf(_liquidator); uint _expectedReward = cdpManager.getCdpColl(cdpIds[0]) + @@ -625,7 +625,7 @@ contract CdpManagerLiquidationTest is eBTCBaseInvariants { // prepare liquidation address _liquidator = users[users.length - 1]; deal(address(eBTCToken), _liquidator, cdpManager.getCdpDebt(userCdpid)); // sugardaddy liquidator - // FIXME _waitUntilRMColldown(); + _waitUntilRMColldown(); uint _liquidatorBalBefore = collateral.balanceOf(_liquidator); uint _expectedReward = ((cdpManager.getCdpDebt(userCdpid) * cdpManager.MCR()) / _newPrice) + From bcf4ddfbc60ca058433be64073bf626c45273e03 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Tue, 29 Aug 2023 13:00:07 +0200 Subject: [PATCH 63/68] chore: comments --- .../contracts/foundry_test/GracePeriod.Sync.t.sol | 13 ++++--------- packages/contracts/foundry_test/GracePeriod.t.sol | 4 ++-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol index 216e2be8e..0acec85e8 100644 --- a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol @@ -27,7 +27,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint256 price = priceFeedMock.fetchPrice(); // SKIPPED CAUSE BORING AF - // == Open == // + // == Open CDP == // console2.log("Open"); uint256 openSnap = vm.snapshot(); @@ -51,7 +51,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { bytes32 safeId = borrowerOperations.openCdp(debt1, bytes32(0), bytes32(0), coll1); vm.stopPrank(); - // Adjust + // == Adjust CDP == // console2.log("Adjust"); dealCollateral(safeUser, 12345); @@ -69,7 +69,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { vm.revertTo(adjustSnap); } - // Close + // == Close CDP == // { console2.log("Close"); uint256 closeSnapshot = vm.snapshot(); @@ -100,10 +100,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { bytes32[] memory cdps = _openRiskyCdps(1); - // Adjust and close one cdp - // TODO - - // Redeem + // == Redemptions == // // Get TCR after Redeem // Snapshot back // Then expect it to work @@ -233,8 +230,6 @@ contract GracePeriodBaseTests is eBTCBaseFixture { uint256 debt1 = 1000e18; uint256 coll1 = _utils.calculateCollAmount(debt1, _curPrice, 1.30e18); // Comfy unliquidatable - // TODO: Add a check here for TCR being computed correctly and sent properly - return _openTestCDP(safeUser, coll1, debt1); } diff --git a/packages/contracts/foundry_test/GracePeriod.t.sol b/packages/contracts/foundry_test/GracePeriod.t.sol index 3ec92c96e..f5a27370b 100644 --- a/packages/contracts/foundry_test/GracePeriod.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.t.sol @@ -20,7 +20,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // == DELAY TEST == // // Delay of 15 minutes is enforced to liquidate CDPs in RM that are not below MCR - DONE - // No Delay for the Portion of CDPs which is below MCR - TODO + // No Delay for the Portion of CDPs which is below MCR - DONE // RM Triggered via Price - DONE // RM Triggered via Split - DONE @@ -31,7 +31,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // RM untriggered via Price - DONE // RM untriggered via Split - DONE - // RM untriggered via User Operations + // RM untriggered via User Operations - DONE // All operations where the system goes off of RM should cancel the countdown /** From 42a192492ea177e5b110473e0b88a05773641220 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Tue, 29 Aug 2023 13:05:47 +0200 Subject: [PATCH 64/68] feat: cleanup debug --- packages/contracts/contracts/CdpManager.sol | 20 ------------------- .../contracts/LiquidationLibrary.sol | 2 -- 2 files changed, 22 deletions(-) diff --git a/packages/contracts/contracts/CdpManager.sol b/packages/contracts/contracts/CdpManager.sol index c252a3722..95b4af081 100644 --- a/packages/contracts/contracts/CdpManager.sol +++ b/packages/contracts/contracts/CdpManager.sol @@ -496,26 +496,6 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { // CEI: Send the stETH drawn to the redeemer activePool.sendStEthColl(msg.sender, totals.ETHToSendToRedeemer); - - // TODO: Debug checks, to remove - require( - activePool.getStEthColl() == - totals.totalCollSharesAtStart - totals.totalETHDrawn - totals.totalCollSharesSurplus, - "live getStEthColl after transfers does not match Grace Period accounting" - ); - require( - activePool.getEBTCDebt() == totals.totalEBTCSupplyAtStart - totals.totalEBTCToRedeem, - "lives getEBTCDebt after transfers does not match Grace Period accounting" - ); - require( - getEntireSystemColl() == - totals.totalCollSharesAtStart - totals.totalETHDrawn - totals.totalCollSharesSurplus, - "live getEntireSystemColl after transfers does not match Grace Period accounting" - ); - require( - _getEntireSystemDebt() == totals.totalEBTCSupplyAtStart - totals.totalEBTCToRedeem, - "lives _getEntireSystemDebt after transfers does not match Grace Period accounting" - ); } // --- Helper functions --- diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 6631dc0e2..af8bcd6fa 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -550,8 +550,6 @@ contract LiquidationLibrary is CdpManagerStorage { // CEI: ensure sending back collateral to liquidator is last thing to do activePool.sendStEthCollAndLiquidatorReward(msg.sender, totalColToSend, totalColReward); - - // checkLiquidateCoolDownAndReset(); // TODO: This works correctly, use this to compare vs new impl } // Function that calculates the amount of collateral to send to liquidator (plus incentive) and the amount of collateral surplus From e8491f33186b51ed25ebfef82bb832f335e3bd2c Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Tue, 29 Aug 2023 08:35:14 -0400 Subject: [PATCH 65/68] refactor + cleanup --- .../contracts/BorrowerOperations.sol | 14 +-- packages/contracts/contracts/CdpManager.sol | 4 +- .../contracts/contracts/CdpManagerStorage.sol | 107 +++++++----------- .../contracts/Interfaces/ICdpManagerData.sol | 4 +- ...ecker.sol => IRecoveryModeGracePeriod.sol} | 8 +- .../contracts/LiquidationLibrary.sol | 13 ++- .../contracts/foundry_test/BaseFixture.sol | 2 +- .../foundry_test/GracePeriod.Sync.t.sol | 2 +- .../contracts/foundry_test/GracePeriod.t.sol | 26 ++--- .../foundry_test/SandwhichSniper.t.sol | 2 +- packages/contracts/test/CdpManagerTest.js | 2 +- .../test/CdpManager_RecoveryModeTest.js | 26 ++--- ...ager_RecoveryMode_Batch_Liqudation_Test.js | 10 +- .../CdpManager_RecoveryMode_Cooldown_Test.js | 36 +++--- .../test/CdpManager_SimpleLiquidation_Test.js | 2 +- .../test/CdpManager_StakingSplitFee_Test.js | 2 +- 16 files changed, 117 insertions(+), 143 deletions(-) rename packages/contracts/contracts/Interfaces/{IRmLiquidationsChecker.sol => IRecoveryModeGracePeriod.sol} (62%) diff --git a/packages/contracts/contracts/BorrowerOperations.sol b/packages/contracts/contracts/BorrowerOperations.sol index dd0a0ec62..26985d81f 100644 --- a/packages/contracts/contracts/BorrowerOperations.sol +++ b/packages/contracts/contracts/BorrowerOperations.sol @@ -399,10 +399,10 @@ contract BorrowerOperations is // We check with newTCR if (newTCR < CCR) { // Notify RM - cdpManager.notifyBeginRM(newTCR); + cdpManager.notifyStartGracePeriod(newTCR); } else { // Notify Back to Normal Mode - cdpManager.notifyEndRM(newTCR); + cdpManager.notifyEndGracePeriod(newTCR); } } else { _requireICRisAboveMCR(vars.ICR); @@ -411,7 +411,7 @@ contract BorrowerOperations is // == Grace Period == // // We are not in RM, no edge case, we always stay above RM // Always Notify Back to Normal Mode - cdpManager.notifyEndRM(newTCR); + cdpManager.notifyEndGracePeriod(newTCR); } // Set the cdp struct's properties @@ -476,7 +476,7 @@ contract BorrowerOperations is // == Grace Period == // // By definition we are not in RM, notify CDPManager to ensure "Glass is on" - cdpManager.notifyEndRM(newTCR); + cdpManager.notifyEndGracePeriod(newTCR); cdpManager.removeStake(_cdpId); @@ -658,10 +658,10 @@ contract BorrowerOperations is // We check with newTCR if (_vars.newTCR < CCR) { // Notify RM - cdpManager.notifyBeginRM(_vars.newTCR); + cdpManager.notifyStartGracePeriod(_vars.newTCR); } else { // Notify Back to Normal Mode - cdpManager.notifyEndRM(_vars.newTCR); + cdpManager.notifyEndGracePeriod(_vars.newTCR); } } else { // if Normal Mode @@ -671,7 +671,7 @@ contract BorrowerOperations is // == Grace Period == // // We are not in RM, no edge case, we always stay above RM // Always Notify Back to Normal Mode - cdpManager.notifyEndRM(_vars.newTCR); + cdpManager.notifyEndGracePeriod(_vars.newTCR); } } diff --git a/packages/contracts/contracts/CdpManager.sol b/packages/contracts/contracts/CdpManager.sol index 95b4af081..cf40a1368 100644 --- a/packages/contracts/contracts/CdpManager.sol +++ b/packages/contracts/contracts/CdpManager.sol @@ -353,7 +353,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { _requireValidMaxFeePercentage(_maxFeePercentage); _requireAfterBootstrapPeriod(); - _applyPendingGlobalState(); // Apply state, we will checkLiquidateCoolDownAndReset at end of function + _applyPendingGlobalState(); // Apply state, we will syncGracePeriod at end of function totals.price = priceFeed.fetchPrice(); { @@ -477,7 +477,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy { totals.ETHToSendToRedeemer = totals.totalETHDrawn - totals.ETHFee; - _syncRecoveryModeGracePeriod( + _syncGracePeriodForGivenValues( totals.totalCollSharesAtStart - totals.totalETHDrawn - totals.totalCollSharesSurplus, totals.totalEBTCSupplyAtStart - totals.totalEBTCToRedeem, totals.price diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index 728a308fb..c6733d75c 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -20,106 +20,79 @@ import "./Dependencies/AuthNoOwner.sol"; contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, AuthNoOwner { // TODO: IMPROVE // NOTE: No packing cause it's the last var, no need for u64 - uint128 public constant UNSET_TIMESTAMP_FLAG = type(uint128).max; + uint128 public constant UNSET_TIMESTAMP = type(uint128).max; // TODO: IMPROVE THIS!!! - uint128 public lastRecoveryModeTimestamp = UNSET_TIMESTAMP_FLAG; // use max to signify + uint128 public lastGracePeriodStartTimestamp = UNSET_TIMESTAMP; // use max to signify uint128 public waitTimeFromRMTriggerToLiquidations = 15 minutes; - // TODO: Pitfal is fee split // NOTE: Solved by calling `checkLiquidateCoolDownAndReset` on external operations from BO + // TODO: Pitfal is fee split // NOTE: Solved by calling `syncGracePeriod` on external operations from BO - /// @dev Trusted Function from BO - /// @dev BO accrues totals before adjusting them - /// To maintain CEI compliance we use this trusted function - function notifyBeginRM(uint256 tcr) external { + /// @notice Start the recovery mode grace period, if the system is in RM and the grace period timestamp has not already been set + /// @dev Trusted function to allow BorrowerOperations actions to set RM Grace Period + /// @dev Assumes BorrowerOperations has correctly calculated and passed in the new system TCR + /// @dev To maintain CEI compliance we use this trusted function + function notifyStartGracePeriod(uint256 tcr) external { _requireCallerIsBorrowerOperations(); - - _notifyBeginRM(tcr); - } - - /// @dev Internal notify called by Redemptions and Liquidations - function _notifyBeginRM(uint256 tcr) internal { - emit TCRNotified(tcr); - - _beginRMLiquidationCooldown(); + _startGracePeriod(tcr); } - /// @dev Trusted Function from BO - /// @dev BO accrues totals before adjusting them - /// To maintain CEI compliance we use this trusted function - function notifyEndRM(uint256 tcr) external { + /// @notice End the recovery mode grace period, if the system is no longer in RM + /// @dev Trusted function to allow BorrowerOperations actions to set RM Grace Period + /// @dev Assumes BorrowerOperations has correctly calculated and passed in the new system TCR + /// @dev To maintain CEI compliance we use this trusted function + function notifyEndGracePeriod(uint256 tcr) external { _requireCallerIsBorrowerOperations(); - - _notifyEndRM(tcr); + _endGracePeriod(tcr); } /// @dev Internal notify called by Redemptions and Liquidations - function _notifyEndRM(uint256 tcr) internal { - emit TCRNotified(tcr); - - _stopRMLiquidationCooldown(); - } + /// @dev Specified TCR is emitted for notification pruposes regardless of whether the Grace Period timestamp is set + function _startGracePeriod(uint256 _tcr) internal { + emit TCRNotified(_tcr); - /// @dev Checks that the system is in RM - function beginRMLiquidationCooldown() external { - // Require we're in RM - uint256 price = priceFeed.fetchPrice(); - bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); - require(isRecoveryMode, "CdpManagerStorage: Not in RM"); - - _beginRMLiquidationCooldown(); - } - - function _beginRMLiquidationCooldown() internal { - // Arm the countdown - if (lastRecoveryModeTimestamp == UNSET_TIMESTAMP_FLAG) { - lastRecoveryModeTimestamp = uint128(block.timestamp); + if (lastGracePeriodStartTimestamp == UNSET_TIMESTAMP) { + lastGracePeriodStartTimestamp = uint128(block.timestamp); emit GracePeriodStart(); } } - /// @dev Checks that the system is not in RM - function stopRMLiquidationCooldown() external { - // Require we're in RM - uint256 price = priceFeed.fetchPrice(); - bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(price)); - require(!isRecoveryMode, "CdpManagerStorage: RM still ongoing"); - - _stopRMLiquidationCooldown(); - } + /// @notice Clear RM Grace Period timestamp if it has been set + /// @notice No input validation, calling function must confirm that the system is not in recovery mode to be valid + /// @dev Specified TCR is emitted for notification pruposes regardless of whether the Grace Period timestamp is set + /// @dev Internal notify called by Redemptions and Liquidations + function _endGracePeriod(uint256 _tcr) internal { + emit TCRNotified(_tcr); - function _stopRMLiquidationCooldown() internal { - // Disarm the countdown - if (lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG) { - lastRecoveryModeTimestamp = UNSET_TIMESTAMP_FLAG; + if (lastGracePeriodStartTimestamp != UNSET_TIMESTAMP) { + lastGracePeriodStartTimestamp = UNSET_TIMESTAMP; emit GracePeriodEnd(); } } /// TODO: obv optimizations - function checkLiquidateCoolDownAndReset() public { + function syncGracePeriod() public { uint256 price = priceFeed.fetchPrice(); uint256 tcr = _getTCR(price); bool isRecoveryMode = _checkRecoveryModeForTCR(tcr); - emit TCRNotified(tcr); - if (isRecoveryMode) { - _beginRMLiquidationCooldown(); + _startGracePeriod(tcr); } else { - _stopRMLiquidationCooldown(); + _endGracePeriod(tcr); } } - /// @dev Sync grace period status given system collShares and debt amounts - function _syncRecoveryModeGracePeriod( + /// @dev Set RM grace period based on specified system collShares, system debt, and price + /// @dev Variant for internal use in redemptions and liquidations + function _syncGracePeriodForGivenValues( uint systemCollShares, uint systemDebt, uint price ) internal { - // Compute new TCR with these + // Compute TCR with specified values uint newTCR = LiquityMath._computeCR( collateral.getPooledEthByShares(systemCollShares), systemDebt, @@ -127,11 +100,11 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut ); if (newTCR < CCR) { - // Notify RM - _notifyBeginRM(newTCR); + // Notify system is in RM + _startGracePeriod(newTCR); } else { - // Notify outside RM - _notifyEndRM(newTCR); + // Notify system is outside RM + _endGracePeriod(newTCR); } } @@ -500,7 +473,7 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut /// @notice Call this if you want to accrue feeSplit function syncPendingGlobalState() public { _applyPendingGlobalState(); // Apply // Could trigger RM - checkLiquidateCoolDownAndReset(); // Synch Grace Period + syncGracePeriod(); // Synch Grace Period } // Update the global index via collateral token diff --git a/packages/contracts/contracts/Interfaces/ICdpManagerData.sol b/packages/contracts/contracts/Interfaces/ICdpManagerData.sol index 4e1ee8291..e7779531e 100644 --- a/packages/contracts/contracts/Interfaces/ICdpManagerData.sol +++ b/packages/contracts/contracts/Interfaces/ICdpManagerData.sol @@ -6,11 +6,11 @@ import "./ICollSurplusPool.sol"; import "./IEBTCToken.sol"; import "./ISortedCdps.sol"; import "./IActivePool.sol"; -import "./IRmLiquidationsChecker.sol"; +import "./IRecoveryModeGracePeriod.sol"; import "../Dependencies/ICollateralTokenOracle.sol"; // Common interface for the Cdp Manager. -interface ICdpManagerData is IRmLiquidationsChecker { +interface ICdpManagerData is IRecoveryModeGracePeriod { // --- Events --- event LiquidationLibraryAddressChanged(address _liquidationLibraryAddress); diff --git a/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol b/packages/contracts/contracts/Interfaces/IRecoveryModeGracePeriod.sol similarity index 62% rename from packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol rename to packages/contracts/contracts/Interfaces/IRecoveryModeGracePeriod.sol index f622eba2e..31a2e1625 100644 --- a/packages/contracts/contracts/Interfaces/IRmLiquidationsChecker.sol +++ b/packages/contracts/contracts/Interfaces/IRecoveryModeGracePeriod.sol @@ -2,16 +2,16 @@ pragma solidity 0.8.17; // Interface for State Updates that can trigger RM Liquidations -interface IRmLiquidationsChecker { +interface IRecoveryModeGracePeriod { event TCRNotified(uint TCR); /// NOTE: Mostly for debugging to ensure synch // NOTE: Ts is implicit in events (it's added by GETH) event GracePeriodStart(); event GracePeriodEnd(); - function checkLiquidateCoolDownAndReset() external; + function syncGracePeriod() external; - function notifyBeginRM(uint256 tcr) external; + function notifyStartGracePeriod(uint256 tcr) external; - function notifyEndRM(uint256 tcr) external; + function notifyEndGracePeriod(uint256 tcr) external; } diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index af8bcd6fa..3d4dbb75d 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -74,11 +74,12 @@ contract LiquidationLibrary is CdpManagerStorage { // == Grace Period == // require( - lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG, - "Grace period not started, call `notifyBeginRM`" + lastGracePeriodStartTimestamp != UNSET_TIMESTAMP, + "Grace period not started, call `notifyStartGracePeriod`" ); require( - block.timestamp > lastRecoveryModeTimestamp + waitTimeFromRMTriggerToLiquidations, + block.timestamp > + lastGracePeriodStartTimestamp + waitTimeFromRMTriggerToLiquidations, "Grace period yet to finish" ); } // Implicit Else Case, Implies ICR < MRC, meaning the CDP is liquidatable @@ -531,7 +532,7 @@ contract LiquidationLibrary is CdpManagerStorage { emit Liquidation(totalDebtToBurn, totalColToSend, totalColReward); - _syncRecoveryModeGracePeriod( + _syncGracePeriodForGivenValues( systemInitialCollShares - totalColToSend - totalColSurplus, systemInitialDebt - totalDebtToBurn, price @@ -1024,8 +1025,8 @@ contract LiquidationLibrary is CdpManagerStorage { // ICR < TCR and we have waited enough return icr < tcr && - lastRecoveryModeTimestamp != UNSET_TIMESTAMP_FLAG && - block.timestamp > lastRecoveryModeTimestamp + waitTimeFromRMTriggerToLiquidations; + lastGracePeriodStartTimestamp != UNSET_TIMESTAMP && + block.timestamp > lastGracePeriodStartTimestamp + waitTimeFromRMTriggerToLiquidations; } function _canLiquidateInCurrentMode( diff --git a/packages/contracts/foundry_test/BaseFixture.sol b/packages/contracts/foundry_test/BaseFixture.sol index bc5857bf0..c300a47e4 100644 --- a/packages/contracts/foundry_test/BaseFixture.sol +++ b/packages/contracts/foundry_test/BaseFixture.sol @@ -504,7 +504,7 @@ contract eBTCBaseFixture is Test, BytecodeReader, LogUtils { // Grace Period, check never reverts so it's safe to use function _waitUntilRMColldown() internal { - cdpManager.checkLiquidateCoolDownAndReset(); + cdpManager.syncGracePeriod(); vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); } } diff --git a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol index 0acec85e8..f55846e88 100644 --- a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol @@ -118,7 +118,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Trigger Liquidations via Split (so price is constant) _triggerRMViaSplit(); - cdpManager.beginRMLiquidationCooldown(); + cdpManager.syncGracePeriod(); vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); // Liquidate 4x diff --git a/packages/contracts/foundry_test/GracePeriod.t.sol b/packages/contracts/foundry_test/GracePeriod.t.sol index f5a27370b..5c2081515 100644 --- a/packages/contracts/foundry_test/GracePeriod.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.t.sol @@ -200,7 +200,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Grace Period not started, expect reverts on liquidations _assertSuccessOnAllLiquidationsDegen(cdp); - cdpManager.beginRMLiquidationCooldown(); + cdpManager.syncGracePeriod(); // 15 mins not elapsed, prove these cdps still revert _assertSuccessOnAllLiquidationsDegen(cdp); @@ -214,7 +214,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Grace Period not started, expect reverts on liquidations _assertRevertOnAllLiquidations(cdps); - cdpManager.beginRMLiquidationCooldown(); + cdpManager.syncGracePeriod(); // 15 mins not elapsed, prove these cdps still revert _assertRevertOnAllLiquidations(cdps); @@ -347,7 +347,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { assertLt(TCR, 1.25e18, "!RM"); // Set grace period before action which exits RM - cdpManager.beginRMLiquidationCooldown(); + cdpManager.syncGracePeriod(); _assertRevertOnAllLiquidations(cdps); @@ -478,9 +478,9 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Grace period timestamp is now uint recoveryModeSetTimestamp = block.timestamp; assertEq( - cdpManager.lastRecoveryModeTimestamp(), + cdpManager.lastGracePeriodStartTimestamp(), block.timestamp, - "lastRecoveryModeTimestamp set time" + "lastGracePeriodStartTimestamp set time" ); // Liquidations still revert @@ -491,9 +491,9 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Grace period timestamp hasn't changed assertEq( - cdpManager.lastRecoveryModeTimestamp(), + cdpManager.lastGracePeriodStartTimestamp(), recoveryModeSetTimestamp, - "lastRecoveryModeTimestamp set time" + "lastGracePeriodStartTimestamp set time" ); // Liquidations work @@ -503,9 +503,9 @@ contract GracePeriodBaseTests is eBTCBaseFixture { function _postExitRMLiquidationChecks(bytes32[] memory cdps) internal { // Grace period timestamp is now assertEq( - cdpManager.lastRecoveryModeTimestamp(), - cdpManager.UNSET_TIMESTAMP_FLAG(), - "lastRecoveryModeTimestamp unset" + cdpManager.lastGracePeriodStartTimestamp(), + cdpManager.UNSET_TIMESTAMP(), + "lastGracePeriodStartTimestamp unset" ); // Liquidations still revert @@ -516,9 +516,9 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Grace period timestamp hasn't changed assertEq( - cdpManager.lastRecoveryModeTimestamp(), - cdpManager.UNSET_TIMESTAMP_FLAG(), - "lastRecoveryModeTimestamp unset" + cdpManager.lastGracePeriodStartTimestamp(), + cdpManager.UNSET_TIMESTAMP(), + "lastGracePeriodStartTimestamp unset" ); // Only liquidations valid under normal work diff --git a/packages/contracts/foundry_test/SandwhichSniper.t.sol b/packages/contracts/foundry_test/SandwhichSniper.t.sol index 4bfa1788c..c4e3a36f9 100644 --- a/packages/contracts/foundry_test/SandwhichSniper.t.sol +++ b/packages/contracts/foundry_test/SandwhichSniper.t.sol @@ -97,7 +97,7 @@ contract SandWhichSniperTest is eBTCBaseFixture { // We can now liquidate victim /** SANDWHICH 3 */ vm.startPrank(users[0]); - vm.expectRevert("Grace period not started, call `notifyBeginRM`"); + vm.expectRevert("Grace period not started, call `notifyStartGracePeriod`"); cdpManager.liquidate(cdpIdVictim); uint256 tcrEnd = cdpManager.getTCR(_newPrice); console.log("tcrEnd liquidation", tcrEnd); diff --git a/packages/contracts/test/CdpManagerTest.js b/packages/contracts/test/CdpManagerTest.js index 6abfd97e2..92c398040 100644 --- a/packages/contracts/test/CdpManagerTest.js +++ b/packages/contracts/test/CdpManagerTest.js @@ -1116,7 +1116,7 @@ contract('CdpManager', async accounts => { await borrowerOperations.addColl(_eCdpId, _eCdpId, _eCdpId, dec(10, 'ether'), { from: E }) // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); diff --git a/packages/contracts/test/CdpManager_RecoveryModeTest.js b/packages/contracts/test/CdpManager_RecoveryModeTest.js index 9ff2c484c..81f904f77 100644 --- a/packages/contracts/test/CdpManager_RecoveryModeTest.js +++ b/packages/contracts/test/CdpManager_RecoveryModeTest.js @@ -2840,7 +2840,7 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -2914,7 +2914,7 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_E.gt(mv._MCR) && ICR_E.lt(TCR)) // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -2982,7 +2982,7 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_E.gt(mv._MCR) && ICR_E.lt(TCR)) // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -3048,7 +3048,7 @@ contract('CdpManager - in Recovery Mode', async accounts => { const entireSystemDebtBefore = await cdpManager.getEntireSystemDebt() // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -3165,7 +3165,7 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_C_Before.gt(mv._MCR) && ICR_C_Before.lt(TCR)) // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -3536,7 +3536,7 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -3588,7 +3588,7 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -3658,7 +3658,7 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_E.gt(mv._MCR) && ICR_E.lt(TCR)) // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -3726,7 +3726,7 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_E.gt(mv._MCR) && ICR_E.lt(TCR)) // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -3792,7 +3792,7 @@ contract('CdpManager - in Recovery Mode', async accounts => { const entireSystemDebtBefore = await cdpManager.getEntireSystemDebt() // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -3847,7 +3847,7 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)) // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -3919,7 +3919,7 @@ contract('CdpManager - in Recovery Mode', async accounts => { assert.isTrue(ICR_C_Before.gt(mv._MCR) && ICR_C_Before.lt(TCR)) // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -4196,7 +4196,7 @@ contract('CdpManager - in Recovery Mode', async accounts => { // but not E as there are not enough funds in liquidator // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); diff --git a/packages/contracts/test/CdpManager_RecoveryMode_Batch_Liqudation_Test.js b/packages/contracts/test/CdpManager_RecoveryMode_Batch_Liqudation_Test.js index 49af06f24..d1e2641a9 100644 --- a/packages/contracts/test/CdpManager_RecoveryMode_Batch_Liqudation_Test.js +++ b/packages/contracts/test/CdpManager_RecoveryMode_Batch_Liqudation_Test.js @@ -77,7 +77,7 @@ contract('CdpManager - in Recovery Mode - back to normal mode in 1 tx', async ac await setup() // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -97,7 +97,7 @@ contract('CdpManager - in Recovery Mode - back to normal mode in 1 tx', async ac let _carolCdpId = await sortedCdps.cdpOfOwnerByIndex(carol, 0); // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -154,7 +154,7 @@ contract('CdpManager - in Recovery Mode - back to normal mode in 1 tx', async ac assert.isTrue(ICR_C.lt(mv._ICR100)) // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -217,7 +217,7 @@ contract('CdpManager - in Recovery Mode - back to normal mode in 1 tx', async ac await setup() // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); @@ -235,7 +235,7 @@ contract('CdpManager - in Recovery Mode - back to normal mode in 1 tx', async ac let _bobCdpId = await sortedCdps.cdpOfOwnerByIndex(bob, 0); // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); diff --git a/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js b/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js index aae7cf237..06adddc1e 100644 --- a/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js +++ b/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js @@ -89,7 +89,7 @@ contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure d assert.isTrue(toBN(_carolICRBefore.toString()).lt(_MCR)); // trigger RM cooldown - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await assertRevert(cdpManager.liquidate(_bobCdpId, {from: owner}), "Grace period yet to finish"); // cooldown only apply those [> MCR & < TCR] @@ -103,10 +103,10 @@ contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure d assert.isFalse((await sortedCdps.contains(_bobCdpId))); }) - it("openCDP() in RM: should notifyBeginRM() if RM persist or notifyEndRM() if RM exit", async() => { + it("openCDP() in RM: should notifyStartGracePeriod() if RM persist or notifyEndGracePeriod() if RM exit", async() => { await openCdp({ ICR: toBN(dec(149, 16)), extraEBTCAmount: toBN(minDebt.toString()).mul(toBN("10")), extraParams: { from: alice } }) - let _initVal = await cdpManager.lastRecoveryModeTimestamp(); + let _initVal = await cdpManager.lastGracePeriodStartTimestamp(); assert.isTrue(_initVal.gt(toBN('0'))); await openCdp({ ICR: toBN(dec(129, 16)), extraParams: { from: carol } }) @@ -115,7 +115,7 @@ contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure d await priceFeed.setPrice(_newPrice); let _tcrBefore = await cdpManager.getTCR(_newPrice); assert.isTrue(toBN(_tcrBefore.toString()).lt(_CCR)); - let _stillInitVal = await cdpManager.lastRecoveryModeTimestamp(); + let _stillInitVal = await cdpManager.lastGracePeriodStartTimestamp(); assert.isTrue(_stillInitVal.eq(_initVal)); // trigger RM cooldown by open a new CDP @@ -125,7 +125,7 @@ contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure d let _tcrInMiddle = await cdpManager.getTCR(_newPrice); console.log('_tcrInMiddle=' + _tcrInMiddle); assert.isTrue(toBN(_tcrInMiddle.toString()).lt(_CCR)); - let _rmTriggerTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + let _rmTriggerTimestamp = await cdpManager.lastGracePeriodStartTimestamp(); assert.isTrue(_rmTriggerTimestamp.gt(toBN('0'))); assert.isTrue(_rmTriggerTimestamp.lt(_initVal)); @@ -134,14 +134,14 @@ contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure d let _tcrFinal = await cdpManager.getTCR(_newPrice); console.log('_tcrFinal=' + _tcrFinal); assert.isTrue(toBN(_tcrFinal.toString()).gt(_CCR)); - let _rmExitTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + let _rmExitTimestamp = await cdpManager.lastGracePeriodStartTimestamp(); assert.isTrue(_rmExitTimestamp.eq(_initVal)); }) - it("closeCDP(): should always notifyEndRM() since TCR after close is aboce CCR", async() => { + it("closeCDP(): should always notifyEndGracePeriod() since TCR after close is aboce CCR", async() => { await openCdp({ ICR: toBN(dec(155, 16)), extraEBTCAmount: toBN(minDebt.toString()).mul(toBN("10")), extraParams: { from: alice } }) - let _initVal = await cdpManager.lastRecoveryModeTimestamp(); + let _initVal = await cdpManager.lastGracePeriodStartTimestamp(); assert.isTrue(_initVal.gt(toBN('0'))); await openCdp({ ICR: toBN(dec(126, 16)), extraParams: { from: carol } }) @@ -156,8 +156,8 @@ contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure d let _aliceICRBefore = await cdpManager.getCurrentICR(_aliceCdpId, _newPrice); console.log('_tcrBefore=' + _tcrBefore + ',_aliceICRBefore=' + _aliceICRBefore); assert.isTrue(toBN(_tcrBefore.toString()).lt(_CCR)); - await cdpManager.checkLiquidateCoolDownAndReset(); - let _rmTriggerTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + await cdpManager.syncGracePeriod(); + let _rmTriggerTimestamp = await cdpManager.lastGracePeriodStartTimestamp(); assert.isTrue(_rmTriggerTimestamp.gt(toBN('0'))); assert.isTrue(_rmTriggerTimestamp.lt(_initVal)); @@ -167,14 +167,14 @@ contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure d assert.isFalse((await sortedCdps.contains(_carolCdpId))); let _tcrAfter = await cdpManager.getTCR(_originalPrice); assert.isTrue(toBN(_tcrAfter.toString()).gt(_CCR)); - let _rmExitTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + let _rmExitTimestamp = await cdpManager.lastGracePeriodStartTimestamp(); assert.isTrue(_rmExitTimestamp.eq(_initVal)); }) - it("adjustCDP(): should notifyBeginRM() if RM persist or notifyEndRM() if RM exit", async() => { + it("adjustCDP(): should notifyStartGracePeriod() if RM persist or notifyEndGracePeriod() if RM exit", async() => { let _dustVal = 123456789; await openCdp({ ICR: toBN(dec(155, 16)), extraEBTCAmount: toBN(minDebt.toString()).mul(toBN("10")), extraParams: { from: alice } }) - let _initVal = await cdpManager.lastRecoveryModeTimestamp(); + let _initVal = await cdpManager.lastGracePeriodStartTimestamp(); assert.isTrue(_initVal.gt(toBN('0'))); await openCdp({ ICR: toBN(dec(126, 16)), extraParams: { from: carol } }) @@ -189,8 +189,8 @@ contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure d let _tcrBefore = await cdpManager.getTCR(_newPrice); let _aliceICRBefore = await cdpManager.getCurrentICR(_aliceCdpId, _newPrice); assert.isTrue(toBN(_tcrBefore.toString()).lt(_CCR)); - await cdpManager.checkLiquidateCoolDownAndReset(); - let _rmTriggerTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + await cdpManager.syncGracePeriod(); + let _rmTriggerTimestamp = await cdpManager.lastGracePeriodStartTimestamp(); assert.isTrue(_rmTriggerTimestamp.gt(toBN('0'))); assert.isTrue(_rmTriggerTimestamp.lt(_initVal)); @@ -199,14 +199,14 @@ contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure d let _tcrAfter = await cdpManager.getTCR(_originalPrice); assert.isTrue(toBN(_tcrAfter.toString()).gt(_CCR)); await borrowerOperations.withdrawEBTC(_carolCdpId, _dustVal, _carolCdpId, _carolCdpId, { from: carol } ); - let _rmExitTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + let _rmExitTimestamp = await cdpManager.lastGracePeriodStartTimestamp(); assert.isTrue(_rmExitTimestamp.eq(_initVal)); // price drops to trigger RM & cooldown again by adjust CDP (add more collateral) await priceFeed.setPrice(_newPrice); await collToken.deposit({from : carol, value: _dustVal}); await borrowerOperations.addColl(_carolCdpId, _carolCdpId, _carolCdpId, _dustVal, { from: carol } ); - let _adjustRmTriggerTimestamp = await cdpManager.lastRecoveryModeTimestamp(); + let _adjustRmTriggerTimestamp = await cdpManager.lastGracePeriodStartTimestamp(); assert.isTrue(_adjustRmTriggerTimestamp.gt(_rmTriggerTimestamp)); assert.isTrue(_adjustRmTriggerTimestamp.lt(_initVal)); @@ -215,7 +215,7 @@ contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure d await borrowerOperations.repayEBTC(_carolCdpId, _carolDebt, _carolCdpId, _carolCdpId, { from: carol } ); let _tcrFinal = await cdpManager.getTCR(_newPrice); assert.isTrue(toBN(_tcrFinal.toString()).gt(_CCR)); - let _rmExitFinal = await cdpManager.lastRecoveryModeTimestamp(); + let _rmExitFinal = await cdpManager.lastGracePeriodStartTimestamp(); assert.isTrue(_rmExitFinal.eq(_initVal)); }) diff --git a/packages/contracts/test/CdpManager_SimpleLiquidation_Test.js b/packages/contracts/test/CdpManager_SimpleLiquidation_Test.js index 44cbf5ace..9ad08df77 100644 --- a/packages/contracts/test/CdpManager_SimpleLiquidation_Test.js +++ b/packages/contracts/test/CdpManager_SimpleLiquidation_Test.js @@ -395,7 +395,7 @@ contract('CdpManager - Simple Liquidation with external liquidators', async acco assert.isTrue(toBN(prevDebtOfOwner.toString()).gt(toBN(aliceDebt.toString()))); // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); diff --git a/packages/contracts/test/CdpManager_StakingSplitFee_Test.js b/packages/contracts/test/CdpManager_StakingSplitFee_Test.js index 13c9081ec..d856da079 100644 --- a/packages/contracts/test/CdpManager_StakingSplitFee_Test.js +++ b/packages/contracts/test/CdpManager_StakingSplitFee_Test.js @@ -284,7 +284,7 @@ contract('CdpManager - Simple Liquidation with external liquidators', async acco let _collBeforeLiquidator = await collToken.balanceOf(owner); // trigger cooldown and pass the liq wait - await cdpManager.checkLiquidateCoolDownAndReset(); + await cdpManager.syncGracePeriod(); await ethers.provider.send("evm_increaseTime", [901]); await ethers.provider.send("evm_mine"); From af0584f8639fd42166fce3e969a53fcf82244b0f Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Tue, 29 Aug 2023 13:00:28 -0400 Subject: [PATCH 66/68] basic setGracePeriod functionality tests --- .../contracts/contracts/CdpManagerStorage.sol | 15 +- .../Interfaces/IRecoveryModeGracePeriod.sol | 1 + .../contracts/LiquidationLibrary.sol | 9 +- .../contracts/foundry_test/BaseFixture.sol | 5 +- .../foundry_test/CDPManager.governance.t.sol | 133 ++++++++++++++++++ .../foundry_test/GracePeriod.Sync.t.sol | 2 +- .../contracts/foundry_test/GracePeriod.t.sol | 8 +- .../CdpManager_RecoveryMode_Cooldown_Test.js | 2 +- 8 files changed, 162 insertions(+), 13 deletions(-) diff --git a/packages/contracts/contracts/CdpManagerStorage.sol b/packages/contracts/contracts/CdpManagerStorage.sol index c6733d75c..9987176f8 100644 --- a/packages/contracts/contracts/CdpManagerStorage.sol +++ b/packages/contracts/contracts/CdpManagerStorage.sol @@ -21,10 +21,11 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut // TODO: IMPROVE // NOTE: No packing cause it's the last var, no need for u64 uint128 public constant UNSET_TIMESTAMP = type(uint128).max; + uint128 public constant MINIMUM_GRACE_PERIOD = 15 minutes; // TODO: IMPROVE THIS!!! uint128 public lastGracePeriodStartTimestamp = UNSET_TIMESTAMP; // use max to signify - uint128 public waitTimeFromRMTriggerToLiquidations = 15 minutes; + uint128 public recoveryModeGracePeriod = MINIMUM_GRACE_PERIOD; // TODO: Pitfal is fee split // NOTE: Solved by calling `syncGracePeriod` on external operations from BO @@ -108,6 +109,18 @@ contract CdpManagerStorage is LiquityBase, ReentrancyGuard, ICdpManagerData, Aut } } + /// @notice Set grace period duratin + /// @notice Permissioned governance function, must set grace period duration above hardcoded minimum + /// @param _gracePeriod new grace period duration, in seconds + function setGracePeriod(uint128 _gracePeriod) external requiresAuth { + require( + _gracePeriod >= MINIMUM_GRACE_PERIOD, + "CdpManager: Grace period below minimum duration" + ); + recoveryModeGracePeriod = _gracePeriod; + emit GracePeriodSet(_gracePeriod); + } + string public constant NAME = "CdpManager"; // --- Connected contract declarations --- diff --git a/packages/contracts/contracts/Interfaces/IRecoveryModeGracePeriod.sol b/packages/contracts/contracts/Interfaces/IRecoveryModeGracePeriod.sol index 31a2e1625..4c546b5ab 100644 --- a/packages/contracts/contracts/Interfaces/IRecoveryModeGracePeriod.sol +++ b/packages/contracts/contracts/Interfaces/IRecoveryModeGracePeriod.sol @@ -8,6 +8,7 @@ interface IRecoveryModeGracePeriod { // NOTE: Ts is implicit in events (it's added by GETH) event GracePeriodStart(); event GracePeriodEnd(); + event GracePeriodSet(uint256 _recoveryModeGracePeriod); function syncGracePeriod() external; diff --git a/packages/contracts/contracts/LiquidationLibrary.sol b/packages/contracts/contracts/LiquidationLibrary.sol index 3d4dbb75d..8caf04fac 100644 --- a/packages/contracts/contracts/LiquidationLibrary.sol +++ b/packages/contracts/contracts/LiquidationLibrary.sol @@ -75,12 +75,11 @@ contract LiquidationLibrary is CdpManagerStorage { // == Grace Period == // require( lastGracePeriodStartTimestamp != UNSET_TIMESTAMP, - "Grace period not started, call `notifyStartGracePeriod`" + "CdpManager: Recovery Mode grace period not started" ); require( - block.timestamp > - lastGracePeriodStartTimestamp + waitTimeFromRMTriggerToLiquidations, - "Grace period yet to finish" + block.timestamp > lastGracePeriodStartTimestamp + recoveryModeGracePeriod, + "CdpManager: Recovery mode grace period still in effect" ); } // Implicit Else Case, Implies ICR < MRC, meaning the CDP is liquidatable @@ -1026,7 +1025,7 @@ contract LiquidationLibrary is CdpManagerStorage { return icr < tcr && lastGracePeriodStartTimestamp != UNSET_TIMESTAMP && - block.timestamp > lastGracePeriodStartTimestamp + waitTimeFromRMTriggerToLiquidations; + block.timestamp > lastGracePeriodStartTimestamp + recoveryModeGracePeriod; } function _canLiquidateInCurrentMode( diff --git a/packages/contracts/foundry_test/BaseFixture.sol b/packages/contracts/foundry_test/BaseFixture.sol index c300a47e4..bd2f28492 100644 --- a/packages/contracts/foundry_test/BaseFixture.sol +++ b/packages/contracts/foundry_test/BaseFixture.sol @@ -54,6 +54,8 @@ contract eBTCBaseFixture is Test, BytecodeReader, LogUtils { bytes4 private constant SET_BETA_SIG = bytes4(keccak256(bytes("setBeta(uint256)"))); bytes4 private constant SET_REDEMPETIONS_PAUSED_SIG = bytes4(keccak256(bytes("setRedemptionsPaused(bool)"))); + bytes4 private constant SET_GRACE_PERIOD_SIG = + bytes4(keccak256(bytes("setGracePeriod(uint128)"))); // EBTCToken bytes4 public constant MINT_SIG = bytes4(keccak256(bytes("mint(address,uint256)"))); @@ -332,6 +334,7 @@ contract eBTCBaseFixture is Test, BytecodeReader, LogUtils { authority.setRoleCapability(3, address(cdpManager), SET_MINUTE_DECAY_FACTOR_SIG, true); authority.setRoleCapability(3, address(cdpManager), SET_BETA_SIG, true); authority.setRoleCapability(3, address(cdpManager), SET_REDEMPETIONS_PAUSED_SIG, true); + authority.setRoleCapability(3, address(cdpManager), SET_GRACE_PERIOD_SIG, true); authority.setRoleCapability(4, address(priceFeedMock), SET_FALLBACK_CALLER_SIG, true); @@ -505,6 +508,6 @@ contract eBTCBaseFixture is Test, BytecodeReader, LogUtils { // Grace Period, check never reverts so it's safe to use function _waitUntilRMColldown() internal { cdpManager.syncGracePeriod(); - vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + vm.warp(block.timestamp + cdpManager.recoveryModeGracePeriod() + 1); } } diff --git a/packages/contracts/foundry_test/CDPManager.governance.t.sol b/packages/contracts/foundry_test/CDPManager.governance.t.sol index ca39a614b..3591d07ba 100644 --- a/packages/contracts/foundry_test/CDPManager.governance.t.sol +++ b/packages/contracts/foundry_test/CDPManager.governance.t.sol @@ -220,4 +220,137 @@ contract CDPManagerGovernanceTest is eBTCBaseFixture { // Confirm variable set assertEq(cdpManager.beta(), newBeta); } + + function test_CdpManagerSetGracePeriodValid_Succeeds(uint128 newGracePeriod) public { + vm.assume(newGracePeriod >= cdpManager.MINIMUM_GRACE_PERIOD()); + (bytes32 whaleCdpId, bytes32 toLiquidateCdpId, address whale) = _initSystemInRecoveryMode(); + + uint oldGracePeriod = cdpManager.recoveryModeGracePeriod(); + + vm.prank(defaultGovernance); + cdpManager.setGracePeriod(newGracePeriod); + + assertEq(cdpManager.recoveryModeGracePeriod(), newGracePeriod); + } + + /// @dev Confirm extending the grace period works + function test_CdpManagerSetGracePeriodValid_IsEnforcedForUnsetGracePeriod( + uint128 newGracePeriod + ) public { + vm.assume(newGracePeriod >= cdpManager.MINIMUM_GRACE_PERIOD() + 2); + vm.assume(newGracePeriod < type(uint128).max / 10); // prevent unrealistic overflow + + (bytes32 whaleCdpId, bytes32 toLiquidateCdpId, address whale) = _initSystemInRecoveryMode(); + + uint oldGracePeriod = cdpManager.recoveryModeGracePeriod(); + + vm.prank(defaultGovernance); + cdpManager.setGracePeriod(newGracePeriod); + + assertEq(cdpManager.recoveryModeGracePeriod(), newGracePeriod); + + _confirmGracePeriodNewDurationEnforced( + oldGracePeriod, + newGracePeriod, + whale, + toLiquidateCdpId + ); + } + + function test_CdpManagerSetGracePeriodInvalid_Reverts(uint128 newGracePeriod) public { + vm.assume(newGracePeriod < cdpManager.MINIMUM_GRACE_PERIOD()); + (bytes32 whaleCdpId, bytes32 toLiquidateCdpId, address whale) = _initSystemInRecoveryMode(); + + uint oldGracePeriod = cdpManager.recoveryModeGracePeriod(); + + vm.prank(defaultGovernance); + vm.expectRevert("CdpManager: Grace period below minimum duration"); + cdpManager.setGracePeriod(newGracePeriod); + + assertEq(cdpManager.recoveryModeGracePeriod(), oldGracePeriod); + } + + function test_CdpManagerSetGracePeriodInvalid_RevertsAndIsNotEnforcedForUnsetGracePeriod( + uint128 newGracePeriod + ) public { + vm.assume(newGracePeriod < cdpManager.MINIMUM_GRACE_PERIOD()); + (bytes32 whaleCdpId, bytes32 toLiquidateCdpId, address whale) = _initSystemInRecoveryMode(); + + uint oldGracePeriod = cdpManager.recoveryModeGracePeriod(); + + vm.prank(defaultGovernance); + vm.expectRevert("CdpManager: Grace period below minimum duration"); + cdpManager.setGracePeriod(newGracePeriod); + + assertEq(cdpManager.recoveryModeGracePeriod(), oldGracePeriod); + } + + /// @dev Assumes newGracePeriod > oldGracePeriod + function _confirmGracePeriodNewDurationEnforced( + uint oldGracePeriod, + uint newGracePeriod, + address actor, + bytes32 toLiquidateCdpId + ) public { + vm.startPrank(actor); + cdpManager.syncGracePeriod(); + uint startTimestamp = block.timestamp; + uint expectedGracePeriodExpiration = cdpManager.recoveryModeGracePeriod() + + cdpManager.lastGracePeriodStartTimestamp(); + + assertEq(startTimestamp, cdpManager.lastGracePeriodStartTimestamp()); + + // Attempt before previous duration, should fail + vm.warp(startTimestamp + oldGracePeriod + 1); + assertLt(block.timestamp, expectedGracePeriodExpiration, "after grace period complete"); + + console.log(1); + + vm.expectRevert("CdpManager: Recovery mode grace period still in effect"); + cdpManager.liquidate(toLiquidateCdpId); + + // Attempt between previous duration and new duration, should fail + vm.warp(startTimestamp + newGracePeriod - 1); + assertLt(block.timestamp, expectedGracePeriodExpiration, "after grace period complete"); + + console.log(2); + + vm.expectRevert("CdpManager: Recovery mode grace period still in effect"); + cdpManager.liquidate(toLiquidateCdpId); + + // Attempt after new duration, should succeed + vm.warp(startTimestamp + newGracePeriod + 1); + assertGe(block.timestamp, expectedGracePeriodExpiration, "before grace period complete"); + + console.log(3); + cdpManager.liquidate(toLiquidateCdpId); + + vm.stopPrank(); + } + + function _initSystemInRecoveryMode() + internal + returns (bytes32 whaleCdpId, bytes32 toLiquidateCdpId, address whale) + { + // Create a whale + whale = _utils.getNextUserAddress(); + + // 2x test price + priceFeedMock.setPrice(2 ether); + uint price = priceFeedMock.fetchPrice(); + + // Open whale CDPs at 220% + toLiquidateCdpId = _openTestCDP(whale, 11.2e18, 10e18); + whaleCdpId = _openTestCDP(whale, 1100.2e18, 1000e18); + + assertEq(cdpManager.getCurrentICR(whaleCdpId, price), 220e16, "unexpected ICR"); + assertEq(cdpManager.getTCR(price), 220e16, "unexpected TCR"); + + // original price + priceFeedMock.setPrice(1 ether); + price = priceFeedMock.fetchPrice(); + + assertEq(cdpManager.getCurrentICR(whaleCdpId, price), 110e16, "unexpected ICR"); + assertEq(cdpManager.getTCR(price), 110e16, "unexpected TCR"); + } } diff --git a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol index f55846e88..aa0467264 100644 --- a/packages/contracts/foundry_test/GracePeriod.Sync.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.Sync.t.sol @@ -119,7 +119,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { // Trigger Liquidations via Split (so price is constant) _triggerRMViaSplit(); cdpManager.syncGracePeriod(); - vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + vm.warp(block.timestamp + cdpManager.recoveryModeGracePeriod() + 1); // Liquidate 4x vm.startPrank(safeUser); diff --git a/packages/contracts/foundry_test/GracePeriod.t.sol b/packages/contracts/foundry_test/GracePeriod.t.sol index 5c2081515..041790fd6 100644 --- a/packages/contracts/foundry_test/GracePeriod.t.sol +++ b/packages/contracts/foundry_test/GracePeriod.t.sol @@ -205,7 +205,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { _assertSuccessOnAllLiquidationsDegen(cdp); // Grace Period Ended, liquidations work - vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + vm.warp(block.timestamp + cdpManager.recoveryModeGracePeriod() + 1); _assertSuccessOnAllLiquidationsDegen(cdp); } @@ -219,7 +219,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { _assertRevertOnAllLiquidations(cdps); // Grace Period Ended, liquidations work - vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + vm.warp(block.timestamp + cdpManager.recoveryModeGracePeriod() + 1); _assertAllLiquidationSuccess(cdps); } @@ -487,7 +487,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { _assertRevertOnAllLiquidations(cdps); // Grace Period Ended - vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + vm.warp(block.timestamp + cdpManager.recoveryModeGracePeriod() + 1); // Grace period timestamp hasn't changed assertEq( @@ -512,7 +512,7 @@ contract GracePeriodBaseTests is eBTCBaseFixture { _assertRevertOnAllLiquidations(cdps); // Grace Period Ended - vm.warp(block.timestamp + cdpManager.waitTimeFromRMTriggerToLiquidations() + 1); + vm.warp(block.timestamp + cdpManager.recoveryModeGracePeriod() + 1); // Grace period timestamp hasn't changed assertEq( diff --git a/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js b/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js index 06adddc1e..559d6261e 100644 --- a/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js +++ b/packages/contracts/test/CdpManager_RecoveryMode_Cooldown_Test.js @@ -49,7 +49,7 @@ contract('CdpManager - Cooldown switch with respect to Recovery Mode to ensure d _MCR = await cdpManager.MCR(); _CCR = await cdpManager.CCR(); LICR = await cdpManager.LICR(); - _coolDownWait = await cdpManager.waitTimeFromRMTriggerToLiquidations(); + _coolDownWait = await cdpManager.recoveryModeGracePeriod(); borrowerOperations = contracts.borrowerOperations; collSurplusPool = contracts.collSurplusPool; collToken = contracts.collateral; From a88766551d1ac15a4cac3482d63dd159bd674a85 Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Tue, 29 Aug 2023 13:04:01 -0400 Subject: [PATCH 67/68] setGracePeriod auth test --- .../foundry_test/CDPManager.governance.t.sol | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/contracts/foundry_test/CDPManager.governance.t.sol b/packages/contracts/foundry_test/CDPManager.governance.t.sol index 3591d07ba..d7a47f7b9 100644 --- a/packages/contracts/foundry_test/CDPManager.governance.t.sol +++ b/packages/contracts/foundry_test/CDPManager.governance.t.sol @@ -221,6 +221,18 @@ contract CDPManagerGovernanceTest is eBTCBaseFixture { assertEq(cdpManager.beta(), newBeta); } + function test_CdpManagerSetGracePeriod_Auth(uint128 newGracePeriod) public { + vm.assume(newGracePeriod >= cdpManager.MINIMUM_GRACE_PERIOD()); + (bytes32 whaleCdpId, bytes32 toLiquidateCdpId, address whale) = _initSystemInRecoveryMode(); + + uint oldGracePeriod = cdpManager.recoveryModeGracePeriod(); + + address noPermissionsUser = _utils.getNextUserAddress(); + vm.prank(noPermissionsUser); + vm.expectRevert("Auth: UNAUTHORIZED"); + cdpManager.setGracePeriod(newGracePeriod); + } + function test_CdpManagerSetGracePeriodValid_Succeeds(uint128 newGracePeriod) public { vm.assume(newGracePeriod >= cdpManager.MINIMUM_GRACE_PERIOD()); (bytes32 whaleCdpId, bytes32 toLiquidateCdpId, address whale) = _initSystemInRecoveryMode(); @@ -233,7 +245,7 @@ contract CDPManagerGovernanceTest is eBTCBaseFixture { assertEq(cdpManager.recoveryModeGracePeriod(), newGracePeriod); } - /// @dev Confirm extending the grace period works + /// @dev Confirm extending the grace period works function test_CdpManagerSetGracePeriodValid_IsEnforcedForUnsetGracePeriod( uint128 newGracePeriod ) public { From 03bac6a9e1321b18f38647fad34376cf39e9c6b4 Mon Sep 17 00:00:00 2001 From: dapp-whisperer Date: Tue, 29 Aug 2023 13:19:17 -0400 Subject: [PATCH 68/68] fix test error message --- packages/contracts/foundry_test/SandwhichSniper.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/foundry_test/SandwhichSniper.t.sol b/packages/contracts/foundry_test/SandwhichSniper.t.sol index c4e3a36f9..feb025774 100644 --- a/packages/contracts/foundry_test/SandwhichSniper.t.sol +++ b/packages/contracts/foundry_test/SandwhichSniper.t.sol @@ -97,7 +97,7 @@ contract SandWhichSniperTest is eBTCBaseFixture { // We can now liquidate victim /** SANDWHICH 3 */ vm.startPrank(users[0]); - vm.expectRevert("Grace period not started, call `notifyStartGracePeriod`"); + vm.expectRevert("CdpManager: Recovery Mode grace period not started"); cdpManager.liquidate(cdpIdVictim); uint256 tcrEnd = cdpManager.getTCR(_newPrice); console.log("tcrEnd liquidation", tcrEnd);