diff --git a/docs/specs.md b/docs/specs.md index 0fec8a9a..fcceb4c7 100644 --- a/docs/specs.md +++ b/docs/specs.md @@ -33,7 +33,6 @@ |EVK-54|Module Borrowing `repay` |If operation enabled, `repay` removes exactly `amount` of underlying tokens as liability for the `receiver` and transfers the underlying tokens from the authenticated account.

This operation is always called through the EVC.

This operation schedules the vault status check.

This operation affects:

* liability balance of the authenticated account
* total liability balance
* total balance of the underlying assets held by the vault | |EVK-58|Module Borrowing `touch` |If operation enabled, `touch` updates the vault state.

This operation is always called through the EVC.

This operation schedules the vault status check. | |EVK-2 |Module Governance |Implements the functions allowing the governor to configure the vault.
The vault uses EVC authentication for the governor, which means that governor actions can be batched together and simulated. However, the vault does not accept advanced EVC authentication methods like sub-accounts, operators or `controlCollateral`.

*Context:*

*Immediately after creation, the factory will call* `initialize` *on the proxy, passing in the creator's address as a parameter. The vault will set its governor to the creator's address. This governor can invoke methods that modify the configuration of the vault.*

*At this point, the creator should configure the vault as desired and then decide if the vault is to be governed or not.*

* *If so, the creator retains the governor role or transfers it to another address.*
* *If not, then the ownership is revoked by setting the governor to* `address(0)`*. No more governance changes can happen on this vault and it is considered finalized.*

*If limited governance is desired, the creator can transfer ownership to a smart contract that can only invoke a sub-set of governance methods, perhaps with only certain parameters, or under certain conditions.*

*Using the same code-base and factories, the Euler Vault Kit allows construction of both managed and unmanaged lending products. Managed vaults are intended to be long-lived and are therefore suitable for passive deposits. If market conditions change, an active governor can reconfigure the vault to optimize or protect users. Alternatively, unmanaged vaults are configured statically, and the users themselves (or a higher-level contract) must actively monitor for risks/opportunities and shift their deposits and positions to new vaults as necessary.*

[https://docs.euler.finance/euler-vault-kit-white-paper/#governed-vs-finalised](https://docs.euler.finance/euler-vault-kit-white-paper/#governed-vs-finalised) | -|EVK-25|Module Governance `clearLTV` |`clearLTV` allows the current governor to clear the LTV config for a given collateral keeping the storage slot initialized. | |EVK-18|Module Governance `convertFees` |If operation enabled, `convertFees` allows anyone to split accrued vault fees and transfer them to the governor-specified fee receiver and Protocol Config-specified fee receiver. The accrued vault fees are split proportionally as per Protocol Config-specified fee share that cannot exceed `MAX_PROTOCOL_FEE_SHARE`. Immediately after the fees are split, the amount of fees accrued by the vault must be set to 0.

This operation affects:

* shares balance of the governor-specified fee receiver (if it's configured)
* shares balance of the Protocol Config-specified fee receiver
* shares balance of the accumulated fees

[https://docs.euler.finance/euler-vault-kit-white-paper/#fees](https://docs.euler.finance/euler-vault-kit-white-paper/#fees) | |EVK-28|Module Governance `setCaps` |`setCaps` allows the current governor to set a supply cap and a borrow cap as per the following specification:

*Supply cap and borrow cap are 16-bit decimal floating point values:*

*\* The least significant 6 bits are the exponent*

*\* The most significant 10 bits are the mantissa, scaled by 100*

*\* The special value of 0 means limit is not set*

*\* This is so that uninitialised storage implies no limit*

*\* For an actual cap value of 0, use a zero mantissa and non-zero exponent*

When converted to assets, the supply cap cannot exceed `2 * MAX_SANE_AMOUNT` and the borrow cap cannot exceed `MAX_SANE_AMOUNT`.

[https://docs.euler.finance/euler-vault-kit-white-paper/#supply-and-borrow-caps](https://docs.euler.finance/euler-vault-kit-white-paper/#supply-and-borrow-caps) | |EVK-72|Module Governance `setConfigFlags` |`setConfigFlags` allows the vault governor to specify the additional configuration of the vault which refers to:

* debt socialization on liquidation
* asset receiver validation whenever the assets are pushed out of the vault | diff --git a/src/EVault/EVault.sol b/src/EVault/EVault.sol index 913c1c20..220000d3 100644 --- a/src/EVault/EVault.sol +++ b/src/EVault/EVault.sol @@ -227,8 +227,6 @@ contract EVault is Dispatch { function setLTV(address collateral, uint16 borrowLTV, uint16 liquidationLTV, uint32 rampDuration) public virtual override use(MODULE_GOVERNANCE) {} - function clearLTV(address collateral) public virtual override use(MODULE_GOVERNANCE) {} - function setMaxLiquidationDiscount(uint16 newDiscount) public virtual override use(MODULE_GOVERNANCE) {} function setLiquidationCoolOffTime(uint16 newCoolOffTime) public virtual override use(MODULE_GOVERNANCE) {} diff --git a/src/EVault/IEVault.sol b/src/EVault/IEVault.sol index 8923dc1f..ceb2fc24 100644 --- a/src/EVault/IEVault.sol +++ b/src/EVault/IEVault.sol @@ -483,11 +483,6 @@ interface IGovernance { /// @param rampDuration Ramp duration in seconds function setLTV(address collateral, uint16 borrowLTV, uint16 liquidationLTV, uint32 rampDuration) external; - /// @notice Completely clears LTV configuratrion, signalling the collateral is not considered safe to liquidate - /// anymore - /// @param collateral Address of the collateral - function clearLTV(address collateral) external; - /// @notice Set a new maximum liquidation discount /// @param newDiscount New maximum liquidation discount in 1e4 scale /// @dev If the discount is zero (the default), the liquidators will not be incentivized to liquidate unhealthy diff --git a/src/EVault/modules/Governance.sol b/src/EVault/modules/Governance.sol index a2c8050d..259c047e 100644 --- a/src/EVault/modules/Governance.sol +++ b/src/EVault/modules/Governance.sol @@ -56,9 +56,9 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT uint16 liquidationLTV, uint16 initialLiquidationLTV, uint48 targetTimestamp, - uint32 rampDuration, - bool initialized + uint32 rampDuration ); + /// @notice Set an interest rate model contract address /// @param newInterestRateModel Address of the new IRM event GovSetInterestRateModel(address newInterestRateModel); @@ -261,16 +261,19 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT } /// @inheritdoc IGovernance - /// @dev When the collateral asset is no longer deemed suitable to sustain debt (and not because of code issues, see - /// `clearLTV`), its LTV setting can be set to 0. Setting a zero liquidation LTV also enforces a zero borrowing LTV - /// (`newBorrowLTV <= newLiquidationLTV`). In such cases, the collateral becomes immediately ineffective for new - /// borrows. However, for liquidation purposes, the LTV can be ramped down over a period of time (`rampDuration`). - /// This ramping helps users avoid hard liquidations with maximum discounts and gives them a chance to close their - /// positions in an orderly fashion. The choice of `rampDuration` depends on market conditions assessed by the - /// governor. They may decide to forgo the ramp entirely by setting the duration to zero, presumably in light of - /// extreme market conditions, where ramping would pose a threat to the vault's solvency. In any case, when the - /// liquidation LTV reaches its target of 0, this asset will no longer support the debt, but it will still be - /// possible to liquidate it at a discount and use the proceeds to repay an unhealthy loan. + /// @dev When the collateral asset is no longer deemed suitable to sustain debt, its LTV setting can be set to 0. + /// Setting a zero liquidation LTV also enforces a zero borrowing LTV (`newBorrowLTV <= newLiquidationLTV`). + /// In such cases, the collateral becomes immediately ineffective for new borrows. However, for liquidation + /// purposes, the LTV can be ramped down over a period of time (`rampDuration`). This ramping helps users avoid hard + /// liquidations with maximum discounts and gives them a chance to close their positions in an orderly fashion. + /// The choice of `rampDuration` depends on market conditions assessed by the governor. They may decide to forgo + /// the ramp entirely by setting the duration to zero, presumably in light of extreme market conditions, where + /// ramping would pose a threat to the vault's solvency. In any case, when the liquidation LTV reaches its target + /// of 0, this asset will no longer support the debt, but it will still be possible to liquidate it at a discount + /// and use the proceeds to repay an unhealthy loan. + /// Setting the LTV to zero will not be sufficient if the collateral is found to be unsafe to call liquidation on, + /// either due to a bug or a code upgrade that allows its transfer function to make arbitrary external calls. + /// In such cases, pausing the vault and conducting an orderly wind-down is recommended. function setLTV(address collateral, uint16 borrowLTV, uint16 liquidationLTV, uint32 rampDuration) public virtual @@ -295,7 +298,7 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT vaultStorage.ltvLookup[collateral] = newLTV; - if (!currentLTV.initialized) vaultStorage.ltvList.push(collateral); + if (!currentLTV.isRecognizedCollateral()) vaultStorage.ltvList.push(collateral); if (!newLiquidationLTV.isZero()) { // Ensure that this collateral can be priced by the configured oracle @@ -309,22 +312,10 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT newLTV.liquidationLTV.toUint16(), newLTV.initialLiquidationLTV.toUint16(), newLTV.targetTimestamp, - newLTV.rampDuration, - !currentLTV.initialized + newLTV.rampDuration ); } - /// @inheritdoc IGovernance - /// @dev When LTV configuration is cleared, attempt to liquidate the collateral will revert. - /// Clearing should only be executed when the collateral is found to be unsafe to liquidate, - /// because e.g. it does external calls on transfer, which would be a critical security threat. - function clearLTV(address collateral) public virtual nonReentrant governorOnly { - uint16 originalLTV = getLTV(collateral, true).toUint16(); - vaultStorage.ltvLookup[collateral].clear(); - - emit GovSetLTV(collateral, 0, 0, originalLTV, 0, 0, false); - } - /// @inheritdoc IGovernance function setMaxLiquidationDiscount(uint16 newDiscount) public virtual nonReentrant governorOnly { // Discount equal 1e4 would cause division by zero error during liquidation diff --git a/src/EVault/shared/types/LTVConfig.sol b/src/EVault/shared/types/LTVConfig.sol index a1a3c73d..9e1e53e5 100644 --- a/src/EVault/shared/types/LTVConfig.sol +++ b/src/EVault/shared/types/LTVConfig.sol @@ -7,7 +7,7 @@ import {ConfigAmount} from "./Types.sol"; /// @title LTVConfig /// @notice This packed struct is used to store LTV configuration of a collateral struct LTVConfig { - // Packed slot: 2 + 2 + 2 + 6 + 4 + 1 = 17 + // Packed slot: 2 + 2 + 2 + 6 + 4 = 16 // The value of borrow LTV for originating positions ConfigAmount borrowLTV; // The value of fully converged liquidation LTV @@ -18,8 +18,6 @@ struct LTVConfig { uint48 targetTimestamp; // The time it takes for the liquidation LTV to converge from the initial value to the fully converged value uint32 rampDuration; - // A flag indicating the LTV configuration was initialized for the collateral - bool initialized; } /// @title LTVConfigLib @@ -68,16 +66,6 @@ library LTVConfigLib { newLTV.initialLiquidationLTV = self.getLTV(true); newLTV.targetTimestamp = uint48(block.timestamp + rampDuration); newLTV.rampDuration = rampDuration; - newLTV.initialized = true; - } - - // When LTV is cleared, the collateral can't be liquidated, as it's deemed unsafe - function clear(LTVConfig storage self) internal { - self.borrowLTV = ConfigAmount.wrap(0); - self.liquidationLTV = ConfigAmount.wrap(0); - self.initialLiquidationLTV = ConfigAmount.wrap(0); - self.targetTimestamp = 0; - self.rampDuration = 0; } } diff --git a/test/invariants/handlers/modules/GovernanceModuleHandler.t.sol b/test/invariants/handlers/modules/GovernanceModuleHandler.t.sol index d0921a5e..bf0bf044 100644 --- a/test/invariants/handlers/modules/GovernanceModuleHandler.t.sol +++ b/test/invariants/handlers/modules/GovernanceModuleHandler.t.sol @@ -44,14 +44,6 @@ contract GovernanceModuleHandler is BaseHandler { assert(true); } - function clearLTV(uint256 i) external { - address collateral = _getRandomBaseAsset(i); - - eTST.clearLTV(collateral); - - assert(true); - } - function setInterestFee(uint16 interestFee) external { eTST.setInterestFee(interestFee); diff --git a/test/unit/evault/modules/Governance/governorOnly.t.sol b/test/unit/evault/modules/Governance/governorOnly.t.sol index 6f85d49e..553d5264 100644 --- a/test/unit/evault/modules/Governance/governorOnly.t.sol +++ b/test/unit/evault/modules/Governance/governorOnly.t.sol @@ -30,7 +30,6 @@ contract GovernanceTest_GovernorOnly is EVaultTestBase { function test_GovernorAdmin() public { eTST.setFeeReceiver(address(0)); eTST.setLTV(address(0), 0, 0, 0); - eTST.clearLTV(address(0)); eTST.setMaxLiquidationDiscount(0); eTST.setLiquidationCoolOffTime(0); eTST.setInterestRateModel(address(0)); @@ -46,7 +45,6 @@ contract GovernanceTest_GovernorOnly is EVaultTestBase { evc.call(address(eTST), address(this), 0, abi.encodeCall(eTST.setFeeReceiver, address(0))); evc.call(address(eTST), address(this), 0, abi.encodeCall(eTST.setLTV, (address(0), 0, 0, 0))); - evc.call(address(eTST), address(this), 0, abi.encodeCall(eTST.clearLTV, address(0))); evc.call(address(eTST), address(this), 0, abi.encodeCall(eTST.setMaxLiquidationDiscount, 0)); evc.call(address(eTST), address(this), 0, abi.encodeCall(eTST.setLiquidationCoolOffTime, 0)); evc.call(address(eTST), address(this), 0, abi.encodeCall(eTST.setInterestRateModel, address(0))); @@ -68,8 +66,6 @@ contract GovernanceTest_GovernorOnly is EVaultTestBase { vm.expectRevert(Errors.E_Unauthorized.selector); eTST.setLTV(address(0), 0, 0, 0); vm.expectRevert(Errors.E_Unauthorized.selector); - eTST.clearLTV(address(0)); - vm.expectRevert(Errors.E_Unauthorized.selector); eTST.setMaxLiquidationDiscount(0); vm.expectRevert(Errors.E_Unauthorized.selector); eTST.setLiquidationCoolOffTime(0); @@ -96,8 +92,6 @@ contract GovernanceTest_GovernorOnly is EVaultTestBase { vm.expectRevert(Errors.E_Unauthorized.selector); evc.call(address(eTST), subAccount, 0, abi.encodeCall(eTST.setLTV, (address(0), 0, 0, 0))); vm.expectRevert(Errors.E_Unauthorized.selector); - evc.call(address(eTST), subAccount, 0, abi.encodeCall(eTST.clearLTV, address(0))); - vm.expectRevert(Errors.E_Unauthorized.selector); evc.call(address(eTST), subAccount, 0, abi.encodeCall(eTST.setMaxLiquidationDiscount, 0)); vm.expectRevert(Errors.E_Unauthorized.selector); evc.call(address(eTST), subAccount, 0, abi.encodeCall(eTST.setLiquidationCoolOffTime, 0)); diff --git a/test/unit/evault/modules/Vault/ltv.t.sol b/test/unit/evault/modules/Vault/ltv.t.sol index 1e2fdd5e..7e5bccd9 100644 --- a/test/unit/evault/modules/Vault/ltv.t.sol +++ b/test/unit/evault/modules/Vault/ltv.t.sol @@ -109,36 +109,6 @@ contract VaultTest_LTV is EVaultTestBase { eTST.setLTV(address(eTST2), 1e4 + 1, 1e4 + 1, 0); } - function test_clearLtv() public { - eTST.setLTV(address(eTST2), 0.5e4, 0.5e4, 0); - - startHoax(borrower); - evc.enableCollateral(borrower, address(eTST2)); - evc.enableController(borrower, address(eTST)); - vm.stopPrank(); - - // No borrow, liquidation is a no-op - (uint256 maxRepay, uint256 maxYield) = eTST.checkLiquidation(depositor, borrower, address(eTST2)); - assertEq(maxRepay, 0); - assertEq(maxYield, 0); - - // setting LTV to 0 doesn't change anything yet - eTST.setLTV(address(eTST2), 0, 0, 0); - - (maxRepay, maxYield) = eTST.checkLiquidation(depositor, borrower, address(eTST2)); - assertEq(maxRepay, 0); - assertEq(maxYield, 0); - - // collateral without LTV - vm.expectRevert(Errors.E_BadCollateral.selector); - eTST.checkLiquidation(depositor, borrower, address(eTST)); - - // same error after clearing LTV - eTST.clearLTV(address(eTST2)); - vm.expectRevert(Errors.E_BadCollateral.selector); - eTST.checkLiquidation(depositor, borrower, address(eTST2)); - } - function test_ltvList() public { assertEq(eTST.LTVList().length, 0); diff --git a/test/unit/evault/shared/Reentrancy.t.sol b/test/unit/evault/shared/Reentrancy.t.sol index 0099d06d..8a8dbf5a 100644 --- a/test/unit/evault/shared/Reentrancy.t.sol +++ b/test/unit/evault/shared/Reentrancy.t.sol @@ -170,9 +170,6 @@ contract MockHookTarget is Test, IHookTarget { uint32(bound(amount2, 0, type(uint32).max)) ); - vm.expectRevert(Errors.E_Reentrancy.selector); - eTST.clearLTV(account1); - vm.expectRevert(Errors.E_Reentrancy.selector); eTST.setInterestRateModel(account1); @@ -407,9 +404,6 @@ contract ReentrancyTest is EVaultTestBase { uint32(bound(amount2, 0, type(uint32).max)) ); - vm.expectRevert(Errors.E_Reentrancy.selector); - eTST.clearLTV(account1); - vm.expectRevert(Errors.E_Reentrancy.selector); eTST.setInterestRateModel(account1);