From 200ed488223480fe66af9539bb5ad01f51d10215 Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Tue, 4 Jun 2024 19:11:19 +0200 Subject: [PATCH 01/76] restrict function visibility --- src/Synths/ESynth.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Synths/ESynth.sol b/src/Synths/ESynth.sol index e845c3fd..0ce5c5d9 100644 --- a/src/Synths/ESynth.sol +++ b/src/Synths/ESynth.sol @@ -140,13 +140,13 @@ contract ESynth is ERC20Collateral, Ownable { /// @notice Checks if an account is ignored for the total supply. /// @param account The account to check. - function isIgnoredForTotalSupply(address account) public view returns (bool) { + function isIgnoredForTotalSupply(address account) external view returns (bool) { return ignoredForTotalSupply.contains(account); } /// @notice Retrieves all the accounts ignored for the total supply. /// @return The list of accounts ignored for the total supply. - function getAllIgnoredForTotalSupply() public view returns (address[] memory) { + function getAllIgnoredForTotalSupply() external view returns (address[] memory) { return ignoredForTotalSupply.values(); } From cb446be3472abc73af16918cf1e51be279550322 Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Wed, 5 Jun 2024 12:13:39 +0200 Subject: [PATCH 02/76] Named parameters in minters mapping --- src/Synths/ESynth.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Synths/ESynth.sol b/src/Synths/ESynth.sol index e845c3fd..bdda621d 100644 --- a/src/Synths/ESynth.sol +++ b/src/Synths/ESynth.sol @@ -21,7 +21,7 @@ contract ESynth is ERC20Collateral, Ownable { uint128 minted; } - mapping(address => MinterData) public minters; + mapping(address minter => MinterData data) public minters; EnumerableSet.AddressSet internal ignoredForTotalSupply; event MinterCapacitySet(address indexed minter, uint256 capacity); From 48849465455e1d6c9217b3ba45ff5ef8e46cbbaa Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Wed, 5 Jun 2024 13:50:45 +0200 Subject: [PATCH 03/76] Use named imports --- src/Synths/IRMSynth.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Synths/IRMSynth.sol b/src/Synths/IRMSynth.sol index dcb520b0..b1380195 100644 --- a/src/Synths/IRMSynth.sol +++ b/src/Synths/IRMSynth.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.0; -import "../InterestRateModels/IIRM.sol"; -import "../interfaces/IPriceOracle.sol"; +import {IIRM} from "../InterestRateModels/IIRM.sol"; +import {IPriceOracle} from "../interfaces/IPriceOracle.sol"; import {IERC20} from "../EVault/IEVault.sol"; /// @title IRMSynth From d6b65485ec4e93016f1ef6f24ccbd18055c8c2eb Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Wed, 5 Jun 2024 15:24:01 +0200 Subject: [PATCH 04/76] gas + code improvements --- src/Synths/ESynth.sol | 2 +- src/Synths/IRMSynth.sol | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Synths/ESynth.sol b/src/Synths/ESynth.sol index e845c3fd..49a7a322 100644 --- a/src/Synths/ESynth.sol +++ b/src/Synths/ESynth.sol @@ -157,7 +157,7 @@ contract ESynth is ERC20Collateral, Ownable { uint256 total = super.totalSupply(); uint256 ignoredLength = ignoredForTotalSupply.length(); // cache for efficiency - for (uint256 i = 0; i < ignoredLength; i++) { + for (uint256 i = 0; i < ignoredLength; ++i) { total -= balanceOf(ignoredForTotalSupply.at(i)); } return total; diff --git a/src/Synths/IRMSynth.sol b/src/Synths/IRMSynth.sol index dcb520b0..bd899d4b 100644 --- a/src/Synths/IRMSynth.sol +++ b/src/Synths/IRMSynth.sol @@ -56,8 +56,7 @@ contract IRMSynth is IIRM { } function computeInterestRate(address, uint256, uint256) external override returns (uint256) { - IRMData memory irmCache = irmStorage; - (uint216 rate, bool updated) = _computeRate(irmCache); + (uint216 rate, bool updated) = _computeRate(irmStorage); if (updated) { irmStorage = IRMData({lastUpdated: uint40(block.timestamp), lastRate: rate}); From b3d074f9b8fbd2d95f9b9445b0241ee7258bc943 Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Wed, 5 Jun 2024 15:27:40 +0200 Subject: [PATCH 05/76] typo fix --- src/Synths/EulerSavingsRate.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index 65113c44..f4bd889c 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -126,7 +126,7 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { /// @notice Mints a certain amount of shares to the account. /// @param shares The amount of assets to mint. /// @param receiver The account to mint the shares to. - /// @return The amount of assets spend. + /// @return The amount of assets spent. function mint(uint256 shares, address receiver) public override nonReentrant returns (uint256) { return super.mint(shares, receiver); } From 9b79936ad12cb87ab3dc1e13141fbe93878e91e2 Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Wed, 5 Jun 2024 15:46:08 +0200 Subject: [PATCH 06/76] Emit events in state changing functions --- src/Synths/EulerSavingsRate.sol | 7 +++++++ src/Synths/IRMSynth.sol | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index 65113c44..2384b2a3 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -38,6 +38,9 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { error Reentrancy(); + event Gulped(uint256 gulped, uint256 interestLeft); + event InterestUpdated(uint256 interestAccrued, uint256 interestLeft); + /// @notice Modifier to require an account status check on the EVC. /// @dev Calls `requireAccountStatusCheck` function from EVC for the specified account after the function body. /// @param account The address of the account to check. @@ -199,6 +202,8 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { // write esrSlotCache back to storage in a single SSTORE esrSlot = esrSlotCache; + + emit Gulped(toGulp, esrSlotCache.interestLeft); } /// @notice Updates the interest and returns the ESR storage slot cache. @@ -215,6 +220,8 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { // Move interest accrued to totalAssets _totalAssets = _totalAssets + accruedInterest; + emit InterestUpdated(accruedInterest, esrSlotCache.interestLeft); + return esrSlotCache; } diff --git a/src/Synths/IRMSynth.sol b/src/Synths/IRMSynth.sol index dcb520b0..b133a5e1 100644 --- a/src/Synths/IRMSynth.sol +++ b/src/Synths/IRMSynth.sol @@ -35,6 +35,8 @@ contract IRMSynth is IIRM { error E_ZeroAddress(); error E_InvalidQuote(); + event InterestUpdated(uint256 rate); + constructor(address synth_, address referenceAsset_, address oracle_, uint256 targetQuoute_) { if (synth_ == address(0) || referenceAsset_ == address(0) || oracle_ == address(0)) { revert E_ZeroAddress(); @@ -53,6 +55,8 @@ contract IRMSynth is IIRM { } irmStorage = IRMData({lastUpdated: uint40(block.timestamp), lastRate: BASE_RATE}); + + emit InterestUpdated(BASE_RATE); } function computeInterestRate(address, uint256, uint256) external override returns (uint256) { @@ -61,6 +65,7 @@ contract IRMSynth is IIRM { if (updated) { irmStorage = IRMData({lastUpdated: uint40(block.timestamp), lastRate: rate}); + emit InterestUpdated(rate); } return rate; From 807ecb7931ac1fcd34e2c339f4b8b944345ddd7c Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Thu, 6 Jun 2024 10:56:28 +0200 Subject: [PATCH 07/76] Only gulp when sufficient shares are minted --- src/Synths/EulerSavingsRate.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index 65113c44..79c79c7c 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -24,6 +24,7 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { uint8 internal constant LOCKED = 2; uint256 internal constant VIRTUAL_AMOUNT = 1e6; + uint256 internal constant MIN_SHARES_FOR_GULP = VIRTUAL_AMOUNT * 10; // At least 10 times the virtual amount of shares should exist for gulp to be enabled uint256 public constant INTEREST_SMEAR = 2 weeks; struct ESRSlot { @@ -188,6 +189,9 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { function gulp() public nonReentrant { ESRSlot memory esrSlotCache = updateInterestAndReturnESRSlotCache(); + // Do not gulp if total supply is too low + if(totalSupply() < MIN_SHARES_FOR_GULP) return; + uint256 assetBalance = IERC20(asset()).balanceOf(address(this)); uint256 toGulp = assetBalance - _totalAssets - esrSlotCache.interestLeft; From 4504852002e74aaa6da8bacda07064aa0bec3168 Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Thu, 6 Jun 2024 11:24:40 +0200 Subject: [PATCH 08/76] Test gulp threshold --- src/Synths/EulerSavingsRate.sol | 2 +- test/unit/esr/ESR.Fuzz.t.sol | 2 +- test/unit/esr/ESR.Gulp.t.sol | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index 79c79c7c..26a12407 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -190,7 +190,7 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { ESRSlot memory esrSlotCache = updateInterestAndReturnESRSlotCache(); // Do not gulp if total supply is too low - if(totalSupply() < MIN_SHARES_FOR_GULP) return; + if (totalSupply() < MIN_SHARES_FOR_GULP) return; uint256 assetBalance = IERC20(asset()).balanceOf(address(this)); uint256 toGulp = assetBalance - _totalAssets - esrSlotCache.interestLeft; diff --git a/test/unit/esr/ESR.Fuzz.t.sol b/test/unit/esr/ESR.Fuzz.t.sol index 62d57e7b..b2d03488 100644 --- a/test/unit/esr/ESR.Fuzz.t.sol +++ b/test/unit/esr/ESR.Fuzz.t.sol @@ -37,7 +37,7 @@ contract ESRFuzzTest is ESRTest { // this tests shows that when you have a very small deposit and a very large interestAmount minted to the contract function testFuzz_gulp_under_uint168(uint256 interestAmount, uint256 depositAmount) public { - depositAmount = bound(depositAmount, 0, type(uint112).max); + depositAmount = bound(depositAmount, 1e7, type(uint112).max); interestAmount = bound(interestAmount, 0, type(uint256).max - depositAmount); // this makes sure that the mint won't cause overflow asset.mint(address(esr), interestAmount); diff --git a/test/unit/esr/ESR.Gulp.t.sol b/test/unit/esr/ESR.Gulp.t.sol index 65ee26d5..ba4aae06 100644 --- a/test/unit/esr/ESR.Gulp.t.sol +++ b/test/unit/esr/ESR.Gulp.t.sol @@ -82,4 +82,19 @@ contract ESRGulpTest is ESRTest { assertEq(esrSlot.lastInterestUpdate, block.timestamp); assertEq(esrSlot.interestSmearEnd, block.timestamp + esr.INTEREST_SMEAR()); } + + function testGulpBelowMinSharesForGulp() public { + uint256 depositAmount = 1337; + doDeposit(user, depositAmount); + + uint256 interestAmount = 10e18; + // Mint interest directly into the contract + asset.mint(address(esr), interestAmount); + esr.gulp(); + skip(esr.INTEREST_SMEAR()); + + EulerSavingsRate.ESRSlot memory esrSlot = esr.getESRSlot(); + assertEq(esr.totalAssets(), depositAmount); + assertEq(esrSlot.interestLeft, 0); + } } From b10e73c3db3bd3a8b2858d0951e95933d91b58dc Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Thu, 6 Jun 2024 11:51:44 +0200 Subject: [PATCH 09/76] Allow withdraws and redeems when a controller is set --- src/Synths/EulerSavingsRate.sol | 14 ++++++-- test/unit/esr/ESR.General.t.sol | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index 65113c44..d948cc8c 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -144,7 +144,12 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { { // Move interest to totalAssets updateInterestAndReturnESRSlotCache(); - return super.withdraw(assets, receiver, owner); + + // Not using super to not call maxWithdraw which would return 0 if a user has a controller set + uint256 shares = previewWithdraw(assets); + _withdraw(_msgSender(), receiver, owner, assets, shares); + + return shares; } /// @notice Redeems a certain amount of shares for assets. @@ -160,7 +165,12 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { { // Move interest to totalAssets updateInterestAndReturnESRSlotCache(); - return super.redeem(shares, receiver, owner); + + // Not using super to not call maxRedeem which would return 0 if a user has a controller set + uint256 assets = previewRedeem(shares); + _withdraw(_msgSender(), receiver, owner, assets, shares); + + return assets; } function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256) { diff --git a/test/unit/esr/ESR.General.t.sol b/test/unit/esr/ESR.General.t.sol index b7f371cd..f8025135 100644 --- a/test/unit/esr/ESR.General.t.sol +++ b/test/unit/esr/ESR.General.t.sol @@ -188,4 +188,62 @@ contract ESRGeneralTest is ESRTest { uint256 maxRedeem = esr.maxRedeem(user); assertEq(maxRedeem, 0); } + + // test withdraw with a controller set which status check succeeds + function test_withdrawWithControllerSetStatusCheckSucceeds() public { + uint256 depositAmount = 100e18; + doDeposit(user, depositAmount); + + uint256 balanceUnderlyingBefore = asset.balanceOf(user); + vm.startPrank(user); + evc.enableController(address(user), address(statusCheck)); + esr.withdraw(depositAmount, user, user); + vm.stopPrank(); + uint256 balanceUnderlyingAfter = asset.balanceOf(user); + + assertEq(balanceUnderlyingAfter, balanceUnderlyingBefore + depositAmount); + } + + // test withdraw with a controller set which status check fails + function test_withdrawWithControllerSetStatusCheckFails() public { + uint256 depositAmount = 100e18; + doDeposit(user, depositAmount); + + vm.startPrank(user); + evc.enableController(address(user), address(statusCheck)); + statusCheck.setShouldFail(true); + vm.expectRevert("MockMinimalStatusCheck: account status check failed"); + esr.withdraw(depositAmount, user, user); + vm.stopPrank(); + } + + // test redeem with a controller set which status check succeeds + function test_redeemWithControllerSetStatusCheckSucceeds() public { + uint256 depositAmount = 100e18; + doDeposit(user, depositAmount); + + uint256 shares = esr.balanceOf(user); + uint256 balanceUnderlyingBefore = asset.balanceOf(user); + vm.startPrank(user); + evc.enableController(address(user), address(statusCheck)); + esr.redeem(shares, user, user); + vm.stopPrank(); + uint256 balanceUnderlyingAfter = asset.balanceOf(user); + + assertEq(balanceUnderlyingAfter, balanceUnderlyingBefore + depositAmount); + } + + // test redeem with a controller set which status check fails + function test_redeemWithControllerSetStatusCheckFails() public { + uint256 depositAmount = 100e18; + doDeposit(user, depositAmount); + + uint256 shares = esr.balanceOf(user); + vm.startPrank(user); + evc.enableController(address(user), address(statusCheck)); + statusCheck.setShouldFail(true); + vm.expectRevert("MockMinimalStatusCheck: account status check failed"); + esr.redeem(shares, user, user); + vm.stopPrank(); + } } From ddbd12149cc791f2d5e25371727ada43a2c73e67 Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Tue, 4 Jun 2024 17:43:05 +0200 Subject: [PATCH 10/76] Add missing docstrings --- src/Synths/ESynth.sol | 5 +++++ src/Synths/EulerSavingsRate.sol | 14 ++++++++++++++ src/Synths/IRMSynth.sol | 11 +++++++++++ src/Synths/PegStabilityModule.sol | 5 +++++ 4 files changed, 35 insertions(+) diff --git a/src/Synths/ESynth.sol b/src/Synths/ESynth.sol index e845c3fd..f833f0a1 100644 --- a/src/Synths/ESynth.sol +++ b/src/Synths/ESynth.sol @@ -21,9 +21,14 @@ contract ESynth is ERC20Collateral, Ownable { uint128 minted; } + /// @notice contains the minting capacity and minted amount for each minter. mapping(address => MinterData) public minters; + /// @notice contains the list of addresses to ignore for the total supply. EnumerableSet.AddressSet internal ignoredForTotalSupply; + /// @notice Emitted when the minting capacity for a minter is set. + /// @param minter The address of the minter. + /// @param capacity The capacity set for the minter. event MinterCapacitySet(address indexed minter, uint256 capacity); error E_CapacityReached(); diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index 65113c44..f3ee65ba 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -23,7 +23,9 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { uint8 internal constant UNLOCKED = 1; uint8 internal constant LOCKED = 2; + /// @notice The virtual amount added to total shares and total assets. uint256 internal constant VIRTUAL_AMOUNT = 1e6; + /// @notice The duration of the interest smear period. uint256 public constant INTEREST_SMEAR = 2 weeks; struct ESRSlot { @@ -33,7 +35,9 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { uint8 locked; } + /// @notice Multiple state variables stored in a single storage slot. ESRSlot internal esrSlot; + /// @notice The total assets accounted for in the vault. uint256 internal _totalAssets; error Reentrancy(); @@ -68,6 +72,11 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { return _totalAssets + interestAccrued(); } + /// @notice Returns the maximum amount of shares that can be redeemed by the specified address. + /// @dev If the account has a controller set it's possible the withdrawal will be reverted by the controller, thus + /// we return 0. + /// @param owner The account owner. + /// @return The maximum amount of shares that can be redeemed. function maxRedeem(address owner) public view override returns (uint256) { // Max redeem can potentially be 0 if there is a liability if (evc.getControllers(owner).length > 0) { @@ -77,6 +86,11 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { return super.maxRedeem(owner); } + /// @notice Returns the maximum amount of assets that can be withdrawn by the specified address. + /// @dev If the account has a controller set it's possible the withdrawal will be reverted by the controller, thus + /// we return 0. + /// @param owner The account owner. + /// @return The maximum amount of assets that can be withdrawn. function maxWithdraw(address owner) public view override returns (uint256) { // Max withdraw can potentially be 0 if there is a liability if (evc.getControllers(owner).length > 0) { diff --git a/src/Synths/IRMSynth.sol b/src/Synths/IRMSynth.sol index dcb520b0..012943da 100644 --- a/src/Synths/IRMSynth.sol +++ b/src/Synths/IRMSynth.sol @@ -19,10 +19,15 @@ contract IRMSynth is IIRM { uint216 public constant ADJUST_ONE = 1.0e18; uint216 public constant ADJUST_INTERVAL = 1 hours; + /// @notice The address of the synthetic asset. address public immutable synth; + /// @notice The address of the reference asset. address public immutable referenceAsset; + /// @notice The address of the oracle. IPriceOracle public immutable oracle; + /// @notice The target quote which the IRM will try to maintain. uint256 public immutable targetQuote; + /// @notice The amount of the quote asset to use for the quote. uint256 public immutable quoteAmount; struct IRMData { @@ -55,6 +60,8 @@ contract IRMSynth is IIRM { irmStorage = IRMData({lastUpdated: uint40(block.timestamp), lastRate: BASE_RATE}); } + /// @notice Computes the interest rate and updates the storage if necessary. + /// @return The interest rate. function computeInterestRate(address, uint256, uint256) external override returns (uint256) { IRMData memory irmCache = irmStorage; (uint216 rate, bool updated) = _computeRate(irmCache); @@ -66,6 +73,8 @@ contract IRMSynth is IIRM { return rate; } + /// @notice Computes the interest rate without updating the storage. + /// @return The interest rate. function computeInterestRateView(address, uint256, uint256) external view override returns (uint256) { (uint216 rate,) = _computeRate(irmStorage); return rate; @@ -102,6 +111,8 @@ contract IRMSynth is IIRM { return (rate, updated); } + /// @notice Retrieves the packed IRM data as a struct. + /// @return The IRM data. function getIRMData() external view returns (IRMData memory) { return irmStorage; } diff --git a/src/Synths/PegStabilityModule.sol b/src/Synths/PegStabilityModule.sol index fe965cbc..2cbd47ce 100644 --- a/src/Synths/PegStabilityModule.sol +++ b/src/Synths/PegStabilityModule.sol @@ -21,11 +21,16 @@ contract PegStabilityModule is EVCUtil { uint256 public constant BPS_SCALE = 100_00; uint256 public constant PRICE_SCALE = 1e18; + /// @notice The synthetic asset. ESynth public immutable synth; + /// @notice The underlying asset. IERC20 public immutable underlying; + /// @notice The conversion price between the synthetic and underlying asset. uint256 public immutable conversionPrice; // 1e18 = 1 SYNTH == 1 UNDERLYING, 0.01e18 = 1 SYNTH == 0.01 UNDERLYING + /// @notice The fee for swapping to the underlying asset in basis points. uint256 public immutable TO_UNDERLYING_FEE; + /// @notice The fee for swapping to the synthetic asset in basis points. uint256 public immutable TO_SYNTH_FEE; error E_ZeroAddress(); From 0ffc50a915002a4ae973c4339068e423d36d6745 Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Thu, 6 Jun 2024 18:52:45 +0200 Subject: [PATCH 11/76] remove _withdraw override and throw original errors on redeem and withdraw failure --- src/Synths/EulerSavingsRate.sol | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index d948cc8c..899dcc91 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -132,6 +132,7 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { } /// @notice Deposits a certain amount of assets to the vault. + /// @dev Overwritten to not call maxWithdraw which would return 0 if there is a controller set, update the accrued interest and update _totalAssets. /// @param assets The amount of assets to deposit. /// @param receiver The recipient of the shares. /// @return The amount of shares minted. @@ -145,14 +146,20 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { // Move interest to totalAssets updateInterestAndReturnESRSlotCache(); - // Not using super to not call maxWithdraw which would return 0 if a user has a controller set + uint256 maxAssets = _convertToAssets(balanceOf(owner), Math.Rounding.Floor); + if (maxAssets < assets) { + revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); + } + uint256 shares = previewWithdraw(assets); _withdraw(_msgSender(), receiver, owner, assets, shares); + _totalAssets = _totalAssets - assets; return shares; } /// @notice Redeems a certain amount of shares for assets. + /// @dev Overwritten to not call maxRedeem which would return 0 if there is a controller set, update the accrued interest and update _totalAssets. /// @param shares The amount of shares to redeem. /// @param receiver The recipient of the assets. /// @return The amount of assets redeemed. @@ -166,9 +173,14 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { // Move interest to totalAssets updateInterestAndReturnESRSlotCache(); - // Not using super to not call maxRedeem which would return 0 if a user has a controller set + uint256 maxShares = balanceOf(owner); + if (maxShares < shares) { + revert ERC4626ExceededMaxRedeem(owner, shares, maxShares); + } + uint256 assets = previewRedeem(shares); _withdraw(_msgSender(), receiver, owner, assets, shares); + _totalAssets = _totalAssets - assets; return assets; } @@ -186,14 +198,6 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { super._deposit(caller, receiver, assets, shares); } - function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares) - internal - override - { - _totalAssets = _totalAssets - assets; - super._withdraw(caller, receiver, owner, assets, shares); - } - /// @notice Smears any donations to this vault as interest. function gulp() public nonReentrant { ESRSlot memory esrSlotCache = updateInterestAndReturnESRSlotCache(); From 561af00443e4db23a7dd836cb6b3ca3c19bfdf09 Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Wed, 5 Jun 2024 11:40:21 +0200 Subject: [PATCH 12/76] Switch to named imports --- src/Synths/ESynth.sol | 17 +++++++++-------- src/Synths/IRMSynth.sol | 7 +++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Synths/ESynth.sol b/src/Synths/ESynth.sol index 07aeeca4..50008708 100644 --- a/src/Synths/ESynth.sol +++ b/src/Synths/ESynth.sol @@ -122,8 +122,8 @@ contract ESynth is ERC20Collateral, Ownable { /// @dev Overriden due to the conflict with the Context definition. /// @dev This function returns the account on behalf of which the current operation is being performed, which is /// either msg.sender or the account authenticated by the EVC. - /// @return The address of the message sender. - function _msgSender() internal view virtual override (ERC20Collateral, Context) returns (address) { + /// @return msgSender The address of the message sender. + function _msgSender() internal view virtual override (ERC20Collateral, Context) returns (address msgSender) { return ERC20Collateral._msgSender(); } @@ -145,21 +145,22 @@ contract ESynth is ERC20Collateral, Ownable { /// @notice Checks if an account is ignored for the total supply. /// @param account The account to check. - function isIgnoredForTotalSupply(address account) public view returns (bool) { + /// @return isIgnored True if the account is ignored for the total supply. False otherwise. + function isIgnoredForTotalSupply(address account) public view returns (bool isIgnored) { return ignoredForTotalSupply.contains(account); } /// @notice Retrieves all the accounts ignored for the total supply. - /// @return The list of accounts ignored for the total supply. - function getAllIgnoredForTotalSupply() public view returns (address[] memory) { + /// @return accounts List of accounts ignored for the total supply. + function getAllIgnoredForTotalSupply() public view returns (address[] memory accounts) { return ignoredForTotalSupply.values(); } /// @notice Retrieves the total supply of the token. /// @dev Overriden to exclude the ignored accounts from the total supply. - /// @return The total supply of the token. - function totalSupply() public view override returns (uint256) { - uint256 total = super.totalSupply(); + /// @return total Total supply of the token. + function totalSupply() public view override returns (uint256 total) { + total = super.totalSupply(); uint256 ignoredLength = ignoredForTotalSupply.length(); // cache for efficiency for (uint256 i = 0; i < ignoredLength; ++i) { diff --git a/src/Synths/IRMSynth.sol b/src/Synths/IRMSynth.sol index af72befa..3165d1cb 100644 --- a/src/Synths/IRMSynth.sol +++ b/src/Synths/IRMSynth.sol @@ -77,10 +77,9 @@ contract IRMSynth is IIRM { return rate; } - /// @notice Computes the interest rate without updating the storage. - /// @return The interest rate. - function computeInterestRateView(address, uint256, uint256) external view override returns (uint256) { - (uint216 rate,) = _computeRate(irmStorage); + /// @return rate The new interest rate + function computeInterestRateView(address, uint256, uint256) external view override returns (uint256 rate) { + (rate,) = _computeRate(irmStorage); return rate; } From 7bd80efcb2b206ac2501378b430da4b201fd3ca4 Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Tue, 4 Jun 2024 14:24:06 +0200 Subject: [PATCH 13/76] docstring fixes --- src/Synths/EulerSavingsRate.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index bddac99a..895be4d4 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -149,10 +149,11 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { return super.mint(shares, receiver); } - /// @notice Deposits a certain amount of assets to the vault. + /// @notice Withdraws a certain amount of assets to the vault. /// @dev Overwritten to not call maxWithdraw which would return 0 if there is a controller set, update the accrued interest and update _totalAssets. - /// @param assets The amount of assets to deposit. + /// @param assets The amount of assets to withdraw. /// @param receiver The recipient of the shares. + /// @param owner The account from which the assets are withdrawn /// @return The amount of shares minted. function withdraw(uint256 assets, address receiver, address owner) public @@ -180,6 +181,7 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { /// @dev Overwritten to not call maxRedeem which would return 0 if there is a controller set, update the accrued interest and update _totalAssets. /// @param shares The amount of shares to redeem. /// @param receiver The recipient of the assets. + /// @param owner The account from which the shares are redeemed. /// @return The amount of assets redeemed. function redeem(uint256 shares, address receiver, address owner) public @@ -258,6 +260,7 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { } /// @notice Returns the amount of interest accrued. + /// @return The amount of interest accrued. function interestAccrued() public view returns (uint256) { return interestAccruedFromCache(esrSlot); } @@ -281,6 +284,7 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { } /// @notice Returns the ESR storage slot as a struct. + /// @return The ESR storage slot as a struct. function getESRSlot() public view returns (ESRSlot memory) { return esrSlot; } From 439896ff606f531c90f1ff9242f46a9b8d8d3736 Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Tue, 2 Jul 2024 15:08:39 +0200 Subject: [PATCH 14/76] formatting fix --- src/Synths/ESynth.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Synths/ESynth.sol b/src/Synths/ESynth.sol index 189232ac..892e58e5 100644 --- a/src/Synths/ESynth.sol +++ b/src/Synths/ESynth.sol @@ -147,7 +147,6 @@ contract ESynth is ERC20Collateral, Ownable { /// @param account The account to check. /// @return isIgnored True if the account is ignored for the total supply. False otherwise. function isIgnoredForTotalSupply(address account) external view returns (bool isIgnored) { - return ignoredForTotalSupply.contains(account); } From c2779f2092dc10c102fdfa1b6c93e9e708bb9f3a Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Tue, 2 Jul 2024 17:29:59 +0100 Subject: [PATCH 15/76] chore: formatting --- src/Synths/EulerSavingsRate.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index c5957cdc..1a286ca0 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -26,7 +26,8 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { /// @notice The virtual amount added to total shares and total assets. uint256 internal constant VIRTUAL_AMOUNT = 1e6; - uint256 internal constant MIN_SHARES_FOR_GULP = VIRTUAL_AMOUNT * 10; // At least 10 times the virtual amount of shares should exist for gulp to be enabled + uint256 internal constant MIN_SHARES_FOR_GULP = VIRTUAL_AMOUNT * 10; // At least 10 times the virtual amount of + // shares should exist for gulp to be enabled uint256 public constant INTEREST_SMEAR = 2 weeks; @@ -161,7 +162,8 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { } /// @notice Withdraws a certain amount of assets to the vault. - /// @dev Overwritten to not call maxWithdraw which would return 0 if there is a controller set, update the accrued interest and update _totalAssets. + /// @dev Overwritten to not call maxWithdraw which would return 0 if there is a controller set, update the accrued + /// interest and update _totalAssets. /// @param assets The amount of assets to withdraw. /// @param receiver The recipient of the shares. /// @param owner The account from which the assets are withdrawn @@ -189,7 +191,8 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { } /// @notice Redeems a certain amount of shares for assets. - /// @dev Overwritten to not call maxRedeem which would return 0 if there is a controller set, update the accrued interest and update _totalAssets. + /// @dev Overwritten to not call maxRedeem which would return 0 if there is a controller set, update the accrued + /// interest and update _totalAssets. /// @param shares The amount of shares to redeem. /// @param receiver The recipient of the assets. /// @param owner The account from which the shares are redeemed. From ca5fca527b8fb2202c32c2d6b616b1930d636bb4 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 3 Jul 2024 11:10:20 +0100 Subject: [PATCH 16/76] fix: OZ L-04 (continuation) --- src/Synths/EulerSavingsRate.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index 1a286ca0..e2c75fd8 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -228,8 +228,8 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { } function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override { - _totalAssets = _totalAssets + assets; super._deposit(caller, receiver, assets, shares); + _totalAssets = _totalAssets + assets; } /// @notice Smears any donations to this vault as interest. From 52b1fb0cba8157188b17f1e89bbbd41a12f7c30e Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 3 Jul 2024 11:11:04 +0100 Subject: [PATCH 17/76] fix: formatting --- src/Synths/EulerSavingsRate.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index e2c75fd8..b0466f4f 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -26,8 +26,8 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { /// @notice The virtual amount added to total shares and total assets. uint256 internal constant VIRTUAL_AMOUNT = 1e6; - uint256 internal constant MIN_SHARES_FOR_GULP = VIRTUAL_AMOUNT * 10; // At least 10 times the virtual amount of - // shares should exist for gulp to be enabled + /// @notice At least 10 times the virtual amount of shares should exist for gulp to be enabled + uint256 internal constant MIN_SHARES_FOR_GULP = VIRTUAL_AMOUNT * 10; uint256 public constant INTEREST_SMEAR = 2 weeks; From 3c2292251bec95848038c9161aa4a51667d57b24 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 3 Jul 2024 11:11:27 +0100 Subject: [PATCH 18/76] test: fix testFuzz_gulp_under_uint168 --- test/unit/esr/ESR.Fuzz.t.sol | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/unit/esr/ESR.Fuzz.t.sol b/test/unit/esr/ESR.Fuzz.t.sol index 713b6e80..bc00a901 100644 --- a/test/unit/esr/ESR.Fuzz.t.sol +++ b/test/unit/esr/ESR.Fuzz.t.sol @@ -38,6 +38,7 @@ contract ESRFuzzTest is ESRTest { // this tests shows that when you have a very small deposit and a very large interestAmount minted to the contract function testFuzz_gulp_under_uint168(uint256 interestAmount, uint256 depositAmount) public { + uint256 MIN_SHARES_FOR_GULP = 10 * 1e6; depositAmount = bound(depositAmount, 0, type(uint112).max); interestAmount = bound(interestAmount, 0, type(uint256).max - depositAmount); // this makes sure that the mint // won't cause overflow @@ -49,10 +50,14 @@ contract ESRFuzzTest is ESRTest { EulerSavingsRate.ESRSlot memory esrSlot = esr.updateInterestAndReturnESRSlotCache(); - if (interestAmount <= type(uint168).max) { - assertEq(esrSlot.interestLeft, interestAmount); + if (depositAmount >= MIN_SHARES_FOR_GULP) { + if (interestAmount <= type(uint168).max) { + assertEq(esrSlot.interestLeft, interestAmount); + } else { + assertEq(esrSlot.interestLeft, type(uint168).max); + } } else { - assertEq(esrSlot.interestLeft, type(uint168).max); + assertEq(esrSlot.interestLeft, 0); } } From d57b84e44e28b44c7ce51f4264adbe402c5a1803 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 3 Jul 2024 11:13:33 +0100 Subject: [PATCH 19/76] fix: formatting --- src/Synths/EulerSavingsRate.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index b0466f4f..dc9360a7 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -27,7 +27,7 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { /// @notice The virtual amount added to total shares and total assets. uint256 internal constant VIRTUAL_AMOUNT = 1e6; /// @notice At least 10 times the virtual amount of shares should exist for gulp to be enabled - uint256 internal constant MIN_SHARES_FOR_GULP = VIRTUAL_AMOUNT * 10; + uint256 internal constant MIN_SHARES_FOR_GULP = VIRTUAL_AMOUNT * 10; uint256 public constant INTEREST_SMEAR = 2 weeks; From 6a30a60e89d2a9e43b6f01916b6bbabf6bcfeda9 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 3 Jul 2024 12:37:55 +0100 Subject: [PATCH 20/76] feat: remove ERC20Collateral, add ERC20EVCCompatible --- src/Synths/ERC20Collateral.sol | 74 ----------------------- src/Synths/ERC20EVCCompatible.sol | 28 +++++++++ src/Synths/ESynth.sol | 22 +++---- test/unit/esvault/ESVaultTestBase.t.sol | 4 +- test/unit/esynth/ESynth.totalSupply.t.sol | 4 +- test/unit/esynth/lib/ESynthTest.sol | 2 +- test/unit/pegStabilityModules/PSM.t.sol | 6 +- 7 files changed, 46 insertions(+), 94 deletions(-) delete mode 100644 src/Synths/ERC20Collateral.sol create mode 100644 src/Synths/ERC20EVCCompatible.sol diff --git a/src/Synths/ERC20Collateral.sol b/src/Synths/ERC20Collateral.sol deleted file mode 100644 index d7901961..00000000 --- a/src/Synths/ERC20Collateral.sol +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.8.0; - -import {ERC20, Context} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; -import {ERC20Permit} from "openzeppelin-contracts/token/ERC20/extensions/ERC20Permit.sol"; -import {ReentrancyGuard} from "openzeppelin-contracts/utils/ReentrancyGuard.sol"; -import {IEVC, EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; - -/// @title ERC20Collateral -/// @custom:security-contact security@euler.xyz -/// @author Euler Labs (https://www.eulerlabs.com/) -/// @notice ERC20Collateral is an ERC20-compatible token with the EVC support which allows it to be used as collateral -/// in other vaults. -abstract contract ERC20Collateral is EVCUtil, ERC20Permit, ReentrancyGuard { - constructor(IEVC _evc_, string memory _name_, string memory _symbol_) - EVCUtil(address(_evc_)) - ERC20(_name_, _symbol_) - ERC20Permit(_name_) - {} - - /// @notice Transfers a certain amount of tokens to a recipient. - /// @dev Overriden to add reentrancy protection. - /// @param to The recipient of the transfer. - /// @param amount The amount shares to transfer. - /// @return A boolean indicating whether the transfer was successful. - function transfer(address to, uint256 amount) public virtual override nonReentrant returns (bool) { - return super.transfer(to, amount); - } - - /// @notice Transfers a certain amount of tokens from a sender to a recipient. - /// @dev Overriden to add reentrancy protection. - /// @param from The sender of the transfer. - /// @param to The recipient of the transfer. - /// @param amount The amount of shares to transfer. - /// @return A boolean indicating whether the transfer was successful. - function transferFrom(address from, address to, uint256 amount) - public - virtual - override - nonReentrant - returns (bool) - { - return super.transferFrom(from, to, amount); - } - - /// @notice Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` - /// (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding - /// this function. - /// @dev Overriden to require account status checks on transfers from non-zero addresses. The account status check - /// must be required on any operation that reduces user's balance. Note that the user balance cannot be modified - // outside of this function as the account status check must always be requested after the balance is modified which - // is ensured by this function. If any user balance modifications are done outside of this function, the contract - // must be modified to request the account status check appropriately. - /// @param from The address from which tokens are transferred or burned. - /// @param to The address to which tokens are transferred or minted. - /// @param value The amount of tokens to transfer, mint, or burn. - function _update(address from, address to, uint256 value) internal virtual override { - super._update(from, to, value); - - if (from != address(0)) { - evc.requireAccountStatusCheck(from); - } - } - - /// @notice Retrieves the message sender in the context of the EVC. - /// @dev Overriden due to the conflict with the Context definition. - /// @dev This function returns the account on behalf of which the current operation is being performed, which is - /// either msg.sender or the account authenticated by the EVC. - /// @return The address of the message sender. - function _msgSender() internal view virtual override (EVCUtil, Context) returns (address) { - return EVCUtil._msgSender(); - } -} diff --git a/src/Synths/ERC20EVCCompatible.sol b/src/Synths/ERC20EVCCompatible.sol new file mode 100644 index 00000000..18e37062 --- /dev/null +++ b/src/Synths/ERC20EVCCompatible.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {ERC20, Context} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; +import {ERC20Permit} from "openzeppelin-contracts/token/ERC20/extensions/ERC20Permit.sol"; +import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; + +/// @title ERC20EVCCompatible +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +/// @notice ERC20EVCCompatible is an ERC20-compatible token with the EVC support. +abstract contract ERC20EVCCompatible is EVCUtil, ERC20Permit { + constructor(address _evc_, string memory _name_, string memory _symbol_) + EVCUtil(_evc_) + ERC20(_name_, _symbol_) + ERC20Permit(_name_) + {} + + /// @notice Retrieves the message sender in the context of the EVC. + /// @dev Overriden due to the conflict with the Context definition. + /// @dev This function returns the account on behalf of which the current operation is being performed, which is + /// either msg.sender or the account authenticated by the EVC. + /// @return The address of the message sender. + function _msgSender() internal view virtual override (EVCUtil, Context) returns (address) { + return EVCUtil._msgSender(); + } +} diff --git a/src/Synths/ESynth.sol b/src/Synths/ESynth.sol index 5ab4e219..7a47cf79 100644 --- a/src/Synths/ESynth.sol +++ b/src/Synths/ESynth.sol @@ -4,17 +4,15 @@ pragma solidity ^0.8.0; import {Ownable} from "openzeppelin-contracts/access/Ownable.sol"; import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; -import {IEVC, EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; -import {ERC20Collateral, ERC20, Context} from "./ERC20Collateral.sol"; +import {ERC20EVCCompatible, Context} from "./ERC20EVCCompatible.sol"; import {IEVault} from "../EVault/IEVault.sol"; /// @title ESynth /// @custom:security-contact security@euler.xyz /// @author Euler Labs (https://www.eulerlabs.com/) -/// @notice ESynth is an ERC20-compatible token with the EVC support which, thanks to relying on the EVC authentication -/// and requesting the account status checks on token transfers and burns, allows it to be used as collateral in other -/// vault. It is meant to be used as an underlying asset of the synthetic asset vault. -contract ESynth is ERC20Collateral, Ownable { +/// @notice ESynth is an ERC20-compatible token with the EVC support. It is meant to be used as an underlying asset of +/// the synthetic asset vault. +contract ESynth is ERC20EVCCompatible, Ownable { using EnumerableSet for EnumerableSet.AddressSet; struct MinterData { @@ -30,8 +28,8 @@ contract ESynth is ERC20Collateral, Ownable { error E_CapacityReached(); error E_NotEVCCompatible(); - constructor(IEVC evc_, string memory name_, string memory symbol_) - ERC20Collateral(evc_, name_, symbol_) + constructor(address evc_, string memory name_, string memory symbol_) + ERC20EVCCompatible(evc_, name_, symbol_) Ownable(msg.sender) {} @@ -47,7 +45,7 @@ contract ESynth is ERC20Collateral, Ownable { /// @notice Mints a certain amount of tokens to the account. /// @param account The account to mint the tokens to. /// @param amount The amount of tokens to mint. - function mint(address account, uint256 amount) external nonReentrant { + function mint(address account, uint256 amount) external { address sender = _msgSender(); MinterData memory minterCache = minters[sender]; @@ -73,7 +71,7 @@ contract ESynth is ERC20Collateral, Ownable { /// have an allowance for the sender. /// @param burnFrom The account to burn the tokens from. /// @param amount The amount of tokens to burn. - function burn(address burnFrom, uint256 amount) external nonReentrant { + function burn(address burnFrom, uint256 amount) external { address sender = _msgSender(); MinterData memory minterCache = minters[sender]; @@ -122,8 +120,8 @@ contract ESynth is ERC20Collateral, Ownable { /// @dev This function returns the account on behalf of which the current operation is being performed, which is /// either msg.sender or the account authenticated by the EVC. /// @return The address of the message sender. - function _msgSender() internal view virtual override (ERC20Collateral, Context) returns (address) { - return ERC20Collateral._msgSender(); + function _msgSender() internal view virtual override (ERC20EVCCompatible, Context) returns (address) { + return ERC20EVCCompatible._msgSender(); } // -------- TotalSupply Management -------- diff --git a/test/unit/esvault/ESVaultTestBase.t.sol b/test/unit/esvault/ESVaultTestBase.t.sol index 32b935b2..6afeb762 100644 --- a/test/unit/esvault/ESVaultTestBase.t.sol +++ b/test/unit/esvault/ESVaultTestBase.t.sol @@ -15,9 +15,9 @@ contract ESVaultTestBase is EVaultTestBase { function setUp() public virtual override { super.setUp(); - assetTSTAsSynth = ESynth(address(new ESynth(evc, "Test Synth", "TST"))); + assetTSTAsSynth = ESynth(address(new ESynth(address(evc), "Test Synth", "TST"))); assetTST = TestERC20(address(assetTSTAsSynth)); - assetTST2AsSynth = ESynth(address(new ESynth(evc, "Test Synth 2", "TST2"))); + assetTST2AsSynth = ESynth(address(new ESynth(address(evc), "Test Synth 2", "TST2"))); assetTST2 = TestERC20(address(assetTST2AsSynth)); eTST = createSynthEVault(address(assetTST)); diff --git a/test/unit/esynth/ESynth.totalSupply.t.sol b/test/unit/esynth/ESynth.totalSupply.t.sol index 65c2e61c..2a1de955 100644 --- a/test/unit/esynth/ESynth.totalSupply.t.sol +++ b/test/unit/esynth/ESynth.totalSupply.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import {Test} from "forge-std/Test.sol"; -import {ESynth, IEVC, Ownable} from "../../../src/Synths/ESynth.sol"; +import {ESynth, Ownable} from "../../../src/Synths/ESynth.sol"; contract ESynthTotalSupplyTest is Test { ESynth synth; @@ -13,7 +13,7 @@ contract ESynthTotalSupplyTest is Test { function setUp() public { vm.startPrank(owner); - synth = new ESynth(IEVC(makeAddr("evc")), "TestSynth", "TS"); + synth = new ESynth(makeAddr("evc"), "TestSynth", "TS"); synth.setCapacity(owner, 1000000e18); vm.stopPrank(); } diff --git a/test/unit/esynth/lib/ESynthTest.sol b/test/unit/esynth/lib/ESynthTest.sol index 37e87135..4c32d8f7 100644 --- a/test/unit/esynth/lib/ESynthTest.sol +++ b/test/unit/esynth/lib/ESynthTest.sol @@ -19,7 +19,7 @@ contract ESynthTest is EVaultTestBase { user1 = vm.addr(1001); user2 = vm.addr(1002); - esynth = ESynth(address(new ESynth(evc, "Test Synth", "TST"))); + esynth = ESynth(address(new ESynth(address(evc), "Test Synth", "TST"))); assetTST = TestERC20(address(esynth)); eTST = createSynthEVault(address(assetTST)); diff --git a/test/unit/pegStabilityModules/PSM.t.sol b/test/unit/pegStabilityModules/PSM.t.sol index 6bf3fb4d..076df910 100644 --- a/test/unit/pegStabilityModules/PSM.t.sol +++ b/test/unit/pegStabilityModules/PSM.t.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; import {PegStabilityModule, EVCUtil} from "../../../src/Synths/PegStabilityModule.sol"; -import {ESynth, IEVC} from "../../../src/Synths/ESynth.sol"; +import {ESynth} from "../../../src/Synths/ESynth.sol"; import {TestERC20} from "../../mocks/TestERC20.sol"; import {EthereumVaultConnector} from "ethereum-vault-connector/EthereumVaultConnector.sol"; import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; @@ -22,7 +22,7 @@ contract PSMTest is Test { PegStabilityModule public psm; - IEVC public evc; + EthereumVaultConnector public evc; address public owner = makeAddr("owner"); address public wallet1 = makeAddr("wallet1"); @@ -37,7 +37,7 @@ contract PSMTest is Test { // Deploy synth vm.prank(owner); - synth = new ESynth(evc, "TestSynth", "TSYNTH"); + synth = new ESynth(address(evc), "TestSynth", "TSYNTH"); // Deploy PSM vm.prank(owner); From f8b969ceead92de35fc181d03e291e0153fc0166 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 3 Jul 2024 12:42:43 +0100 Subject: [PATCH 21/76] fix: change constructor param type --- src/Synths/EulerSavingsRate.sol | 5 ++--- src/Synths/PegStabilityModule.sol | 2 +- test/unit/esr/lib/ESRTest.sol | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index 30cf87bf..51ad23f2 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -6,7 +6,6 @@ import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; import {ERC4626} from "openzeppelin-contracts/token/ERC20/extensions/ERC4626.sol"; -import {IEVC} from "ethereum-vault-connector/interfaces/IEthereumVaultConnector.sol"; import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; /// @title EulerSavingsRate @@ -55,8 +54,8 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { esrSlot.locked = UNLOCKED; } - constructor(IEVC _evc, address _asset, string memory _name, string memory _symbol) - EVCUtil(address(_evc)) + constructor(address _evc, address _asset, string memory _name, string memory _symbol) + EVCUtil(_evc) ERC4626(IERC20(_asset)) ERC20(_name, _symbol) { diff --git a/src/Synths/PegStabilityModule.sol b/src/Synths/PegStabilityModule.sol index adc64b18..c079b4bb 100644 --- a/src/Synths/PegStabilityModule.sol +++ b/src/Synths/PegStabilityModule.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import {EVCUtil, IEVC} from "ethereum-vault-connector/utils/EVCUtil.sol"; +import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {ESynth} from "./ESynth.sol"; diff --git a/test/unit/esr/lib/ESRTest.sol b/test/unit/esr/lib/ESRTest.sol index 8ae9c6fa..74dd1a29 100644 --- a/test/unit/esr/lib/ESRTest.sol +++ b/test/unit/esr/lib/ESRTest.sol @@ -20,7 +20,7 @@ contract ESRTest is Test { function setUp() public virtual { asset = new MockToken(); evc = new EVC(); - esr = new EulerSavingsRate(evc, address(asset), NAME, SYMBOL); + esr = new EulerSavingsRate(address(evc), address(asset), NAME, SYMBOL); // Set a non zero timestamp vm.warp(420); From 39a142eb4f7d8b52a57576f505a7696378571431 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 3 Jul 2024 13:12:48 +0100 Subject: [PATCH 22/76] feat: change transferFrom method order --- src/EVault/shared/lib/SafeERC20Lib.sol | 24 ++++++++++--------- .../modules/Vault/balancesNoInterest.t.sol | 4 ++-- test/unit/evault/modules/Vault/borrow.t.sol | 4 ++-- test/unit/evault/modules/Vault/deposit.t.sol | 16 ++++++------- .../evault/modules/Vault/maliciousToken.t.sol | 16 ++++++------- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/EVault/shared/lib/SafeERC20Lib.sol b/src/EVault/shared/lib/SafeERC20Lib.sol index d1b99af6..49161cc3 100644 --- a/src/EVault/shared/lib/SafeERC20Lib.sol +++ b/src/EVault/shared/lib/SafeERC20Lib.sol @@ -11,8 +11,7 @@ import {IPermit2} from "../../../interfaces/IPermit2.sol"; /// @author Euler Labs (https://www.eulerlabs.com/) /// @notice The library provides helpers for ERC20 transfers, including Permit2 support library SafeERC20Lib { - error E_TransferFromFailed(bytes errorTransferFrom, bytes errorPermit2); - error E_Permit2AmountOverflow(); + error E_TransferFromFailed(bytes errorPermit2, bytes errorTransferFrom); // If no code exists under the token address, the function will succeed. EVault ensures this is not the case in // `initialize`. @@ -26,18 +25,21 @@ library SafeERC20Lib { } function safeTransferFrom(IERC20 token, address from, address to, uint256 value, address permit2) internal { - (bool success, bytes memory tryData) = trySafeTransferFrom(token, from, to, value); - bytes memory fallbackData; - if (!success && permit2 != address(0)) { - if (value > type(uint160).max) { - revert E_TransferFromFailed(tryData, abi.encodePacked(E_Permit2AmountOverflow.selector)); - } - // it's now safe to down-cast value to uint160 - (success, fallbackData) = + bool success; + bytes memory permit2Data; + bytes memory transferData; + + if (permit2 != address(0) && value <= type(uint160).max) { + // it's safe to down-cast value to uint160 + (success, permit2Data) = permit2.call(abi.encodeCall(IPermit2.transferFrom, (from, to, uint160(value), address(token)))); } - if (!success) revert E_TransferFromFailed(tryData, fallbackData); + if (!success) { + (success, transferData) = trySafeTransferFrom(token, from, to, value); + } + + if (!success) revert E_TransferFromFailed(permit2Data, transferData); } // If no code exists under the token address, the function will succeed. EVault ensures this is not the case in diff --git a/test/unit/evault/modules/Vault/balancesNoInterest.t.sol b/test/unit/evault/modules/Vault/balancesNoInterest.t.sol index 0f26b06e..b149d379 100644 --- a/test/unit/evault/modules/Vault/balancesNoInterest.t.sol +++ b/test/unit/evault/modules/Vault/balancesNoInterest.t.sol @@ -63,8 +63,8 @@ contract VaultTest_BalancesNoInterest is EVaultTestBase { vm.expectRevert( abi.encodeWithSelector( SafeERC20Lib.E_TransferFromFailed.selector, - abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds balance"), - abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0) + abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0), + abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds balance") ) ); eTST.deposit(1, user1); diff --git a/test/unit/evault/modules/Vault/borrow.t.sol b/test/unit/evault/modules/Vault/borrow.t.sol index 04125b1c..e8ce0b66 100644 --- a/test/unit/evault/modules/Vault/borrow.t.sol +++ b/test/unit/evault/modules/Vault/borrow.t.sol @@ -179,8 +179,8 @@ contract VaultTest_Borrow is EVaultTestBase { vm.expectRevert( abi.encodeWithSelector( SafeERC20Lib.E_TransferFromFailed.selector, - abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance"), - abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0) + abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0), + abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance") ) ); eTST.repay(type(uint256).max, borrower); diff --git a/test/unit/evault/modules/Vault/deposit.t.sol b/test/unit/evault/modules/Vault/deposit.t.sol index f4cdd67b..ec4291d7 100644 --- a/test/unit/evault/modules/Vault/deposit.t.sol +++ b/test/unit/evault/modules/Vault/deposit.t.sol @@ -243,8 +243,8 @@ contract VaultTest_Deposit is EVaultTestBase { vm.expectRevert( abi.encodeWithSelector( SafeERC20Lib.E_TransferFromFailed.selector, - abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance"), - abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0) + abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0), + abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance") ) ); eTST.deposit(amount, user); @@ -275,8 +275,8 @@ contract VaultTest_Deposit is EVaultTestBase { vm.expectRevert( abi.encodeWithSelector( SafeERC20Lib.E_TransferFromFailed.selector, - abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance"), - abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0) + abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0), + abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance") ) ); eTST.deposit(amount, user); @@ -339,8 +339,8 @@ contract VaultTest_Deposit is EVaultTestBase { vm.expectRevert( abi.encodeWithSelector( SafeERC20Lib.E_TransferFromFailed.selector, - abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance"), - abi.encodeWithSelector(InsufficientAllowance.selector, amount - 1) + abi.encodeWithSelector(InsufficientAllowance.selector, amount - 1), + abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance") ) ); evc.batch(items); @@ -352,8 +352,8 @@ contract VaultTest_Deposit is EVaultTestBase { vm.expectRevert( abi.encodeWithSelector( SafeERC20Lib.E_TransferFromFailed.selector, - abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance"), - abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 1) + abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 1), + abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance") ) ); eTST.deposit(amount, user); diff --git a/test/unit/evault/modules/Vault/maliciousToken.t.sol b/test/unit/evault/modules/Vault/maliciousToken.t.sol index d2d96dd7..c1de5f38 100644 --- a/test/unit/evault/modules/Vault/maliciousToken.t.sol +++ b/test/unit/evault/modules/Vault/maliciousToken.t.sol @@ -91,8 +91,8 @@ contract VaultTest_MaliciousToken is EVaultTestBase { vm.expectRevert( abi.encodeWithSelector( SafeERC20Lib.E_TransferFromFailed.selector, - abi.encodeWithSignature("Error(string)", "revert behaviour"), - abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0) + abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0), + abi.encodeWithSignature("Error(string)", "revert behaviour") ) ); eTST2.repay(1e18, user3); @@ -103,8 +103,8 @@ contract VaultTest_MaliciousToken is EVaultTestBase { vm.expectRevert( abi.encodeWithSelector( SafeERC20Lib.E_TransferFromFailed.selector, - abi.encodeWithSignature("Error(string)", "revert behaviour"), - abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0) + abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0), + abi.encodeWithSignature("Error(string)", "revert behaviour") ) ); startHoax(user1); @@ -120,8 +120,8 @@ contract VaultTest_MaliciousToken is EVaultTestBase { vm.expectRevert( abi.encodeWithSelector( SafeERC20Lib.E_TransferFromFailed.selector, - abi.encodeWithSignature("E_Reentrancy()"), - abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0) + abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0), + abi.encodeWithSignature("E_Reentrancy()") ) ); eTST.deposit(1e18, user1); @@ -135,8 +135,8 @@ contract VaultTest_MaliciousToken is EVaultTestBase { vm.expectRevert( abi.encodeWithSelector( SafeERC20Lib.E_TransferFromFailed.selector, - abi.encodeWithSignature("E_Reentrancy()"), - abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0) + abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0), + abi.encodeWithSignature("E_Reentrancy()") ) ); eTST.deposit(1e18, user1); From 27be669c4146eea47e31e6f58cc8e9696060fa64 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 3 Jul 2024 13:22:37 +0100 Subject: [PATCH 23/76] chore: update specs --- docs/specs.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/specs.md b/docs/specs.md index f48bce45..28eabad2 100644 --- a/docs/specs.md +++ b/docs/specs.md @@ -16,7 +16,8 @@ |EVK-71|Miscellaneous Hooked Operations |Some of the operation types of the vault may be configured to call the hook target when being performed thanks to which the vault supports a limited hooking functionality. When operation is performed, the vault must check if the corresponding operation is set in hooked ops.

If the corresponding operation is set in the hooked ops AND the hook target configured is a compatible smart contract, the hook target is `call`ed using the same `msg.data` as was provided to the vault, along with the EVC-authenticated caller appended as trailing calldata. If the call is successful, the execution of the operation must carry on as usual. If the call is unsuccessful, the operation must revert with the hook target error bubbled up.

If the corresponding operation is set in the hooked ops AND the hook target configured is address zero, then the vault operation fails unconditionally as it is considered disabled.

In addition to user-invokable functions, the operation than can also be hooked is `checkVaultStatus`. The hook target will be invoked when the EVC calls `checkVaultStatus` on the vault, which is typically at the end of any batch that has interacted with the vault. This hook can be used to reject operations that violate "post-condition" properties of the vault.

[https://docs.euler.finance/euler-vault-kit-white-paper/#hooks](https://docs.euler.finance/euler-vault-kit-white-paper/#hooks) | |EVK-65|Miscellaneous Interest and Fees Accrual |Any state-modifying operation that affects the vault balances and liabilities, must operate on the up to date vault state. In order to keep the vault state up to date, when loading the cache, the vault state is updated considering the following:

* the interest is accrued since the last update time using the current interest rate
* the fees are cut from the accrued interest as per the interest fee parameter
* new vault shares are minted to grant ownership over the fees to the virtual temporary fee holder account

[https://docs.euler.finance/euler-vault-kit-white-paper/#interest](https://docs.euler.finance/euler-vault-kit-white-paper/#interest) | |EVK-69|Miscellaneous LTV Ramping |Vaults must implement LTV ramping allowing the governor to change the LTV of the collateral without putting outstanding loans into immediate violation.

When using the `setLTV` function, the governor can specify the target LTV and the ramp duration. When an account wants to take on a new liability or modify its existing position, the account health must be evaluated using the target LTV value configured. For the purpose of liquidations however, the account health must be evaluated by calculating the current LTV, assuming it changes linearly from the original LTV stored to the target LTV configured in the configured ramp duration time.

[https://docs.euler.finance/euler-vault-kit-white-paper/#ltv-ramping](https://docs.euler.finance/euler-vault-kit-white-paper/#ltv-ramping) | -|EVK-74|Miscellaneous Pull Assets |Whenever the underlying assets are being pulled into the vault, the operation must try to transfer the assets from the specified account to the vault directly. If the direct transfer fails and `permit2` contract address is configured, the operation must try to transfer the assets from the specified account to the vault using `permit2` contract. | +|EVK-74|Miscellaneous Pull Assets |Whenever the underlying assets are being pulled into the vault, if `permit2` contract address is configured and the transferred amount fits `uint160`, the operation must try to transfer the assets from the specified account to the vault using `permit2` contract. If the transfer using `permit2` contract fails or is not possible, the operation must try to transfer the assets from the specified account to the vault directly. + | |EVK-73|Miscellaneous Push Assets |Whenever the underlying assets are being pushed out of the vault, the operation must revert if:

* the receiver address is `address(0)`, OR
* if asset receiver validation enabled AND the receiver address is a known virtual account | |EVK-63|Miscellaneous Re-entrancy |Each state-modifying function must be re-entrancy protected.

Each static function that reads regions of the state that may be unsafe to access during an ongoing operation on the vault must be read-only re-entrancy protected.

The installed hook target contract address can bypass the vault's read-only reentrancy protection. This means that hook functions can call view methods on the vault during their operation. However, hooks cannot perform state changing operations because of the normal reentrancy lock. | |EVK-68|Miscellaneous Vault Status Checks and Snapshots |Any state-modifying function of the vault which loads the vault cache must schedule the vault status check. This is in order not only to potentially check whether the vault status is valid at the end of the call frame, but also to update the interest rate.

Before the vault status check is scheduled, if the supply cap or the borrow cap can be exceeded (any of them is not defined as max possible value), a snapshot of the initial state of the vault must be created which includes:

* the total amount of assets held by the vault
* the total amount of liabilities issued by the vault

If the status of any of the vaults for which the vault status check has been scheduled is invalid, the call frame in which checks are performed must revert.

[https://evc.wtf/docs/concepts/internals/vault-status-checks](https://evc.wtf/docs/concepts/internals/vault-status-checks) | From b3afaf49bca479d4e8280729e1f8f0df6c3648c8 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 3 Jul 2024 13:40:31 +0100 Subject: [PATCH 24/76] chore: update specs --- docs/specs.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/specs.md b/docs/specs.md index f48bce45..c92f0400 100644 --- a/docs/specs.md +++ b/docs/specs.md @@ -72,8 +72,8 @@ |EVK-13|Protocol Config |The Protocol Config contract is the representative of the DAO's interests in the ecosystem. What vaults allow this contract to control is strictly limited.

Functions implemented that are called by the vaults:

`isValidInterestFee` - determines whether the value of the interest fee is allowed; vaults only invoke this if the governor attempts to set an interest fee outside of the range <`GUARANTEED_INTEREST_FEE_MIN`, `GUARANTEED_INTEREST_FEE_MAX`>

`protocolFeeConfig` - called when fees are converted; it returns the following the recipient address for the DAO's share of the fees and the fraction of the interest fees that should be sent to the DAO

[https://docs.euler.finance/euler-vault-kit-white-paper/#protocolconfig](https://docs.euler.finance/euler-vault-kit-white-paper/#protocolconfig) | |EVK-83|Sequence Registry |The Sequence Registry contract provides an interface for reserving sequence IDs. This contract maintains sequence counters associated with opaque designator strings. Each counter starts at 1. Anybody can reserve a sequence ID by calling `reserveSeqIdfunction`. The only guarantee provided is that no two reservations for the same designator will get the same ID. | |EVK-75|Synthetic Asset Vault |Synthetic asset vault is a special configuration of a vault which uses hooks to disable `mint`, `redeem`, `skim` and `repayWithShares` operations. The `deposit` operation is also disabled for all the callers except the underlying synth asset address itself, disallowing deposits from normal users.

Instead of a utilization based interest rate model, synthetic vaults use a reactive interest rate model which adjusts the interest rate based on the trading price of the synthetic asset. This mechanism, the peg stability module and the savings rate module aim to keep the synthetic asset pegged as tight as possible to the peg asset. The price feed used when borrowing from the synthetic vault is the asset which the synthetic asset is pegged to, creating a CDP based synthetic asset. | -|EVK-81|Synthetic Asset Vault `ERC20Collateral` |`ERC20Collateral` is an `ERC20`-compatible token with the EVC support which allows it to be used as collateral in other vault.

`ERC20Collateral` tokens comply with the following specification:

* Whenever the contract is called via the EVC (`msg.sender == EVC`), the current on behalf of account is fetched from the EVC and used as an authenticated caller address. This ensures liquidations can be processed via `EVC.controlCollateral` function
* The account status check is requested after every operation that can potentially negatively affect the account health (i.e. whenever the tokens are transferred out of the account). This ensures the account is never unhealthy after the operation | -|EVK-77|Synthetic Asset Vault `ESynth` |`ESynth` is an `ERC20`-compatible token with the EVC support which, thanks to relying on the EVC authentication and requesting the account status checks on token transfers and burns, allows it to be used as collateral in other vault. It is meant to be used as an underlying asset of the synthetic asset vault.

`ESynth` tokens comply with the following specification:

**ERC20Collateral**

`ESynth` inherits from `ERC20Collateral` making it compliant with the `ERC20Collateral` specification.

**Minting**

The owner of the contract can set a minting capacity by calling `setCapacity(address minter, uint128 capacity)` for any address which allows them to mint the synthetic asset up to the defined amount. Minters mint by calling `mint(address account, uint256 amount)`.

**Burning**

Any address can burn the synthetic from another address given they have the allowance to do so. The owner is exempt from this restriction when burning from the `ESynth` contract itself. When one burns from an address and they have a previously minted amount, the minted amount is reduced by the burned amount, freeing up their minting capacity. Burning can be done by calling `burn(address account, uint256 amount)`. Account status check is requested for the account from which the assets are burned.

**Allocating to the synthetic asset vault**

The owner can allocate assets held by the asset contract itself to a synthetic asset vault created specifically for this asset by calling `allocate(address vault, uint256 amount)`. This serves as a protocol deposit to the vault. Any allocation needs to be first minted by a minter or owner into the synthetic asset contract. The vault into which the assets are allocated must be integrated with the same EVC instance as the `ESynth` contract itself. On allocation, the vault is added to the addresses whose balances are ignored when calculating the `totalSupply`.

**Deallocating from the synthetic asset vault**

The owner can deallocate synthetic assets from the vault by calling `deallocate(address vault, uint256 amount)`. This serves as a protocol withdraw from the synthetic asset vault. Assets deallocated from the vault will be transferred into the synthetic asset contract itself and can in turn be burned by the owner.

**Total supply adjustments**

Since the protocol deposits into synthetic asset vaults are not backed by any collateral and are not in circulation, they are excluded from the `totalSupply` calculation. After calling `allocate()`, target vaults are automatically excluded. Additional addresses whose balances should be ignored can be managed by the owner by calling `addIgnoredForTotalSupply(address account)` and `removeIgnoredForTotalSupply(address account)`. | +|EVK-81|Synthetic Asset Vault `ERC20EVKCompatible` |`ERC20EVKCompatible` is an `ERC20`-compatible token with the EVC support.

Whenever the contract is called via the EVC (`msg.sender == EVC`), the current on behalf of account is fetched from the EVC and used as an authenticated caller address. This ensures the `ERC20EVKCompatible` is compatible with the EVC authentication system. | +|EVK-77|Synthetic Asset Vault `ESynth` |`ESynth` is an `ERC20`-compatible token with the EVC support. It is meant to be used as an underlying asset of the synthetic asset vault.

`ESynth` tokens comply with the following specification:

**ERC20EVCCompatible**

`ESynth` inherits from `ERC20EVCCompatible` making it compliant with the `ERC20EVCCompatible` specification.

**Minting**

The owner of the contract can set a minting capacity by calling `setCapacity(address minter, uint128 capacity)` for any address which allows them to mint the synthetic asset up to the defined amount. Minters mint by calling `mint(address account, uint256 amount)`.

**Burning**

Any address can burn the synthetic from another address given they have the allowance to do so. The owner is exempt from this restriction when burning from the `ESynth` contract itself. When one burns from an address and they have a previously minted amount, the minted amount is reduced by the burned amount, freeing up their minting capacity. Burning can be done by calling `burn(address account, uint256 amount)`.

**Allocating to the synthetic asset vault**

The owner can allocate assets held by the asset contract itself to a synthetic asset vault created specifically for this asset by calling `allocate(address vault, uint256 amount)`. This serves as a protocol deposit to the vault. Any allocation needs to be first minted by a minter or owner into the synthetic asset contract. The vault into which the assets are allocated must be integrated with the same EVC instance as the `ESynth` contract itself. On allocation, the vault is added to the addresses whose balances are ignored when calculating the `totalSupply`.

**Deallocating from the synthetic asset vault**

The owner can deallocate synthetic assets from the vault by calling `deallocate(address vault, uint256 amount)`. This serves as a protocol withdraw from the synthetic asset vault. Assets deallocated from the vault will be transferred into the synthetic asset contract itself and can in turn be burned by the owner.

**Total supply adjustments**

Since the protocol deposits into synthetic asset vaults are not backed by any collateral and are not in circulation, they are excluded from the `totalSupply` calculation. After calling `allocate()`, target vaults are automatically excluded. Additional addresses whose balances should be ignored can be managed by the owner by calling `addIgnoredForTotalSupply(address account)` and `removeIgnoredForTotalSupply(address account)`. | |EVK-79|Synthetic Asset Vault `EulerSavingsRate` |`EulerSavingsRate` is a `ERC4626`-compatible vault which allows users to deposit the underlying asset and receive interest in the form of the same underlying asset. On withdraw, redeem and transfers, the account status checks must be requested for the account which health might be negatively affected. Thanks to that, the shares of the `EulerSavingsRate`vault might be used as collateral by other EVC-compatible vaults.

Account balances must be tracked internally in a donation attack resistant manner.

Anyone can transfer the underlying asset into the vault and call `gulp` which distributes those directly transferred assets to the shareholders over the `INTEREST_SMEAR` period. Accrued interest must adjust the exchange rate accordingly.

On `gulp`, any interest which has not been yet distributed is smeared for an additional `INTEREST_SMEAR` period. In theory, this means that interest could be smeared indefinitely by continuously calling `gulp`. In practice, it is expected that the interest will keep accruing, negating any negative side effects which may come from the smearing mechanism. | |EVK-78|Synthetic Asset Vault `IRMSynth` |Synthetic asset vaults use a different interest rate model than the standard vaults. The `IRMSynth` interest rate model is a simple reactive rate model which adjusts the interest rate up when it trades below the `targetQuote` and down when it trades above or at the `targetQuote`.

**Parameters**

* `targetQuote` price being targeted by the IRM
* `MAX_RATE` maximum rate charged
* `BASE_RATE` minimum and starting rate for the IRM
* `ADJUST_FACTOR` factor by which the previous rate is adjusted per `ADJUST_INTERVAL`
* `ADJUST_INTERVAL` time that needs to pass before the rate can be adjusted again

**Algorithm**

1. If the `ADJUST_INTERVAL` did not pass, the previous rate must be returned 2. If the oracle configured returns a zero quote, the previous rate must be returned 3. If the synthetic asset trades below the `targetQuote`, the rate must be raised by the factor of `ADJUST_FACTOR` 4. If the synthetic asset is trading at the `targetQuote` or below, the rate must be lowered by the factor of `ADJUST_FACTOR` 5. Minimum `BASE_RATE` must be enforced 6. Maximum `MAX_RATE` must be enforced 7. The updated rate the timestamp of the last update must be stored | |EVK-80|Synthetic Asset Vault `PegStabilityModule` |The `PegStabilityModule` is granted minting rights on the `ESynth` and must allow slippage-free conversion from and to the underlying asset as per configured `conversionPrice`. On deployment, the fee for swaps to synthetic asset and to underlying asset are defined. These fees must accrue to the `PegStabilityModule` contract and can not be withdrawn, serving as a permanent reserve to support the peg. Swapping to the synthetic asset is possible up to the minting cap granted for the `PegStabilityModule` in the `ESynth`. Swapping to the underlying asset is possible up to the amount of the underlying asset held by the `PegStabilityModule`. | From 8edac4914f2c4f92d938a7ece352cd22556bb9ed Mon Sep 17 00:00:00 2001 From: Doug Hoyte Date: Wed, 3 Jul 2024 13:21:54 -0400 Subject: [PATCH 25/76] remove unnecessary check for 0 length symbol/names --- src/EVault/modules/Token.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EVault/modules/Token.sol b/src/EVault/modules/Token.sol index 3aec5ed3..fab2fbbc 100644 --- a/src/EVault/modules/Token.sol +++ b/src/EVault/modules/Token.sol @@ -18,12 +18,12 @@ abstract contract TokenModule is IToken, BalanceUtils { /// @inheritdoc IERC20 function name() public view virtual reentrantOK returns (string memory) { - return bytes(vaultStorage.name).length > 0 ? vaultStorage.name : "Unnamed Euler Vault"; + return vaultStorage.name; } /// @inheritdoc IERC20 function symbol() public view virtual reentrantOK returns (string memory) { - return bytes(vaultStorage.symbol).length > 0 ? vaultStorage.symbol : "UNKNOWN"; + return vaultStorage.symbol; } /// @inheritdoc IERC20 From 45c2932e60e6ea81fb63efede2b953d896907150 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Thu, 4 Jul 2024 18:05:44 +0100 Subject: [PATCH 26/76] fix: cantina 488 --- src/EVault/shared/Constants.sol | 4 ++-- src/GenericFactory/GenericFactory.sol | 5 +++-- test/unit/evault/modules/Governance/hookedOps.t.sol | 2 +- test/unit/evault/modules/Initialize/errors.t.sol | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/EVault/shared/Constants.sol b/src/EVault/shared/Constants.sol index ac206290..b0fcbbc2 100644 --- a/src/EVault/shared/Constants.sol +++ b/src/EVault/shared/Constants.sol @@ -11,8 +11,8 @@ uint256 constant MAX_SANE_AMOUNT = type(uint112).max; // Last 31 bits are zeros to ensure max debt rounded up equals max sane amount. uint256 constant MAX_SANE_DEBT_AMOUNT = uint256(MAX_SANE_AMOUNT) << INTERNAL_DEBT_PRECISION_SHIFT; // proxy trailing calldata length in bytes. -// Three addresses, 20 bytes each: vault underlying asset, oracle and unit of account. -uint256 constant PROXY_METADATA_LENGTH = 60; +// Three addresses, 20 bytes each: vault underlying asset, oracle and unit of account + 4 empty bytes. +uint256 constant PROXY_METADATA_LENGTH = 64; // gregorian calendar uint256 constant SECONDS_PER_YEAR = 365.2425 * 86400; // max interest rate accepted from IRM. 1,000,000% APY: floor(((1000000 / 100 + 1)**(1/(86400*365.2425)) - 1) * 1e27) diff --git a/src/GenericFactory/GenericFactory.sol b/src/GenericFactory/GenericFactory.sol index 43946010..34fbffc8 100644 --- a/src/GenericFactory/GenericFactory.sol +++ b/src/GenericFactory/GenericFactory.sol @@ -123,12 +123,13 @@ contract GenericFactory is MetaProxyDeployer { if (desiredImplementation == address(0) || desiredImplementation != _implementation) revert E_Implementation(); + bytes memory prefixTrailingData = abi.encodePacked(bytes4(0), trailingData); address proxy; if (upgradeable) { - proxy = address(new BeaconProxy(trailingData)); + proxy = address(new BeaconProxy(prefixTrailingData)); } else { - proxy = deployMetaProxy(desiredImplementation, trailingData); + proxy = deployMetaProxy(desiredImplementation, prefixTrailingData); } proxyLookup[proxy] = diff --git a/test/unit/evault/modules/Governance/hookedOps.t.sol b/test/unit/evault/modules/Governance/hookedOps.t.sol index d0eb481f..489faa0e 100644 --- a/test/unit/evault/modules/Governance/hookedOps.t.sol +++ b/test/unit/evault/modules/Governance/hookedOps.t.sol @@ -69,7 +69,7 @@ contract Governance_HookedOps is EVaultTestBase { } function getHookCalldata(bytes memory data, address sender) internal view returns (bytes memory) { - data = abi.encodePacked(data, eTST.asset(), eTST.oracle(), eTST.unitOfAccount()); + data = abi.encodePacked(data, bytes4(0), eTST.asset(), eTST.oracle(), eTST.unitOfAccount()); if (sender != address(0)) data = abi.encodePacked(data, sender); diff --git a/test/unit/evault/modules/Initialize/errors.t.sol b/test/unit/evault/modules/Initialize/errors.t.sol index f1a807ed..18424e8e 100644 --- a/test/unit/evault/modules/Initialize/errors.t.sol +++ b/test/unit/evault/modules/Initialize/errors.t.sol @@ -29,7 +29,7 @@ contract InitializeTests is EVaultTestBase, MetaProxyDeployer { } function test_asset_is_a_contract() public { - bytes memory trailingData = abi.encodePacked(address(0), address(1), address(2)); + bytes memory trailingData = abi.encodePacked(bytes4(0), address(0), address(1), address(2)); address proxy = deployMetaProxy(address(new Initialize(integrations)), trailingData); vm.expectRevert(Errors.E_BadAddress.selector); From 67533d4e9e8270d55cafb2e4b69b2862d2abb948 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Thu, 4 Jul 2024 18:11:34 +0100 Subject: [PATCH 27/76] fix: cantina 490 --- src/EVault/shared/BalanceUtils.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/EVault/shared/BalanceUtils.sol b/src/EVault/shared/BalanceUtils.sol index 1c6105e8..21479b00 100644 --- a/src/EVault/shared/BalanceUtils.sol +++ b/src/EVault/shared/BalanceUtils.sol @@ -80,6 +80,10 @@ abstract contract BalanceUtils is Base { Shares newFromBalance = origFromBalance.subUnchecked(amount); user.setBalance(newFromBalance); + if (fromBalanceForwarderEnabled) { + balanceTracker.balanceTrackerHook(from, newFromBalance.toUint(), isControlCollateralInProgress()); + } + // update to user = vaultStorage.users[to]; @@ -89,11 +93,7 @@ abstract contract BalanceUtils is Base { Shares newToBalance = origToBalance + amount; user.setBalance(newToBalance); - if (fromBalanceForwarderEnabled) { - balanceTracker.balanceTrackerHook(from, newFromBalance.toUint(), isControlCollateralInProgress()); - } - - if (toBalanceForwarderEnabled && from != to) { + if (toBalanceForwarderEnabled) { balanceTracker.balanceTrackerHook(to, newToBalance.toUint(), false); } } From f97f20c79bb013eb63c0b1cefd3d0ac2d6f1ae68 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Fri, 5 Jul 2024 14:19:37 +0200 Subject: [PATCH 28/76] fix negative interest in transfer debt logs; remove return value from pullDebt --- src/EVault/EVault.sol | 2 +- src/EVault/IEVault.sol | 5 ++- src/EVault/modules/Borrowing.sol | 6 +-- src/EVault/shared/BorrowUtils.sol | 10 +++-- test/unit/evault/modules/Vault/borrow.t.sol | 44 +++++++++++++++++++++ 5 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/EVault/EVault.sol b/src/EVault/EVault.sol index d1149c27..e8c4f515 100644 --- a/src/EVault/EVault.sol +++ b/src/EVault/EVault.sol @@ -122,7 +122,7 @@ contract EVault is Dispatch { function repayWithShares(uint256 amount, address receiver) public virtual override callThroughEVC use(MODULE_BORROWING) returns (uint256 shares, uint256 debt) {} - function pullDebt(uint256 amount, address from) public virtual override callThroughEVC use(MODULE_BORROWING) returns (uint256) {} + function pullDebt(uint256 amount, address from) public virtual override callThroughEVC use(MODULE_BORROWING) {} function flashLoan(uint256 amount, bytes calldata data) public virtual override use(MODULE_BORROWING) {} diff --git a/src/EVault/IEVault.sol b/src/EVault/IEVault.sol index 9ed1f988..fb18d71e 100644 --- a/src/EVault/IEVault.sol +++ b/src/EVault/IEVault.sol @@ -250,8 +250,9 @@ interface IBorrowing { /// @notice Take over debt from another account /// @param amount Amount of debt in asset units (use max uint256 for all the account's debt) /// @param from Account to pull the debt from - /// @return Amount of debt pulled in asset units. - function pullDebt(uint256 amount, address from) external returns (uint256); + /// @dev Due to internal debt precision accounting, the liability reported on either or both accounts after + /// calling `pullDebt` may not match the `amount` requested precisely + function pullDebt(uint256 amount, address from) external; /// @notice Request a flash-loan. A onFlashLoan() callback in msg.sender will be invoked, which must repay the loan /// to the main Euler address prior to returning. diff --git a/src/EVault/modules/Borrowing.sol b/src/EVault/modules/Borrowing.sol index 4d318724..0f4217f1 100644 --- a/src/EVault/modules/Borrowing.sol +++ b/src/EVault/modules/Borrowing.sol @@ -128,19 +128,17 @@ abstract contract BorrowingModule is IBorrowing, AssetTransfers, BalanceUtils, L } /// @inheritdoc IBorrowing - function pullDebt(uint256 amount, address from) public virtual nonReentrant returns (uint256) { + function pullDebt(uint256 amount, address from) public virtual nonReentrant { (VaultCache memory vaultCache, address account) = initOperation(OP_PULL_DEBT, CHECKACCOUNT_CALLER); if (from == account) revert E_SelfTransfer(); Assets assets = amount == type(uint256).max ? getCurrentOwed(vaultCache, from).toAssetsUp() : amount.toAssets(); - if (assets.isZero()) return 0; + if (assets.isZero()) return; transferBorrow(vaultCache, from, account, assets); emit PullDebt(from, account, assets.toUint()); - - return assets.toUint(); } /// @inheritdoc IBorrowing diff --git a/src/EVault/shared/BorrowUtils.sol b/src/EVault/shared/BorrowUtils.sol index 69329ad0..e60faf14 100644 --- a/src/EVault/shared/BorrowUtils.sol +++ b/src/EVault/shared/BorrowUtils.sol @@ -95,13 +95,17 @@ abstract contract BorrowUtils is Base { toOwed = toOwed + amount; setUserBorrow(vaultCache, to, toOwed); - logRepay(from, assets, fromOwedPrev.toAssetsUp(), fromOwed.toAssetsUp()); + // with small fractional debt amounts the interest calculation could be negative in `logRepay` + Assets fromPrevAssets = fromOwedPrev.toAssetsUp(); + Assets fromAssets = fromOwed.toAssetsUp(); + Assets repayAssets = fromPrevAssets > assets + fromAssets ? fromPrevAssets.subUnchecked(fromAssets) : assets; + logRepay(from, repayAssets, fromPrevAssets, fromAssets); // with small fractional debt amounts the interest calculation could be negative in `logBorrow` Assets toPrevAssets = toOwedPrev.toAssetsUp(); Assets toAssets = toOwed.toAssetsUp(); - if (assets + toPrevAssets > toAssets) assets = toAssets - toPrevAssets; - logBorrow(to, assets, toPrevAssets, toAssets); + Assets borrowAssets = assets + toPrevAssets > toAssets ? toAssets.subUnchecked(toPrevAssets) : assets; + logBorrow(to, borrowAssets, toPrevAssets, toAssets); } function computeInterestRate(VaultCache memory vaultCache) internal virtual returns (uint256) { diff --git a/test/unit/evault/modules/Vault/borrow.t.sol b/test/unit/evault/modules/Vault/borrow.t.sol index 04125b1c..5bd9f521 100644 --- a/test/unit/evault/modules/Vault/borrow.t.sol +++ b/test/unit/evault/modules/Vault/borrow.t.sol @@ -679,6 +679,50 @@ contract VaultTest_Borrow is EVaultTestBase { assertEq(eTST.debtOf(borrower), 0); } + function test_repayLogsTransferDebt() external { + eTST.setInterestRateModel(address(new IRMTestFixed())); + + startHoax(borrower); + + evc.enableController(borrower, address(eTST)); + evc.enableCollateral(borrower, address(eTST2)); + assetTST.approve(address(eTST), type(uint256).max); + assetTST.mint(borrower, 1000e18); + + eTST.borrow(1, borrower); + + assetTST2.transfer(borrower2, type(uint256).max / 2); + + startHoax(borrower2); + + assetTST2.approve(address(eTST2), type(uint256).max); + eTST2.deposit(10e18, borrower2); + + evc.enableController(borrower2, address(eTST)); + evc.enableCollateral(borrower2, address(eTST2)); + assetTST.approve(address(eTST), type(uint256).max); + assetTST.mint(borrower2, 1000e18); + + skip(10 days); + + // a little interest accrued (0.3%) + assertEq(owedTo1e5(eTST.debtOfExact(borrower)), 1.00274e5); + + // record interest in storage + startHoax(borrower); + eTST.borrow(1, borrower); + + // now borrower in LogRepay would receive amount = 2, prevOwed = 3, owed = 0. + // Amount is adjusted to 3 and interest accrued is 0, so no event is emitted + startHoax(borrower2); + vm.recordLogs(); + vm.expectEmit(); + emit Events.Repay(borrower, 3); + eTST.pullDebt(2, borrower); + + assertEq(vm.getRecordedLogs().length, 11); // InterestAccrued would be the 12th event + } + function test_borrowLogsTransferDebt() external { eTST.setInterestRateModel(address(new IRMTestFixed())); From a288f5ebc808e706e333bfae265e9e81deb3f9d8 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Fri, 5 Jul 2024 13:27:37 +0100 Subject: [PATCH 29/76] fix: cantina 68 --- src/Synths/ESynth.sol | 4 ++- test/unit/esynth/ESynth.totalSupply.t.sol | 36 +++++++++++++---------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/Synths/ESynth.sol b/src/Synths/ESynth.sol index 5ab4e219..7489e40b 100644 --- a/src/Synths/ESynth.sol +++ b/src/Synths/ESynth.sol @@ -33,7 +33,9 @@ contract ESynth is ERC20Collateral, Ownable { constructor(IEVC evc_, string memory name_, string memory symbol_) ERC20Collateral(evc_, name_, symbol_) Ownable(msg.sender) - {} + { + ignoredForTotalSupply.add(address(this)); + } /// @notice Sets the minting capacity for a minter. /// @dev Can only be called by the owner of the contract. diff --git a/test/unit/esynth/ESynth.totalSupply.t.sol b/test/unit/esynth/ESynth.totalSupply.t.sol index 65c2e61c..80ce217f 100644 --- a/test/unit/esynth/ESynth.totalSupply.t.sol +++ b/test/unit/esynth/ESynth.totalSupply.t.sol @@ -28,8 +28,9 @@ contract ESynthTotalSupplyTest is Test { bool success = synth.addIgnoredForTotalSupply(ignored1); address[] memory ignored = synth.getAllIgnoredForTotalSupply(); - assertEq(ignored.length, 1); - assertEq(ignored[0], ignored1); + assertEq(ignored.length, 2); + assertEq(ignored[0], address(synth)); + assertEq(ignored[1], ignored1); assertTrue(success); } @@ -40,8 +41,9 @@ contract ESynthTotalSupplyTest is Test { vm.stopPrank(); address[] memory ignored = synth.getAllIgnoredForTotalSupply(); - assertEq(ignored.length, 1); - assertEq(ignored[0], ignored1); + assertEq(ignored.length, 2); + assertEq(ignored[0], address(synth)); + assertEq(ignored[1], ignored1); assertFalse(success); } @@ -57,7 +59,8 @@ contract ESynthTotalSupplyTest is Test { vm.stopPrank(); address[] memory ignored = synth.getAllIgnoredForTotalSupply(); - assertEq(ignored.length, 0); + assertEq(ignored[0], address(synth)); + assertEq(ignored.length, 1); assertTrue(success); } @@ -67,29 +70,32 @@ contract ESynthTotalSupplyTest is Test { vm.stopPrank(); address[] memory ignored = synth.getAllIgnoredForTotalSupply(); - assertEq(ignored.length, 0); + assertEq(ignored[0], address(synth)); + assertEq(ignored.length, 1); assertFalse(success); } - function test_totalSupply_nothingIgnored() public { + function test_totalSupply_nothingIgnoredExceptSynth() public { vm.startPrank(owner); - synth.mint(ignored1, 100); - synth.mint(ignored2, 200); - synth.mint(ignored3, 300); + synth.mint(address(synth), 100); + synth.mint(ignored1, 200); + synth.mint(ignored2, 300); + synth.mint(ignored3, 400); vm.stopPrank(); - assertEq(synth.totalSupply(), 600); + assertEq(synth.totalSupply(), 900); } function test_TotalSupplyAddresses_ignored() public { vm.startPrank(owner); - synth.mint(ignored1, 100); - synth.mint(ignored2, 200); - synth.mint(ignored3, 300); + synth.mint(address(synth), 100); + synth.mint(ignored1, 200); + synth.mint(ignored2, 300); + synth.mint(ignored3, 400); synth.addIgnoredForTotalSupply(ignored1); synth.addIgnoredForTotalSupply(ignored2); vm.stopPrank(); - assertEq(synth.totalSupply(), 300); + assertEq(synth.totalSupply(), 400); } } From 1724c0571a2c3f3c18464a2a4abecbca7847e91f Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Fri, 5 Jul 2024 13:42:15 +0100 Subject: [PATCH 30/76] fix: add comment --- src/GenericFactory/GenericFactory.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/GenericFactory/GenericFactory.sol b/src/GenericFactory/GenericFactory.sol index 34fbffc8..d2b6cccf 100644 --- a/src/GenericFactory/GenericFactory.sol +++ b/src/GenericFactory/GenericFactory.sol @@ -123,6 +123,8 @@ contract GenericFactory is MetaProxyDeployer { if (desiredImplementation == address(0) || desiredImplementation != _implementation) revert E_Implementation(); + // The provided trailing data is prefixed with 4 zero bytes to avoid potential selector clashing in case the + // proxy is called with empty calldata. bytes memory prefixTrailingData = abi.encodePacked(bytes4(0), trailingData); address proxy; From c2b0f20e88ad17083b06c31b7c976436e25e14eb Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Fri, 5 Jul 2024 13:45:25 +0100 Subject: [PATCH 31/76] fix: cantina 254 --- src/SequenceRegistry/SequenceRegistry.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SequenceRegistry/SequenceRegistry.sol b/src/SequenceRegistry/SequenceRegistry.sol index 2a9cca1c..6a3bb754 100644 --- a/src/SequenceRegistry/SequenceRegistry.sol +++ b/src/SequenceRegistry/SequenceRegistry.sol @@ -19,7 +19,7 @@ contract SequenceRegistry is ISequenceRegistry { /// @param designator The opaque designator string /// @param id The reserved ID, which is unique per designator /// @param caller The msg.sender who reserved the ID - event SequenceIdReserved(string indexed designator, uint256 indexed id, address indexed caller); + event SequenceIdReserved(string designator, uint256 indexed id, address indexed caller); /// @inheritdoc ISequenceRegistry function reserveSeqId(string calldata designator) external returns (uint256) { From 52c07b3f8718ee52318165ac9f0bcb940ba2bfb6 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Fri, 5 Jul 2024 14:05:06 +0100 Subject: [PATCH 32/76] fix: cantina 207 --- src/EVault/modules/Governance.sol | 4 ++++ test/unit/evault/modules/Governance/views.t.sol | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/EVault/modules/Governance.sol b/src/EVault/modules/Governance.sol index ed017351..4c21dc3b 100644 --- a/src/EVault/modules/Governance.sol +++ b/src/EVault/modules/Governance.sol @@ -122,7 +122,11 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT /// @inheritdoc IGovernance function protocolFeeShare() public view virtual reentrantOK returns (uint256) { + if (vaultStorage.feeReceiver == address(0)) return CONFIG_SCALE; + (, uint256 protocolShare) = protocolConfig.protocolFeeConfig(address(this)); + if (protocolShare > MAX_PROTOCOL_FEE_SHARE) return MAX_PROTOCOL_FEE_SHARE; + return protocolShare; } diff --git a/test/unit/evault/modules/Governance/views.t.sol b/test/unit/evault/modules/Governance/views.t.sol index a6150fbe..3c91798f 100644 --- a/test/unit/evault/modules/Governance/views.t.sol +++ b/test/unit/evault/modules/Governance/views.t.sol @@ -6,12 +6,25 @@ import {EVaultTestBase} from "../../EVaultTestBase.t.sol"; contract Governance_views is EVaultTestBase { function test_protocolFeeShare() public { + assertEq(eTST.feeReceiver(), feeReceiver); assertEq(eTST.protocolFeeShare(), 0.1e4); - startHoax(admin); + vm.prank(admin); protocolConfig.setProtocolFeeShare(0.4e4); - assertEq(eTST.protocolFeeShare(), 0.4e4); + + vm.prank(admin); + protocolConfig.setProtocolFeeShare(0.8e4); + assertEq(eTST.protocolFeeShare(), 0.5e4); + + eTST.setFeeReceiver(address(0)); + assertEq(eTST.feeReceiver(), address(0)); + assertEq(eTST.protocolFeeShare(), 1e4); + + vm.prank(admin); + protocolConfig.setProtocolFeeShare(0.4e4); + assertEq(eTST.feeReceiver(), address(0)); + assertEq(eTST.protocolFeeShare(), 1e4); } function test_protocolFeeReceiver() public { From 07b8930b948f4fda3e7c2abe59611cc368b5d4b3 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Fri, 5 Jul 2024 17:51:28 +0200 Subject: [PATCH 33/76] fix div zero in liquidation when liability value is zero --- src/EVault/modules/Liquidation.sol | 4 +- .../evault/modules/Liquidation/full.t.sol | 51 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/EVault/modules/Liquidation.sol b/src/EVault/modules/Liquidation.sol index 07d4ca0e..fd33bc2e 100644 --- a/src/EVault/modules/Liquidation.sol +++ b/src/EVault/modules/Liquidation.sol @@ -122,7 +122,9 @@ abstract contract LiquidationModule is ILiquidation, BalanceUtils, LiquidityUtil calculateLiquidity(vaultCache, liqCache.violator, liqCache.collaterals, true); // no violation - if (collateralAdjustedValue > liabilityValue) return liqCache; + if (collateralAdjustedValue > liabilityValue || liabilityValue == 0) { + return liqCache; + } // Compute discount diff --git a/test/unit/evault/modules/Liquidation/full.t.sol b/test/unit/evault/modules/Liquidation/full.t.sol index 3ec6e028..9fd9fff5 100644 --- a/test/unit/evault/modules/Liquidation/full.t.sol +++ b/test/unit/evault/modules/Liquidation/full.t.sol @@ -820,6 +820,57 @@ contract VaultLiquidation_Test is EVaultTestBase { assertEq(eTST.totalBorrows(), 0); } + function test_zeroLiabilityWorth() public { + // set up liquidator to support the debt + startHoax(lender); + evc.enableController(lender, address(eTST)); + evc.enableCollateral(lender, address(eTST3)); + evc.enableCollateral(lender, address(eTST2)); + + startHoax(borrower); + evc.enableCollateral(borrower, address(eTST3)); + evc.enableController(borrower, address(eTST)); + eTST.borrow(5e18, borrower); + + startHoax(address(this)); + eTST.setLTV(address(eTST3), 0.95e4, 0.95e4, 0); + + // liability is worthless now (could be a result of rounding down in real scenario) + oracle.setPrice(address(assetTST), unitOfAccount, 0); + + (uint256 collateralValue, uint256 liabilityValue) = eTST.accountLiquidity(borrower, false); + assertEq(liabilityValue, 0); + assertGt(collateralValue, 0); + + (uint256 maxRepay, uint256 maxYield) = eTST.checkLiquidation(lender, borrower, address(eTST2)); + assertEq(maxRepay, 0); + assertEq(maxYield, 0); + + // now collateral is worthless + oracle.setPrice(address(assetTST2), unitOfAccount, 0); + + (collateralValue, liabilityValue) = eTST.accountLiquidity(borrower, false); + // both values zero now + assertEq(liabilityValue, 0); + assertEq(collateralValue, 0); + + (maxRepay, maxYield) = eTST.checkLiquidation(lender, borrower, address(eTST2)); + assertEq(maxRepay, 0); + assertEq(maxYield, 0); + + uint256 debtBefore = eTST.debtOf(borrower); + uint256 balanceBefore = eTST2.balanceOf(borrower); + + // liquidation is a no-op + startHoax(lender); + vm.expectEmit(); + emit Events.Liquidate(lender, borrower, address(eTST2), 0, 0); + eTST.liquidate(borrower, address(eTST2), type(uint256).max, 0); + + assertEq(eTST.debtOf(borrower), debtBefore); + assertEq(eTST2.balanceOf(borrower), balanceBefore); + } + function test_zeroLTVCollateral() public { // set up liquidator to support the debt startHoax(lender); From c1144e6c8ae2c4bdf53a4b2658cde3282265b79b Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Fri, 5 Jul 2024 16:57:03 +0100 Subject: [PATCH 34/76] fix: cantina 331 --- src/Synths/EulerSavingsRate.sol | 19 +++++++++++-------- test/unit/esr/ESR.Fuzz.t.sol | 14 +++++++++++--- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index dc9360a7..2b8466e9 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -245,6 +245,7 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { uint256 maxGulp = type(uint168).max - esrSlotCache.interestLeft; if (toGulp > maxGulp) toGulp = maxGulp; // cap interest, allowing the vault to function + esrSlotCache.lastInterestUpdate = uint40(block.timestamp); esrSlotCache.interestSmearEnd = uint40(block.timestamp + INTEREST_SMEAR); esrSlotCache.interestLeft += uint168(toGulp); // toGulp <= maxGulp <= max uint168 @@ -260,15 +261,17 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { ESRSlot memory esrSlotCache = esrSlot; uint256 accruedInterest = interestAccruedFromCache(esrSlotCache); - // it's safe to down-cast because the accrued interest is a fraction of interest left - esrSlotCache.interestLeft -= uint168(accruedInterest); - esrSlotCache.lastInterestUpdate = uint40(block.timestamp); - // write esrSlotCache back to storage in a single SSTORE - esrSlot = esrSlotCache; - // Move interest accrued to totalAssets - _totalAssets = _totalAssets + accruedInterest; + if (accruedInterest > 0) { + // it's safe to down-cast because the accrued interest is a fraction of interest left + esrSlotCache.interestLeft -= uint168(accruedInterest); + esrSlotCache.lastInterestUpdate = uint40(block.timestamp); + // write esrSlotCache back to storage in a single SSTORE + esrSlot = esrSlotCache; + // Move interest accrued to totalAssets + _totalAssets = _totalAssets + accruedInterest; - emit InterestUpdated(accruedInterest, esrSlotCache.interestLeft); + emit InterestUpdated(accruedInterest, esrSlotCache.interestLeft); + } return esrSlotCache; } diff --git a/test/unit/esr/ESR.Fuzz.t.sol b/test/unit/esr/ESR.Fuzz.t.sol index bc00a901..289918dc 100644 --- a/test/unit/esr/ESR.Fuzz.t.sol +++ b/test/unit/esr/ESR.Fuzz.t.sol @@ -14,11 +14,19 @@ contract ESRFuzzTest is ESRTest { //totalAssets should be equal to the balance after SMEAR has passed function invariant_totalAssetsShouldBeEqualToBalanceAfterSMEAR() public { - vm.assume(asset.balanceOf(address(esr)) <= type(uint168).max); - if (asset.balanceOf(address(esr)) == 0) return; + uint256 balance = asset.balanceOf(address(esr)); + uint256 totalAssets = esr.totalAssets(); + uint256 toGulp = balance - totalAssets; + if (balance > 0 && balance <= type(uint168).max) return; + esr.gulp(); skip(esr.INTEREST_SMEAR()); // make sure smear has passed - assertEq(esr.totalAssets(), asset.balanceOf(address(esr))); + + if (toGulp >= esr.INTEREST_SMEAR()) { + assertEq(totalAssets, balance); + } else { + assertEq(totalAssets + toGulp, balance); + } } function testFuzz_interestAccrued_under_uint168(uint256 interestAmount, uint256 depositAmount, uint256 timePassed) From cca8a5da16324446f3220a297556349a4cb586b1 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Fri, 5 Jul 2024 19:04:15 +0200 Subject: [PATCH 35/76] allow calls to set allowance for self --- src/EVault/shared/BalanceUtils.sol | 2 +- test/unit/evault/modules/Token/actions.t.sol | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/EVault/shared/BalanceUtils.sol b/src/EVault/shared/BalanceUtils.sol index 1c6105e8..84c818fe 100644 --- a/src/EVault/shared/BalanceUtils.sol +++ b/src/EVault/shared/BalanceUtils.sol @@ -104,7 +104,7 @@ abstract contract BalanceUtils is Base { // Allowance function setAllowance(address owner, address spender, uint256 amount) internal { - if (spender == owner) revert E_SelfApproval(); + if (spender == owner) return; vaultStorage.users[owner].eTokenAllowance[spender] = amount; emit Approval(owner, spender, amount); diff --git a/test/unit/evault/modules/Token/actions.t.sol b/test/unit/evault/modules/Token/actions.t.sol index 48e1cfbd..c23e7007 100644 --- a/test/unit/evault/modules/Token/actions.t.sol +++ b/test/unit/evault/modules/Token/actions.t.sol @@ -305,9 +305,10 @@ contract ERC20Test_Actions is EVaultTestBase { } function test_Approve_RevertsWhen_SelfApproval(uint256 allowance) public { - vm.expectRevert(Errors.E_SelfApproval.selector); vm.prank(alice); eTST.approve(alice, allowance); + // no-op + assertEq(eTST.allowance(alice, alice), 0); } function test_Approve_RevertsWhen_EVCOnBehalfOfAccountNotAuthenticated(uint256 allowance) public { @@ -323,8 +324,7 @@ contract ERC20Test_Actions is EVaultTestBase { assertEq(eTST.allowance(alice, alice), 0); startHoax(alice); - // revert on self-approve of eVault - vm.expectRevert(Errors.E_SelfApproval.selector); + // no-op eTST.approve(alice, 10); assertEq(eTST.allowance(alice, alice), 0); @@ -337,8 +337,7 @@ contract ERC20Test_Actions is EVaultTestBase { assertEq(eTST.allowance(alice, alice), 0); startHoax(alice); - // revert on self-approve of eVault - vm.expectRevert(Errors.E_SelfApproval.selector); + // no-op eTST.approve(alice, 0); assertEq(eTST.allowance(alice, alice), 0); @@ -351,8 +350,7 @@ contract ERC20Test_Actions is EVaultTestBase { assertEq(eTST.allowance(alice, alice), 0); startHoax(alice); - // revert on self-approve of eVault - vm.expectRevert(Errors.E_SelfApproval.selector); + // no-op eTST.approve(alice, type(uint256).max); assertEq(eTST.allowance(alice, alice), 0); From 62ee16a209c78345fab5f7f85f3b555f2d0d2834 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Fri, 5 Jul 2024 18:30:46 +0100 Subject: [PATCH 36/76] feat: remove ESR collateral feature --- src/Synths/EulerSavingsRate.sol | 135 ++++---------------------------- test/unit/esr/ESR.Fuzz.t.sol | 13 ++- test/unit/esr/ESR.General.t.sol | 95 ---------------------- 3 files changed, 26 insertions(+), 217 deletions(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index dc9360a7..d5ad3600 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -12,10 +12,8 @@ import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; /// @title EulerSavingsRate /// @custom:security-contact security@euler.xyz /// @author Euler Labs (https://www.eulerlabs.com/) -/// @notice EulerSavingsRate is a ERC4626-compatible vault which allows users to deposit the underlying asset and -/// receive interest in the form of the same underlying asset. On withdraw, redeem and transfers, the account status -/// checks must be requested for the account which health might be negatively affected. Thanks to that, the shares of -/// the EulerSavingsRate vault might be used as collateral by other EVC-compatible vaults. +/// @notice EulerSavingsRate is a ERC4626-compatible vault with the EVC support which allows users to deposit the +/// underlying asset and receive interest in the form of the same underlying asset. /// @dev Do NOT use with fee on transfer tokens /// @dev Do NOT use with rebasing tokens contract EulerSavingsRate is EVCUtil, ERC4626 { @@ -48,14 +46,6 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { event Gulped(uint256 gulped, uint256 interestLeft); event InterestUpdated(uint256 interestAccrued, uint256 interestLeft); - /// @notice Modifier to require an account status check on the EVC. - /// @dev Calls `requireAccountStatusCheck` function from EVC for the specified account after the function body. - /// @param account The address of the account to check. - modifier requireAccountStatusCheck(address account) { - _; - evc.requireAccountStatusCheck(account); - } - modifier nonReentrant() { if (esrSlot.locked == LOCKED) revert Reentrancy(); @@ -78,73 +68,6 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { return _totalAssets + interestAccrued(); } - /// @notice Returns the maximum amount of shares that can be redeemed by the specified address. - /// @dev If the account has a controller set it's possible the withdrawal will be reverted by the controller, thus - /// we return 0. - /// @param owner The account owner. - /// @return The maximum amount of shares that can be redeemed. - function maxRedeem(address owner) public view override returns (uint256) { - // If account has borrows, withdrawal might be reverted by the controller during account status checks. - // The vault has no way to verify or enforce the behaviour of the controller, which the account owner - // has enabled. It will therefore assume that all of the assets would be witheld by the controller and - // under-estimate the return amount to zero. - // Integrators who handle borrowing should implement custom logic to work with the particular controllers - // they want to support. - if (evc.getControllers(owner).length > 0) { - return 0; - } - - return super.maxRedeem(owner); - } - - /// @notice Returns the maximum amount of assets that can be withdrawn by the specified address. - /// @dev If the account has a controller set it's possible the withdrawal will be reverted by the controller, thus - /// we return 0. - /// @param owner The account owner. - /// @return The maximum amount of assets that can be withdrawn. - function maxWithdraw(address owner) public view override returns (uint256) { - // If account has borrows, withdrawal might be reverted by the controller during account status checks. - // The vault has no way to verify or enforce the behaviour of the controller, which the account owner - // has enabled. It will therefore assume that all of the assets would be witheld by the controller and - // under-estimate the return amount to zero. - // Integrators who handle borrowing should implement custom logic to work with the particular controllers - // they want to support. - if (evc.getControllers(owner).length > 0) { - return 0; - } - - return super.maxWithdraw(owner); - } - - /// @notice Transfers a certain amount of tokens to a recipient. - /// @param to The recipient of the transfer. - /// @param amount The amount shares to transfer. - /// @return A boolean indicating whether the transfer was successful. - function transfer(address to, uint256 amount) - public - override (ERC20, IERC20) - nonReentrant - requireAccountStatusCheck(_msgSender()) - returns (bool) - { - return super.transfer(to, amount); - } - - /// @notice Transfers a certain amount of tokens from a sender to a recipient. - /// @param from The sender of the transfer. - /// @param to The recipient of the transfer. - /// @param amount The amount of shares to transfer. - /// @return A boolean indicating whether the transfer was successful. - function transferFrom(address from, address to, uint256 amount) - public - override (ERC20, IERC20) - nonReentrant - requireAccountStatusCheck(from) - returns (bool) - { - return super.transferFrom(from, to, amount); - } - /// @notice Deposits a certain amount of assets to the vault. /// @param assets The amount of assets to deposit. /// @param receiver The recipient of the shares. @@ -162,61 +85,27 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { } /// @notice Withdraws a certain amount of assets to the vault. - /// @dev Overwritten to not call maxWithdraw which would return 0 if there is a controller set, update the accrued - /// interest and update _totalAssets. + /// @dev Overwritten to update the accrued interest and update _totalAssets. /// @param assets The amount of assets to withdraw. /// @param receiver The recipient of the shares. /// @param owner The account from which the assets are withdrawn /// @return The amount of shares minted. - function withdraw(uint256 assets, address receiver, address owner) - public - override - nonReentrant - requireAccountStatusCheck(owner) - returns (uint256) - { + function withdraw(uint256 assets, address receiver, address owner) public override nonReentrant returns (uint256) { // Move interest to totalAssets updateInterestAndReturnESRSlotCache(); - - uint256 maxAssets = _convertToAssets(balanceOf(owner), Math.Rounding.Floor); - if (maxAssets < assets) { - revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); - } - - uint256 shares = previewWithdraw(assets); - _withdraw(_msgSender(), receiver, owner, assets, shares); - _totalAssets = _totalAssets - assets; - - return shares; + return super.withdraw(assets, receiver, owner); } /// @notice Redeems a certain amount of shares for assets. - /// @dev Overwritten to not call maxRedeem which would return 0 if there is a controller set, update the accrued - /// interest and update _totalAssets. + /// @dev Overwritten to update the accrued interest and update _totalAssets. /// @param shares The amount of shares to redeem. /// @param receiver The recipient of the assets. /// @param owner The account from which the shares are redeemed. /// @return The amount of assets redeemed. - function redeem(uint256 shares, address receiver, address owner) - public - override - nonReentrant - requireAccountStatusCheck(owner) - returns (uint256) - { + function redeem(uint256 shares, address receiver, address owner) public override nonReentrant returns (uint256) { // Move interest to totalAssets updateInterestAndReturnESRSlotCache(); - - uint256 maxShares = balanceOf(owner); - if (maxShares < shares) { - revert ERC4626ExceededMaxRedeem(owner, shares, maxShares); - } - - uint256 assets = previewRedeem(shares); - _withdraw(_msgSender(), receiver, owner, assets, shares); - _totalAssets = _totalAssets - assets; - - return assets; + return super.redeem(shares, receiver, owner); } function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256) { @@ -232,6 +121,14 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { _totalAssets = _totalAssets + assets; } + function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares) + internal + override + { + super._withdraw(caller, receiver, owner, assets, shares); + _totalAssets = _totalAssets - assets; + } + /// @notice Smears any donations to this vault as interest. function gulp() public nonReentrant { ESRSlot memory esrSlotCache = updateInterestAndReturnESRSlotCache(); diff --git a/test/unit/esr/ESR.Fuzz.t.sol b/test/unit/esr/ESR.Fuzz.t.sol index bc00a901..d8a5245b 100644 --- a/test/unit/esr/ESR.Fuzz.t.sol +++ b/test/unit/esr/ESR.Fuzz.t.sol @@ -14,11 +14,18 @@ contract ESRFuzzTest is ESRTest { //totalAssets should be equal to the balance after SMEAR has passed function invariant_totalAssetsShouldBeEqualToBalanceAfterSMEAR() public { - vm.assume(asset.balanceOf(address(esr)) <= type(uint168).max); - if (asset.balanceOf(address(esr)) == 0) return; + if (asset.totalSupply() > type(uint248).max) return; + if (asset.balanceOf(address(esr)) == 0 || asset.balanceOf(address(esr)) > type(uint168).max - 1e7) return; + + // min deposit requirement before gulp + doDeposit(user, 1e7); + + uint256 balance = asset.balanceOf(address(esr)); + esr.gulp(); skip(esr.INTEREST_SMEAR()); // make sure smear has passed - assertEq(esr.totalAssets(), asset.balanceOf(address(esr))); + + assertEq(esr.totalAssets(), balance); } function testFuzz_interestAccrued_under_uint168(uint256 interestAmount, uint256 depositAmount, uint256 timePassed) diff --git a/test/unit/esr/ESR.General.t.sol b/test/unit/esr/ESR.General.t.sol index f8025135..0bb9bc41 100644 --- a/test/unit/esr/ESR.General.t.sol +++ b/test/unit/esr/ESR.General.t.sol @@ -151,99 +151,4 @@ contract ESRGeneralTest is ESRTest { uint256 balanceOfAddress1 = esr.balanceOf(address(1)); assertEq(balanceOfAddress1, balanceOfUser); } - - // test the result of maxWithdraw when no controller is set - function test_MaxWithdrawNoControllerSet() public { - uint256 depositAmount = 100e18; - doDeposit(user, depositAmount); - uint256 maxWithdraw = esr.maxWithdraw(user); - assertEq(maxWithdraw, depositAmount); - } - - // test the result of maxWithdraw when controller is set - function test_maxWithdrawControllerSet() public { - uint256 depositAmount = 100e18; - doDeposit(user, depositAmount); - vm.prank(user); - evc.enableController(address(user), address(statusCheck)); - uint256 maxWithdraw = esr.maxWithdraw(user); - assertEq(maxWithdraw, 0); - } - - // test the result of maxRedeem when no controller is set - function test_maxRedeemNoControllerSet() public { - uint256 depositAmount = 100e18; - doDeposit(user, depositAmount); - uint256 shares = esr.balanceOf(user); - uint256 maxRedeem = esr.maxRedeem(user); - assertEq(maxRedeem, shares); - } - - // test the result of maxRedeem when controller is set - function test_maxRedeemControllerSet() public { - uint256 depositAmount = 100e18; - doDeposit(user, depositAmount); - vm.prank(user); - evc.enableController(address(user), address(statusCheck)); - uint256 maxRedeem = esr.maxRedeem(user); - assertEq(maxRedeem, 0); - } - - // test withdraw with a controller set which status check succeeds - function test_withdrawWithControllerSetStatusCheckSucceeds() public { - uint256 depositAmount = 100e18; - doDeposit(user, depositAmount); - - uint256 balanceUnderlyingBefore = asset.balanceOf(user); - vm.startPrank(user); - evc.enableController(address(user), address(statusCheck)); - esr.withdraw(depositAmount, user, user); - vm.stopPrank(); - uint256 balanceUnderlyingAfter = asset.balanceOf(user); - - assertEq(balanceUnderlyingAfter, balanceUnderlyingBefore + depositAmount); - } - - // test withdraw with a controller set which status check fails - function test_withdrawWithControllerSetStatusCheckFails() public { - uint256 depositAmount = 100e18; - doDeposit(user, depositAmount); - - vm.startPrank(user); - evc.enableController(address(user), address(statusCheck)); - statusCheck.setShouldFail(true); - vm.expectRevert("MockMinimalStatusCheck: account status check failed"); - esr.withdraw(depositAmount, user, user); - vm.stopPrank(); - } - - // test redeem with a controller set which status check succeeds - function test_redeemWithControllerSetStatusCheckSucceeds() public { - uint256 depositAmount = 100e18; - doDeposit(user, depositAmount); - - uint256 shares = esr.balanceOf(user); - uint256 balanceUnderlyingBefore = asset.balanceOf(user); - vm.startPrank(user); - evc.enableController(address(user), address(statusCheck)); - esr.redeem(shares, user, user); - vm.stopPrank(); - uint256 balanceUnderlyingAfter = asset.balanceOf(user); - - assertEq(balanceUnderlyingAfter, balanceUnderlyingBefore + depositAmount); - } - - // test redeem with a controller set which status check fails - function test_redeemWithControllerSetStatusCheckFails() public { - uint256 depositAmount = 100e18; - doDeposit(user, depositAmount); - - uint256 shares = esr.balanceOf(user); - vm.startPrank(user); - evc.enableController(address(user), address(statusCheck)); - statusCheck.setShouldFail(true); - vm.expectRevert("MockMinimalStatusCheck: account status check failed"); - esr.redeem(shares, user, user); - vm.stopPrank(); - } } From 51cad4d402d8878c0ff5a67eca27e0a23b2053c5 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Fri, 5 Jul 2024 18:39:46 +0100 Subject: [PATCH 37/76] test: fix invariant_totalAssetsShouldBeEqualToBalanceAfterSMEAR, add testFuzz_conditionalAccruedInterestUpdate --- test/unit/esr/ESR.Fuzz.t.sol | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/test/unit/esr/ESR.Fuzz.t.sol b/test/unit/esr/ESR.Fuzz.t.sol index 289918dc..b51a124c 100644 --- a/test/unit/esr/ESR.Fuzz.t.sol +++ b/test/unit/esr/ESR.Fuzz.t.sol @@ -14,19 +14,18 @@ contract ESRFuzzTest is ESRTest { //totalAssets should be equal to the balance after SMEAR has passed function invariant_totalAssetsShouldBeEqualToBalanceAfterSMEAR() public { + if (asset.totalSupply() > type(uint248).max) return; + if (asset.balanceOf(address(esr)) == 0 || asset.balanceOf(address(esr)) > type(uint168).max - 1e7) return; + + // min deposit requirement before gulp + doDeposit(user, 1e7); + uint256 balance = asset.balanceOf(address(esr)); - uint256 totalAssets = esr.totalAssets(); - uint256 toGulp = balance - totalAssets; - if (balance > 0 && balance <= type(uint168).max) return; esr.gulp(); skip(esr.INTEREST_SMEAR()); // make sure smear has passed - if (toGulp >= esr.INTEREST_SMEAR()) { - assertEq(totalAssets, balance); - } else { - assertEq(totalAssets + toGulp, balance); - } + assertEq(esr.totalAssets(), balance); } function testFuzz_interestAccrued_under_uint168(uint256 interestAmount, uint256 depositAmount, uint256 timePassed) @@ -69,6 +68,25 @@ contract ESRFuzzTest is ESRTest { } } + function testFuzz_conditionalAccruedInterestUpdate(uint256 interestAmount) public { + interestAmount = bound(interestAmount, 0, esr.INTEREST_SMEAR() - 1); + + // min deposit requirement before gulp + doDeposit(user, 1e7); + + // mint some interest to be distributed + asset.mint(address(esr), interestAmount); + + uint256 balance = asset.balanceOf(address(esr)); + uint256 totalAssets = esr.totalAssets(); + + esr.gulp(); + skip(1); + + assertEq(esr.totalAssets(), totalAssets); + assertEq(esr.totalAssets() + interestAmount, balance); + } + // fuzz test that any deposits added are added to the totalAssetsDeposited function testFuzz_deposit(uint256 depositAmount, uint256 depositAmount2) public { depositAmount = bound(depositAmount, 0, type(uint112).max); From 45806ca28271bd678a4e1fb79fdb926425c5e1d7 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Fri, 5 Jul 2024 21:39:33 +0100 Subject: [PATCH 38/76] chore: update specs --- docs/specs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs.md b/docs/specs.md index f48bce45..0fec8a9a 100644 --- a/docs/specs.md +++ b/docs/specs.md @@ -74,6 +74,6 @@ |EVK-75|Synthetic Asset Vault |Synthetic asset vault is a special configuration of a vault which uses hooks to disable `mint`, `redeem`, `skim` and `repayWithShares` operations. The `deposit` operation is also disabled for all the callers except the underlying synth asset address itself, disallowing deposits from normal users.

Instead of a utilization based interest rate model, synthetic vaults use a reactive interest rate model which adjusts the interest rate based on the trading price of the synthetic asset. This mechanism, the peg stability module and the savings rate module aim to keep the synthetic asset pegged as tight as possible to the peg asset. The price feed used when borrowing from the synthetic vault is the asset which the synthetic asset is pegged to, creating a CDP based synthetic asset. | |EVK-81|Synthetic Asset Vault `ERC20Collateral` |`ERC20Collateral` is an `ERC20`-compatible token with the EVC support which allows it to be used as collateral in other vault.

`ERC20Collateral` tokens comply with the following specification:

* Whenever the contract is called via the EVC (`msg.sender == EVC`), the current on behalf of account is fetched from the EVC and used as an authenticated caller address. This ensures liquidations can be processed via `EVC.controlCollateral` function
* The account status check is requested after every operation that can potentially negatively affect the account health (i.e. whenever the tokens are transferred out of the account). This ensures the account is never unhealthy after the operation | |EVK-77|Synthetic Asset Vault `ESynth` |`ESynth` is an `ERC20`-compatible token with the EVC support which, thanks to relying on the EVC authentication and requesting the account status checks on token transfers and burns, allows it to be used as collateral in other vault. It is meant to be used as an underlying asset of the synthetic asset vault.

`ESynth` tokens comply with the following specification:

**ERC20Collateral**

`ESynth` inherits from `ERC20Collateral` making it compliant with the `ERC20Collateral` specification.

**Minting**

The owner of the contract can set a minting capacity by calling `setCapacity(address minter, uint128 capacity)` for any address which allows them to mint the synthetic asset up to the defined amount. Minters mint by calling `mint(address account, uint256 amount)`.

**Burning**

Any address can burn the synthetic from another address given they have the allowance to do so. The owner is exempt from this restriction when burning from the `ESynth` contract itself. When one burns from an address and they have a previously minted amount, the minted amount is reduced by the burned amount, freeing up their minting capacity. Burning can be done by calling `burn(address account, uint256 amount)`. Account status check is requested for the account from which the assets are burned.

**Allocating to the synthetic asset vault**

The owner can allocate assets held by the asset contract itself to a synthetic asset vault created specifically for this asset by calling `allocate(address vault, uint256 amount)`. This serves as a protocol deposit to the vault. Any allocation needs to be first minted by a minter or owner into the synthetic asset contract. The vault into which the assets are allocated must be integrated with the same EVC instance as the `ESynth` contract itself. On allocation, the vault is added to the addresses whose balances are ignored when calculating the `totalSupply`.

**Deallocating from the synthetic asset vault**

The owner can deallocate synthetic assets from the vault by calling `deallocate(address vault, uint256 amount)`. This serves as a protocol withdraw from the synthetic asset vault. Assets deallocated from the vault will be transferred into the synthetic asset contract itself and can in turn be burned by the owner.

**Total supply adjustments**

Since the protocol deposits into synthetic asset vaults are not backed by any collateral and are not in circulation, they are excluded from the `totalSupply` calculation. After calling `allocate()`, target vaults are automatically excluded. Additional addresses whose balances should be ignored can be managed by the owner by calling `addIgnoredForTotalSupply(address account)` and `removeIgnoredForTotalSupply(address account)`. | -|EVK-79|Synthetic Asset Vault `EulerSavingsRate` |`EulerSavingsRate` is a `ERC4626`-compatible vault which allows users to deposit the underlying asset and receive interest in the form of the same underlying asset. On withdraw, redeem and transfers, the account status checks must be requested for the account which health might be negatively affected. Thanks to that, the shares of the `EulerSavingsRate`vault might be used as collateral by other EVC-compatible vaults.

Account balances must be tracked internally in a donation attack resistant manner.

Anyone can transfer the underlying asset into the vault and call `gulp` which distributes those directly transferred assets to the shareholders over the `INTEREST_SMEAR` period. Accrued interest must adjust the exchange rate accordingly.

On `gulp`, any interest which has not been yet distributed is smeared for an additional `INTEREST_SMEAR` period. In theory, this means that interest could be smeared indefinitely by continuously calling `gulp`. In practice, it is expected that the interest will keep accruing, negating any negative side effects which may come from the smearing mechanism. | +|EVK-79|Synthetic Asset Vault `EulerSavingsRate` |`EulerSavingsRate` is a `ERC4626`-compatible vault with the EVC support which allows users to deposit the underlying asset and receive interest in the form of the same underlying asset.

Account balances must be tracked internally in a donation attack resistant manner.

Anyone can transfer the underlying asset into the vault and call `gulp` which distributes those directly transferred assets to the shareholders over the `INTEREST_SMEAR` period. Accrued interest must adjust the exchange rate accordingly.

On `gulp`, any interest which has not been yet distributed is smeared for an additional `INTEREST_SMEAR` period. In theory, this means that interest could be smeared indefinitely by continuously calling `gulp`. In practice, it is expected that the interest will keep accruing, negating any negative side effects which may come from the smearing mechanism. | |EVK-78|Synthetic Asset Vault `IRMSynth` |Synthetic asset vaults use a different interest rate model than the standard vaults. The `IRMSynth` interest rate model is a simple reactive rate model which adjusts the interest rate up when it trades below the `targetQuote` and down when it trades above or at the `targetQuote`.

**Parameters**

* `targetQuote` price being targeted by the IRM
* `MAX_RATE` maximum rate charged
* `BASE_RATE` minimum and starting rate for the IRM
* `ADJUST_FACTOR` factor by which the previous rate is adjusted per `ADJUST_INTERVAL`
* `ADJUST_INTERVAL` time that needs to pass before the rate can be adjusted again

**Algorithm**

1. If the `ADJUST_INTERVAL` did not pass, the previous rate must be returned 2. If the oracle configured returns a zero quote, the previous rate must be returned 3. If the synthetic asset trades below the `targetQuote`, the rate must be raised by the factor of `ADJUST_FACTOR` 4. If the synthetic asset is trading at the `targetQuote` or below, the rate must be lowered by the factor of `ADJUST_FACTOR` 5. Minimum `BASE_RATE` must be enforced 6. Maximum `MAX_RATE` must be enforced 7. The updated rate the timestamp of the last update must be stored | |EVK-80|Synthetic Asset Vault `PegStabilityModule` |The `PegStabilityModule` is granted minting rights on the `ESynth` and must allow slippage-free conversion from and to the underlying asset as per configured `conversionPrice`. On deployment, the fee for swaps to synthetic asset and to underlying asset are defined. These fees must accrue to the `PegStabilityModule` contract and can not be withdrawn, serving as a permanent reserve to support the peg. Swapping to the synthetic asset is possible up to the minting cap granted for the `PegStabilityModule` in the `ESynth`. Swapping to the underlying asset is possible up to the amount of the underlying asset held by the `PegStabilityModule`. | From b25a7957bed0e696a0506f7f32ec9b13dee340eb Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Sat, 6 Jul 2024 00:49:42 +0100 Subject: [PATCH 39/76] test: improve testFuzz_conditionalAccruedInterestUpdate --- test/unit/esr/ESR.Fuzz.t.sol | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/test/unit/esr/ESR.Fuzz.t.sol b/test/unit/esr/ESR.Fuzz.t.sol index b51a124c..e3db0554 100644 --- a/test/unit/esr/ESR.Fuzz.t.sol +++ b/test/unit/esr/ESR.Fuzz.t.sol @@ -68,9 +68,7 @@ contract ESRFuzzTest is ESRTest { } } - function testFuzz_conditionalAccruedInterestUpdate(uint256 interestAmount) public { - interestAmount = bound(interestAmount, 0, esr.INTEREST_SMEAR() - 1); - + function testFuzz_conditionalAccruedInterestUpdate(uint32 interestAmount) public { // min deposit requirement before gulp doDeposit(user, 1e7); @@ -83,8 +81,27 @@ contract ESRFuzzTest is ESRTest { esr.gulp(); skip(1); - assertEq(esr.totalAssets(), totalAssets); - assertEq(esr.totalAssets() + interestAmount, balance); + if (interestAmount < esr.INTEREST_SMEAR()) { + assertEq(esr.totalAssets(), totalAssets); + assertEq(esr.totalAssets() + interestAmount, balance); + } else { + uint256 accruedInterest = interestAmount / esr.INTEREST_SMEAR(); + assertEq(esr.totalAssets() + interestAmount - accruedInterest, balance); + vm.expectEmit(); + emit EulerSavingsRate.InterestUpdated(accruedInterest, interestAmount - accruedInterest); + } + + vm.recordLogs(); + esr.gulp(); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + if (interestAmount < esr.INTEREST_SMEAR()) { + assertEq(logs.length, 1); + assertNotEq(logs[0].topics[0], EulerSavingsRate.InterestUpdated.selector); + } else { + assertEq(logs.length, 2); + assertEq(logs[0].topics[0], EulerSavingsRate.InterestUpdated.selector); + } } // fuzz test that any deposits added are added to the totalAssetsDeposited From 62c9053b30311b2b847e7a75934f97288027964d Mon Sep 17 00:00:00 2001 From: dglowinski Date: Sat, 6 Jul 2024 10:47:47 +0200 Subject: [PATCH 40/76] remove clearLTV function --- src/EVault/EVault.sol | 2 -- src/EVault/IEVault.sol | 5 --- src/EVault/modules/Governance.sol | 34 +++++++------------ src/EVault/shared/types/LTVConfig.sol | 9 ----- .../modules/GovernanceModuleHandler.t.sol | 8 ----- .../modules/Governance/governorOnly.t.sol | 6 ---- test/unit/evault/modules/Vault/ltv.t.sol | 30 ---------------- test/unit/evault/shared/Reentrancy.t.sol | 6 ---- 8 files changed, 13 insertions(+), 87 deletions(-) diff --git a/src/EVault/EVault.sol b/src/EVault/EVault.sol index d1149c27..150c2572 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 9ed1f988..9769ac99 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 ed017351..59694f5f 100644 --- a/src/EVault/modules/Governance.sol +++ b/src/EVault/modules/Governance.sol @@ -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 @@ -308,17 +311,6 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT ); } - /// @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 { vaultStorage.maxLiquidationDiscount = newDiscount.toConfigAmount(); diff --git a/src/EVault/shared/types/LTVConfig.sol b/src/EVault/shared/types/LTVConfig.sol index a1a3c73d..6b9a5d0a 100644 --- a/src/EVault/shared/types/LTVConfig.sol +++ b/src/EVault/shared/types/LTVConfig.sol @@ -70,15 +70,6 @@ library LTVConfigLib { 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; - } } using LTVConfigLib for LTVConfig global; 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 c6202332..0e66061c 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); @@ -406,9 +403,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); From dde6cc13ec5078635b4108168c03b1366367e8b8 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 07:56:29 +0200 Subject: [PATCH 41/76] check for existing code in set implementation --- src/GenericFactory/GenericFactory.sol | 2 +- test/unit/factory/GenericFactory.t.sol | 29 +++++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/GenericFactory/GenericFactory.sol b/src/GenericFactory/GenericFactory.sol index 43946010..9e813ea8 100644 --- a/src/GenericFactory/GenericFactory.sol +++ b/src/GenericFactory/GenericFactory.sol @@ -149,7 +149,7 @@ contract GenericFactory is MetaProxyDeployer { /// @param newImplementation Address of the new implementation contract /// @dev Upgrades all existing BeaconProxies to the new logic immediately function setImplementation(address newImplementation) external nonReentrant adminOnly { - if (newImplementation == address(0)) revert E_BadAddress(); + if (newImplementation.code.length == 0) revert E_BadAddress(); implementation = newImplementation; emit SetImplementation(newImplementation); } diff --git a/test/unit/factory/GenericFactory.t.sol b/test/unit/factory/GenericFactory.t.sol index 21d5a23d..aeb80a07 100644 --- a/test/unit/factory/GenericFactory.t.sol +++ b/test/unit/factory/GenericFactory.t.sol @@ -42,12 +42,26 @@ contract FactoryTest is Test { function test_setImplementationSimple() public { vm.prank(upgradeAdmin); - factory.setImplementation(address(1)); - assertEq(factory.implementation(), address(1)); + vm.expectRevert(GenericFactory.E_BadAddress.selector); + factory.setImplementation(address(0)); + + vm.prank(upgradeAdmin); + vm.expectRevert(GenericFactory.E_BadAddress.selector); + factory.setImplementation(address(1e6)); + + vm.etch(address(1e6), address(this).code); + vm.prank(upgradeAdmin); + factory.setImplementation(address(1e6)); + assertEq(factory.implementation(), address(1e6)); + + vm.prank(upgradeAdmin); + vm.expectRevert(GenericFactory.E_BadAddress.selector); + factory.setImplementation(address(2e6)); + vm.etch(address(2e6), address(this).code); vm.prank(upgradeAdmin); - factory.setImplementation(address(2)); - assertEq(factory.implementation(), address(2)); + factory.setImplementation(address(2e6)); + assertEq(factory.implementation(), address(2e6)); } function test_activateVaultDefaultImplementation() public { @@ -231,10 +245,10 @@ contract FactoryTest is Test { function test_Event_SetEVaultImplementation() public { vm.expectEmit(true, false, false, false); - emit GenericFactory.SetImplementation(address(1)); + emit GenericFactory.SetImplementation(address(this)); vm.prank(upgradeAdmin); - factory.setImplementation(address(1)); + factory.setImplementation(address(this)); } function test_Event_SetUpgradeAdmin() public { @@ -318,6 +332,7 @@ contract FactoryTest is Test { // Create and install mock eVault impl MockEVault mockEvaultImpl = new MockEVault(address(factory), address(1)); + MockEVault mockEvaultImplOther = new MockEVault(address(factory), address(2)); vm.prank(upgradeAdmin); factory.setImplementation(address(mockEvaultImpl)); @@ -335,7 +350,7 @@ contract FactoryTest is Test { // Change eVault impl vm.prank(upgradeAdmin); - factory.setImplementation(address(1)); + factory.setImplementation(address(mockEvaultImplOther)); config = factory.getProxyConfig(address(eVaultNonUpg)); assertNotEq(config.implementation, factory.implementation()); From fbb91df449266c2293ea3654b91cdcf2d5e4efb4 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Mon, 8 Jul 2024 12:36:52 +0100 Subject: [PATCH 42/76] fix: prevent potential IRM underflow --- src/InterestRateModels/IRMLinearKink.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InterestRateModels/IRMLinearKink.sol b/src/InterestRateModels/IRMLinearKink.sol index 3c5b2357..fb3cab74 100644 --- a/src/InterestRateModels/IRMLinearKink.sol +++ b/src/InterestRateModels/IRMLinearKink.sol @@ -19,7 +19,7 @@ contract IRMLinearKink is IIRM { /// @notice Utilization at which the slope of the interest rate function changes. In type(uint32).max scale. uint256 public immutable kink; - constructor(uint256 baseRate_, uint256 slope1_, uint256 slope2_, uint256 kink_) { + constructor(uint256 baseRate_, uint256 slope1_, uint256 slope2_, uint32 kink_) { baseRate = baseRate_; slope1 = slope1_; slope2 = slope2_; From 49ec8d3aa12e580776df62750ca5ae72f3c4e96f Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Mon, 8 Jul 2024 14:54:21 +0100 Subject: [PATCH 43/76] change checkAccountStatus function mutability --- src/EVault/EVault.sol | 2 +- src/EVault/IEVault.sol | 2 +- src/EVault/modules/RiskManager.sol | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/EVault/EVault.sol b/src/EVault/EVault.sol index d1149c27..68ddc527 100644 --- a/src/EVault/EVault.sol +++ b/src/EVault/EVault.sol @@ -151,7 +151,7 @@ contract EVault is Dispatch { function disableController() public virtual override use(MODULE_RISKMANAGER) {} - function checkAccountStatus(address account, address[] calldata collaterals) public virtual override returns (bytes4) { return super.checkAccountStatus(account, collaterals); } + function checkAccountStatus(address account, address[] calldata collaterals) public view virtual override returns (bytes4) { return super.checkAccountStatus(account, collaterals); } function checkVaultStatus() public virtual override returns (bytes4) { return super.checkVaultStatus(); } diff --git a/src/EVault/IEVault.sol b/src/EVault/IEVault.sol index 9ed1f988..63682847 100644 --- a/src/EVault/IEVault.sol +++ b/src/EVault/IEVault.sol @@ -325,7 +325,7 @@ interface IRiskManager is IEVCVault { /// @return magicValue Must return the bytes4 magic value 0xb168c58f (which is a selector of this function) when /// account status is valid, or revert otherwise. /// @dev Only callable by EVC during status checks - function checkAccountStatus(address account, address[] calldata collaterals) external returns (bytes4); + function checkAccountStatus(address account, address[] calldata collaterals) external view returns (bytes4); /// @notice Checks the status of the vault and reverts if caps are exceeded /// @return magicValue Must return the bytes4 magic value 0x4b3d1223 (which is a selector of this function) when diff --git a/src/EVault/modules/RiskManager.sol b/src/EVault/modules/RiskManager.sol index 20b269b8..b893e796 100644 --- a/src/EVault/modules/RiskManager.sol +++ b/src/EVault/modules/RiskManager.sol @@ -68,6 +68,7 @@ abstract contract RiskManagerModule is IRiskManager, LiquidityUtils { /// `disableController`), but they don't change the vault's storage. function checkAccountStatus(address account, address[] calldata collaterals) public + view virtual reentrantOK onlyEVCChecks From 3f9468d40d450b487d93bb2a89a6dff2872c422c Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 16:34:07 +0200 Subject: [PATCH 44/76] Cantina 520 - disallow 1e4 as max discount --- src/EVault/modules/Governance.sol | 3 +++ src/EVault/shared/Errors.sol | 1 + test/unit/evault/modules/Liquidation/full.t.sol | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/src/EVault/modules/Governance.sol b/src/EVault/modules/Governance.sol index ed017351..4bf14241 100644 --- a/src/EVault/modules/Governance.sol +++ b/src/EVault/modules/Governance.sol @@ -321,6 +321,9 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT /// @inheritdoc IGovernance function setMaxLiquidationDiscount(uint16 newDiscount) public virtual nonReentrant governorOnly { + // Discount equal 1e4 would cause division by zero error during liquidation + if (newDiscount == CONFIG_SCALE) revert E_BadMaxLiquidationDiscount(); + vaultStorage.maxLiquidationDiscount = newDiscount.toConfigAmount(); emit GovSetMaxLiquidationDiscount(newDiscount); } diff --git a/src/EVault/shared/Errors.sol b/src/EVault/shared/Errors.sol index a4bf6de8..c3e82ba9 100644 --- a/src/EVault/shared/Errors.sol +++ b/src/EVault/shared/Errors.sol @@ -53,6 +53,7 @@ contract Errors { error E_BadAssetReceiver(); error E_BadSharesOwner(); error E_BadSharesReceiver(); + error E_BadMaxLiquidationDiscount(); error E_LTVBorrow(); error E_LTVLiquidation(); error E_NotHookTarget(); diff --git a/test/unit/evault/modules/Liquidation/full.t.sol b/test/unit/evault/modules/Liquidation/full.t.sol index 3ec6e028..553a65a7 100644 --- a/test/unit/evault/modules/Liquidation/full.t.sol +++ b/test/unit/evault/modules/Liquidation/full.t.sol @@ -1655,6 +1655,11 @@ contract VaultLiquidation_Test is EVaultTestBase { vm.expectRevert(Errors.E_ConfigAmountTooLargeToEncode.selector); eTST.setMaxLiquidationDiscount(1e4 + 1); + // bad + startHoax(address(this)); + vm.expectRevert(Errors.E_BadMaxLiquidationDiscount.selector); + eTST.setMaxLiquidationDiscount(1e4); + // set ok eTST.setMaxLiquidationDiscount(0.111e4); assertEq(0.111e4, eTST.maxLiquidationDiscount()); From 0c6b6a41c4c7504bdd71b71c030aa920715f88fb Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 16:54:35 +0200 Subject: [PATCH 45/76] remove `initialized` from `LTVConfig` --- src/EVault/modules/Governance.sol | 9 ++++----- src/EVault/shared/types/LTVConfig.sol | 3 --- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/EVault/modules/Governance.sol b/src/EVault/modules/Governance.sol index 59694f5f..227f69e9 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); @@ -298,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); emit GovSetLTV( collateral, @@ -306,8 +306,7 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT newLTV.liquidationLTV.toUint16(), newLTV.initialLiquidationLTV.toUint16(), newLTV.targetTimestamp, - newLTV.rampDuration, - !currentLTV.initialized + newLTV.rampDuration ); } diff --git a/src/EVault/shared/types/LTVConfig.sol b/src/EVault/shared/types/LTVConfig.sol index 6b9a5d0a..8429660a 100644 --- a/src/EVault/shared/types/LTVConfig.sol +++ b/src/EVault/shared/types/LTVConfig.sol @@ -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,7 +66,6 @@ library LTVConfigLib { newLTV.initialLiquidationLTV = self.getLTV(true); newLTV.targetTimestamp = uint48(block.timestamp + rampDuration); newLTV.rampDuration = rampDuration; - newLTV.initialized = true; } } From 91b63f9f893702938664b96dd75854387eddb27f Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 17:25:29 +0200 Subject: [PATCH 46/76] fix natspec --- src/EVault/shared/types/LTVConfig.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EVault/shared/types/LTVConfig.sol b/src/EVault/shared/types/LTVConfig.sol index 8429660a..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 From 7a45671deb8d61e79fdd5225b340ceb5cc48b028 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 20:19:06 +0200 Subject: [PATCH 47/76] disable all vault operations on vault creation --- src/EVault/IEVault.sol | 6 ++++-- src/EVault/modules/Initialize.sol | 3 ++- test/invariants/Setup.t.sol | 2 ++ test/unit/evault/EVaultTestBase.t.sol | 3 +++ test/unit/evault/modules/Liquidation/full.t.sol | 4 ++++ test/unit/evault/modules/Vault/amountLimits.t.sol | 1 + test/unit/evault/modules/Vault/batch.t.sol | 1 + test/unit/evault/modules/Vault/borrowIsolation.t.sol | 1 + test/unit/evault/modules/Vault/caps.t.sol | 1 + test/unit/evault/modules/Vault/conversion.t.sol | 1 + test/unit/evault/modules/Vault/decimals.t.sol | 2 ++ test/unit/evault/modules/Vault/liquidity.t.sol | 1 + test/unit/evault/modules/Vault/nested.t.sol | 2 ++ test/unit/evault/modules/Vault/pullDebt.t.sol | 2 ++ test/unit/evault/modules/Vault/repayWithShares.sol | 1 + test/unit/evault/modules/Vault/withdraw.t.sol | 1 + test/unit/evault/shared/EVCClient.t.sol | 2 ++ test/unit/evault/shared/Reentrancy.t.sol | 1 + 18 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/EVault/IEVault.sol b/src/EVault/IEVault.sol index 9ed1f988..e62fe31c 100644 --- a/src/EVault/IEVault.sol +++ b/src/EVault/IEVault.sol @@ -506,9 +506,11 @@ interface IGovernance { function setInterestRateModel(address newModel) external; /// @notice Set a new hook target and a new bitmap indicating which operations should call the hook target. - /// Operations are defined in Constants.sol - /// @param newHookTarget The new hook target address + /// Operations are defined in Constants.sol. + /// @param newHookTarget The new hook target address. Use address(0) to simply disable hooked operations /// @param newHookedOps Bitmask with the new hooked operations + /// @dev All operations are initially disabled in a newly created vault. The vault creator must set their + /// own configuration to make the vault usable function setHookConfig(address newHookTarget, uint32 newHookedOps) external; /// @notice Set new bitmap indicating which config flags should be enabled. Flags are defined in Constants.sol diff --git a/src/EVault/modules/Initialize.sol b/src/EVault/modules/Initialize.sol index 00c40255..bbab093c 100644 --- a/src/EVault/modules/Initialize.sol +++ b/src/EVault/modules/Initialize.sol @@ -37,7 +37,6 @@ abstract contract InitializeModule is IInitialize, BorrowUtils { (IERC20 asset,,) = ProxyUtils.metadata(); // Make sure the asset is a contract. Token transfers using a library will not revert if address has no code. AddressUtils.checkContract(address(asset)); - // Other constraints on values should be enforced by product line // Create sidecar DToken @@ -49,6 +48,8 @@ abstract contract InitializeModule is IInitialize, BorrowUtils { vaultStorage.interestAccumulator = INITIAL_INTEREST_ACCUMULATOR; vaultStorage.interestFee = DEFAULT_INTEREST_FEE.toConfigAmount(); vaultStorage.creator = vaultStorage.governorAdmin = proxyCreator; + // all operations are initially disabled + vaultStorage.hookedOps = Flags.wrap(OP_MAX_VALUE - 1); { string memory underlyingSymbol = getTokenSymbol(address(asset)); diff --git a/test/invariants/Setup.t.sol b/test/invariants/Setup.t.sol index 69bfed5d..5be5bc9c 100644 --- a/test/invariants/Setup.t.sol +++ b/test/invariants/Setup.t.sol @@ -103,12 +103,14 @@ contract Setup is BaseTest { eTST = EVaultExtended( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount)) ); + eTST.setHookConfig(address(0), 0); eTST.setInterestRateModel(address(new IRMTestDefault())); vaults.push(address(eTST)); eTST2 = EVaultExtended( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST2), address(oracle), unitOfAccount)) ); + eTST2.setHookConfig(address(0), 0); eTST2.setInterestRateModel(address(new IRMTestDefault())); vaults.push(address(eTST2)); } diff --git a/test/unit/evault/EVaultTestBase.t.sol b/test/unit/evault/EVaultTestBase.t.sol index ed537c2f..f413c7c1 100644 --- a/test/unit/evault/EVaultTestBase.t.sol +++ b/test/unit/evault/EVaultTestBase.t.sol @@ -133,6 +133,7 @@ contract EVaultTestBase is AssertionsCustomTypes, Test, DeployPermit2 { eTST = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount)) ); + eTST.setHookConfig(address(0), 0); eTST.setInterestRateModel(address(new IRMTestDefault())); eTST.setMaxLiquidationDiscount(0.2e4); eTST.setFeeReceiver(feeReceiver); @@ -140,6 +141,7 @@ contract EVaultTestBase is AssertionsCustomTypes, Test, DeployPermit2 { eTST2 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST2), address(oracle), unitOfAccount)) ); + eTST2.setHookConfig(address(0), 0); eTST2.setInterestRateModel(address(new IRMTestDefault())); eTST2.setMaxLiquidationDiscount(0.2e4); eTST2.setFeeReceiver(feeReceiver); @@ -152,6 +154,7 @@ contract EVaultTestBase is AssertionsCustomTypes, Test, DeployPermit2 { IEVault v = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(asset), address(oracle), unitOfAccount)) ); + v.setHookConfig(address(0), 0); v.setInterestRateModel(address(new IRMTestDefault())); v.setInterestFee(1e4); diff --git a/test/unit/evault/modules/Liquidation/full.t.sol b/test/unit/evault/modules/Liquidation/full.t.sol index 3ec6e028..d5e0d515 100644 --- a/test/unit/evault/modules/Liquidation/full.t.sol +++ b/test/unit/evault/modules/Liquidation/full.t.sol @@ -50,16 +50,19 @@ contract VaultLiquidation_Test is EVaultTestBase { eWETH = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetWETH), address(oracle), unitOfAccount)) ); + eWETH.setHookConfig(address(0), 0); eWETH.setInterestRateModel(address(new IRMTestZero())); eTST3 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount)) ); + eTST3.setHookConfig(address(0), 0); eTST3.setInterestRateModel(address(new IRMTestZero())); eTST4 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST4), address(oracle), unitOfAccount)) ); + eTST4.setHookConfig(address(0), 0); eTST4.setInterestRateModel(address(new IRMTestZero())); eTST.setLTV(address(eWETH), 0.3e4, 0.3e4, 0); @@ -1395,6 +1398,7 @@ contract VaultLiquidation_Test is EVaultTestBase { address(0), true, abi.encodePacked(address(assetTST), address(oracle), address(assetTST)) ) ); + eTSTx.setHookConfig(address(0), 0); eTSTx.setLTV(address(eTST2), 0.95e4, 0.95e4, 0); eTSTx.setMaxLiquidationDiscount(0.2e4); diff --git a/test/unit/evault/modules/Vault/amountLimits.t.sol b/test/unit/evault/modules/Vault/amountLimits.t.sol index 6a49f00f..cbbddf6a 100644 --- a/test/unit/evault/modules/Vault/amountLimits.t.sol +++ b/test/unit/evault/modules/Vault/amountLimits.t.sol @@ -25,6 +25,7 @@ contract VaultTest_AmountLimits is EVaultTestBase { eTST3 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount)) ); + eTST3.setHookConfig(address(0), 0); assetTST.mint(user1, type(uint256).max / 2); startHoax(user1); diff --git a/test/unit/evault/modules/Vault/batch.t.sol b/test/unit/evault/modules/Vault/batch.t.sol index 701075d9..ca766a0b 100644 --- a/test/unit/evault/modules/Vault/batch.t.sol +++ b/test/unit/evault/modules/Vault/batch.t.sol @@ -30,6 +30,7 @@ contract VaultTest_Batch is EVaultTestBase { eTST3 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount)) ); + eTST3.setHookConfig(address(0), 0); startHoax(address(this)); eTST.setInterestRateModel(address(new IRMTestZero())); diff --git a/test/unit/evault/modules/Vault/borrowIsolation.t.sol b/test/unit/evault/modules/Vault/borrowIsolation.t.sol index 28e9efa9..ca3ef168 100644 --- a/test/unit/evault/modules/Vault/borrowIsolation.t.sol +++ b/test/unit/evault/modules/Vault/borrowIsolation.t.sol @@ -29,6 +29,7 @@ contract VaultTest_BorrowIsolation is EVaultTestBase { eTST3 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount)) ); + eTST3.setHookConfig(address(0), 0); startHoax(address(this)); eTST.setInterestRateModel(address(new IRMTestZero())); diff --git a/test/unit/evault/modules/Vault/caps.t.sol b/test/unit/evault/modules/Vault/caps.t.sol index d24cfecf..6fd16be9 100644 --- a/test/unit/evault/modules/Vault/caps.t.sol +++ b/test/unit/evault/modules/Vault/caps.t.sol @@ -32,6 +32,7 @@ contract VaultTest_Caps is EVaultTestBase { eTST3 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount)) ); + eTST3.setHookConfig(address(0), 0); eTST.setLTV(address(eTST2), 0.3e4, 0.3e4, 0); eTST.setLTV(address(eTST3), 1e4, 1e4, 0); diff --git a/test/unit/evault/modules/Vault/conversion.t.sol b/test/unit/evault/modules/Vault/conversion.t.sol index d7015113..66f369cf 100644 --- a/test/unit/evault/modules/Vault/conversion.t.sol +++ b/test/unit/evault/modules/Vault/conversion.t.sol @@ -46,6 +46,7 @@ contract VaultTest_Conversion is EVaultTestBase { eTST0 = EVaultHarness( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount)) ); + eTST0.setHookConfig(address(0), 0); eTST0.setInterestRateModel(address(new IRMTestDefault())); } diff --git a/test/unit/evault/modules/Vault/decimals.t.sol b/test/unit/evault/modules/Vault/decimals.t.sol index d71be668..ad52c0a2 100644 --- a/test/unit/evault/modules/Vault/decimals.t.sol +++ b/test/unit/evault/modules/Vault/decimals.t.sol @@ -33,11 +33,13 @@ contract VaultTest_Decimals is EVaultTestBase { eTST3 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount)) ); + eTST3.setHookConfig(address(0), 0); assetTST4 = new TestERC20("Test TST 4", "TST4", 0, false); eTST4 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST4), address(oracle), unitOfAccount)) ); + eTST4.setHookConfig(address(0), 0); startHoax(address(this)); eTST.setInterestRateModel(address(new IRMTestZero())); diff --git a/test/unit/evault/modules/Vault/liquidity.t.sol b/test/unit/evault/modules/Vault/liquidity.t.sol index c7347a74..321e2959 100644 --- a/test/unit/evault/modules/Vault/liquidity.t.sol +++ b/test/unit/evault/modules/Vault/liquidity.t.sol @@ -33,6 +33,7 @@ contract VaultTest_Liquidity is EVaultTestBase { eTST3 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount)) ); + eTST3.setHookConfig(address(0), 0); startHoax(address(this)); eTST.setInterestRateModel(address(new IRMTestZero())); diff --git a/test/unit/evault/modules/Vault/nested.t.sol b/test/unit/evault/modules/Vault/nested.t.sol index a9405e08..c648fc72 100644 --- a/test/unit/evault/modules/Vault/nested.t.sol +++ b/test/unit/evault/modules/Vault/nested.t.sol @@ -29,6 +29,7 @@ contract VaultTest_Nested is EVaultTestBase { eTSTNested = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(eTST), address(oracle), unitOfAccount)) ); + eTSTNested.setHookConfig(address(0), 0); eTSTNested.setInterestRateModel(address(new IRMTestDefault())); depositor = makeAddr("depositor"); @@ -143,6 +144,7 @@ contract VaultTest_Nested is EVaultTestBase { eTSTDoubleNested = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(eTSTNested), address(oracle), unitOfAccount)) ); + eTSTDoubleNested.setHookConfig(address(0), 0); eTSTDoubleNested.setInterestRateModel(address(new IRMTestDefault())); eTSTDoubleNested.setLTV(address(eTST2), 0.9e4, 0.9e4, 0); diff --git a/test/unit/evault/modules/Vault/pullDebt.t.sol b/test/unit/evault/modules/Vault/pullDebt.t.sol index 4c3ae1a0..b5e27fa7 100644 --- a/test/unit/evault/modules/Vault/pullDebt.t.sol +++ b/test/unit/evault/modules/Vault/pullDebt.t.sol @@ -33,9 +33,11 @@ contract VaultTest_PullDebt is EVaultTestBase { eTST3 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount)) ); + eTST3.setHookConfig(address(0), 0); eTST4 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST4), address(oracle), unitOfAccount)) ); + eTST4.setHookConfig(address(0), 0); eTST.setInterestRateModel(address(new IRMTestZero())); eTST4.setInterestRateModel(address(new IRMTestZero())); diff --git a/test/unit/evault/modules/Vault/repayWithShares.sol b/test/unit/evault/modules/Vault/repayWithShares.sol index 753a604e..17e7330b 100644 --- a/test/unit/evault/modules/Vault/repayWithShares.sol +++ b/test/unit/evault/modules/Vault/repayWithShares.sol @@ -30,6 +30,7 @@ contract VaultTest_RepayWithShares is EVaultTestBase { eTST3 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount)) ); + eTST3.setHookConfig(address(0), 0); startHoax(address(this)); eTST.setInterestRateModel(address(new IRMTestZero())); diff --git a/test/unit/evault/modules/Vault/withdraw.t.sol b/test/unit/evault/modules/Vault/withdraw.t.sol index 0cef846c..dc861fba 100644 --- a/test/unit/evault/modules/Vault/withdraw.t.sol +++ b/test/unit/evault/modules/Vault/withdraw.t.sol @@ -37,6 +37,7 @@ contract VaultTest_Withdraw is EVaultTestBase { eTST3 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount)) ); + eTST3.setHookConfig(address(0), 0); eTST.setInterestRateModel(address(new IRMTestZero())); eTST2.setInterestRateModel(address(new IRMTestZero())); diff --git a/test/unit/evault/shared/EVCClient.t.sol b/test/unit/evault/shared/EVCClient.t.sol index e6af00c7..1968bfbd 100644 --- a/test/unit/evault/shared/EVCClient.t.sol +++ b/test/unit/evault/shared/EVCClient.t.sol @@ -56,6 +56,7 @@ contract EVCClientUnitTest is EVaultTestBase { eTST3 = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount)) ); + eTST3.setHookConfig(address(0), 0); eTST3.setInterestRateModel(address(new IRMTestDefault())); } @@ -171,6 +172,7 @@ contract EVCClientUnitTest is EVaultTestBase { IEVault v = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST2), address(oracle), unitOfAccount)) ); + v.setHookConfig(address(0), 0); v.setInterestRateModel(address(new IRMTestDefault())); return address(v); diff --git a/test/unit/evault/shared/Reentrancy.t.sol b/test/unit/evault/shared/Reentrancy.t.sol index c6202332..0099d06d 100644 --- a/test/unit/evault/shared/Reentrancy.t.sol +++ b/test/unit/evault/shared/Reentrancy.t.sol @@ -212,6 +212,7 @@ contract ReentrancyTest is EVaultTestBase { eTST = IEVault( factory.createProxy(address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount)) ); + eTST.setHookConfig(address(0), 0); vm.assume(sender != address(0) && sender != address(eTST)); From 74e0e0108ff54b825166fc992c21ef4e1b1c42e3 Mon Sep 17 00:00:00 2001 From: Doug Hoyte Date: Mon, 8 Jul 2024 17:01:19 -0400 Subject: [PATCH 48/76] In setLTV(), attempt to price the collateral in order to detect self-collateral config mistake (cantina 141) --- src/EVault/modules/Governance.sol | 6 ++++ test/mocks/MockPriceOracle.sol | 14 ++++++-- .../evault/modules/Vault/selfCollateral.t.sol | 35 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 test/unit/evault/modules/Vault/selfCollateral.t.sol diff --git a/src/EVault/modules/Governance.sol b/src/EVault/modules/Governance.sol index ed017351..1b7f95d9 100644 --- a/src/EVault/modules/Governance.sol +++ b/src/EVault/modules/Governance.sol @@ -297,6 +297,12 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT if (!currentLTV.initialized) vaultStorage.ltvList.push(collateral); + if (!newLiquidationLTV.isZero()) { + // Ensure that this collateral can be priced by the configured oracle + VaultCache memory vaultCache = updateVault(); + vaultCache.oracle.getQuote(1e18, collateral, vaultCache.unitOfAccount); + } + emit GovSetLTV( collateral, newLTV.borrowLTV.toUint16(), diff --git a/test/mocks/MockPriceOracle.sol b/test/mocks/MockPriceOracle.sol index e5c41b2d..757e8aaf 100644 --- a/test/mocks/MockPriceOracle.sol +++ b/test/mocks/MockPriceOracle.sol @@ -53,8 +53,18 @@ contract MockPriceOracle { } function calculateQuote(address base, uint256 amount, uint256 p) internal view returns (uint256) { - (bool success,) = base.staticcall(abi.encodeCall(IERC4626.asset, ())); - if (base.code.length > 0 && success) amount = IEVault(base).convertToAssets(amount); + // While base is a vault (for the purpose of the mock, if it implements asset()), then call + // convertToAssets() to price its shares. This is similar to how EulerRouter implements + // "resolved" vaults. + + while (base.code.length > 0) { + (bool success, bytes memory data) = base.staticcall(abi.encodeCall(IERC4626.asset, ())); + if (!success) break; + + (address asset) = abi.decode(data, (address)); + amount = IEVault(base).convertToAssets(amount); + base = asset; + } return amount * p / 1e18; } diff --git a/test/unit/evault/modules/Vault/selfCollateral.t.sol b/test/unit/evault/modules/Vault/selfCollateral.t.sol new file mode 100644 index 00000000..ebb777d0 --- /dev/null +++ b/test/unit/evault/modules/Vault/selfCollateral.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {EVaultTestBase, IEVault, IRMTestDefault} from "../../EVaultTestBase.t.sol"; + +import "../../../../../src/EVault/shared/types/Types.sol"; +import "../../../../../src/EVault/shared/Constants.sol"; + +contract VaultTest_SelfCollateral is EVaultTestBase { + using TypesLib for uint256; + + IEVault public eeTST; + + function setUp() public override { + super.setUp(); + + eeTST = IEVault( + factory.createProxy(address(0), true, abi.encodePacked(address(eTST), address(oracle), unitOfAccount)) + ); + eeTST.setInterestRateModel(address(new IRMTestDefault())); + + oracle.setPrice(address(assetTST), unitOfAccount, 1e18); + oracle.setPrice(address(eTST), unitOfAccount, 1e18); + oracle.setPrice(address(eeTST), unitOfAccount, 1e18); + } + + function test_selfCollateralDisallowed() public { + vm.expectRevert(Errors.E_InvalidLTVAsset.selector); + eTST.setLTV(address(eTST), 0.9e4, 0.9e4, 0); + + vm.expectRevert(Errors.E_Reentrancy.selector); + eTST.setLTV(address(eeTST), 0.9e4, 0.9e4, 0); + } +} From 679baa70f8701938d8476f217dc674b6ccaccf5e Mon Sep 17 00:00:00 2001 From: Doug Hoyte Date: Tue, 9 Jul 2024 07:29:51 -0400 Subject: [PATCH 49/76] don't bother updating the vault in setLTV: can just get values directly from metadata --- src/EVault/modules/Governance.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EVault/modules/Governance.sol b/src/EVault/modules/Governance.sol index 1b7f95d9..ba562cd2 100644 --- a/src/EVault/modules/Governance.sol +++ b/src/EVault/modules/Governance.sol @@ -299,8 +299,8 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT if (!newLiquidationLTV.isZero()) { // Ensure that this collateral can be priced by the configured oracle - VaultCache memory vaultCache = updateVault(); - vaultCache.oracle.getQuote(1e18, collateral, vaultCache.unitOfAccount); + (, IPriceOracle _oracle, address _unitOfAccount) = ProxyUtils.metadata(); + _oracle.getQuote(1e18, collateral, _unitOfAccount); } emit GovSetLTV( From e8f537493fae51c9b1912bc4395109ee2dd86c24 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 15 Jul 2024 14:35:47 +0200 Subject: [PATCH 50/76] remove error --- src/EVault/shared/Errors.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/EVault/shared/Errors.sol b/src/EVault/shared/Errors.sol index a4bf6de8..fcbe4cb8 100644 --- a/src/EVault/shared/Errors.sol +++ b/src/EVault/shared/Errors.sol @@ -9,7 +9,6 @@ pragma solidity ^0.8.0; contract Errors { error E_Initialized(); error E_ProxyMetadata(); - error E_SelfApproval(); error E_SelfTransfer(); error E_InsufficientAllowance(); error E_InsufficientCash(); From d366c8d347b67cd6692f00d0ab343d1ccd4505f4 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 15 Jul 2024 15:51:25 +0200 Subject: [PATCH 51/76] update specs --- docs/specs.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/specs.md b/docs/specs.md index f48bce45..2662c8c5 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 | From 379a256b903431fa63e2b7e3150d8bef3f1c7ee2 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 15 Jul 2024 18:47:11 +0200 Subject: [PATCH 52/76] move deposit call to module --- src/EVault/EVault.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EVault/EVault.sol b/src/EVault/EVault.sol index d1149c27..48b9a8a8 100644 --- a/src/EVault/EVault.sol +++ b/src/EVault/EVault.sol @@ -83,7 +83,7 @@ contract EVault is Dispatch { function creator() public view virtual override useView(MODULE_VAULT) returns (address) {} - function deposit(uint256 amount, address receiver) public virtual override callThroughEVC returns (uint256) { return super.deposit(amount, receiver); } + function deposit(uint256 amount, address receiver) public virtual override callThroughEVC use(MODULE_VAULT) returns (uint256) {} function mint(uint256 amount, address receiver) public virtual override callThroughEVC use(MODULE_VAULT) returns (uint256) {} From ead25cb078222e41780a569d649863fcd50b35ae Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Tue, 16 Jul 2024 17:03:28 +0200 Subject: [PATCH 53/76] fix: OZ L-04 (continuation) --- src/Synths/EulerSavingsRate.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index d5ad3600..b76341fd 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -125,8 +125,8 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { internal override { - super._withdraw(caller, receiver, owner, assets, shares); _totalAssets = _totalAssets - assets; + super._withdraw(caller, receiver, owner, assets, shares); } /// @notice Smears any donations to this vault as interest. From 468681d48c362779ffaa06ac521df1351e3367d7 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Thu, 18 Jul 2024 10:44:30 +0200 Subject: [PATCH 54/76] feat: make ESynth governance functions consistent with the EVK --- .gitmodules | 6 ++-- lib/ethereum-vault-connector | 2 +- src/Synths/ESynth.sol | 15 ++++++---- test/unit/esynth/ESynthGeneral.t.sol | 44 ++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/.gitmodules b/.gitmodules index a074a2c2..3acf41a0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,12 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/ethereum-vault-connector"] - path = lib/ethereum-vault-connector - url = https://github.com/euler-xyz/ethereum-vault-connector [submodule "lib/permit2"] path = lib/permit2 url = https://github.com/Uniswap/permit2 [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/ethereum-vault-connector"] + path = lib/ethereum-vault-connector + url = https://github.com/euler-xyz/ethereum-vault-connector diff --git a/lib/ethereum-vault-connector b/lib/ethereum-vault-connector index 0229f62f..5c73f731 160000 --- a/lib/ethereum-vault-connector +++ b/lib/ethereum-vault-connector @@ -1 +1 @@ -Subproject commit 0229f62f92856201e1f33bee9e59daf68938ba34 +Subproject commit 5c73f731ddb0d6bd1c35baf177954a317d608922 diff --git a/src/Synths/ESynth.sol b/src/Synths/ESynth.sol index ccb055cb..ea3be94e 100644 --- a/src/Synths/ESynth.sol +++ b/src/Synths/ESynth.sol @@ -44,7 +44,7 @@ contract ESynth is ERC20Collateral, Ownable { /// @dev Can only be called by the owner of the contract. /// @param minter The address of the minter to set the capacity for. /// @param capacity The capacity to set for the minter. - function setCapacity(address minter, uint128 capacity) external onlyOwner { + function setCapacity(address minter, uint128 capacity) external onlyEVCAccountOwner onlyOwner { minters[minter].capacity = capacity; emit MinterCapacitySet(minter, capacity); } @@ -106,7 +106,7 @@ contract ESynth is ERC20Collateral, Ownable { /// @dev Adds the vault to the list of accounts to ignore for the total supply. /// @param vault The vault to deposit the cash in. /// @param amount The amount of cash to deposit. - function allocate(address vault, uint256 amount) external onlyOwner { + function allocate(address vault, uint256 amount) external onlyEVCAccountOwner onlyOwner { if (IEVault(vault).EVC() != address(evc)) { revert E_NotEVCCompatible(); } @@ -118,7 +118,7 @@ contract ESynth is ERC20Collateral, Ownable { /// @notice Withdraw cash from the attached vault to this contract. /// @param vault The vault to withdraw the cash from. /// @param amount The amount of cash to withdraw. - function deallocate(address vault, uint256 amount) external onlyOwner { + function deallocate(address vault, uint256 amount) external onlyEVCAccountOwner onlyOwner { IEVault(vault).withdraw(amount, address(this), address(this)); } @@ -136,14 +136,19 @@ contract ESynth is ERC20Collateral, Ownable { /// @notice Adds an account to the list of accounts to ignore for the total supply. /// @param account The account to add to the list. /// @return success True when the account was not on the list and was added. False otherwise. - function addIgnoredForTotalSupply(address account) external onlyOwner returns (bool success) { + function addIgnoredForTotalSupply(address account) external onlyEVCAccountOwner onlyOwner returns (bool success) { return ignoredForTotalSupply.add(account); } /// @notice Removes an account from the list of accounts to ignore for the total supply. /// @param account The account to remove from the list. /// @return success True when the account was on the list and was removed. False otherwise. - function removeIgnoredForTotalSupply(address account) external onlyOwner returns (bool success) { + function removeIgnoredForTotalSupply(address account) + external + onlyEVCAccountOwner + onlyOwner + returns (bool success) + { return ignoredForTotalSupply.remove(account); } diff --git a/test/unit/esynth/ESynthGeneral.t.sol b/test/unit/esynth/ESynthGeneral.t.sol index 7c81306a..d091efa5 100644 --- a/test/unit/esynth/ESynthGeneral.t.sol +++ b/test/unit/esynth/ESynthGeneral.t.sol @@ -130,4 +130,48 @@ contract ESynthGeneralTest is ESynthTest { vm.expectRevert(ESynth.E_NotEVCCompatible.selector); esynth.allocate(address(wrongEVC), amount); } + + function test_GovernanceModifiers(address owner, uint8 id, address nonOwner, uint128 amount) public { + vm.assume(owner != address(0) && owner != address(evc)); + vm.assume(!evc.haveCommonOwner(owner, nonOwner) && nonOwner != address(evc)); + vm.assume(id != 0); + + vm.prank(owner); + esynth = ESynth(address(new ESynth(evc, "Test Synth", "TST"))); + + // succeeds if called directly by an owner + vm.prank(owner); + esynth.setCapacity(address(this), amount); + + // fails if called by a non-owner + vm.prank(nonOwner); + vm.expectRevert(); + esynth.setCapacity(address(this), amount); + + // succeeds if called by an owner through the EVC + vm.prank(owner); + evc.call(address(esynth), owner, 0, abi.encodeCall(ESynth.setCapacity, (address(this), amount))); + + // fails if called by non-owner through the EVC + vm.prank(nonOwner); + vm.expectRevert(); + evc.call(address(esynth), nonOwner, 0, abi.encodeCall(ESynth.setCapacity, (address(this), amount))); + + // fails if called by a sub-account of an owner through the EVC + vm.prank(owner); + vm.expectRevert(); + evc.call( + address(esynth), + address(uint160(owner) ^ id), + 0, + abi.encodeCall(ESynth.setCapacity, (address(this), amount)) + ); + + // fails if called by the owner operator through the EVC + vm.prank(owner); + evc.setAccountOperator(owner, nonOwner, true); + vm.prank(nonOwner); + vm.expectRevert(); + evc.call(address(esynth), owner, 0, abi.encodeCall(ESynth.setCapacity, (address(this), amount))); + } } From 9798ccd0c768902d29035e83acec9389c70a9a52 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 09:42:48 +0200 Subject: [PATCH 55/76] Cantina 125 - fix comment in checkLiquidity --- src/EVault/shared/LiquidityUtils.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EVault/shared/LiquidityUtils.sol b/src/EVault/shared/LiquidityUtils.sol index 1de279dd..f288dc53 100644 --- a/src/EVault/shared/LiquidityUtils.sol +++ b/src/EVault/shared/LiquidityUtils.sol @@ -30,7 +30,7 @@ abstract contract LiquidityUtils is BorrowUtils, LTVUtils { liabilityValue = getLiabilityValue(vaultCache, account, vaultStorage.users[account].getOwed(), liquidation); } - // Check that the value of the collateral, adjusted for borrowing LTV, is equal or greater than the liability value. + // Check that the value of the collateral, adjusted for borrowing LTV, is greater than the liability value. // Since this function uses bid/ask prices, it should only be used within the account status check, and not // for determining whether an account can be liquidated (which uses mid-point prices). function checkLiquidity(VaultCache memory vaultCache, address account, address[] memory collaterals) From 4a6d5ebb0bffd8270c4d7f4b495edec9ce46ca2c Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 10:59:35 +0200 Subject: [PATCH 56/76] Cantina 79 - fix comment in IRMSynth --- src/Synths/IRMSynth.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Synths/IRMSynth.sol b/src/Synths/IRMSynth.sol index 0dea4b9f..ffc49e65 100644 --- a/src/Synths/IRMSynth.sol +++ b/src/Synths/IRMSynth.sol @@ -101,7 +101,7 @@ contract IRMSynth is IIRM { // If the quote is less than the target, increase the rate rate = rate * ADJUST_FACTOR / ADJUST_ONE; } else { - // If the quote is greater than the target, decrease the rate + // If the quote is greater than or equal to the target, decrease the rate rate = rate * ADJUST_ONE / ADJUST_FACTOR; } From 9940555b50464381e8cd05db6e82f4a08abdc754 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 11:08:14 +0200 Subject: [PATCH 57/76] Cantina 85 - add comment about modules deployment --- src/EVault/Dispatch.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/EVault/Dispatch.sol b/src/EVault/Dispatch.sol index ec148e69..7baaa177 100644 --- a/src/EVault/Dispatch.sol +++ b/src/EVault/Dispatch.sol @@ -60,6 +60,9 @@ abstract contract Dispatch is address governance; } + /// @notice EVault's constructor + /// @dev It is highly recommended to deploy fresh modules for every new EVault deployment. Particular care must + /// also be taken to ensure the modules are deployed with the exact same values of the `Integrations` struct. constructor(Integrations memory integrations, DeployedModules memory modules) Base(integrations) { MODULE_INITIALIZE = AddressUtils.checkContract(modules.initialize); MODULE_TOKEN = AddressUtils.checkContract(modules.token); From 819262ae246433e589f5e903dfb6b34eaa844639 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 11:31:09 +0200 Subject: [PATCH 58/76] improve maxLiquidationDiscount natspec --- src/EVault/IEVault.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/EVault/IEVault.sol b/src/EVault/IEVault.sol index 647c67d6..4f3fd0b0 100644 --- a/src/EVault/IEVault.sol +++ b/src/EVault/IEVault.sol @@ -432,6 +432,9 @@ interface IGovernance { /// @notice Retrieves the maximum liquidation discount /// @return The maximum liquidation discount in 1e4 scale + /// @dev The default value, which is zero, is deliberately bad, as it means there would be no incentive to liquidate + /// unhealthy users. The vault creator must take care to properly select the limit, given the underlying and + /// collaterals used. function maxLiquidationDiscount() external view returns (uint16); /// @notice Retrieves liquidation cool-off time, which must elapse after successful account status check before From 3f1bff1ffd0bc3d4333dc80fe6a365f3f1af5154 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 12:08:44 +0200 Subject: [PATCH 59/76] Cantina 89 - fix ESR.withdraw natspec --- src/Synths/EulerSavingsRate.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol index e89a761f..bb1bb13c 100644 --- a/src/Synths/EulerSavingsRate.sol +++ b/src/Synths/EulerSavingsRate.sol @@ -84,12 +84,12 @@ contract EulerSavingsRate is EVCUtil, ERC4626 { return super.mint(shares, receiver); } - /// @notice Withdraws a certain amount of assets to the vault. + /// @notice Withdraws a certain amount of assets from the vault. /// @dev Overwritten to update the accrued interest and update _totalAssets. /// @param assets The amount of assets to withdraw. - /// @param receiver The recipient of the shares. - /// @param owner The account from which the assets are withdrawn - /// @return The amount of shares minted. + /// @param receiver The recipient of the assets. + /// @param owner The holder of shares to burn. + /// @return The amount of shares burned. function withdraw(uint256 assets, address receiver, address owner) public override nonReentrant returns (uint256) { // Move interest to totalAssets updateInterestAndReturnESRSlotCache(); From daf525fbe8dbb8612e7889bd584f16a8b3b75478 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 12:13:40 +0200 Subject: [PATCH 60/76] Cantina 116 - fix DToken.symbol spec --- docs/specs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs.md b/docs/specs.md index 1eaa96fa..7991857b 100644 --- a/docs/specs.md +++ b/docs/specs.md @@ -1,7 +1,7 @@ |Requirement ID |Title |Description | |------|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |EVK-60|Beacon Proxy |The [beacon proxy](https://docs.openzeppelin.com/contracts/4.x/api/proxy#beacon) is a proxy for which the beacon contract is the `msg.sender` address which constructs it. The constructor allows to pass up to 128 bytes of `trailingData` that must be appended to any `delegatecall` performed by the proxy. | -|EVK-11|DToken |DToken is a contract deployed on EVault contract initialization. It implements a subset of `ERC20` view functions that are related to the vault debt.

Functions implemented:

`name` - returns the name of the associated `EVault` preceded with "Debt token of "

`symbol` - returns the symbol of the associated `EVault` preceded with "d"

`decimals` - returns the decimals of the associated `EVault`

`totalSupply` - returns the total borrows of the associated `EVault`

`balanceOf` - returns individual debt of the user in the associated `EVault`

`allowance` - always returns 0

`asset` - returns the asset of the associated `EVault`

`emitTransfer` - emits the `Transfer` event. Callable only by the associated `EVault`

**DToken contract blocks calls to all the other public ERC20 functions.**

[https://docs.euler.finance/euler-vault-kit-white-paper/#dtoken](https://docs.euler.finance/euler-vault-kit-white-paper/#dtoken) | +|EVK-11|DToken |DToken is a contract deployed on EVault contract initialization. It implements a subset of `ERC20` view functions that are related to the vault debt.

Functions implemented:

`name` - returns the name of the associated `EVault` preceded with "Debt token of "

`symbol` - returns the symbol of the associated `EVault` followed by "-DEBT"

`decimals` - returns the decimals of the associated `EVault`

`totalSupply` - returns the total borrows of the associated `EVault`

`balanceOf` - returns individual debt of the user in the associated `EVault`

`allowance` - always returns 0

`asset` - returns the asset of the associated `EVault`

`emitTransfer` - emits the `Transfer` event. Callable only by the associated `EVault`

**DToken contract blocks calls to all the other public ERC20 functions.**

[https://docs.euler.finance/euler-vault-kit-white-paper/#dtoken](https://docs.euler.finance/euler-vault-kit-white-paper/#dtoken) | |EVK-9 |Dispatch |Implements helper functions and modifiers used for calls dispatching.

For code organisation purposes, and also to comfortably stay below code size limits at full optimisation levels, the component implementation contracts are organised into modules. The primary entry point contract `EVault` serves as a dispatcher that determines which module should be invoked. `EVault` inherits from *all* the modules, although functions that are overridden by a dispatching routine are considered dead code by the Solidity compiler and are not compiled in. In addition to being included in the `EVault` dispatcher contract, modules are also deployed separately so that they can be invoked with `delegatecall`.

This pattern allows functions to be handled in the following ways:

* Implemented directly: No external module is invoked. This is the most gas efficient, and is especially important for view methods that are called frequently by other contracts.
* `use(MODULE_XXX)`: The indicated module is invoked with `delegatecall`
* `useView(MODULE_XXX)`: The implementation contract uses `staticcall` to call `viewDelegate()` on itself, which then does a `delegatecall` to the indicated module.

In order to implement a function directly, it is sufficient to have no mention of it in the dispatcher. However, for documentation and consistency, the code overrides with a function that calls the corresponding module's function with `super()`. This wrapper function is inlined away by the compiler.

To delegate a function to a module, the code overwrites the function signature in the dispatcher with either the `use` or `useView` modifier and an empty code block as implementation. The empty code block removes the function, while the modifier causes the router to `delegatecall` into the module.

Modules are static, meaning they cannot be upgraded. Code upgrades require deploying a new implementation which refers to the new module, and then updating the implementation storage slot in the factory. Only upgradeable instances will be affected.

[**`delegatecall` into view functions**​](https://docs.euler.finance/euler-vault-kit-white-paper/#delegatecall-into-view-functions)

Solidity doesn't allow for view functions to `delegatecall`. To be able to remove view functions from the dispatcher codebase and `delegatecall` them instead into the modules, the dispatching mechanism uses the `useView` modifier. This modifier makes the view function `staticcall` back into the dispatcher to a `viewDelegate` function which is `non-payable`. This `viewDelegate` function can now `delegatecall` into the implementation of the view function in the module.

The issue, background and a proposed patch to the Solidity compiler described in [this solc issue](https://github.com/ethereum/solidity/issues/14577).

[**Gas vs Code-Size Tradeoff**​](https://docs.euler.finance/euler-vault-kit-white-paper/#gas-vs-code-size-tradeoff)

The top level module dispatch system in `EVault` serves as a dispatcher where you can mix and match where the code should physically be located - in the contract itself, or `delegatecall`ed to one of the modules. The routing of specific functions to specific modules is hardcoded into the dispatcher.

In order to decide if a function should be implemented directly or delegated to a module, its gas-importance and code size should be evaluated. Functions that are frequently called on-chain will benefit from being implemented directly to avoid the `delegatecall` overhead. This is especially important for `view` functions because of the `viewDelegate()` overhead. On the other hand, large functions should be delegated to ensure that the `EVault` dispatcher can fit within the 24 Kb code-size limit,

[**`callThroughEVC`**​](https://docs.euler.finance/euler-vault-kit-white-paper/#callthroughevc)

The `EVault` dispatcher makes use of another modifier called `callThroughEVC`. This modifier has two execution flows. Either it executes the function normally (dispatched or directly) if the call comes from the EVC, or calls into the EVC's `call()` method with the existing calldata prepended with the address of the dispatcher, `msg.sender`, and `msg.value`, which then calls back into the Vault for normal execution.

[https://docs.euler.finance/euler-vault-kit-white-paper/#static-modules](https://docs.euler.finance/euler-vault-kit-white-paper/#static-modules)| |EVK-35|Dispatch `callThroughEVC` |`callThroughEVC` is a modifier which:

* if `msg.sender` is the EVC, executes the body of the function * if `msg.sender` is not the EVC, invokes the EVC `call` function with the following arguments:

* `targetContract` must be equal to `address(this)`

* `onBehalfOfAccount` must be equal to `msg.sender`

* `value` must be equal to the `msg.value`

* `data` must be equal to the `msg.data`

`callThroughEVC` reverts if the call to the EVC is unsuccessful.

The expectation of the call to the EVC is that the EVC will call back into this contract and execute the function, on which `callThroughEVC` modifier was used, within the EVC checks deferred context.

[https://evc.wtf/docs/concepts/internals/call](https://evc.wtf/docs/concepts/internals/call)

[https://evc.wtf/docs/concepts/internals/checks-deferrable-call](https://evc.wtf/docs/concepts/internals/checks-deferrable-call) | |EVK-34|Dispatch `useView` |`useView` is a modifier which `staticcall`s into `function viewDelegate()` on this contract (`address(this)`). The calldata is as follows:

```bash viewDelegate.selector + module address + (msg.data with the proxy metadata stripped at the end) + caller address ```

`useView` reverts if `staticcall` is unsuccessful.

`viewDelegate` is a function which `delegatecall`s into the `module` address provided by `useView` with as per current `msg.data`.

`viewDelegate` reverts if not self-called OR if `delegatecall` is unsuccessful. | From 32e2097774639eb8ce3ba95b8ffaf39a90d81bb3 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 12:17:22 +0200 Subject: [PATCH 61/76] Cantina 117 - update viewDelegate specs --- docs/specs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs.md b/docs/specs.md index 7991857b..59cc2820 100644 --- a/docs/specs.md +++ b/docs/specs.md @@ -2,7 +2,7 @@ |------|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |EVK-60|Beacon Proxy |The [beacon proxy](https://docs.openzeppelin.com/contracts/4.x/api/proxy#beacon) is a proxy for which the beacon contract is the `msg.sender` address which constructs it. The constructor allows to pass up to 128 bytes of `trailingData` that must be appended to any `delegatecall` performed by the proxy. | |EVK-11|DToken |DToken is a contract deployed on EVault contract initialization. It implements a subset of `ERC20` view functions that are related to the vault debt.

Functions implemented:

`name` - returns the name of the associated `EVault` preceded with "Debt token of "

`symbol` - returns the symbol of the associated `EVault` followed by "-DEBT"

`decimals` - returns the decimals of the associated `EVault`

`totalSupply` - returns the total borrows of the associated `EVault`

`balanceOf` - returns individual debt of the user in the associated `EVault`

`allowance` - always returns 0

`asset` - returns the asset of the associated `EVault`

`emitTransfer` - emits the `Transfer` event. Callable only by the associated `EVault`

**DToken contract blocks calls to all the other public ERC20 functions.**

[https://docs.euler.finance/euler-vault-kit-white-paper/#dtoken](https://docs.euler.finance/euler-vault-kit-white-paper/#dtoken) | -|EVK-9 |Dispatch |Implements helper functions and modifiers used for calls dispatching.

For code organisation purposes, and also to comfortably stay below code size limits at full optimisation levels, the component implementation contracts are organised into modules. The primary entry point contract `EVault` serves as a dispatcher that determines which module should be invoked. `EVault` inherits from *all* the modules, although functions that are overridden by a dispatching routine are considered dead code by the Solidity compiler and are not compiled in. In addition to being included in the `EVault` dispatcher contract, modules are also deployed separately so that they can be invoked with `delegatecall`.

This pattern allows functions to be handled in the following ways:

* Implemented directly: No external module is invoked. This is the most gas efficient, and is especially important for view methods that are called frequently by other contracts.
* `use(MODULE_XXX)`: The indicated module is invoked with `delegatecall`
* `useView(MODULE_XXX)`: The implementation contract uses `staticcall` to call `viewDelegate()` on itself, which then does a `delegatecall` to the indicated module.

In order to implement a function directly, it is sufficient to have no mention of it in the dispatcher. However, for documentation and consistency, the code overrides with a function that calls the corresponding module's function with `super()`. This wrapper function is inlined away by the compiler.

To delegate a function to a module, the code overwrites the function signature in the dispatcher with either the `use` or `useView` modifier and an empty code block as implementation. The empty code block removes the function, while the modifier causes the router to `delegatecall` into the module.

Modules are static, meaning they cannot be upgraded. Code upgrades require deploying a new implementation which refers to the new module, and then updating the implementation storage slot in the factory. Only upgradeable instances will be affected.

[**`delegatecall` into view functions**​](https://docs.euler.finance/euler-vault-kit-white-paper/#delegatecall-into-view-functions)

Solidity doesn't allow for view functions to `delegatecall`. To be able to remove view functions from the dispatcher codebase and `delegatecall` them instead into the modules, the dispatching mechanism uses the `useView` modifier. This modifier makes the view function `staticcall` back into the dispatcher to a `viewDelegate` function which is `non-payable`. This `viewDelegate` function can now `delegatecall` into the implementation of the view function in the module.

The issue, background and a proposed patch to the Solidity compiler described in [this solc issue](https://github.com/ethereum/solidity/issues/14577).

[**Gas vs Code-Size Tradeoff**​](https://docs.euler.finance/euler-vault-kit-white-paper/#gas-vs-code-size-tradeoff)

The top level module dispatch system in `EVault` serves as a dispatcher where you can mix and match where the code should physically be located - in the contract itself, or `delegatecall`ed to one of the modules. The routing of specific functions to specific modules is hardcoded into the dispatcher.

In order to decide if a function should be implemented directly or delegated to a module, its gas-importance and code size should be evaluated. Functions that are frequently called on-chain will benefit from being implemented directly to avoid the `delegatecall` overhead. This is especially important for `view` functions because of the `viewDelegate()` overhead. On the other hand, large functions should be delegated to ensure that the `EVault` dispatcher can fit within the 24 Kb code-size limit,

[**`callThroughEVC`**​](https://docs.euler.finance/euler-vault-kit-white-paper/#callthroughevc)

The `EVault` dispatcher makes use of another modifier called `callThroughEVC`. This modifier has two execution flows. Either it executes the function normally (dispatched or directly) if the call comes from the EVC, or calls into the EVC's `call()` method with the existing calldata prepended with the address of the dispatcher, `msg.sender`, and `msg.value`, which then calls back into the Vault for normal execution.

[https://docs.euler.finance/euler-vault-kit-white-paper/#static-modules](https://docs.euler.finance/euler-vault-kit-white-paper/#static-modules)| +|EVK-9 |Dispatch |Implements helper functions and modifiers used for calls dispatching.

For code organisation purposes, and also to comfortably stay below code size limits at full optimisation levels, the component implementation contracts are organised into modules. The primary entry point contract `EVault` serves as a dispatcher that determines which module should be invoked. `EVault` inherits from *all* the modules, although functions that are overridden by a dispatching routine are considered dead code by the Solidity compiler and are not compiled in. In addition to being included in the `EVault` dispatcher contract, modules are also deployed separately so that they can be invoked with `delegatecall`.

This pattern allows functions to be handled in the following ways:

* Implemented directly: No external module is invoked. This is the most gas efficient, and is especially important for view methods that are called frequently by other contracts.
* `use(MODULE_XXX)`: The indicated module is invoked with `delegatecall`
* `useView(MODULE_XXX)`: The implementation contract uses `staticcall` to call `viewDelegate()` on itself, which then does a `delegatecall` to the indicated module.

In order to implement a function directly, it is sufficient to have no mention of it in the dispatcher. However, for documentation and consistency, the code overrides with a function that calls the corresponding module's function with `super()`. This wrapper function is inlined away by the compiler.

To delegate a function to a module, the code overwrites the function signature in the dispatcher with either the `use` or `useView` modifier and an empty code block as implementation. The empty code block removes the function, while the modifier causes the router to `delegatecall` into the module.

Modules are static, meaning they cannot be upgraded. Code upgrades require deploying a new implementation which refers to the new module, and then updating the implementation storage slot in the factory. Only upgradeable instances will be affected.

[**`delegatecall` into view functions**​](https://docs.euler.finance/euler-vault-kit-white-paper/#delegatecall-into-view-functions)

Solidity doesn't allow for view functions to `delegatecall`. To be able to remove view functions from the dispatcher codebase and `delegatecall` them instead into the modules, the dispatching mechanism uses the `useView` modifier. This modifier makes the view function `staticcall` back into the dispatcher to a `viewDelegate` function which is `payable` for gas optimization. This `viewDelegate` function can now `delegatecall` into the implementation of the view function in the module.

The issue, background and a proposed patch to the Solidity compiler described in [this solc issue](https://github.com/ethereum/solidity/issues/14577).

[**Gas vs Code-Size Tradeoff**​](https://docs.euler.finance/euler-vault-kit-white-paper/#gas-vs-code-size-tradeoff)

The top level module dispatch system in `EVault` serves as a dispatcher where you can mix and match where the code should physically be located - in the contract itself, or `delegatecall`ed to one of the modules. The routing of specific functions to specific modules is hardcoded into the dispatcher.

In order to decide if a function should be implemented directly or delegated to a module, its gas-importance and code size should be evaluated. Functions that are frequently called on-chain will benefit from being implemented directly to avoid the `delegatecall` overhead. This is especially important for `view` functions because of the `viewDelegate()` overhead. On the other hand, large functions should be delegated to ensure that the `EVault` dispatcher can fit within the 24 Kb code-size limit,

[**`callThroughEVC`**​](https://docs.euler.finance/euler-vault-kit-white-paper/#callthroughevc)

The `EVault` dispatcher makes use of another modifier called `callThroughEVC`. This modifier has two execution flows. Either it executes the function normally (dispatched or directly) if the call comes from the EVC, or calls into the EVC's `call()` method with the existing calldata prepended with the address of the dispatcher, `msg.sender`, and `msg.value`, which then calls back into the Vault for normal execution.

[https://docs.euler.finance/euler-vault-kit-white-paper/#static-modules](https://docs.euler.finance/euler-vault-kit-white-paper/#static-modules)| |EVK-35|Dispatch `callThroughEVC` |`callThroughEVC` is a modifier which:

* if `msg.sender` is the EVC, executes the body of the function * if `msg.sender` is not the EVC, invokes the EVC `call` function with the following arguments:

* `targetContract` must be equal to `address(this)`

* `onBehalfOfAccount` must be equal to `msg.sender`

* `value` must be equal to the `msg.value`

* `data` must be equal to the `msg.data`

`callThroughEVC` reverts if the call to the EVC is unsuccessful.

The expectation of the call to the EVC is that the EVC will call back into this contract and execute the function, on which `callThroughEVC` modifier was used, within the EVC checks deferred context.

[https://evc.wtf/docs/concepts/internals/call](https://evc.wtf/docs/concepts/internals/call)

[https://evc.wtf/docs/concepts/internals/checks-deferrable-call](https://evc.wtf/docs/concepts/internals/checks-deferrable-call) | |EVK-34|Dispatch `useView` |`useView` is a modifier which `staticcall`s into `function viewDelegate()` on this contract (`address(this)`). The calldata is as follows:

```bash viewDelegate.selector + module address + (msg.data with the proxy metadata stripped at the end) + caller address ```

`useView` reverts if `staticcall` is unsuccessful.

`viewDelegate` is a function which `delegatecall`s into the `module` address provided by `useView` with as per current `msg.data`.

`viewDelegate` reverts if not self-called OR if `delegatecall` is unsuccessful. | |EVK-33|Dispatch `use` |`use` is a modifier which `delegatecall`s into provided `module` address as per current `msg.data`. `use` reverts if `delegatecall` is unsuccessful. | From 9c28d48cb6632bc095938a879526c0c3b85c985f Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 12:30:37 +0200 Subject: [PATCH 62/76] Cantina 150 - improve setInterestRateModel natspec --- src/EVault/IEVault.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/EVault/IEVault.sol b/src/EVault/IEVault.sol index 4f3fd0b0..175e6e3f 100644 --- a/src/EVault/IEVault.sol +++ b/src/EVault/IEVault.sol @@ -502,6 +502,8 @@ interface IGovernance { /// @notice Set a new interest rate model contract /// @param newModel The new IRM address + /// @dev If the new model reverts, perhaps due to governor error, the vault will silently use a zero interest + /// rate. Governor should make sure the new interest rates are computed as expected. function setInterestRateModel(address newModel) external; /// @notice Set a new hook target and a new bitmap indicating which operations should call the hook target. From 390c0b9c0140bce4e6d3a214487b6fa750c96923 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 12:39:32 +0200 Subject: [PATCH 63/76] Cantina 447 - improve natspec for repayWithShares --- src/EVault/IEVault.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/EVault/IEVault.sol b/src/EVault/IEVault.sol index 175e6e3f..8e8d9a42 100644 --- a/src/EVault/IEVault.sol +++ b/src/EVault/IEVault.sol @@ -245,6 +245,8 @@ interface IBorrowing { /// @return shares Amount of shares burned /// @return debt Amount of debt removed in assets /// @dev Equivalent to withdrawing and repaying, but no assets are needed to be present in the vault + /// @dev Contrary to a regular `repay`, if account is unhealthy, the repay amount must bring the account back to + /// health, or the operation will revert during account status check function repayWithShares(uint256 amount, address receiver) external returns (uint256 shares, uint256 debt); /// @notice Take over debt from another account From b3723cb6434b460728470ed41a69e169c416b692 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 12:48:03 +0200 Subject: [PATCH 64/76] Cantina 169 - fix pushAssets natspec --- src/EVault/shared/AssetTransfers.sol | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/EVault/shared/AssetTransfers.sol b/src/EVault/shared/AssetTransfers.sol index 83a269d0..095b1305 100644 --- a/src/EVault/shared/AssetTransfers.sol +++ b/src/EVault/shared/AssetTransfers.sol @@ -20,15 +20,16 @@ abstract contract AssetTransfers is Base { vaultStorage.cash = vaultCache.cash = vaultCache.cash + amount; } - /// @dev If the `CFG_EVC_COMPATIBLE_ASSET` flag is set, the function will protect users from mistakenly sending - /// funds to the EVC sub-accounts. Functions that push tokens out (`withdraw`, `redeem`, `borrow`) accept a - /// `receiver` argument. If the user sets one of their sub-accounts (not the owner) as the receiver, funds would be - /// lost because a regular asset doesn't support the EVC's sub-accounts. The private key to a sub-account (not the - /// owner) is not known, so the user would not be able to move the funds out. The function will make a best effort - /// to prevent this by checking if the receiver of the token is recognized by EVC as a non-owner sub-account. In - /// other words, if there is an account registered in EVC as the owner for the intended receiver, the transfer will - /// be prevented. However, there is no guarantee that EVC will have the owner registered. If the asset itself is - /// compatible with EVC, it is safe to not set the flag and send the asset to a non-owner sub-account. + /// @dev If the `CFG_EVC_COMPATIBLE_ASSET` flag is not set (default), the function will protect users from + /// mistakenly sending funds to the EVC sub-accounts. Functions that push tokens out (`withdraw`, `redeem`, + /// `borrow`) accept a `receiver` argument. If the user sets one of their sub-accounts (not the owner) as the + /// receiver, funds would be lost because a regular asset doesn't support the EVC's sub-accounts. The private key to + /// a sub-account (not the owner) is not known, so the user would not be able to move the funds out. The function + /// will make a best effort to prevent this by checking if the receiver of the token is recognized by EVC as a + /// non-owner sub-account. In other words, if there is an account registered in EVC as the owner for the intended + /// receiver, the transfer will be prevented. However, there is no guarantee that EVC will have the owner + /// registered. If the asset itself is compatible with EVC, it is safe to set the flag and send the asset to a + /// non-owner sub-account. function pushAssets(VaultCache memory vaultCache, address to, Assets amount) internal virtual { if ( to == address(0) From dc5597682806b1e9b20cf5f47e204a7818b357ed Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 13:13:41 +0200 Subject: [PATCH 65/76] Cantina 199 - fix typo in IRMSynth constructor --- src/Synths/IRMSynth.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Synths/IRMSynth.sol b/src/Synths/IRMSynth.sol index ffc49e65..2bb6d9c9 100644 --- a/src/Synths/IRMSynth.sol +++ b/src/Synths/IRMSynth.sol @@ -43,7 +43,7 @@ contract IRMSynth is IIRM { event InterestUpdated(uint256 rate); - constructor(address synth_, address referenceAsset_, address oracle_, uint256 targetQuoute_) { + constructor(address synth_, address referenceAsset_, address oracle_, uint256 targetQuote_) { if (synth_ == address(0) || referenceAsset_ == address(0) || oracle_ == address(0)) { revert E_ZeroAddress(); } @@ -51,7 +51,7 @@ contract IRMSynth is IIRM { synth = synth_; referenceAsset = referenceAsset_; oracle = IPriceOracle(oracle_); - targetQuote = targetQuoute_; + targetQuote = targetQuote_; quoteAmount = 10 ** IERC20(synth_).decimals(); // Refusing to proceed with worthless asset From 551f2bf03b7662c57c58b2c3955448ba1583760c Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 13:17:01 +0200 Subject: [PATCH 66/76] Cantina 206 - fix protocolFeeShare natspec --- src/EVault/IEVault.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EVault/IEVault.sol b/src/EVault/IEVault.sol index 8e8d9a42..94fc59fa 100644 --- a/src/EVault/IEVault.sol +++ b/src/EVault/IEVault.sol @@ -384,7 +384,7 @@ interface IGovernance { function protocolConfigAddress() external view returns (address); /// @notice Retrieves the protocol fee share - /// @return A percentage share of fees accrued belonging to the protocol. In wad scale (1e18) + /// @return A percentage share of fees accrued belonging to the protocol, in 1e4 scale function protocolFeeShare() external view returns (uint256); /// @notice Retrieves the address which will receive protocol's fees From 7730f7bc831eb2190baf2ae966614fd3fa20083e Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 8 Jul 2024 13:28:59 +0200 Subject: [PATCH 67/76] Cantina 220 - improve liquidation slippage params natspec --- src/EVault/IEVault.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/EVault/IEVault.sol b/src/EVault/IEVault.sol index 94fc59fa..326b1506 100644 --- a/src/EVault/IEVault.sol +++ b/src/EVault/IEVault.sol @@ -287,9 +287,11 @@ interface ILiquidation { /// @param violator Address that may be in collateral violation /// @param collateral Collateral which is to be seized /// @param repayAssets The amount of underlying debt to be transferred from violator to sender, in asset units (use - /// max uint256 to repay the maximum possible amount). + /// max uint256 to repay the maximum possible amount). Meant as slippage check together with `minYieldBalance` /// @param minYieldBalance The minimum acceptable amount of collateral to be transferred from violator to sender, in - /// collateral balance units (shares for vaults) + /// collateral balance units (shares for vaults). Meant as slippage check together with `repayAssets` + /// @dev If `repayAssets` is set to max uint256 it is assumed the caller will perform their own slippage checks to + /// make sure they are not taking on too much debt. This option is mainly meant for smart contract liquidators function liquidate(address violator, address collateral, uint256 repayAssets, uint256 minYieldBalance) external; } From bd3b727245ff59e5bd25a6aa6ad65a3174585b61 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 15 Jul 2024 14:34:30 +0200 Subject: [PATCH 68/76] Cantina 551 - add comment about multiple repays above supply cap --- src/EVault/modules/RiskManager.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/EVault/modules/RiskManager.sol b/src/EVault/modules/RiskManager.sol index b893e796..873385cf 100644 --- a/src/EVault/modules/RiskManager.sol +++ b/src/EVault/modules/RiskManager.sol @@ -109,6 +109,8 @@ abstract contract RiskManagerModule is IRiskManager, LiquidityUtils { // Borrows are rounded down, because total assets could increase during repays. // This could happen when repaid user debt is rounded up to assets and used to increase cash, // while totalBorrows would be adjusted by only the exact debt, less than the increase in cash. + // If multiple accounts need to repay while the supply cap is exceeded they should do so in + // separate batches. uint256 supply = vaultCache.cash.toUint() + vaultCache.totalBorrows.toAssetsDown().toUint(); if (supply > vaultCache.supplyCap && supply > prevSupply) revert E_SupplyCapExceeded(); From 7680f9f5c5c306c2d075fa0d73a2d965d9851e23 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Mon, 15 Jul 2024 18:48:51 +0200 Subject: [PATCH 69/76] formatting --- src/EVault/IEVault.sol | 2 +- src/EVault/modules/RiskManager.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EVault/IEVault.sol b/src/EVault/IEVault.sol index 326b1506..b3bbebb0 100644 --- a/src/EVault/IEVault.sol +++ b/src/EVault/IEVault.sol @@ -290,7 +290,7 @@ interface ILiquidation { /// max uint256 to repay the maximum possible amount). Meant as slippage check together with `minYieldBalance` /// @param minYieldBalance The minimum acceptable amount of collateral to be transferred from violator to sender, in /// collateral balance units (shares for vaults). Meant as slippage check together with `repayAssets` - /// @dev If `repayAssets` is set to max uint256 it is assumed the caller will perform their own slippage checks to + /// @dev If `repayAssets` is set to max uint256 it is assumed the caller will perform their own slippage checks to /// make sure they are not taking on too much debt. This option is mainly meant for smart contract liquidators function liquidate(address violator, address collateral, uint256 repayAssets, uint256 minYieldBalance) external; } diff --git a/src/EVault/modules/RiskManager.sol b/src/EVault/modules/RiskManager.sol index 873385cf..4f78db4d 100644 --- a/src/EVault/modules/RiskManager.sol +++ b/src/EVault/modules/RiskManager.sol @@ -109,7 +109,7 @@ abstract contract RiskManagerModule is IRiskManager, LiquidityUtils { // Borrows are rounded down, because total assets could increase during repays. // This could happen when repaid user debt is rounded up to assets and used to increase cash, // while totalBorrows would be adjusted by only the exact debt, less than the increase in cash. - // If multiple accounts need to repay while the supply cap is exceeded they should do so in + // If multiple accounts need to repay while the supply cap is exceeded they should do so in // separate batches. uint256 supply = vaultCache.cash.toUint() + vaultCache.totalBorrows.toAssetsDown().toUint(); From 32898036d943ef1a76a999e7b33085c08400c848 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Tue, 16 Jul 2024 12:40:33 +0200 Subject: [PATCH 70/76] improve checkLiquidity comment --- src/EVault/shared/LiquidityUtils.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EVault/shared/LiquidityUtils.sol b/src/EVault/shared/LiquidityUtils.sol index f288dc53..e6b68e28 100644 --- a/src/EVault/shared/LiquidityUtils.sol +++ b/src/EVault/shared/LiquidityUtils.sol @@ -30,9 +30,9 @@ abstract contract LiquidityUtils is BorrowUtils, LTVUtils { liabilityValue = getLiabilityValue(vaultCache, account, vaultStorage.users[account].getOwed(), liquidation); } - // Check that the value of the collateral, adjusted for borrowing LTV, is greater than the liability value. - // Since this function uses bid/ask prices, it should only be used within the account status check, and not - // for determining whether an account can be liquidated (which uses mid-point prices). + // Check that there is no liability, or the value of the collateral, adjusted for borrowing LTV, is greater than the + // liability value. Since this function uses bid/ask prices, it should only be used within the account status check, + // and not for determining whether an account can be liquidated (which uses mid-point prices). function checkLiquidity(VaultCache memory vaultCache, address account, address[] memory collaterals) internal view From e44d4428c0d3f765c1c7de05a70cd2cb90817382 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Fri, 5 Jul 2024 11:33:46 +0200 Subject: [PATCH 71/76] add min liability value for socialization --- src/EVault/modules/Liquidation.sol | 20 ++- .../evault/modules/Liquidation/full.t.sol | 136 +++++++++++++++++- 2 files changed, 144 insertions(+), 12 deletions(-) diff --git a/src/EVault/modules/Liquidation.sol b/src/EVault/modules/Liquidation.sol index fd33bc2e..589428fc 100644 --- a/src/EVault/modules/Liquidation.sol +++ b/src/EVault/modules/Liquidation.sol @@ -16,6 +16,11 @@ import "../shared/types/Types.sol"; abstract contract LiquidationModule is ILiquidation, BalanceUtils, LiquidityUtils { using TypesLib for uint256; + // Minimum debt value, before liquidation, which enables debt socialization. + // With small debt positions, when vault is nearly empty, rounding in the pricing oracle + // may have an outsized impact on vault's shares to assets exchange rate after debt socialization. + uint256 constant MIN_SOCIALIZATION_LIABILITY_VALUE = 1e6; + struct LiquidationCache { address liquidator; address violator; @@ -24,6 +29,7 @@ abstract contract LiquidationModule is ILiquidation, BalanceUtils, LiquidityUtil Assets liability; Assets repay; uint256 yieldBalance; + uint256 liabilityValue; } /// @inheritdoc ILiquidation @@ -118,18 +124,19 @@ abstract contract LiquidationModule is ILiquidation, BalanceUtils, LiquidityUtil { // Check account health - (uint256 collateralAdjustedValue, uint256 liabilityValue) = + uint256 collateralAdjustedValue; + (collateralAdjustedValue, liqCache.liabilityValue) = calculateLiquidity(vaultCache, liqCache.violator, liqCache.collaterals, true); // no violation - if (collateralAdjustedValue > liabilityValue || liabilityValue == 0) { + if (collateralAdjustedValue > liqCache.liabilityValue || liqCache.liabilityValue == 0) { return liqCache; } // Compute discount // discountFactor = health score = 1 - discount - uint256 discountFactor = collateralAdjustedValue * 1e18 / liabilityValue; + uint256 discountFactor = collateralAdjustedValue * 1e18 / liqCache.liabilityValue; { uint256 minDiscountFactor; unchecked { @@ -156,7 +163,7 @@ abstract contract LiquidationModule is ILiquidation, BalanceUtils, LiquidityUtil return liqCache; } - uint256 maxRepayValue = liabilityValue; + uint256 maxRepayValue = liqCache.liabilityValue; uint256 maxYieldValue = maxRepayValue * 1e18 / discountFactor; // Limit yield to borrower's available collateral, and reduce repay if necessary. This can happen when @@ -169,7 +176,7 @@ abstract contract LiquidationModule is ILiquidation, BalanceUtils, LiquidityUtil maxYieldValue = collateralValue; } - liqCache.repay = (maxRepayValue * liqCache.liability.toUint() / liabilityValue).toAssets(); + liqCache.repay = (maxRepayValue * liqCache.liability.toUint() / liqCache.liabilityValue).toAssets(); liqCache.yieldBalance = maxYieldValue * collateralBalance / collateralValue; return liqCache; @@ -213,7 +220,8 @@ abstract contract LiquidationModule is ILiquidation, BalanceUtils, LiquidityUtil // Handle debt socialization if ( - vaultCache.configFlags.isNotSet(CFG_DONT_SOCIALIZE_DEBT) && liqCache.liability > liqCache.repay + liqCache.liabilityValue >= MIN_SOCIALIZATION_LIABILITY_VALUE + && vaultCache.configFlags.isNotSet(CFG_DONT_SOCIALIZE_DEBT) && liqCache.liability > liqCache.repay && checkNoCollateral(liqCache.violator, liqCache.collaterals) ) { Assets owedRemaining = liqCache.liability.subUnchecked(liqCache.repay); diff --git a/test/unit/evault/modules/Liquidation/full.t.sol b/test/unit/evault/modules/Liquidation/full.t.sol index 490ea028..06843286 100644 --- a/test/unit/evault/modules/Liquidation/full.t.sol +++ b/test/unit/evault/modules/Liquidation/full.t.sol @@ -11,6 +11,7 @@ import {IEVC} from "ethereum-vault-connector/interfaces/IEthereumVaultConnector. import {TestERC20} from "../../../../mocks/TestERC20.sol"; import {IRMTestFixed} from "../../../../mocks/IRMTestFixed.sol"; import {IRMTestZero} from "../../../../mocks/IRMTestZero.sol"; +import {IRMMax} from "../../../../mocks/IRMMax.sol"; import "forge-std/Test.sol"; @@ -628,7 +629,7 @@ contract VaultLiquidation_Test is EVaultTestBase { assertEq(eTST2.balanceOf(borrower), 0); } - function test_debtSocialization() public { + function test_debtSocialization_basic() public { // set up liquidator to support the debt startHoax(lender); evc.enableController(lender, address(eTST)); @@ -704,6 +705,130 @@ contract VaultLiquidation_Test is EVaultTestBase { assertEq(eTST.balanceOf(lender), maxYield); } + function test_debtSocialization_minLiabilityValue() public { + // set up liquidator to support the debt + startHoax(lender); + evc.enableController(lender, address(eTST)); + evc.enableCollateral(lender, address(eTST3)); + evc.enableCollateral(lender, address(eTST2)); + + startHoax(address(this)); + eTST.setLTV(address(eTST3), 0.95e4, 0.95e4, 0); + eTST.setLTV(address(eTST2), 0.99e4, 0.99e4, 0); + + startHoax(borrower); + eTST2.redeem(type(uint256).max, borrower, borrower); + eTST2.deposit(2.7e6, borrower); + + evc.enableController(borrower, address(eTST)); + eTST.borrow(0.45e6, borrower); + + startHoax(bystander); + evc.enableController(bystander, address(eTST)); + eTST.borrow(1e6, bystander); + + uint256 collateralValue; + uint256 liabilityValue; + + vm.stopPrank(); + eTST.setInterestRateModel(address(new IRMMax())); + + // withdraw remaining deposit to bump utilization + startHoax(lender); + eTST.redeem(eTST.maxRedeem(lender), lender, lender); + + uint256 snapshot = vm.snapshot(); + + (collateralValue, liabilityValue) = eTST.accountLiquidity(borrower, true); + + // just below min socialization liability value + assertEq(liabilityValue, 0.99e6); + + uint256 prevTotalBorrows = eTST.totalBorrows(); + assertEq(prevTotalBorrows, 1.45e6); + + // collateral price falls + oracle.setPrice(address(assetTST2), unitOfAccount, 0.3e18); + + (uint256 maxRepay, uint256 maxYield) = eTST.checkLiquidation(lender, borrower, address(eTST2)); + + // full collateral is liquidatable, bad debt will remain + assertEq(maxYield, 2.7e6); + + address[] memory collaterals = evc.getCollaterals(borrower); + assertEq(collaterals.length, 1); + + eTST.liquidate(borrower, address(eTST2), maxRepay, 0); + + (collateralValue, liabilityValue) = eTST.accountLiquidity(borrower, true); + assertEq(collateralValue, 0); + // non socialized bad debt remains + assertEq(liabilityValue, 0.3339e6); + + // total borrows unchanged + assertEq(eTST.totalBorrows(), prevTotalBorrows); + + // wait for remaining bad debt to increase above the socialization limit + + skip(50 days); + + (collateralValue, liabilityValue) = eTST.accountLiquidity(borrower, true); + + // value above max + assertGt(liabilityValue, 1e6); + + uint256 borrowerPrevDebt = eTST.debtOf(borrower); + uint256 lenderPrevDebt = eTST.debtOf(lender); + prevTotalBorrows = eTST.totalBorrows(); + + (maxRepay, maxYield) = eTST.checkLiquidation(lender, borrower, address(eTST2)); + // both repay and yield are zero, but socialization can happen + assertEq(maxRepay, 0); + assertEq(maxYield, 0); + + eTST.liquidate(borrower, address(eTST2), type(uint256).max, 0); + + // no new debt for liquidator + assertEq(eTST.debtOf(lender), lenderPrevDebt); + // bad debt is removed and socialized + assertEq(eTST.debtOf(borrower), 0); + assertEq(eTST.totalBorrows(), prevTotalBorrows - borrowerPrevDebt); + + // if debt value was originally above the cap, socialization would happen in the first liquidation + + vm.revertTo(snapshot); + + skip(3 days); + + (collateralValue, liabilityValue) = eTST.accountLiquidity(borrower, true); + + // just above min socialization liability value + assertEq(liabilityValue, 1.067803e6); + + prevTotalBorrows = eTST.totalBorrows(); + borrowerPrevDebt = eTST.debtOf(borrower); + + // collateral price falls + oracle.setPrice(address(assetTST2), unitOfAccount, 0.3e18); + + (maxRepay, maxYield) = eTST.checkLiquidation(lender, borrower, address(eTST2)); + + // full collateral is liquidatable, bad debt will remain + assertEq(maxYield, 2.7e6); + + eTST.liquidate(borrower, address(eTST2), maxRepay, 0); + + (collateralValue, liabilityValue) = eTST.accountLiquidity(borrower, true); + assertEq(collateralValue, 0); + // debt is socialized + assertEq(liabilityValue, 0); + assertEq(eTST.debtOf(borrower), 0); + + // total borrows lowered + assertLt(eTST.totalBorrows(), prevTotalBorrows); + assertApproxEqAbs(eTST.totalBorrows(), prevTotalBorrows - (borrowerPrevDebt - maxRepay), 1); + } + function test_zeroCollateralWorth() public { // set up liquidator to support the debt startHoax(lender); @@ -971,16 +1096,15 @@ contract VaultLiquidation_Test is EVaultTestBase { eTST.liquidate(borrower, address(eTST2), 0, 0); - //violator - assertEq(eTST.debtOf(borrower), 0); + // violator assertEq(eTST2.balanceOf(borrower), 0); + // debt is not socialized because it's under MIN_SOCIALIZATION_LIABILITY_VALUE + assertEq(eTST.debtOf(borrower), 2); + assertEq(eTST.totalBorrows(), 2); // liquidator: assertEq(eTST.debtOf(lender), 0); assertEq(eTST2.balanceOf(lender), 45); - - // total borrows - assertEq(eTST.totalBorrows(), 0); } // yield value converted to balance rounds down to 0. equivalent to pullDebt From 337b6de69ad89a7b5811a1fc9184ae64aa24a7a6 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Thu, 4 Jul 2024 22:29:19 +0100 Subject: [PATCH 72/76] fix: cantina 111 & 477 --- src/Synths/PegStabilityModule.sol | 51 ++++--- test/unit/pegStabilityModules/PSM.t.sol | 183 +++++++++++++----------- 2 files changed, 135 insertions(+), 99 deletions(-) diff --git a/src/Synths/PegStabilityModule.sol b/src/Synths/PegStabilityModule.sol index 06b1f27c..b2daec02 100644 --- a/src/Synths/PegStabilityModule.sol +++ b/src/Synths/PegStabilityModule.sol @@ -5,19 +5,21 @@ pragma solidity ^0.8.0; import {EVCUtil, IEVC} from "ethereum-vault-connector/utils/EVCUtil.sol"; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; +import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; import {ESynth} from "./ESynth.sol"; /// @title PegStabilityModule /// @custom:security-contact security@euler.xyz /// @author Euler Labs (https://www.eulerlabs.com/) /// @notice The PegStabilityModule is granted minting rights on the ESynth and must allow slippage-free conversion from -/// and to the underlying asset as per configured conversionPrice. On deployment, the fee for swaps to synthetic asset +/// and to the underlying asset as per configured CONVERSION_PRICE. On deployment, the fee for swaps to synthetic asset /// and to underlying asset are defined. These fees must accrue to the PegStabilityModule contract and can not be /// withdrawn, serving as a permanent reserve to support the peg. Swapping to the synthetic asset is possible up to the /// minting cap granted for the PegStabilityModule in the ESynth. Swapping to the underlying asset is possible up to the /// amount of the underlying asset held by the PegStabilityModule. contract PegStabilityModule is EVCUtil { using SafeERC20 for IERC20; + using Math for uint256; uint256 public constant BPS_SCALE = 100_00; uint256 public constant PRICE_SCALE = 1e18; @@ -26,45 +28,52 @@ contract PegStabilityModule is EVCUtil { ESynth public immutable synth; /// @notice The underlying asset. IERC20 public immutable underlying; - /// @notice The conversion price between the synthetic and underlying asset. - uint256 public immutable conversionPrice; // 1e18 = 1 SYNTH == 1 UNDERLYING, 0.01e18 = 1 SYNTH == 0.01 UNDERLYING /// @notice The fee for swapping to the underlying asset in basis points. uint256 public immutable TO_UNDERLYING_FEE; /// @notice The fee for swapping to the synthetic asset in basis points. uint256 public immutable TO_SYNTH_FEE; + /// @notice The conversion price between the synthetic and underlying asset. + uint256 public immutable CONVERSION_PRICE; error E_ZeroAddress(); error E_FeeExceedsBPS(); + error E_ZeroConversionPrice(); /// @param _evc The address of the EVC. /// @param _synth The address of the synthetic asset. /// @param _underlying The address of the underlying asset. - /// @param toUnderlyingFeeBPS The fee for swapping to the underlying asset in basis points. eg: 100 = 1% - /// @param toSynthFeeBPS The fee for swapping to the synthetic asset in basis points. eg: 100 = 1% + /// @param _toUnderlyingFeeBPS The fee for swapping to the underlying asset in basis points. eg: 100 = 1% + /// @param _toSynthFeeBPS The fee for swapping to the synthetic asset in basis points. eg: 100 = 1% /// @param _conversionPrice The conversion price between the synthetic and underlying asset. - /// eg: 1e18 = 1 SYNTH == 1 UNDERLYING, 0.01e18 = 1 SYNTH == 0.01 UNDERLYING + /// @dev _conversionPrice = 10**underlyingDecimals corresponds to 1:1 peg + /// @dev if underlying is 18 decimals, _conversionPrice = 1e18 corresponds to 1:1 peg + /// @dev if underlying is 6 decimals, _conversionPrice = 1e6 corresponds to 1:1 peg constructor( address _evc, address _synth, address _underlying, - uint256 toUnderlyingFeeBPS, - uint256 toSynthFeeBPS, + uint256 _toUnderlyingFeeBPS, + uint256 _toSynthFeeBPS, uint256 _conversionPrice ) EVCUtil(_evc) { - if (toUnderlyingFeeBPS >= BPS_SCALE || toSynthFeeBPS >= BPS_SCALE) { + if (_synth == address(0) || _underlying == address(0)) { + revert E_ZeroAddress(); + } + + if (_toUnderlyingFeeBPS >= BPS_SCALE || _toSynthFeeBPS >= BPS_SCALE) { revert E_FeeExceedsBPS(); } - if (_evc == address(0) || _synth == address(0) || _underlying == address(0)) { - revert E_ZeroAddress(); + if (_conversionPrice == 0) { + revert E_ZeroConversionPrice(); } synth = ESynth(_synth); underlying = IERC20(_underlying); - TO_UNDERLYING_FEE = toUnderlyingFeeBPS; - TO_SYNTH_FEE = toSynthFeeBPS; - conversionPrice = _conversionPrice; + TO_UNDERLYING_FEE = _toUnderlyingFeeBPS; + TO_SYNTH_FEE = _toSynthFeeBPS; + CONVERSION_PRICE = _conversionPrice; } /// @notice Swaps the given amount of synth to underlying given an input amount of synth. @@ -135,27 +144,33 @@ contract PegStabilityModule is EVCUtil { /// @param amountIn The amount of synth to swap. /// @return The amount of underlying received. function quoteToUnderlyingGivenIn(uint256 amountIn) public view returns (uint256) { - return amountIn * (BPS_SCALE - TO_UNDERLYING_FEE) * conversionPrice / BPS_SCALE / PRICE_SCALE; + return amountIn.mulDiv( + (BPS_SCALE - TO_UNDERLYING_FEE) * CONVERSION_PRICE, BPS_SCALE * PRICE_SCALE, Math.Rounding.Floor + ); } /// @notice Quotes the amount of underlying given an output amount of synth. /// @param amountOut The amount of underlying to receive. /// @return The amount of synth swapped. function quoteToUnderlyingGivenOut(uint256 amountOut) public view returns (uint256) { - return amountOut * BPS_SCALE * PRICE_SCALE / (BPS_SCALE - TO_UNDERLYING_FEE) / conversionPrice; + return amountOut.mulDiv( + (BPS_SCALE + TO_UNDERLYING_FEE) * PRICE_SCALE, BPS_SCALE * CONVERSION_PRICE, Math.Rounding.Ceil + ); } /// @notice Quotes the amount of synth given an input amount of underlying. /// @param amountIn The amount of underlying to swap. /// @return The amount of synth received. function quoteToSynthGivenIn(uint256 amountIn) public view returns (uint256) { - return amountIn * (BPS_SCALE - TO_SYNTH_FEE) * PRICE_SCALE / BPS_SCALE / conversionPrice; + return + amountIn.mulDiv((BPS_SCALE - TO_SYNTH_FEE) * PRICE_SCALE, BPS_SCALE * CONVERSION_PRICE, Math.Rounding.Floor); } /// @notice Quotes the amount of synth given an output amount of underlying. /// @param amountOut The amount of synth to receive. /// @return The amount of underlying swapped. function quoteToSynthGivenOut(uint256 amountOut) public view returns (uint256) { - return amountOut * BPS_SCALE * conversionPrice / (BPS_SCALE - TO_SYNTH_FEE) / PRICE_SCALE; + return + amountOut.mulDiv((BPS_SCALE + TO_SYNTH_FEE) * CONVERSION_PRICE, BPS_SCALE * PRICE_SCALE, Math.Rounding.Ceil); } } diff --git a/test/unit/pegStabilityModules/PSM.t.sol b/test/unit/pegStabilityModules/PSM.t.sol index 6bf3fb4d..f6443cb1 100644 --- a/test/unit/pegStabilityModules/PSM.t.sol +++ b/test/unit/pegStabilityModules/PSM.t.sol @@ -32,52 +32,73 @@ contract PSMTest is Test { // Deploy EVC evc = new EthereumVaultConnector(); - // Deploy underlying - underlying = new TestERC20("TestUnderlying", "TUNDERLYING", 18, false); - // Deploy synth vm.prank(owner); synth = new ESynth(evc, "TestSynth", "TSYNTH"); + // Deploy underlying + underlying = new TestERC20("TestUnderlying", "TUNDERLYING", 18, false); + // Deploy PSM vm.prank(owner); psm = new PegStabilityModule( address(evc), address(synth), address(underlying), TO_UNDERLYING_FEE, TO_SYNTH_FEE, CONVERSION_PRICE ); + } + + function fuzzSetUp( + uint8 underlyingDecimals, + uint256 _toUnderlyingFeeBPS, + uint256 _toSynthFeeBPS, + uint256 _conversionPrice + ) internal { + // Redeploy underlying + underlying = new TestERC20("TestUnderlying", "TUNDERLYING", underlyingDecimals, false); + + // Redeploy PSM + vm.prank(owner); + psm = new PegStabilityModule( + address(evc), address(synth), address(underlying), _toUnderlyingFeeBPS, _toSynthFeeBPS, _conversionPrice + ); // Give PSM and wallets some underlying - underlying.mint(address(psm), 100e18); - underlying.mint(wallet1, 100e18); - underlying.mint(wallet2, 100e18); + uint128 amount = uint128(100 * 10 ** underlyingDecimals); + underlying.mint(address(psm), amount); + underlying.mint(wallet1, amount); + underlying.mint(wallet2, amount); // Approve PSM to spend underlying vm.prank(wallet1); - underlying.approve(address(psm), 100e18); + underlying.approve(address(psm), type(uint256).max); vm.prank(wallet2); - underlying.approve(address(psm), 100e18); + underlying.approve(address(psm), type(uint256).max); // Set PSM as minter + amount = 100 * 10 ** 18; vm.prank(owner); - synth.setCapacity(address(psm), 100e18); + synth.setCapacity(address(psm), amount); // Mint some synth to wallets vm.startPrank(owner); - synth.setCapacity(owner, 200e18); - synth.mint(wallet1, 100e18); - synth.mint(wallet2, 100e18); + synth.setCapacity(owner, uint128(2 * amount)); + synth.mint(wallet1, amount); + synth.mint(wallet2, amount); vm.stopPrank(); // Set approvals for PSM vm.prank(wallet1); - synth.approve(address(psm), 100e18); + synth.approve(address(psm), type(uint256).max); vm.prank(wallet2); - synth.approve(address(psm), 100e18); + synth.approve(address(psm), type(uint256).max); } function testConstructor() public view { + assertEq(address(psm.EVC()), address(evc)); assertEq(address(psm.synth()), address(synth)); + assertEq(address(psm.underlying()), address(underlying)); assertEq(psm.TO_UNDERLYING_FEE(), TO_UNDERLYING_FEE); assertEq(psm.TO_SYNTH_FEE(), TO_SYNTH_FEE); + assertEq(psm.CONVERSION_PRICE(), CONVERSION_PRICE); } function testConstructorToUnderlyingFeeExceedsBPS() public { @@ -115,9 +136,21 @@ contract PSMTest is Test { ); } - function testSwapToUnderlyingGivenIn() public { - uint256 amountIn = 10e18; - uint256 expectedAmountOut = amountIn * (BPS_SCALE - TO_UNDERLYING_FEE) / BPS_SCALE; + function testConstructorZeroConversionPrice() public { + vm.expectRevert(PegStabilityModule.E_ZeroConversionPrice.selector); + new PegStabilityModule(address(evc), address(synth), address(underlying), TO_UNDERLYING_FEE, TO_SYNTH_FEE, 0); + } + + function testSwapToUnderlyingGivenIn(uint8 underlyingDecimals, uint256 fee, uint256 amountInNoDecimals) public { + underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18)); + fee = bound(fee, 0, BPS_SCALE - 1); + amountInNoDecimals = bound(amountInNoDecimals, 1, 100); + fuzzSetUp(underlyingDecimals, fee, 0, 10 ** underlyingDecimals); + + uint256 amountIn = amountInNoDecimals * 10 ** 18; + uint256 expectedAmountOut = amountInNoDecimals * 10 ** underlyingDecimals * (BPS_SCALE - fee) / BPS_SCALE; + + assertEq(psm.quoteToUnderlyingGivenIn(amountIn), expectedAmountOut); uint256 swapperSynthBalanceBefore = synth.balanceOf(wallet1); uint256 receiverBalanceBefore = underlying.balanceOf(wallet2); @@ -135,9 +168,16 @@ contract PSMTest is Test { assertEq(psmUnderlyingBalanceAfter, psmUnderlyingBalanceBefore - expectedAmountOut); } - function testSwapToUnderlyingGivenOut() public { - uint256 amountOut = 10e18; - uint256 expectedAmountIn = amountOut * BPS_SCALE / (BPS_SCALE - TO_UNDERLYING_FEE); + function testSwapToUnderlyingGivenOut(uint8 underlyingDecimals, uint256 fee, uint256 amountOutNoDecimals) public { + underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18)); + fee = bound(fee, 0, BPS_SCALE - 1); + amountOutNoDecimals = bound(amountOutNoDecimals, 1, 50); + fuzzSetUp(underlyingDecimals, fee, 0, 10 ** underlyingDecimals); + + uint256 amountOut = amountOutNoDecimals * 10 ** underlyingDecimals; + uint256 expectedAmountIn = amountOutNoDecimals * 10 ** 18 * (BPS_SCALE + fee) / BPS_SCALE; + + assertEq(psm.quoteToUnderlyingGivenOut(amountOut), expectedAmountIn); uint256 swapperSynthBalanceBefore = synth.balanceOf(wallet1); uint256 receiverBalanceBefore = underlying.balanceOf(wallet2); @@ -155,9 +195,16 @@ contract PSMTest is Test { assertEq(psmUnderlyingBalanceAfter, psmUnderlyingBalanceBefore - amountOut); } - function testSwapToSynthGivenIn() public { - uint256 amountIn = 10e18; - uint256 expectedAmountOut = amountIn * (BPS_SCALE - TO_SYNTH_FEE) / BPS_SCALE; + function testSwapToSynthGivenIn(uint8 underlyingDecimals, uint256 fee, uint256 amountInNoDecimals) public { + underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18)); + fee = bound(fee, 0, BPS_SCALE - 1); + amountInNoDecimals = bound(amountInNoDecimals, 1, 100); + fuzzSetUp(underlyingDecimals, 0, fee, 10 ** underlyingDecimals); + + uint256 amountIn = amountInNoDecimals * 10 ** underlyingDecimals; + uint256 expectedAmountOut = amountInNoDecimals * 10 ** 18 * (BPS_SCALE - fee) / BPS_SCALE; + + assertEq(psm.quoteToSynthGivenIn(amountIn), expectedAmountOut); uint256 swapperUnderlyingBalanceBefore = underlying.balanceOf(wallet1); uint256 receiverSynthBalanceBefore = synth.balanceOf(wallet2); @@ -175,9 +222,16 @@ contract PSMTest is Test { assertEq(psmUnderlyingBalanceAfter, psmUnderlyingBalanceBefore + amountIn); } - function testSwapToSynthGivenOut() public { - uint256 amountOut = 10e18; - uint256 expectedAmountIn = amountOut * BPS_SCALE / (BPS_SCALE - TO_SYNTH_FEE); + function testSwapToSynthGivenOut(uint8 underlyingDecimals, uint256 fee, uint256 amountOutNoDecimals) public { + underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18)); + fee = bound(fee, 0, BPS_SCALE - 1); + amountOutNoDecimals = bound(amountOutNoDecimals, 1, 50); + fuzzSetUp(underlyingDecimals, 0, fee, 10 ** underlyingDecimals); + + uint256 amountOut = amountOutNoDecimals * 10 ** 18; + uint256 expectedAmountIn = amountOutNoDecimals * 10 ** underlyingDecimals * (BPS_SCALE + fee) / BPS_SCALE; + + assertEq(psm.quoteToSynthGivenOut(amountOut), expectedAmountIn); uint256 swapperUnderlyingBalanceBefore = underlying.balanceOf(wallet1); uint256 receiverSynthBalanceBefore = synth.balanceOf(wallet2); @@ -195,66 +249,33 @@ contract PSMTest is Test { assertEq(psmUnderlyingBalanceAfter, psmUnderlyingBalanceBefore + expectedAmountIn); } - // Test quotes - function testQuoteToUnderlyingGivenIn() public view { - uint256 amountIn = 10e18; - uint256 expectedAmountOut = amountIn * (BPS_SCALE - TO_UNDERLYING_FEE) / BPS_SCALE; - - uint256 amountOut = psm.quoteToUnderlyingGivenIn(amountIn); - - assertEq(amountOut, expectedAmountOut); - } - - function testQuoteToUnderlyingGivenOut() public view { - uint256 amountOut = 10e18; - uint256 expectedAmountIn = amountOut * BPS_SCALE / (BPS_SCALE - TO_UNDERLYING_FEE); - - uint256 amountIn = psm.quoteToUnderlyingGivenOut(amountOut); - - assertEq(amountIn, expectedAmountIn); - } - - function testQuoteToSynthGivenIn() public view { - uint256 amountIn = 10e18; - uint256 expectedAmountOut = amountIn * (BPS_SCALE - TO_SYNTH_FEE) / BPS_SCALE; + function testSanityPriceConversions(uint8 underlyingDecimals, uint256 amount, uint256 multiplier) public { + underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18)); + amount = bound(amount, 1, 100); + multiplier = bound(multiplier, 1, 10000); + fuzzSetUp(underlyingDecimals, 0, 0, 10 ** underlyingDecimals * multiplier / 100); - uint256 amountOut = psm.quoteToSynthGivenIn(amountIn); + uint256 synthAmount = amount * 10 ** 18; + uint256 underlyingAmount = amount * 10 ** underlyingDecimals * multiplier / 100; - assertEq(amountOut, expectedAmountOut); + assertEq(psm.quoteToSynthGivenIn(underlyingAmount), synthAmount); + assertEq(psm.quoteToSynthGivenOut(synthAmount), underlyingAmount); + assertEq(psm.quoteToUnderlyingGivenIn(synthAmount), underlyingAmount); + assertEq(psm.quoteToUnderlyingGivenOut(underlyingAmount), synthAmount); } - function testQuoteToSynthGivenOut() public view { - uint256 amountOut = 10e18; - uint256 expectedAmountIn = amountOut * BPS_SCALE / (BPS_SCALE - TO_SYNTH_FEE); - - uint256 amountIn = psm.quoteToSynthGivenOut(amountOut); - - assertEq(amountIn, expectedAmountIn); - } - - function testSanityPriceConversionToSynth() public { - uint256 price = 0.25e18; - - uint256 synthAmount = 1e18; - uint256 underlyingAmount = 0.25e18; - - PegStabilityModule psmNoFee = - new PegStabilityModule(address(evc), address(synth), address(underlying), 0, 0, price); - - assertEq(psmNoFee.quoteToSynthGivenIn(underlyingAmount), synthAmount); - assertEq(psmNoFee.quoteToSynthGivenOut(synthAmount), underlyingAmount); + function testRoundingPriceConversionsEqualDecimals() public { + assertEq(psm.quoteToSynthGivenIn(1), 0); + assertEq(psm.quoteToSynthGivenOut(1), 2); + assertEq(psm.quoteToUnderlyingGivenIn(1), 0); + assertEq(psm.quoteToUnderlyingGivenOut(1), 2); } - function testSanityPriceConversionToUnderlying() public { - uint256 price = 0.25e18; - - uint256 synthAmount = 1e18; - uint256 underlyingAmount = 0.25e18; - - PegStabilityModule psmNoFee = - new PegStabilityModule(address(evc), address(synth), address(underlying), 0, 0, price); - - assertEq(psmNoFee.quoteToUnderlyingGivenIn(synthAmount), underlyingAmount); - assertEq(psmNoFee.quoteToUnderlyingGivenOut(underlyingAmount), synthAmount); + function testRoundingPriceConversionsDiffDecimals() public { + fuzzSetUp(8, 0, 0, 1e8); + assertEq(psm.quoteToSynthGivenIn(1), 1e10); + assertEq(psm.quoteToSynthGivenOut(1), 1); + assertEq(psm.quoteToUnderlyingGivenIn(1), 0); + assertEq(psm.quoteToUnderlyingGivenOut(1), 1e10); } } From 4ed81e72fc7e66c7a486b41963f1419e12711124 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Fri, 5 Jul 2024 10:31:50 +0100 Subject: [PATCH 73/76] test: improve --- test/unit/pegStabilityModules/PSM.t.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/unit/pegStabilityModules/PSM.t.sol b/test/unit/pegStabilityModules/PSM.t.sol index f6443cb1..499e090a 100644 --- a/test/unit/pegStabilityModules/PSM.t.sol +++ b/test/unit/pegStabilityModules/PSM.t.sol @@ -271,11 +271,13 @@ contract PSMTest is Test { assertEq(psm.quoteToUnderlyingGivenOut(1), 2); } - function testRoundingPriceConversionsDiffDecimals() public { - fuzzSetUp(8, 0, 0, 1e8); - assertEq(psm.quoteToSynthGivenIn(1), 1e10); + function testRoundingPriceConversionsDiffDecimals(uint8 underlyingDecimals) public { + underlyingDecimals = uint8(bound(underlyingDecimals, 6, 17)); + fuzzSetUp(underlyingDecimals, 0, 0, 10 ** underlyingDecimals); + + assertEq(psm.quoteToSynthGivenIn(1), 10 ** (18 - underlyingDecimals)); assertEq(psm.quoteToSynthGivenOut(1), 1); assertEq(psm.quoteToUnderlyingGivenIn(1), 0); - assertEq(psm.quoteToUnderlyingGivenOut(1), 1e10); + assertEq(psm.quoteToUnderlyingGivenOut(1), 10 ** (18 - underlyingDecimals)); } } From 96d81c824cfd4be0a4f17db8ab5cc1f7e08a8d9b Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Thu, 18 Jul 2024 08:35:20 +0200 Subject: [PATCH 74/76] fix: revert cantina 111 --- src/Synths/PegStabilityModule.sol | 4 ++-- test/unit/pegStabilityModules/PSM.t.sol | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Synths/PegStabilityModule.sol b/src/Synths/PegStabilityModule.sol index b2daec02..1e353085 100644 --- a/src/Synths/PegStabilityModule.sol +++ b/src/Synths/PegStabilityModule.sol @@ -154,7 +154,7 @@ contract PegStabilityModule is EVCUtil { /// @return The amount of synth swapped. function quoteToUnderlyingGivenOut(uint256 amountOut) public view returns (uint256) { return amountOut.mulDiv( - (BPS_SCALE + TO_UNDERLYING_FEE) * PRICE_SCALE, BPS_SCALE * CONVERSION_PRICE, Math.Rounding.Ceil + BPS_SCALE * PRICE_SCALE, (BPS_SCALE - TO_UNDERLYING_FEE) * CONVERSION_PRICE, Math.Rounding.Ceil ); } @@ -171,6 +171,6 @@ contract PegStabilityModule is EVCUtil { /// @return The amount of underlying swapped. function quoteToSynthGivenOut(uint256 amountOut) public view returns (uint256) { return - amountOut.mulDiv((BPS_SCALE + TO_SYNTH_FEE) * CONVERSION_PRICE, BPS_SCALE * PRICE_SCALE, Math.Rounding.Ceil); + amountOut.mulDiv(BPS_SCALE * CONVERSION_PRICE, (BPS_SCALE - TO_SYNTH_FEE) * PRICE_SCALE, Math.Rounding.Ceil); } } diff --git a/test/unit/pegStabilityModules/PSM.t.sol b/test/unit/pegStabilityModules/PSM.t.sol index 499e090a..e77e356f 100644 --- a/test/unit/pegStabilityModules/PSM.t.sol +++ b/test/unit/pegStabilityModules/PSM.t.sol @@ -62,7 +62,7 @@ contract PSMTest is Test { ); // Give PSM and wallets some underlying - uint128 amount = uint128(100 * 10 ** underlyingDecimals); + uint128 amount = uint128(1e6 * 10 ** underlyingDecimals); underlying.mint(address(psm), amount); underlying.mint(wallet1, amount); underlying.mint(wallet2, amount); @@ -74,7 +74,7 @@ contract PSMTest is Test { underlying.approve(address(psm), type(uint256).max); // Set PSM as minter - amount = 100 * 10 ** 18; + amount = 1e6 * 10 ** 18; vm.prank(owner); synth.setCapacity(address(psm), amount); @@ -171,11 +171,12 @@ contract PSMTest is Test { function testSwapToUnderlyingGivenOut(uint8 underlyingDecimals, uint256 fee, uint256 amountOutNoDecimals) public { underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18)); fee = bound(fee, 0, BPS_SCALE - 1); - amountOutNoDecimals = bound(amountOutNoDecimals, 1, 50); + amountOutNoDecimals = bound(amountOutNoDecimals, 1, 100); fuzzSetUp(underlyingDecimals, fee, 0, 10 ** underlyingDecimals); uint256 amountOut = amountOutNoDecimals * 10 ** underlyingDecimals; - uint256 expectedAmountIn = amountOutNoDecimals * 10 ** 18 * (BPS_SCALE + fee) / BPS_SCALE; + uint256 expectedAmountIn = + (amountOutNoDecimals * 10 ** 18 * BPS_SCALE + BPS_SCALE - fee - 1) / (BPS_SCALE - fee); assertEq(psm.quoteToUnderlyingGivenOut(amountOut), expectedAmountIn); @@ -225,11 +226,12 @@ contract PSMTest is Test { function testSwapToSynthGivenOut(uint8 underlyingDecimals, uint256 fee, uint256 amountOutNoDecimals) public { underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18)); fee = bound(fee, 0, BPS_SCALE - 1); - amountOutNoDecimals = bound(amountOutNoDecimals, 1, 50); + amountOutNoDecimals = bound(amountOutNoDecimals, 1, 100); fuzzSetUp(underlyingDecimals, 0, fee, 10 ** underlyingDecimals); uint256 amountOut = amountOutNoDecimals * 10 ** 18; - uint256 expectedAmountIn = amountOutNoDecimals * 10 ** underlyingDecimals * (BPS_SCALE + fee) / BPS_SCALE; + uint256 expectedAmountIn = + (amountOutNoDecimals * 10 ** underlyingDecimals * BPS_SCALE + BPS_SCALE - fee - 1) / (BPS_SCALE - fee); assertEq(psm.quoteToSynthGivenOut(amountOut), expectedAmountIn); From 3883206cc17cfb033596a4133e02fad7c988ffbf Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 7 Aug 2024 11:06:40 +0200 Subject: [PATCH 75/76] fix: natspec typos --- src/Synths/ERC20EVCCompatible.sol | 2 +- src/Synths/ESynth.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Synths/ERC20EVCCompatible.sol b/src/Synths/ERC20EVCCompatible.sol index 18e37062..c0b678c9 100644 --- a/src/Synths/ERC20EVCCompatible.sol +++ b/src/Synths/ERC20EVCCompatible.sol @@ -18,7 +18,7 @@ abstract contract ERC20EVCCompatible is EVCUtil, ERC20Permit { {} /// @notice Retrieves the message sender in the context of the EVC. - /// @dev Overriden due to the conflict with the Context definition. + /// @dev Overridden due to the conflict with the Context definition. /// @dev This function returns the account on behalf of which the current operation is being performed, which is /// either msg.sender or the account authenticated by the EVC. /// @return The address of the message sender. diff --git a/src/Synths/ESynth.sol b/src/Synths/ESynth.sol index 16c019b1..cece73cf 100644 --- a/src/Synths/ESynth.sol +++ b/src/Synths/ESynth.sol @@ -123,7 +123,7 @@ contract ESynth is ERC20EVCCompatible, Ownable { } /// @notice Retrieves the message sender in the context of the EVC. - /// @dev Overriden due to the conflict with the Context definition. + /// @dev Overridden due to the conflict with the Context definition. /// @dev This function returns the account on behalf of which the current operation is being performed, which is /// either msg.sender or the account authenticated by the EVC. /// @return msgSender The address of the message sender. @@ -166,7 +166,7 @@ contract ESynth is ERC20EVCCompatible, Ownable { } /// @notice Retrieves the total supply of the token. - /// @dev Overriden to exclude the ignored accounts from the total supply. + /// @dev Overridden to exclude the ignored accounts from the total supply. /// @return total Total supply of the token. function totalSupply() public view override returns (uint256 total) { total = super.totalSupply(); From 0262f3458a757ecb9e1584d62ad0d2e6ae9f65a0 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 7 Aug 2024 11:07:34 +0200 Subject: [PATCH 76/76] fix: whitepaper typo --- docs/whitepaper.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/whitepaper.md b/docs/whitepaper.md index 3e970df3..4d732dc8 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -560,7 +560,7 @@ In essence, a liquidation is equivalent to a stop-loss order. As long as you set **This section is still a work-in-progress and is subject to change** -Since the EVK is a *kit*, it attempts to be maximally flexible and doesn't enforce policy decisions on vault creators. This means that it is possible to create vaults with insecure or malicious configurations. Furthermore, an otherwise secure vault may be insecure because it accapts an [insecure collateral as collateral](#untrusted-collaterals) (or a collateral vault itself accepts insecure collateral, etc, recursively). +Since the EVK is a *kit*, it attempts to be maximally flexible and doesn't enforce policy decisions on vault creators. This means that it is possible to create vaults with insecure or malicious configurations. Furthermore, an otherwise secure vault may be insecure because it accepts an [insecure collateral as collateral](#untrusted-collaterals) (or a collateral vault itself accepts insecure collateral, etc, recursively). Perspectives provide a mechanism for validating properties of a vault using on-chain verifiable logic. A perspective is any contract that implements the following interface: