From 299c8817103574d7f5534846eb6da459fbaac63c Mon Sep 17 00:00:00 2001 From: Philip Paetz Date: Thu, 21 Aug 2025 15:25:48 +0200 Subject: [PATCH 01/79] feat: make gas token more flexible --- contracts/src/AddressesRegistry.sol | 8 ++++---- contracts/src/BorrowerOperations.sol | 8 ++++---- contracts/src/GasPool.sol | 6 +++--- contracts/src/Interfaces/IAddressesRegistry.sol | 4 ++-- contracts/src/TroveManager.sol | 8 ++++---- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/contracts/src/AddressesRegistry.sol b/contracts/src/AddressesRegistry.sol index 3a6a65063..b2e7e2f03 100644 --- a/contracts/src/AddressesRegistry.sol +++ b/contracts/src/AddressesRegistry.sol @@ -24,7 +24,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { IMultiTroveGetter public multiTroveGetter; ICollateralRegistry public collateralRegistry; IBoldToken public boldToken; - IWETH public WETH; + IERC20Metadata public gasToken; // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied uint256 public immutable CCR; @@ -66,7 +66,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { event MultiTroveGetterAddressChanged(address _multiTroveGetterAddress); event CollateralRegistryAddressChanged(address _collateralRegistryAddress); event BoldTokenAddressChanged(address _boldTokenAddress); - event WETHAddressChanged(address _wethAddress); + event GasTokenAddressChanged(address _gasTokenAddress); constructor( address _owner, @@ -111,7 +111,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { multiTroveGetter = _vars.multiTroveGetter; collateralRegistry = _vars.collateralRegistry; boldToken = _vars.boldToken; - WETH = _vars.WETH; + gasToken = _vars.gasToken; emit CollTokenAddressChanged(address(_vars.collToken)); emit BorrowerOperationsAddressChanged(address(_vars.borrowerOperations)); @@ -130,7 +130,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { emit MultiTroveGetterAddressChanged(address(_vars.multiTroveGetter)); emit CollateralRegistryAddressChanged(address(_vars.collateralRegistry)); emit BoldTokenAddressChanged(address(_vars.boldToken)); - emit WETHAddressChanged(address(_vars.WETH)); + emit GasTokenAddressChanged(address(_vars.gasToken)); _renounceOwnership(); } diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 85e771fd2..fa2d0e48d 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -28,7 +28,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // A doubly linked list of Troves, sorted by their collateral ratios ISortedTroves internal sortedTroves; // Wrapped ETH for liquidation reserve (gas compensation) - IWETH internal immutable WETH; + IERC20Metadata internal immutable gasToken; // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied uint256 public immutable CCR; @@ -173,7 +173,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio collToken = _addressesRegistry.collToken(); - WETH = _addressesRegistry.WETH(); + gasToken = _addressesRegistry.gasToken(); CCR = _addressesRegistry.CCR(); SCR = _addressesRegistry.SCR(); @@ -373,7 +373,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // Mint the requested _boldAmount to the borrower and mint the gas comp to the GasPool vars.boldToken.mint(msg.sender, _boldAmount); - WETH.transferFrom(msg.sender, gasPoolAddress, ETH_GAS_COMPENSATION); + gasToken.transferFrom(msg.sender, gasPoolAddress, ETH_GAS_COMPENSATION); return vars.troveId; } @@ -738,7 +738,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio activePoolCached.mintAggInterestAndAccountForTroveChange(troveChange, batchManager); // Return ETH gas compensation - WETH.transferFrom(gasPoolAddress, receiver, ETH_GAS_COMPENSATION); + gasToken.transferFrom(gasPoolAddress, receiver, ETH_GAS_COMPENSATION); // Burn the remainder of the Trove's entire debt from the user boldTokenCached.burn(msg.sender, trove.entireDebt); diff --git a/contracts/src/GasPool.sol b/contracts/src/GasPool.sol index cadd7a6dc..917bb5f95 100644 --- a/contracts/src/GasPool.sol +++ b/contracts/src/GasPool.sol @@ -17,13 +17,13 @@ import "./Interfaces/ITroveManager.sol"; */ contract GasPool { constructor(IAddressesRegistry _addressesRegistry) { - IWETH WETH = _addressesRegistry.WETH(); + IERC20Metadata gasToken = _addressesRegistry.gasToken(); IBorrowerOperations borrowerOperations = _addressesRegistry.borrowerOperations(); ITroveManager troveManager = _addressesRegistry.troveManager(); // Allow BorrowerOperations to refund gas compensation - WETH.approve(address(borrowerOperations), type(uint256).max); + gasToken.approve(address(borrowerOperations), type(uint256).max); // Allow TroveManager to pay gas compensation to liquidator - WETH.approve(address(troveManager), type(uint256).max); + gasToken.approve(address(troveManager), type(uint256).max); } } diff --git a/contracts/src/Interfaces/IAddressesRegistry.sol b/contracts/src/Interfaces/IAddressesRegistry.sol index 5d2f45faf..ff1172603 100644 --- a/contracts/src/Interfaces/IAddressesRegistry.sol +++ b/contracts/src/Interfaces/IAddressesRegistry.sol @@ -37,7 +37,7 @@ interface IAddressesRegistry { IMultiTroveGetter multiTroveGetter; ICollateralRegistry collateralRegistry; IBoldToken boldToken; - IWETH WETH; + IERC20Metadata gasToken; } function CCR() external returns (uint256); @@ -64,7 +64,7 @@ interface IAddressesRegistry { function multiTroveGetter() external view returns (IMultiTroveGetter); function collateralRegistry() external view returns (ICollateralRegistry); function boldToken() external view returns (IBoldToken); - function WETH() external returns (IWETH); + function gasToken() external view returns (IERC20Metadata); function setAddresses(AddressVars memory _vars) external; } diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 389586974..9bd74cbb3 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -26,8 +26,8 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // A doubly linked list of Troves, sorted by their interest rate ISortedTroves public sortedTroves; ICollateralRegistry internal collateralRegistry; - // Wrapped ETH for liquidation reserve (gas compensation) - IWETH internal immutable WETH; + // Gas token for liquidation reserve (gas compensation) + IERC20Metadata internal immutable gasToken; // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied uint256 public immutable CCR; @@ -200,7 +200,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { collSurplusPool = _addressesRegistry.collSurplusPool(); boldToken = _addressesRegistry.boldToken(); sortedTroves = _addressesRegistry.sortedTroves(); - WETH = _addressesRegistry.WETH(); + gasToken = _addressesRegistry.gasToken(); collateralRegistry = _addressesRegistry.collateralRegistry(); emit TroveNFTAddressChanged(address(troveNFT)); @@ -536,7 +536,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { function _sendGasCompensation(IActivePool _activePool, address _liquidator, uint256 _eth, uint256 _coll) internal { if (_eth > 0) { - WETH.transferFrom(gasPoolAddress, _liquidator, _eth); + gasToken.transferFrom(gasPoolAddress, _liquidator, _eth); } if (_coll > 0) { From 412c3edb7a2dc08dba4884a72bb3dcdc34d99836 Mon Sep 17 00:00:00 2001 From: Philip Paetz Date: Thu, 21 Aug 2025 15:25:58 +0200 Subject: [PATCH 02/79] feat: make stability pool upgradable --- .../src/Dependencies/LiquityBaseInit.sol | 73 +++++++++++++++++++ contracts/src/StabilityPool.sol | 25 +++++-- 2 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 contracts/src/Dependencies/LiquityBaseInit.sol diff --git a/contracts/src/Dependencies/LiquityBaseInit.sol b/contracts/src/Dependencies/LiquityBaseInit.sol new file mode 100644 index 000000000..a645d4931 --- /dev/null +++ b/contracts/src/Dependencies/LiquityBaseInit.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.24; + +import "./Constants.sol"; +import "./LiquityMath.sol"; +import "../Interfaces/IAddressesRegistry.sol"; +import "../Interfaces/IActivePool.sol"; +import "../Interfaces/IDefaultPool.sol"; +import "../Interfaces/IPriceFeed.sol"; +import "../Interfaces/ILiquityBase.sol"; +import "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; + +/* + * Base contract for TroveManager, BorrowerOperations and StabilityPool. Contains global system constants and + * common functions. + */ +contract LiquityBaseInit is Initializable, ILiquityBase { + IActivePool public activePool; + IDefaultPool internal defaultPool; + IPriceFeed internal priceFeed; + + event ActivePoolAddressChanged(address _newActivePoolAddress); + event DefaultPoolAddressChanged(address _newDefaultPoolAddress); + event PriceFeedAddressChanged(address _newPriceFeedAddress); + + function __LiquityBase_init(IAddressesRegistry _addressesRegistry) internal onlyInitializing { + activePool = _addressesRegistry.activePool(); + defaultPool = _addressesRegistry.defaultPool(); + priceFeed = _addressesRegistry.priceFeed(); + + emit ActivePoolAddressChanged(address(activePool)); + emit DefaultPoolAddressChanged(address(defaultPool)); + emit PriceFeedAddressChanged(address(priceFeed)); + } + + // --- Gas compensation functions --- + + function getEntireBranchColl() public view returns (uint256 entireSystemColl) { + uint256 activeColl = activePool.getCollBalance(); + uint256 liquidatedColl = defaultPool.getCollBalance(); + + return activeColl + liquidatedColl; + } + + function getEntireBranchDebt() public view returns (uint256 entireSystemDebt) { + uint256 activeDebt = activePool.getBoldDebt(); + uint256 closedDebt = defaultPool.getBoldDebt(); + + return activeDebt + closedDebt; + } + + function _getTCR(uint256 _price) internal view returns (uint256 TCR) { + uint256 entireSystemColl = getEntireBranchColl(); + uint256 entireSystemDebt = getEntireBranchDebt(); + + TCR = LiquityMath._computeCR(entireSystemColl, entireSystemDebt, _price); + + return TCR; + } + + function _checkBelowCriticalThreshold(uint256 _price, uint256 _CCR) internal view returns (bool) { + uint256 TCR = _getTCR(_price); + + return TCR < _CCR; + } + + function _calcInterest(uint256 _weightedDebt, uint256 _period) internal pure returns (uint256) { + return (_weightedDebt * _period) / ONE_YEAR / DECIMAL_PRECISION; + } + + uint256[47] private __gap; +} diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 8dd1668c9..f0b590a7e 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -9,7 +9,7 @@ import "./Interfaces/IAddressesRegistry.sol"; import "./Interfaces/IStabilityPoolEvents.sol"; import "./Interfaces/ITroveManager.sol"; import "./Interfaces/IBoldToken.sol"; -import "./Dependencies/LiquityBase.sol"; +import "./Dependencies/LiquityBaseInit.sol"; /* * The Stability Pool holds Bold tokens deposited by Stability Pool depositors. @@ -115,14 +115,14 @@ import "./Dependencies/LiquityBase.sol"; * * */ -contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { +contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabilityPoolEvents { using SafeERC20 for IERC20; string public constant NAME = "StabilityPool"; - IERC20 public immutable collToken; - ITroveManager public immutable troveManager; - IBoldToken public immutable boldToken; + IERC20 public collToken; + ITroveManager public troveManager; + IBoldToken public boldToken; uint256 internal collBalance; // deposited coll tracker @@ -189,7 +189,20 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { event TroveManagerAddressChanged(address _newTroveManagerAddress); event BoldTokenAddressChanged(address _newBoldTokenAddress); - constructor(IAddressesRegistry _addressesRegistry) LiquityBase(_addressesRegistry) { + /** + * @dev Should be called with disable=true in deployments when it's accessed through a Proxy. + * Call this with disable=false during testing, when used without a proxy. + */ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(bool disable) { + if (disable) { + _disableInitializers(); + } + } + + function initialize(IAddressesRegistry _addressesRegistry) external initializer { + __LiquityBase_init(_addressesRegistry); + collToken = _addressesRegistry.collToken(); troveManager = _addressesRegistry.troveManager(); boldToken = _addressesRegistry.boldToken(); From 06912e4d961b8aa557117caa530e94ade652f443 Mon Sep 17 00:00:00 2001 From: Philip Paetz Date: Thu, 21 Aug 2025 17:02:44 +0200 Subject: [PATCH 03/79] feat: fix tests and remove unneeded files --- .gitmodules | 3 + contracts/foundry.lock | 17 + contracts/foundry.toml | 4 + .../lib/openzeppelin-contracts-upgradeable | 1 + contracts/remappings.txt | 1 + .../script/Dependencies/GovernanceProxy.sol | 280 --- contracts/script/DeployGovernance.s.sol | 231 -- contracts/script/DeployLiquity2.s.sol | 1126 ++------- .../script/DeployOnlyExchangeHelpers.s.sol | 55 - contracts/script/DeploySomeCurvePools.s.sol | 56 - contracts/script/GenerateStakingRewards.s.sol | 83 - .../script/Interfaces/Balancer/IVault.sol | 15 - .../Interfaces/Balancer/IWeightedPool.sol | 27 - contracts/script/LiquidateTrove.s.sol | 48 - contracts/script/OpenTroves.s.sol | 178 -- contracts/script/ProvideCurveLiquidity.s.sol | 22 - contracts/script/ProvideUniV3Liquidity.s.sol | 149 -- contracts/script/RedeemCollateral.s.sol | 56 - contracts/script/RedeployWETHZappers.s.sol | 40 - contracts/script/bold-vanity.ts | 45 - contracts/script/coverage.sh | 31 - contracts/script/governance.ts | 193 -- contracts/src/HintHelpers.sol | 5 + contracts/src/Interfaces/IFPMMFactory.sol | 208 ++ contracts/src/Interfaces/IStabilityPool.sol | 3 + contracts/src/Interfaces/IStableTokenV3.sol | 212 ++ contracts/src/MultiTroveGetter.sol | 3 + contracts/src/TroveNFT.sol | 2 + contracts/src/Zappers/BaseZapper.sol | 72 - contracts/src/Zappers/GasCompZapper.sol | 324 --- .../src/Zappers/Interfaces/IExchange.sol | 9 - .../Zappers/Interfaces/IExchangeHelpers.sol | 11 - .../Zappers/Interfaces/IFlashLoanProvider.sol | 20 - .../Zappers/Interfaces/IFlashLoanReceiver.sol | 25 - .../Zappers/Interfaces/ILeverageZapper.sol | 44 - contracts/src/Zappers/Interfaces/IZapper.sol | 39 - contracts/src/Zappers/LeftoversSweep.sol | 63 - contracts/src/Zappers/LeverageLSTZapper.sol | 206 -- contracts/src/Zappers/LeverageWETHZapper.sol | 201 -- .../Modules/Exchanges/Curve/ICurveFactory.sol | 23 - .../Modules/Exchanges/Curve/ICurvePool.sol | 10 - .../Curve/ICurveStableswapNGFactory.sol | 32 - .../Curve/ICurveStableswapNGPool.sol | 10 - .../Modules/Exchanges/CurveExchange.sol | 65 - .../Exchanges/HybridCurveUniV3Exchange.sol | 154 -- .../HybridCurveUniV3ExchangeHelpers.sol | 98 - .../Modules/Exchanges/UniV3Exchange.sol | 96 - .../UniswapV3/INonfungiblePositionManager.sol | 157 -- .../Exchanges/UniswapV3/IPoolInitializer.sol | 20 - .../Modules/Exchanges/UniswapV3/IQuoterV2.sol | 88 - .../Exchanges/UniswapV3/ISwapRouter.sol | 65 - .../Exchanges/UniswapV3/IUniswapV3Factory.sol | 64 - .../Exchanges/UniswapV3/IUniswapV3Pool.sol | 271 --- .../Exchanges/UniswapV3/UniPriceConverter.sol | 31 - .../Balancer/vault/IFlashLoanRecipient.sol | 37 - .../FlashLoans/Balancer/vault/IVault.sol | 56 - .../Modules/FlashLoans/BalancerFlashLoan.sol | 120 - contracts/src/Zappers/WETHZapper.sol | 319 --- contracts/src/tokens/StableTokenV3.sol | 295 +++ .../tokens/patched/ERC20PermitUpgradeable.sol | 118 + .../src/tokens/patched/ERC20Upgradeable.sol | 394 ++++ contracts/src/tokens/patched/README.md | 75 + contracts/test/AnchoredInvariantsTest.t.sol | 2 +- contracts/test/AnchoredSPInvariantsTest.t.sol | 1834 +++++++-------- contracts/test/E2E.t.sol | 468 ---- contracts/test/InitiativeSmarDEX.t.sol | 81 - contracts/test/Invariants.t.sol | 2 +- contracts/test/SPInvariants.t.sol | 2 +- .../TestContracts/BaseMultiCollateralTest.sol | 2 + contracts/test/TestContracts/BaseTest.sol | 7 - contracts/test/TestContracts/Deployment.t.sol | 356 +-- contracts/test/TestContracts/DevTestSetup.sol | 7 +- contracts/test/Utils/E2EHelpers.sol | 339 --- contracts/test/Utils/UniPriceConverterLog.sol | 46 - contracts/test/Utils/UseDeployment.sol | 15 +- contracts/test/liquidationsLST.t.sol | 2 +- contracts/test/multicollateral.t.sol | 4 +- contracts/test/shutdown.t.sol | 2 +- contracts/test/stabilityPool.t.sol | 40 +- contracts/test/troveNFT.t.sol | 2 +- contracts/test/zapperGasComp.t.sol | 888 ------- contracts/test/zapperLeverage.t.sol | 2064 ----------------- contracts/test/zapperWETH.t.sol | 932 -------- 83 files changed, 2457 insertions(+), 11344 deletions(-) create mode 100644 contracts/foundry.lock create mode 160000 contracts/lib/openzeppelin-contracts-upgradeable delete mode 100644 contracts/script/Dependencies/GovernanceProxy.sol delete mode 100644 contracts/script/DeployGovernance.s.sol delete mode 100644 contracts/script/DeployOnlyExchangeHelpers.s.sol delete mode 100644 contracts/script/DeploySomeCurvePools.s.sol delete mode 100644 contracts/script/GenerateStakingRewards.s.sol delete mode 100644 contracts/script/Interfaces/Balancer/IVault.sol delete mode 100644 contracts/script/Interfaces/Balancer/IWeightedPool.sol delete mode 100644 contracts/script/LiquidateTrove.s.sol delete mode 100644 contracts/script/OpenTroves.s.sol delete mode 100644 contracts/script/ProvideCurveLiquidity.s.sol delete mode 100644 contracts/script/ProvideUniV3Liquidity.s.sol delete mode 100644 contracts/script/RedeemCollateral.s.sol delete mode 100644 contracts/script/RedeployWETHZappers.s.sol delete mode 100644 contracts/script/bold-vanity.ts delete mode 100755 contracts/script/coverage.sh delete mode 100644 contracts/script/governance.ts create mode 100644 contracts/src/Interfaces/IFPMMFactory.sol create mode 100644 contracts/src/Interfaces/IStableTokenV3.sol delete mode 100644 contracts/src/Zappers/BaseZapper.sol delete mode 100644 contracts/src/Zappers/GasCompZapper.sol delete mode 100644 contracts/src/Zappers/Interfaces/IExchange.sol delete mode 100644 contracts/src/Zappers/Interfaces/IExchangeHelpers.sol delete mode 100644 contracts/src/Zappers/Interfaces/IFlashLoanProvider.sol delete mode 100644 contracts/src/Zappers/Interfaces/IFlashLoanReceiver.sol delete mode 100644 contracts/src/Zappers/Interfaces/ILeverageZapper.sol delete mode 100644 contracts/src/Zappers/Interfaces/IZapper.sol delete mode 100644 contracts/src/Zappers/LeftoversSweep.sol delete mode 100644 contracts/src/Zappers/LeverageLSTZapper.sol delete mode 100644 contracts/src/Zappers/LeverageWETHZapper.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/Curve/ICurveFactory.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/Curve/ICurvePool.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGFactory.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGPool.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/CurveExchange.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3Exchange.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/UniV3Exchange.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/UniswapV3/IPoolInitializer.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Factory.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Pool.sol delete mode 100644 contracts/src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol delete mode 100644 contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IFlashLoanRecipient.sol delete mode 100644 contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IVault.sol delete mode 100644 contracts/src/Zappers/Modules/FlashLoans/BalancerFlashLoan.sol delete mode 100644 contracts/src/Zappers/WETHZapper.sol create mode 100644 contracts/src/tokens/StableTokenV3.sol create mode 100644 contracts/src/tokens/patched/ERC20PermitUpgradeable.sol create mode 100644 contracts/src/tokens/patched/ERC20Upgradeable.sol create mode 100644 contracts/src/tokens/patched/README.md delete mode 100644 contracts/test/E2E.t.sol delete mode 100644 contracts/test/InitiativeSmarDEX.t.sol delete mode 100644 contracts/test/Utils/E2EHelpers.sol delete mode 100644 contracts/test/Utils/UniPriceConverterLog.sol delete mode 100644 contracts/test/zapperGasComp.t.sol delete mode 100644 contracts/test/zapperLeverage.t.sol delete mode 100644 contracts/test/zapperWETH.t.sol diff --git a/.gitmodules b/.gitmodules index 5ad0fdc56..e2289312d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,6 +7,9 @@ [submodule "contracts/lib/Solady"] path = contracts/lib/Solady url = https://github.com/Vectorized/Solady +[submodule "contracts/lib/openzeppelin-contracts-upgradeable"] + path = contracts/lib/openzeppelin-contracts-upgradeable + url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable [submodule "contracts/lib/V2-gov"] path = contracts/lib/V2-gov url = https://github.com/liquity/V2-gov diff --git a/contracts/foundry.lock b/contracts/foundry.lock new file mode 100644 index 000000000..42a7865fe --- /dev/null +++ b/contracts/foundry.lock @@ -0,0 +1,17 @@ +{ + "lib/forge-std": { + "rev": "726a6ee5fc8427a0013d6f624e486c9130c0e336" + }, + "lib/Solady": { + "rev": "362b2efd20f38aea7252b391e5e016633ff79641" + }, + "lib/openzeppelin-contracts-upgradeable": { + "rev": "58fa0f81c4036f1a3b616fdffad2fd27e5d5ce21" + }, + "lib/openzeppelin-contracts": { + "rev": "bd325d56b4c62c9c5c1aff048c37c6bb18ac0290" + }, + "lib/V2-gov": { + "rev": "e7ed5341f2f54fb9bf89497a7be294c61f21ebe3" + } +} \ No newline at end of file diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 950446267..652e3a5f2 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -37,3 +37,7 @@ broadcast = 'broadcast-e2e' no_storage_caching = true # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options + +[lint] +# Excludes info/notes from 'forge build' and 'forge lint' output per default as it's quite noisy +severity = ["high", "med", "low"] \ No newline at end of file diff --git a/contracts/lib/openzeppelin-contracts-upgradeable b/contracts/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 000000000..58fa0f81c --- /dev/null +++ b/contracts/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 58fa0f81c4036f1a3b616fdffad2fd27e5d5ce21 diff --git a/contracts/remappings.txt b/contracts/remappings.txt index c17beb9f8..82ab48fc7 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -1 +1,2 @@ openzeppelin/=lib/V2-gov/lib/openzeppelin-contracts/ +openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/ diff --git a/contracts/script/Dependencies/GovernanceProxy.sol b/contracts/script/Dependencies/GovernanceProxy.sol deleted file mode 100644 index 558eac14d..000000000 --- a/contracts/script/Dependencies/GovernanceProxy.sol +++ /dev/null @@ -1,280 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {IERC20} from "openzeppelin/contracts/interfaces/IERC20.sol"; -import {IGovernance} from "V2-gov/src/interfaces/IGovernance.sol"; -import {ILQTYStaking} from "V2-gov/src/interfaces/ILQTYStaking.sol"; -import {IMultiDelegateCall} from "V2-gov/src/interfaces/IMultiDelegateCall.sol"; -import {IUserProxyFactory} from "V2-gov/src/interfaces/IUserProxyFactory.sol"; -import {PermitParams} from "V2-gov/src/utils/Types.sol"; -import {Governance} from "V2-gov/src/Governance.sol"; - -contract GovernanceProxy is IGovernance, IUserProxyFactory, IMultiDelegateCall { - Governance public immutable governance; - IERC20 public immutable lqty; - IERC20 public immutable bold; - - constructor(Governance _governance) { - governance = _governance; - lqty = _governance.lqty(); - bold = _governance.bold(); - - address userProxy = _governance.deriveUserProxyAddress(address(this)); - lqty.approve(userProxy, type(uint256).max); - bold.approve(address(_governance), type(uint256).max); - } - - function registerInitialInitiatives(address[] memory) external pure override { - revert("GovernanceProxy: not-implemented"); - } - - function stakingV1() external view override returns (ILQTYStaking) { - return governance.stakingV1(); - } - - function EPOCH_START() external view override returns (uint256) { - return governance.EPOCH_START(); - } - - function EPOCH_DURATION() external view override returns (uint256) { - return governance.EPOCH_DURATION(); - } - - function EPOCH_VOTING_CUTOFF() external view override returns (uint256) { - return governance.EPOCH_VOTING_CUTOFF(); - } - - function MIN_CLAIM() external view override returns (uint256) { - return governance.MIN_CLAIM(); - } - - function MIN_ACCRUAL() external view override returns (uint256) { - return governance.MIN_ACCRUAL(); - } - - function REGISTRATION_FEE() external view override returns (uint256) { - return governance.REGISTRATION_FEE(); - } - - function REGISTRATION_THRESHOLD_FACTOR() external view override returns (uint256) { - return governance.REGISTRATION_THRESHOLD_FACTOR(); - } - - function UNREGISTRATION_THRESHOLD_FACTOR() external view override returns (uint256) { - return governance.UNREGISTRATION_THRESHOLD_FACTOR(); - } - - function UNREGISTRATION_AFTER_EPOCHS() external view override returns (uint256) { - return governance.UNREGISTRATION_AFTER_EPOCHS(); - } - - function VOTING_THRESHOLD_FACTOR() external view override returns (uint256) { - return governance.VOTING_THRESHOLD_FACTOR(); - } - - function boldAccrued() external view override returns (uint256) { - return governance.boldAccrued(); - } - - function votesSnapshot() external view override returns (uint256 votes, uint256 forEpoch) { - return governance.votesSnapshot(); - } - - function votesForInitiativeSnapshot(address _initiative) - external - view - override - returns (uint256 votes, uint256 forEpoch, uint256 lastCountedEpoch, uint256 vetos) - { - return governance.votesForInitiativeSnapshot(_initiative); - } - - function userStates(address _user) - external - view - override - returns (uint256 unallocatedLQTY, uint256 unallocatedOffset, uint256 allocatedLQTY, uint256 allocatedOffset) - { - return governance.userStates(_user); - } - - function initiativeStates(address _initiative) - external - view - override - returns (uint256 voteLQTY, uint256 voteOffset, uint256 vetoLQTY, uint256 vetoOffset, uint256 lastEpochClaim) - { - return governance.initiativeStates(_initiative); - } - - function globalState() external view override returns (uint256 countedVoteLQTY, uint256 countedVoteOffset) { - return governance.globalState(); - } - - function lqtyAllocatedByUserToInitiative(address _user, address _initiative) - external - view - override - returns (uint256 voteLQTY, uint256 voteOffset, uint256 vetoLQTY, uint256 vetoOffset, uint256 atEpoch) - { - return governance.lqtyAllocatedByUserToInitiative(_user, _initiative); - } - - function registeredInitiatives(address _initiative) external view override returns (uint256 atEpoch) { - return governance.registeredInitiatives(_initiative); - } - - function depositLQTY(uint256 _lqtyAmount) external override { - governance.depositLQTY(_lqtyAmount); - } - - function depositLQTY(uint256 _lqtyAmount, bool _doSendRewards, address _recipient) external override { - governance.depositLQTY(_lqtyAmount, _doSendRewards, _recipient); - } - - function depositLQTYViaPermit(uint256, PermitParams calldata) external pure override { - revert("GovernanceProxy: not-implemented"); - } - - function depositLQTYViaPermit(uint256, PermitParams calldata, bool, address) external pure override { - revert("GovernanceProxy: not-implemented"); - } - - function withdrawLQTY(uint256 _lqtyAmount) external override { - governance.withdrawLQTY(_lqtyAmount); - } - - function withdrawLQTY(uint256 _lqtyAmount, bool _doSendRewards, address _recipient) external override { - governance.withdrawLQTY(_lqtyAmount, _doSendRewards, _recipient); - } - - function claimFromStakingV1(address _rewardRecipient) - external - override - returns (uint256 lusdSent, uint256 ethSent) - { - return governance.claimFromStakingV1(_rewardRecipient); - } - - function epoch() external view override returns (uint256) { - return governance.epoch(); - } - - function epochStart() external view override returns (uint256) { - return governance.epochStart(); - } - - function secondsWithinEpoch() external view override returns (uint256) { - return governance.secondsWithinEpoch(); - } - - function lqtyToVotes(uint256 _lqtyAmount, uint256 _timestamp, uint256 _offset) - external - pure - override - returns (uint256) - { - uint256 prod = _lqtyAmount * _timestamp; - return prod > _offset ? prod - _offset : 0; - } - - function calculateVotingThreshold() external override returns (uint256) { - return governance.calculateVotingThreshold(); - } - - function calculateVotingThreshold(uint256 _votes) external view override returns (uint256) { - return governance.calculateVotingThreshold(_votes); - } - - function getTotalVotesAndState() - external - view - override - returns (VoteSnapshot memory snapshot, GlobalState memory state, bool shouldUpdate) - { - return governance.getTotalVotesAndState(); - } - - function getInitiativeSnapshotAndState(address _initiative) - external - view - override - returns ( - InitiativeVoteSnapshot memory initiativeSnapshot, - InitiativeState memory initiativeState, - bool shouldUpdate - ) - { - return governance.getInitiativeSnapshotAndState(_initiative); - } - - function getLatestVotingThreshold() external view override returns (uint256) { - return governance.getLatestVotingThreshold(); - } - - function snapshotVotesForInitiative(address _initiative) - external - override - returns (VoteSnapshot memory voteSnapshot, InitiativeVoteSnapshot memory initiativeVoteSnapshot) - { - return governance.snapshotVotesForInitiative(_initiative); - } - - function getInitiativeState(address _initiative) - external - override - returns (InitiativeStatus status, uint256 lastEpochClaim, uint256 claimableAmount) - { - return governance.getInitiativeState(_initiative); - } - - function getInitiativeState( - address _initiative, - VoteSnapshot memory _votesSnapshot, - InitiativeVoteSnapshot memory _votesForInitiativeSnapshot, - InitiativeState memory _initiativeState - ) external view override returns (InitiativeStatus status, uint256 lastEpochClaim, uint256 claimableAmount) { - return governance.getInitiativeState(_initiative, _votesSnapshot, _votesForInitiativeSnapshot, _initiativeState); - } - - function registerInitiative(address _initiative) external override { - governance.registerInitiative(_initiative); - } - - function unregisterInitiative(address _initiative) external override { - governance.unregisterInitiative(_initiative); - } - - function allocateLQTY( - address[] calldata _resetInitiatives, - address[] memory _initiatives, - int256[] memory _absoluteLQTYVotes, - int256[] memory absoluteLQTYVetos - ) external override { - governance.allocateLQTY(_resetInitiatives, _initiatives, _absoluteLQTYVotes, absoluteLQTYVetos); - } - - function resetAllocations(address[] calldata _initiativesToReset, bool _checkAll) external { - governance.resetAllocations(_initiativesToReset, _checkAll); - } - - function claimForInitiative(address _initiative) external override returns (uint256 claimed) { - return governance.claimForInitiative(_initiative); - } - - function userProxyImplementation() external view override returns (address) { - return governance.userProxyImplementation(); - } - - function deriveUserProxyAddress(address _user) external view override returns (address) { - return governance.deriveUserProxyAddress(_user); - } - - function deployUserProxy() external override returns (address userProxyAddress) { - return governance.deployUserProxy(); - } - - function multiDelegateCall(bytes[] calldata inputs) external override returns (bytes[] memory returnValues) { - return governance.multiDelegateCall(inputs); - } -} diff --git a/contracts/script/DeployGovernance.s.sol b/contracts/script/DeployGovernance.s.sol deleted file mode 100644 index 88ffa4701..000000000 --- a/contracts/script/DeployGovernance.s.sol +++ /dev/null @@ -1,231 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.24; - -import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; - -import {Script} from "forge-std/Script.sol"; - -import {ICurveStableSwapFactoryNG} from "test/Interfaces/Curve/ICurveStableSwapFactoryNG.sol"; -import {ICurveStableSwapNG} from "test/Interfaces/Curve/ICurveStableSwapNG.sol"; -import {ILiquidityGaugeV6} from "test/Interfaces/Curve/ILiquidityGaugeV6.sol"; - -import {IGovernance} from "V2-gov/src/interfaces/IGovernance.sol"; - -import {Governance} from "V2-gov/src/Governance.sol"; -import {CurveV2GaugeRewards} from "V2-gov/src/CurveV2GaugeRewards.sol"; - -import "forge-std/console2.sol"; - -library AddressArray { - using Strings for *; - using AddressArray for *; - - function toJSON(address addr) internal pure returns (string memory) { - return string.concat('"', addr.toHexString(), '"'); - } - - function toJSON(address[] memory addresses) internal pure returns (string memory) { - if (addresses.length == 0) return "[]"; - - string memory commaSeparatedStrings = addresses[0].toJSON(); - for (uint256 i = 1; i < addresses.length; ++i) { - commaSeparatedStrings = string.concat(commaSeparatedStrings, ",", addresses[i].toJSON()); - } - - return string.concat("[", commaSeparatedStrings, "]"); - } -} - -contract DeployGovernance is Script { - using Strings for *; - using AddressArray for *; - - struct DeployGovernanceParams { - uint256 epochStart; - address deployer; - bytes32 salt; - address stakingV1; - address lqty; - address lusd; - address bold; - } - - address constant LUSD = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0; - address constant CRV = 0xD533a949740bb3306d119CC777fa900bA034cd52; - address constant FUNDS_SAFE = 0xF06016D822943C42e3Cb7FC3a6A3B1889C1045f8; - address constant DEFI_COLLECTIVE_GRANTS_ADDRESS = 0xDc6f869d2D34E4aee3E89A51f2Af6D54F0F7f690; - - // Governance Constants - uint128 private constant REGISTRATION_FEE = 100e18; - uint128 private constant REGISTRATION_THRESHOLD_FACTOR = 0.0001e18; // 0.01% - uint128 private constant UNREGISTRATION_THRESHOLD_FACTOR = 1e18 + 1; - uint16 private constant UNREGISTRATION_AFTER_EPOCHS = 4; - uint128 private constant VOTING_THRESHOLD_FACTOR = 0.02e18; - uint88 private constant MIN_CLAIM = 0; - uint88 private constant MIN_ACCRUAL = 0; - uint32 internal constant EPOCH_DURATION = 7 days; - uint32 private constant EPOCH_VOTING_CUTOFF = 6 days; - - // CurveV2GaugeRewards Constants - uint256 private constant DURATION = 7 days; - - // Contracts - Governance private governance; - address[] private initialInitiatives; - - ICurveStableSwapNG private curveUsdcBoldPool; - ILiquidityGaugeV6 private curveUsdcBoldGauge; - CurveV2GaugeRewards private curveUsdcBoldInitiative; - - ICurveStableSwapNG private curveLusdBoldPool; - ILiquidityGaugeV6 private curveLusdBoldGauge; - CurveV2GaugeRewards private curveLusdBoldInitiative; - - address private defiCollectiveInitiative; - - function deployGovernance( - DeployGovernanceParams memory p, - address _curveFactoryAddress, - address _curveUsdcBoldPoolAddress, - address _curveLusdBoldPoolAddress - ) internal returns (address, string memory) { - (address governanceAddress, IGovernance.Configuration memory governanceConfiguration) = - computeGovernanceAddressAndConfig(p); - - governance = new Governance{salt: p.salt}( - p.lqty, p.lusd, p.stakingV1, p.bold, governanceConfiguration, p.deployer, initialInitiatives - ); - - assert(governanceAddress == address(governance)); - - curveUsdcBoldPool = ICurveStableSwapNG(_curveUsdcBoldPoolAddress); - curveLusdBoldPool = ICurveStableSwapNG(_curveLusdBoldPoolAddress); - - if (block.chainid == 1) { - // mainnet - (curveUsdcBoldGauge, curveUsdcBoldInitiative) = deployCurveV2GaugeRewards({ - _governance: governance, - _bold: p.bold, - _curveFactoryAddress: _curveFactoryAddress, - _curvePool: curveUsdcBoldPool - }); - - (curveLusdBoldGauge, curveLusdBoldInitiative) = deployCurveV2GaugeRewards({ - _governance: governance, - _bold: p.bold, - _curveFactoryAddress: _curveFactoryAddress, - _curvePool: curveLusdBoldPool - }); - - initialInitiatives.push(address(curveUsdcBoldInitiative)); - initialInitiatives.push(address(curveLusdBoldInitiative)); - initialInitiatives.push(defiCollectiveInitiative = DEFI_COLLECTIVE_GRANTS_ADDRESS); - } else { - initialInitiatives.push(makeAddr("initiative1")); - initialInitiatives.push(makeAddr("initiative2")); - initialInitiatives.push(makeAddr("initiative3")); - } - - governance.registerInitialInitiatives{gas: 600000}(initialInitiatives); - - return (governanceAddress, _getGovernanceManifestJson(p)); - } - - function computeGovernanceAddress(DeployGovernanceParams memory p) internal pure returns (address) { - (address governanceAddress,) = computeGovernanceAddressAndConfig(p); - return governanceAddress; - } - - function computeGovernanceAddressAndConfig(DeployGovernanceParams memory p) - internal - pure - returns (address, IGovernance.Configuration memory) - { - IGovernance.Configuration memory governanceConfiguration = IGovernance.Configuration({ - registrationFee: REGISTRATION_FEE, - registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, - unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, - votingThresholdFactor: VOTING_THRESHOLD_FACTOR, - minClaim: MIN_CLAIM, - minAccrual: MIN_ACCRUAL, - epochStart: p.epochStart, - epochDuration: EPOCH_DURATION, - epochVotingCutoff: EPOCH_VOTING_CUTOFF - }); - - bytes memory bytecode = abi.encodePacked( - type(Governance).creationCode, - abi.encode(p.lqty, p.lusd, p.stakingV1, p.bold, governanceConfiguration, p.deployer, new address[](0)) - ); - - address governanceAddress = vm.computeCreate2Address(p.salt, keccak256(bytecode)); - return (governanceAddress, governanceConfiguration); - } - - function deployCurveV2GaugeRewards( - IGovernance _governance, - address _bold, - address _curveFactoryAddress, - ICurveStableSwapNG _curvePool - ) private returns (ILiquidityGaugeV6 gauge, CurveV2GaugeRewards curveV2GaugeRewards) { - ICurveStableSwapFactoryNG curveFactory = ICurveStableSwapFactoryNG(_curveFactoryAddress); - gauge = ILiquidityGaugeV6(curveFactory.deploy_gauge(address(_curvePool))); - curveV2GaugeRewards = new CurveV2GaugeRewards(address(_governance), _bold, CRV, address(gauge), DURATION); - - // add BOLD as reward token - gauge.add_reward(_bold, address(curveV2GaugeRewards)); - - // add LUSD as reward token to be distributed by the Funds Safe - gauge.add_reward(LUSD, FUNDS_SAFE); - - // renounce gauge manager role - gauge.set_gauge_manager(address(0)); - } - - function _getGovernanceDeploymentConstants(DeployGovernanceParams memory p) internal pure returns (string memory) { - return string.concat( - "{", - string.concat( - string.concat('"REGISTRATION_FEE":"', REGISTRATION_FEE.toString(), '",'), - string.concat('"REGISTRATION_THRESHOLD_FACTOR":"', REGISTRATION_THRESHOLD_FACTOR.toString(), '",'), - string.concat('"UNREGISTRATION_THRESHOLD_FACTOR":"', UNREGISTRATION_THRESHOLD_FACTOR.toString(), '",'), - string.concat('"UNREGISTRATION_AFTER_EPOCHS":"', UNREGISTRATION_AFTER_EPOCHS.toString(), '",'), - string.concat('"VOTING_THRESHOLD_FACTOR":"', VOTING_THRESHOLD_FACTOR.toString(), '",'), - string.concat('"MIN_CLAIM":"', MIN_CLAIM.toString(), '",'), - string.concat('"MIN_ACCRUAL":"', MIN_ACCRUAL.toString(), '",'), - string.concat('"EPOCH_START":"', p.epochStart.toString(), '",') - ), - string.concat( - string.concat('"EPOCH_DURATION":"', EPOCH_DURATION.toString(), '",'), - string.concat('"EPOCH_VOTING_CUTOFF":"', EPOCH_VOTING_CUTOFF.toString(), '",'), - string.concat('"FUNDS_SAFE":"', FUNDS_SAFE.toHexString(), '"') // no comma - ), - "}" - ); - } - - function _getGovernanceManifestJson(DeployGovernanceParams memory p) internal view returns (string memory) { - return string.concat( - "{", - string.concat( - string.concat('"constants":', _getGovernanceDeploymentConstants(p), ","), - string.concat('"governance":"', address(governance).toHexString(), '",'), - string.concat('"curveUsdcBoldPool":"', address(curveUsdcBoldPool).toHexString(), '",'), - string.concat('"curveUsdcBoldGauge":"', address(curveUsdcBoldGauge).toHexString(), '",'), - string.concat('"curveUsdcBoldInitiative":"', address(curveUsdcBoldInitiative).toHexString(), '",'), - string.concat('"curveLusdBoldPool":"', address(curveLusdBoldPool).toHexString(), '",'), - string.concat('"curveLusdBoldGauge":"', address(curveLusdBoldGauge).toHexString(), '",'), - string.concat('"curveLusdBoldInitiative":"', address(curveLusdBoldInitiative).toHexString(), '",') - ), - string.concat( - string.concat('"defiCollectiveInitiative":"', defiCollectiveInitiative.toHexString(), '",'), - string.concat('"stakingV1":"', p.stakingV1.toHexString(), '",'), - string.concat('"LQTYToken":"', p.lqty.toHexString(), '",'), - string.concat('"LUSDToken":"', p.lusd.toHexString(), '",'), - string.concat('"initialInitiatives":', initialInitiatives.toJSON()) // no comma - ), - "}" - ); - } -} diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index ba6cdc4b4..58e2e0d8f 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -4,13 +4,18 @@ pragma solidity 0.8.24; import {StdCheats} from "forge-std/StdCheats.sol"; import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; -import {IERC20 as IERC20_GOV} from "openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20 as IERC20_GOV} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {ProxyAdmin} from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from + "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IFPMMFactory} from "src/interfaces/IFPMMFactory.sol"; +import {ETH_GAS_COMPENSATION} from "src/Dependencies/Constants.sol"; +import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; import {StringFormatting} from "test/Utils/StringFormatting.sol"; import {Accounts} from "test/TestContracts/Accounts.sol"; import {ERC20Faucet} from "test/TestContracts/ERC20Faucet.sol"; -import {ETH_GAS_COMPENSATION} from "src/Dependencies/Constants.sol"; -import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; +import {WETHTester} from "test/TestContracts/WETHTester.sol"; import "src/AddressesRegistry.sol"; import "src/ActivePool.sol"; import "src/BoldToken.sol"; @@ -24,127 +29,23 @@ import "src/HintHelpers.sol"; import "src/MultiTroveGetter.sol"; import "src/SortedTroves.sol"; import "src/StabilityPool.sol"; -import "src/PriceFeeds/WETHPriceFeed.sol"; -import "src/PriceFeeds/WSTETHPriceFeed.sol"; -import "src/PriceFeeds/RETHPriceFeed.sol"; + import "src/CollateralRegistry.sol"; +import "src/tokens/StableTokenV3.sol"; +import "src/interfaces/IStableTokenV3.sol"; import "test/TestContracts/PriceFeedTestnet.sol"; import "test/TestContracts/MetadataDeployment.sol"; import "test/Utils/Logging.sol"; import "test/Utils/StringEquality.sol"; -import "src/Zappers/WETHZapper.sol"; -import "src/Zappers/GasCompZapper.sol"; -import "src/Zappers/LeverageLSTZapper.sol"; -import "src/Zappers/LeverageWETHZapper.sol"; -import "src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol"; -import {BalancerFlashLoan} from "src/Zappers/Modules/FlashLoans/BalancerFlashLoan.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGFactory.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Pool.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Factory.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol"; -import "src/Zappers/Modules/Exchanges/HybridCurveUniV3Exchange.sol"; -import {WETHTester} from "test/TestContracts/WETHTester.sol"; import "forge-std/console2.sol"; -import {IRateProvider, IWeightedPool, IWeightedPoolFactory} from "./Interfaces/Balancer/IWeightedPool.sol"; -import {IVault} from "./Interfaces/Balancer/IVault.sol"; -import {MockStakingV1} from "V2-gov/test/mocks/MockStakingV1.sol"; - -import {DeployGovernance} from "./DeployGovernance.s.sol"; - -function _latestUTCMidnightBetweenWednesdayAndThursday() view returns (uint256) { - return block.timestamp / 1 weeks * 1 weeks; -} -contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, MetadataDeployment, Logging { +contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { using Strings for *; using StringFormatting for *; using StringEquality for string; - string constant DEPLOYMENT_MODE_COMPLETE = "complete"; - string constant DEPLOYMENT_MODE_BOLD_ONLY = "bold-only"; - string constant DEPLOYMENT_MODE_USE_EXISTING_BOLD = "use-existing-bold"; - - uint256 constant NUM_BRANCHES = 3; - - address WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - address USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - - // used for gas compensation and as collateral of the first branch - // tapping disallowed - IWETH WETH; - IERC20Metadata USDC; - address WSTETH_ADDRESS = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; - address RETH_ADDRESS = 0xae78736Cd615f374D3085123A210448E74Fc6393; - address ETH_ORACLE_ADDRESS = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; - address RETH_ORACLE_ADDRESS = 0x536218f9E9Eb48863970252233c8F271f554C2d0; - address STETH_ORACLE_ADDRESS = 0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8; - uint256 ETH_USD_STALENESS_THRESHOLD = 24 hours; - uint256 STETH_USD_STALENESS_THRESHOLD = 24 hours; - uint256 RETH_ETH_STALENESS_THRESHOLD = 48 hours; - - // V1 - address LQTY_ADDRESS = 0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D; - address LQTY_STAKING_ADDRESS = 0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d; - address LUSD_ADDRESS = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0; - - address internal lqty; - address internal stakingV1; - address internal lusd; - - // Curve - ICurveStableswapNGFactory curveStableswapFactory; - // https://docs.curve.fi/deployments/amm/#stableswap-ng - // Sepolia - ICurveStableswapNGFactory constant curveStableswapFactorySepolia = - ICurveStableswapNGFactory(0xfb37b8D939FFa77114005e61CFc2e543d6F49A81); - // Mainnet - ICurveStableswapNGFactory constant curveStableswapFactoryMainnet = - ICurveStableswapNGFactory(0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf); - uint128 constant BOLD_TOKEN_INDEX = 0; - uint128 constant OTHER_TOKEN_INDEX = 1; - - // Uni V3 - uint24 constant UNIV3_FEE = 0.3e4; - uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05% - uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01% - ISwapRouter uniV3Router; - IQuoterV2 uniV3Quoter; - IUniswapV3Factory uniswapV3Factory; - INonfungiblePositionManager uniV3PositionManager; - // https://docs.uniswap.org/contracts/v3/reference/deployments/ethereum-deployments - // Sepolia - ISwapRouter constant uniV3RouterSepolia = ISwapRouter(0x65669fE35312947050C450Bd5d36e6361F85eC12); - IQuoterV2 constant uniV3QuoterSepolia = IQuoterV2(0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3); - IUniswapV3Factory constant uniswapV3FactorySepolia = IUniswapV3Factory(0x0227628f3F023bb0B980b67D528571c95c6DaC1c); - INonfungiblePositionManager constant uniV3PositionManagerSepolia = - INonfungiblePositionManager(0x1238536071E1c677A632429e3655c799b22cDA52); - // Mainnet - ISwapRouter constant uniV3RouterMainnet = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - IQuoterV2 constant uniV3QuoterMainnet = IQuoterV2(0x61fFE014bA17989E743c5F6cB21bF9697530B21e); - IUniswapV3Factory constant uniswapV3FactoryMainnet = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); - INonfungiblePositionManager constant uniV3PositionManagerMainnet = - INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); - - // Balancer - IVault constant balancerVault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - IWeightedPoolFactory balancerFactory; - // Sepolia - // https://docs.balancer.fi/reference/contracts/deployment-addresses/sepolia.html - IWeightedPoolFactory constant balancerFactorySepolia = - IWeightedPoolFactory(0x7920BFa1b2041911b354747CA7A6cDD2dfC50Cfd); - // Mainnet - // https://docs.balancer.fi/reference/contracts/deployment-addresses/mainnet.html - IWeightedPoolFactory constant balancerFactoryMainnet = - IWeightedPoolFactory(0x897888115Ada5773E02aA29F775430BFB5F34c51); - bytes32 SALT; address deployer; - bool useTestnetPriceFeeds; - - uint256 lastTroveIndex; struct LiquityContracts { IAddressesRegistry addressesRegistry; @@ -161,9 +62,6 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, GasPool gasPool; IInterestRouter interestRouter; IERC20Metadata collToken; - WETHZapper wethZapper; - GasCompZapper gasCompZapper; - ILeverageZapper leverageZapper; } struct LiquityContractAddresses { @@ -181,11 +79,6 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, address interestRouter; } - struct Zappers { - WETHZapper wethZapper; - GasCompZapper gasCompZapper; - } - struct TroveManagerParams { uint256 CCR; uint256 MCR; @@ -216,333 +109,69 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, } struct DeploymentResult { - LiquityContracts[] contractsArray; + LiquityContracts contracts; ICollateralRegistry collateralRegistry; - IBoldToken boldToken; - ICurveStableswapNGPool usdcCurvePool; HintHelpers hintHelpers; MultiTroveGetter multiTroveGetter; - IExchangeHelpers exchangeHelpers; + ProxyAdmin proxyAdmin; + IStableTokenV3 stableToken; + address stabilityPoolImpl; + address stableTokenV3Impl; + address fpmm; + } + + struct DeploymentConfig { + address USDm_ALFAJORES_ADDRESS; + address proxyAdmin; + address fpmmFactory; + address fpmmImplementation; + address referenceRateFeedID; + string stableTokenName; + string stableTokenSymbol; + // Parameters for the TroveManager + uint256 CCR; + uint256 MCR; + uint256 SCR; + uint256 BCR; + uint256 LIQUIDATION_PENALTY_SP; + uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; } + DeploymentConfig internal CONFIG = DeploymentConfig({ + USDm_ALFAJORES_ADDRESS: 0x9E2d4412d0f434cC85500b79447d9323a7416f09, + proxyAdmin: 0xe4DdacCAdb64114215FCe8251B57B2AEB5C2C0E2, + fpmmFactory: 0xd8098494a749a3fDAD2D2e7Fa5272D8f274D8FF6, + fpmmImplementation: 0x0292efcB331C6603eaa29D570d12eB336D6c01d6, + referenceRateFeedID: 0x206B25Ea01E188Ee243131aFdE526bA6E131a016, + stableTokenName: "EUR.m Test", + stableTokenSymbol: "EUR.m", + // TODO: reconsider these values + CCR: 150e16, + MCR: 110e16, + SCR: 110e16, + BCR: 40e16, + LIQUIDATION_PENALTY_SP: 5e16, + LIQUIDATION_PENALTY_REDISTRIBUTION: 10e16 + }); + function run() external { string memory saltStr = vm.envOr("SALT", block.timestamp.toString()); SALT = keccak256(bytes(saltStr)); - if (vm.envBytes("DEPLOYER").length == 20) { - // address - deployer = vm.envAddress("DEPLOYER"); - vm.startBroadcast(deployer); - } else { - // private key - uint256 privateKey = vm.envUint("DEPLOYER"); - deployer = vm.addr(privateKey); - vm.startBroadcast(privateKey); - } - - string memory deploymentMode = vm.envOr("DEPLOYMENT_MODE", DEPLOYMENT_MODE_COMPLETE); - require( - deploymentMode.eq(DEPLOYMENT_MODE_COMPLETE) || deploymentMode.eq(DEPLOYMENT_MODE_BOLD_ONLY) - || deploymentMode.eq(DEPLOYMENT_MODE_USE_EXISTING_BOLD), - string.concat("Bad deployment mode: ", deploymentMode) - ); - - uint256 epochStart = vm.envOr( - "EPOCH_START", - (block.chainid == 1 ? _latestUTCMidnightBetweenWednesdayAndThursday() : block.timestamp) - EPOCH_DURATION - ); - - useTestnetPriceFeeds = vm.envOr("USE_TESTNET_PRICEFEEDS", false); + uint256 privateKey = vm.envUint("DEPLOYER"); + deployer = vm.addr(privateKey); + vm.startBroadcast(privateKey); _log("Deployer: ", deployer.toHexString()); _log("Deployer balance: ", deployer.balance.decimal()); - _log("Deployment mode: ", deploymentMode); _log("CREATE2 salt: ", 'keccak256(bytes("', saltStr, '")) = ', uint256(SALT).toHexString()); - _log("Governance epoch start: ", epochStart.toString()); - _log("Use testnet PriceFeeds: ", useTestnetPriceFeeds ? "yes" : "no"); - - // Deploy Bold or pick up existing deployment - bytes memory boldBytecode = bytes.concat(type(BoldToken).creationCode, abi.encode(deployer)); - address boldAddress = vm.computeCreate2Address(SALT, keccak256(boldBytecode)); - BoldToken boldToken; - - if (deploymentMode.eq(DEPLOYMENT_MODE_USE_EXISTING_BOLD)) { - require(boldAddress.code.length > 0, string.concat("BOLD not found at ", boldAddress.toHexString())); - boldToken = BoldToken(boldAddress); - - // Check BOLD is untouched - require(boldToken.totalSupply() == 0, "Some BOLD has been minted!"); - require(boldToken.collateralRegistryAddress() == address(0), "Collateral registry already set"); - require(boldToken.owner() == deployer, "Not BOLD owner"); - } else { - boldToken = new BoldToken{salt: SALT}(deployer); - assert(address(boldToken) == boldAddress); - } - - if (deploymentMode.eq(DEPLOYMENT_MODE_BOLD_ONLY)) { - vm.writeFile("deployment-manifest.json", string.concat('{"boldToken":"', boldAddress.toHexString(), '"}')); - return; - } - - if (block.chainid == 1) { - // mainnet - WETH = IWETH(WETH_ADDRESS); - USDC = IERC20Metadata(USDC_ADDRESS); - curveStableswapFactory = curveStableswapFactoryMainnet; - uniV3Router = uniV3RouterMainnet; - uniV3Quoter = uniV3QuoterMainnet; - uniswapV3Factory = uniswapV3FactoryMainnet; - uniV3PositionManager = uniV3PositionManagerMainnet; - balancerFactory = balancerFactoryMainnet; - lqty = LQTY_ADDRESS; - stakingV1 = LQTY_STAKING_ADDRESS; - lusd = LUSD_ADDRESS; - } else { - // sepolia, local - if (block.chainid == 31337) { - // local - WETH = new WETHTester({_tapAmount: 100 ether, _tapPeriod: 1 days}); - } else { - // sepolia - WETH = new WETHTester({_tapAmount: 0, _tapPeriod: type(uint256).max}); - } - USDC = new ERC20Faucet("USDC", "USDC", 0, type(uint256).max); - curveStableswapFactory = curveStableswapFactorySepolia; - uniV3Router = uniV3RouterSepolia; - uniV3Quoter = uniV3QuoterSepolia; - uniswapV3Factory = uniswapV3FactorySepolia; - uniV3PositionManager = uniV3PositionManagerSepolia; - balancerFactory = balancerFactorySepolia; - // Needed for Governance (they will be constants for mainnet) - lqty = address(new ERC20Faucet("Liquity", "LQTY", 100 ether, 1 days)); - lusd = address(new ERC20Faucet("Liquity USD", "LUSD", 100 ether, 1 days)); - stakingV1 = address(new MockStakingV1(IERC20_GOV(lqty), IERC20_GOV(lusd))); - - // Let stakingV1 spend anyone's LQTY without approval, like in the real LQTYStaking - ERC20Faucet(lqty).mock_setWildcardSpender(address(stakingV1), true); - } - - TroveManagerParams[] memory troveManagerParamsArray = new TroveManagerParams[](NUM_BRANCHES); - - // WETH - troveManagerParamsArray[0] = TroveManagerParams({ - CCR: CCR_WETH, - MCR: MCR_WETH, - SCR: SCR_WETH, - BCR: BCR_ALL, - LIQUIDATION_PENALTY_SP: LIQUIDATION_PENALTY_SP_WETH, - LIQUIDATION_PENALTY_REDISTRIBUTION: LIQUIDATION_PENALTY_REDISTRIBUTION_WETH - }); - - // wstETH - troveManagerParamsArray[1] = TroveManagerParams({ - CCR: CCR_SETH, - MCR: MCR_SETH, - SCR: SCR_SETH, - BCR: BCR_ALL, - LIQUIDATION_PENALTY_SP: LIQUIDATION_PENALTY_SP_SETH, - LIQUIDATION_PENALTY_REDISTRIBUTION: LIQUIDATION_PENALTY_REDISTRIBUTION_SETH - }); + _log("Chain ID: ", block.chainid.toString()); - // rETH (same as wstETH) - troveManagerParamsArray[2] = troveManagerParamsArray[1]; - - string[] memory collNames = new string[](2); - string[] memory collSymbols = new string[](2); - collNames[0] = "Wrapped liquid staked Ether 2.0"; - collSymbols[0] = "wstETH"; - collNames[1] = "Rocket Pool ETH"; - collSymbols[1] = "rETH"; - - DeployGovernanceParams memory deployGovernanceParams = DeployGovernanceParams({ - epochStart: epochStart, - deployer: deployer, - salt: SALT, - stakingV1: stakingV1, - lqty: lqty, - lusd: lusd, - bold: boldAddress - }); - - DeploymentResult memory deployed = - _deployAndConnectContracts(troveManagerParamsArray, collNames, collSymbols, deployGovernanceParams); - - if (block.chainid == 11155111) { - // Provide liquidity for zaps if we're on Sepolia - ERC20Faucet monkeyBalls = new ERC20Faucet("MonkeyBalls", "MB", 0, type(uint256).max); - for (uint256 i = 0; i < deployed.contractsArray.length; ++i) { - PriceFeedTestnet(address(deployed.contractsArray[i].priceFeed)).setPrice(2_000 ether); - _provideFlashloanLiquidity(ERC20Faucet(address(deployed.contractsArray[i].collToken)), monkeyBalls); - if (i == 0) { - // WETH, we do USDC-WETH - (uint256 price,) = deployed.contractsArray[0].priceFeed.fetchPrice(); - uint256 token1Amount = 1_000_000 ether; - _provideUniV3Liquidity( - ERC20Faucet(address(USDC)), ERC20Faucet(address(WETH)), token1Amount, price, UNIV3_FEE_USDC_WETH - ); - } else { - // LSTs, we do WETH-LST - uint256 token1Amount = 1_000 ether; - _provideUniV3Liquidity( - ERC20Faucet(address(WETH)), - ERC20Faucet(address(deployed.contractsArray[i].collToken)), - token1Amount, - 1 ether, - UNIV3_FEE_WETH_COLL - ); - } - } - - _provideCurveLiquidity(deployed.boldToken, deployed.contractsArray[0]); - - // deployed.contractsArray[1].collToken.mint(deployer, 1 ether); - // deployed.contractsArray[1].collToken.approve(address(deployed.contractsArray[1].leverageZapper), 1 ether); - // deployed.contractsArray[1].leverageZapper.openLeveragedTroveWithRawETH{value: ETH_GAS_COMPENSATION}( - // ILeverageZapper.OpenLeveragedTroveParams({ - // owner: deployer, - // ownerIndex: 1, - // collAmount: 1 ether, - // flashLoanAmount: 1 ether, - // boldAmount: 2_000 ether, - // upperHint: 0, - // lowerHint: 0, - // annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - // batchManager: address(0), - // maxUpfrontFee: type(uint256).max, - // addManager: address(0), - // removeManager: address(0), - // receiver: address(0) - // }) - // ); - } - - ICurveStableswapNGPool lusdCurvePool; - if (block.chainid == 1) { - lusdCurvePool = _deployCurvePool(deployed.boldToken, IERC20Metadata(LUSD_ADDRESS)); - } - - // Governance - (address governanceAddress, string memory governanceManifest) = deployGovernance( - deployGovernanceParams, - address(curveStableswapFactory), - address(deployed.usdcCurvePool), - address(lusdCurvePool) - ); - address computedGovernanceAddress = computeGovernanceAddress(deployGovernanceParams); - assert(governanceAddress == computedGovernanceAddress); + DeploymentResult memory deployed = _deployAndConnectContracts(); vm.stopBroadcast(); - vm.writeFile("deployment-manifest.json", _getManifestJson(deployed, governanceManifest)); - - if (vm.envOr("OPEN_DEMO_TROVES", false)) { - // Anvil default accounts - // TODO: get accounts from env - uint256[] memory demoAccounts = new uint256[](8); - demoAccounts[0] = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; - demoAccounts[1] = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; - demoAccounts[2] = 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a; - demoAccounts[3] = 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6; - demoAccounts[4] = 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a; - demoAccounts[5] = 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba; - demoAccounts[6] = 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e; - demoAccounts[7] = 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356; - - DemoTroveParams[] memory demoTroves = new DemoTroveParams[](24); - - demoTroves[0] = DemoTroveParams(0, demoAccounts[0], 0, 35 ether, 2_800 ether, 5.0e16); - demoTroves[1] = DemoTroveParams(0, demoAccounts[1], 0, 47 ether, 2_400 ether, 4.7e16); - demoTroves[2] = DemoTroveParams(0, demoAccounts[2], 0, 40 ether, 4_000 ether, 3.3e16); - demoTroves[3] = DemoTroveParams(0, demoAccounts[3], 0, 75 ether, 6_000 ether, 4.3e16); - demoTroves[4] = DemoTroveParams(0, demoAccounts[4], 0, 29 ether, 2_280 ether, 5.0e16); - demoTroves[5] = DemoTroveParams(0, demoAccounts[5], 0, 58.37 ether, 4_400 ether, 4.7e16); - demoTroves[6] = DemoTroveParams(0, demoAccounts[6], 0, 43.92 ether, 5_500 ether, 3.8e16); - demoTroves[7] = DemoTroveParams(0, demoAccounts[7], 0, 57.2 ether, 6_000 ether, 4.3e16); - - demoTroves[8] = DemoTroveParams(1, demoAccounts[0], 0, 31 ether, 2_000 ether, 3.3e16); - demoTroves[9] = DemoTroveParams(1, demoAccounts[1], 0, 26 ether, 2_000 ether, 4.1e16); - demoTroves[10] = DemoTroveParams(1, demoAccounts[2], 0, 28 ether, 2_300 ether, 3.8e16); - demoTroves[11] = DemoTroveParams(1, demoAccounts[3], 0, 32 ether, 2_200 ether, 4.3e16); - demoTroves[12] = DemoTroveParams(1, demoAccounts[4], 0, 95 ether, 12_000 ether, 7.0e16); - demoTroves[13] = DemoTroveParams(1, demoAccounts[5], 0, 97 ether, 4_000 ether, 4.4e16); - demoTroves[14] = DemoTroveParams(1, demoAccounts[6], 0, 81 ether, 11_000 ether, 3.3e16); - demoTroves[15] = DemoTroveParams(1, demoAccounts[7], 0, 94 ether, 12_800 ether, 4.4e16); - - demoTroves[16] = DemoTroveParams(2, demoAccounts[0], 0, 45 ether, 3_000 ether, 2.4e16); - demoTroves[17] = DemoTroveParams(2, demoAccounts[1], 0, 35 ether, 2_100 ether, 5.0e16); - demoTroves[18] = DemoTroveParams(2, demoAccounts[2], 0, 67 ether, 2_200 ether, 4.5e16); - demoTroves[19] = DemoTroveParams(2, demoAccounts[3], 0, 32 ether, 4_900 ether, 3.2e16); - demoTroves[20] = DemoTroveParams(2, demoAccounts[4], 0, 82 ether, 4_500 ether, 6.9e16); - demoTroves[21] = DemoTroveParams(2, demoAccounts[5], 0, 74 ether, 7_300 ether, 4.1e16); - demoTroves[22] = DemoTroveParams(2, demoAccounts[6], 0, 54 ether, 6_900 ether, 2.9e16); - demoTroves[23] = DemoTroveParams(2, demoAccounts[7], 0, 65 ether, 8_100 ether, 1.5e16); - - for (uint256 i = 0; i < deployed.contractsArray.length; i++) { - tapFaucet(demoAccounts, deployed.contractsArray[i]); - } - - openDemoTroves(demoTroves, deployed.contractsArray); - } - } - - function tapFaucet(uint256[] memory accounts, LiquityContracts memory contracts) internal { - for (uint256 i = 0; i < accounts.length; i++) { - ERC20Faucet token = ERC20Faucet(address(contracts.collToken)); - - vm.startBroadcast(accounts[i]); - token.tap(); - vm.stopBroadcast(); - - console2.log( - "%s.tap() => %s (balance: %s)", - token.symbol(), - vm.addr(accounts[i]), - string.concat(formatAmount(token.balanceOf(vm.addr(accounts[i])), 18, 2), " ", token.symbol()) - ); - } - } - - function openDemoTroves(DemoTroveParams[] memory demoTroves, LiquityContracts[] memory contractsArray) internal { - for (uint256 i = 0; i < demoTroves.length; i++) { - console2.log( - "openTrove({ coll: %18e, borrow: %18e, rate: %18e%% })", - demoTroves[i].coll, - demoTroves[i].debt, - demoTroves[i].annualInterestRate * 100 - ); - - DemoTroveParams memory trove = demoTroves[i]; - LiquityContracts memory contracts = contractsArray[trove.collIndex]; - - vm.startBroadcast(trove.owner); - - IERC20 collToken = IERC20(contracts.collToken); - IERC20 wethToken = IERC20(contracts.addressesRegistry.WETH()); - - // Approve collToken to BorrowerOperations - if (collToken == wethToken) { - wethToken.approve(address(contracts.borrowerOperations), trove.coll + ETH_GAS_COMPENSATION); - } else { - wethToken.approve(address(contracts.borrowerOperations), ETH_GAS_COMPENSATION); - collToken.approve(address(contracts.borrowerOperations), trove.coll); - } - - IBorrowerOperations(contracts.borrowerOperations).openTrove( - vm.addr(trove.owner), // _owner - trove.ownerIndex, // _ownerIndex - trove.coll, // _collAmount - trove.debt, // _boldAmount - 0, // _upperHint - 0, // _lowerHint - trove.annualInterestRate, // _annualInterestRate - type(uint256).max, // _maxUpfrontFee - address(0), // _addManager - address(0), // _removeManager - address(0) // _receiver - ); - - vm.stopBroadcast(); - } + vm.writeFile("script/deployment-manifest.json", _getManifestJson(deployed)); } // See: https://solidity-by-example.org/app/create2/ @@ -550,177 +179,167 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry)); } - function _deployAndConnectContracts( - TroveManagerParams[] memory troveManagerParamsArray, - string[] memory _collNames, - string[] memory _collSymbols, - DeployGovernanceParams memory _deployGovernanceParams - ) internal returns (DeploymentResult memory r) { - assert(_collNames.length == troveManagerParamsArray.length - 1); - assert(_collSymbols.length == troveManagerParamsArray.length - 1); - - DeploymentVars memory vars; - vars.numCollaterals = troveManagerParamsArray.length; - r.boldToken = BoldToken(_deployGovernanceParams.bold); - - // USDC and USDC-BOLD pool - r.usdcCurvePool = _deployCurvePool(r.boldToken, USDC); - - r.contractsArray = new LiquityContracts[](vars.numCollaterals); - vars.collaterals = new IERC20Metadata[](vars.numCollaterals); - vars.addressesRegistries = new IAddressesRegistry[](vars.numCollaterals); - vars.troveManagers = new ITroveManager[](vars.numCollaterals); + function _deployAndConnectContracts() internal returns (DeploymentResult memory r) { + _deployProxyInfrastructure(r); + _deployStableToken(r); + _deployFPMM(r); + + TroveManagerParams memory troveManagerParams = TroveManagerParams({ + CCR: CONFIG.CCR, + MCR: CONFIG.MCR, + SCR: CONFIG.SCR, + BCR: CONFIG.BCR, + LIQUIDATION_PENALTY_SP: CONFIG.LIQUIDATION_PENALTY_SP, + LIQUIDATION_PENALTY_REDISTRIBUTION: CONFIG.LIQUIDATION_PENALTY_REDISTRIBUTION + }); - // Collaterals - if (block.chainid == 1 && !useTestnetPriceFeeds) { - // mainnet - // ETH - vars.collaterals[0] = IERC20Metadata(WETH); + IAddressesRegistry addressesRegistry = new AddressesRegistry( + deployer, + troveManagerParams.CCR, + troveManagerParams.MCR, + troveManagerParams.BCR, + troveManagerParams.SCR, + troveManagerParams.LIQUIDATION_PENALTY_SP, + troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION + ); - // wstETH - vars.collaterals[1] = IERC20Metadata(WSTETH_ADDRESS); + address troveManagerAddress = vm.computeCreate2Address( + SALT, keccak256(getBytecode(type(TroveManager).creationCode, address(addressesRegistry))) + ); - // RETH - vars.collaterals[2] = IERC20Metadata(RETH_ADDRESS); - } else { - // Sepolia - // Use WETH as collateral for the first branch - vars.collaterals[0] = WETH; + IERC20Metadata collToken = IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS); - // Deploy plain ERC20Faucets for the rest of the branches - for (vars.i = 1; vars.i < vars.numCollaterals; vars.i++) { - vars.collaterals[vars.i] = new ERC20Faucet( - _collNames[vars.i - 1], // _name - _collSymbols[vars.i - 1], // _symbol - 100 ether, // _tapAmount - 1 days // _tapPeriod - ); - } - } + IERC20Metadata[] memory collaterals = new IERC20Metadata[](1); + collaterals[0] = collToken; - // Deploy AddressesRegistries and get TroveManager addresses - for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { - (IAddressesRegistry addressesRegistry, address troveManagerAddress) = - _deployAddressesRegistry(troveManagerParamsArray[vars.i]); - vars.addressesRegistries[vars.i] = addressesRegistry; - vars.troveManagers[vars.i] = ITroveManager(troveManagerAddress); - } + ITroveManager[] memory troveManagers = new ITroveManager[](1); + troveManagers[0] = ITroveManager(troveManagerAddress); - r.collateralRegistry = new CollateralRegistry(r.boldToken, vars.collaterals, vars.troveManagers); + r.collateralRegistry = new CollateralRegistry(IBoldToken(address(r.stableToken)), collaterals, troveManagers); r.hintHelpers = new HintHelpers(r.collateralRegistry); r.multiTroveGetter = new MultiTroveGetter(r.collateralRegistry); - // Deploy per-branch contracts for each branch - for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { - vars.contracts = _deployAndConnectCollateralContracts( - vars.collaterals[vars.i], - r.boldToken, - r.collateralRegistry, - r.usdcCurvePool, - vars.addressesRegistries[vars.i], - address(vars.troveManagers[vars.i]), - r.hintHelpers, - r.multiTroveGetter, - computeGovernanceAddress(_deployGovernanceParams) - ); - r.contractsArray[vars.i] = vars.contracts; - } + IPriceFeed priceFeed = new PriceFeedTestnet(); - r.boldToken.setCollateralRegistry(address(r.collateralRegistry)); + r.contracts = + _deployAndConnectCollateralContracts(collToken, priceFeed, addressesRegistry, troveManagerAddress, r); + } + + function _deployProxyInfrastructure(DeploymentResult memory r) internal { + r.proxyAdmin = ProxyAdmin(CONFIG.proxyAdmin); + r.stableTokenV3Impl = address(new StableTokenV3{salt: SALT}(true)); + r.stabilityPoolImpl = address(new StabilityPool{salt: SALT}(true)); - // exchange helpers - r.exchangeHelpers = new HybridCurveUniV3ExchangeHelpers( - USDC, - WETH, - r.usdcCurvePool, - OTHER_TOKEN_INDEX, // USDC Curve pool index - BOLD_TOKEN_INDEX, // BOLD Curve pool index - UNIV3_FEE_USDC_WETH, - UNIV3_FEE_WETH_COLL, - uniV3Quoter + assert( + address(r.stableTokenV3Impl) + == vm.computeCreate2Address( + SALT, keccak256(bytes.concat(type(StableTokenV3).creationCode, abi.encode(true))) + ) + ); + assert( + address(r.stabilityPoolImpl) + == vm.computeCreate2Address( + SALT, keccak256(bytes.concat(type(StabilityPool).creationCode, abi.encode(true))) + ) ); } - function _deployAddressesRegistry(TroveManagerParams memory _troveManagerParams) - internal - returns (IAddressesRegistry, address) - { - IAddressesRegistry addressesRegistry = new AddressesRegistry( - deployer, - _troveManagerParams.CCR, - _troveManagerParams.MCR, - _troveManagerParams.BCR, - _troveManagerParams.SCR, - _troveManagerParams.LIQUIDATION_PENALTY_SP, - _troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION - ); - address troveManagerAddress = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(TroveManager).creationCode, address(addressesRegistry))) + function _deployStableToken(DeploymentResult memory r) internal { + r.stableToken = IStableTokenV3( + address(new TransparentUpgradeableProxy(address(r.stableTokenV3Impl), address(r.proxyAdmin), "")) ); + } - return (addressesRegistry, troveManagerAddress); + function _deployFPMM(DeploymentResult memory r) internal { + r.fpmm = IFPMMFactory(CONFIG.fpmmFactory).deployFPMM( + CONFIG.fpmmImplementation, address(r.stableToken), CONFIG.USDm_ALFAJORES_ADDRESS, CONFIG.referenceRateFeedID + ); } function _deployAndConnectCollateralContracts( IERC20Metadata _collToken, - IBoldToken _boldToken, - ICollateralRegistry _collateralRegistry, - ICurveStableswapNGPool _usdcCurvePool, + IPriceFeed _priceFeed, IAddressesRegistry _addressesRegistry, address _troveManagerAddress, - IHintHelpers _hintHelpers, - IMultiTroveGetter _multiTroveGetter, - address _governance + DeploymentResult memory r ) internal returns (LiquityContracts memory contracts) { LiquityContractAddresses memory addresses; contracts.collToken = _collToken; - - // Deploy all contracts, using testers for TM and PriceFeed contracts.addressesRegistry = _addressesRegistry; + contracts.priceFeed = _priceFeed; + // TODO: replace with governance timelock on mainnet + contracts.interestRouter = IInterestRouter(0x56fD3F2bEE130e9867942D0F463a16fBE49B8d81); + + addresses.troveManager = _troveManagerAddress; - // Deploy Metadata contracts.metadataNFT = deployMetadata(SALT); addresses.metadataNFT = vm.computeCreate2Address( SALT, keccak256(getBytecode(type(MetadataNFT).creationCode, address(initializedFixedAssetReader))) ); assert(address(contracts.metadataNFT) == addresses.metadataNFT); - contracts.interestRouter = IInterestRouter(_governance); - addresses.borrowerOperations = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(BorrowerOperations).creationCode, address(contracts.addressesRegistry))) - ); - addresses.troveManager = _troveManagerAddress; - addresses.troveNFT = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(TroveNFT).creationCode, address(contracts.addressesRegistry))) - ); - addresses.stabilityPool = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(StabilityPool).creationCode, address(contracts.addressesRegistry))) - ); - addresses.activePool = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry))) + addresses.borrowerOperations = + _computeCreate2Address(type(BorrowerOperations).creationCode, address(contracts.addressesRegistry)); + addresses.troveNFT = _computeCreate2Address(type(TroveNFT).creationCode, address(contracts.addressesRegistry)); + addresses.activePool = + _computeCreate2Address(type(ActivePool).creationCode, address(contracts.addressesRegistry)); + addresses.defaultPool = + _computeCreate2Address(type(DefaultPool).creationCode, address(contracts.addressesRegistry)); + addresses.gasPool = _computeCreate2Address(type(GasPool).creationCode, address(contracts.addressesRegistry)); + addresses.collSurplusPool = + _computeCreate2Address(type(CollSurplusPool).creationCode, address(contracts.addressesRegistry)); + addresses.sortedTroves = + _computeCreate2Address(type(SortedTroves).creationCode, address(contracts.addressesRegistry)); + + // Deploy StabilityPool proxy + address stabilityPool = + address(new TransparentUpgradeableProxy(address(r.stabilityPoolImpl), address(r.proxyAdmin), "")); + + contracts.stabilityPool = IStabilityPool(stabilityPool); + // Set up addresses in registry + _setupAddressesRegistry(contracts, addresses, r); + + // Deploy core protocol contracts + _deployProtocolContracts(contracts, addresses); + + IStabilityPool(stabilityPool).initialize(contracts.addressesRegistry); + + address[] memory minters = new address[](2); + minters[0] = address(contracts.borrowerOperations); + minters[1] = address(contracts.activePool); + + address[] memory burners = new address[](4); + burners[0] = address(contracts.troveManager); + burners[1] = address(r.collateralRegistry); + burners[2] = address(contracts.borrowerOperations); + burners[3] = address(contracts.stabilityPool); + + address[] memory operators = new address[](1); + operators[0] = address(contracts.stabilityPool); + + r.stableToken.initialize( + CONFIG.stableTokenName, + CONFIG.stableTokenSymbol, + new address[](0), + new uint256[](0), + minters, + burners, + operators ); - addresses.defaultPool = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(DefaultPool).creationCode, address(contracts.addressesRegistry))) - ); - addresses.gasPool = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(GasPool).creationCode, address(contracts.addressesRegistry))) - ); - addresses.collSurplusPool = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(CollSurplusPool).creationCode, address(contracts.addressesRegistry))) - ); - addresses.sortedTroves = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(SortedTroves).creationCode, address(contracts.addressesRegistry))) - ); - - contracts.priceFeed = _deployPriceFeed(address(_collToken), addresses.borrowerOperations); + } + function _setupAddressesRegistry( + LiquityContracts memory contracts, + LiquityContractAddresses memory addresses, + DeploymentResult memory r + ) internal { IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ - collToken: _collToken, + collToken: contracts.collToken, borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), troveManager: ITroveManager(addresses.troveManager), troveNFT: ITroveNFT(addresses.troveNFT), metadataNFT: IMetadataNFT(addresses.metadataNFT), - stabilityPool: IStabilityPool(addresses.stabilityPool), + stabilityPool: contracts.stabilityPool, priceFeed: contracts.priceFeed, activePool: IActivePool(addresses.activePool), defaultPool: IDefaultPool(addresses.defaultPool), @@ -728,18 +347,21 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, collSurplusPool: ICollSurplusPool(addresses.collSurplusPool), sortedTroves: ISortedTroves(addresses.sortedTroves), interestRouter: contracts.interestRouter, - hintHelpers: _hintHelpers, - multiTroveGetter: _multiTroveGetter, - collateralRegistry: _collateralRegistry, - boldToken: _boldToken, - WETH: WETH + hintHelpers: r.hintHelpers, + multiTroveGetter: r.multiTroveGetter, + collateralRegistry: r.collateralRegistry, + boldToken: IBoldToken(address(r.stableToken)), + gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS) }); contracts.addressesRegistry.setAddresses(addressVars); + } + function _deployProtocolContracts(LiquityContracts memory contracts, LiquityContractAddresses memory addresses) + internal + { contracts.borrowerOperations = new BorrowerOperations{salt: SALT}(contracts.addressesRegistry); contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry); contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); - contracts.stabilityPool = new StabilityPool{salt: SALT}(contracts.addressesRegistry); contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry); contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); @@ -749,344 +371,19 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, assert(address(contracts.borrowerOperations) == addresses.borrowerOperations); assert(address(contracts.troveManager) == addresses.troveManager); assert(address(contracts.troveNFT) == addresses.troveNFT); - assert(address(contracts.stabilityPool) == addresses.stabilityPool); assert(address(contracts.activePool) == addresses.activePool); assert(address(contracts.defaultPool) == addresses.defaultPool); assert(address(contracts.gasPool) == addresses.gasPool); assert(address(contracts.collSurplusPool) == addresses.collSurplusPool); assert(address(contracts.sortedTroves) == addresses.sortedTroves); - - // Connect contracts - _boldToken.setBranchAddresses( - address(contracts.troveManager), - address(contracts.stabilityPool), - address(contracts.borrowerOperations), - address(contracts.activePool) - ); - - // deploy zappers - (contracts.gasCompZapper, contracts.wethZapper, contracts.leverageZapper) = - _deployZappers(contracts.addressesRegistry, contracts.collToken, _boldToken, _usdcCurvePool); - } - - function _deployPriceFeed(address _collTokenAddress, address _borroweOperationsAddress) - internal - returns (IPriceFeed) - { - if (block.chainid == 1 && !useTestnetPriceFeeds) { - // mainnet - // ETH - if (_collTokenAddress == address(WETH)) { - return new WETHPriceFeed(ETH_ORACLE_ADDRESS, ETH_USD_STALENESS_THRESHOLD, _borroweOperationsAddress); - } else if (_collTokenAddress == WSTETH_ADDRESS) { - // wstETH - return new WSTETHPriceFeed( - ETH_ORACLE_ADDRESS, - STETH_ORACLE_ADDRESS, - WSTETH_ADDRESS, - ETH_USD_STALENESS_THRESHOLD, - STETH_USD_STALENESS_THRESHOLD, - _borroweOperationsAddress - ); - } - // RETH - assert(_collTokenAddress == RETH_ADDRESS); - return new RETHPriceFeed( - ETH_ORACLE_ADDRESS, - RETH_ORACLE_ADDRESS, - RETH_ADDRESS, - ETH_USD_STALENESS_THRESHOLD, - RETH_ETH_STALENESS_THRESHOLD, - _borroweOperationsAddress - ); - } - - // Sepolia - return new PriceFeedTestnet(); - } - - function _deployZappers( - IAddressesRegistry _addressesRegistry, - IERC20 _collToken, - IBoldToken _boldToken, - ICurveStableswapNGPool _usdcCurvePool - ) internal returns (GasCompZapper gasCompZapper, WETHZapper wethZapper, ILeverageZapper leverageZapper) { - IFlashLoanProvider flashLoanProvider = new BalancerFlashLoan(); - - IExchange hybridExchange = new HybridCurveUniV3Exchange( - _collToken, - _boldToken, - USDC, - WETH, - _usdcCurvePool, - OTHER_TOKEN_INDEX, // USDC Curve pool index - BOLD_TOKEN_INDEX, // BOLD Curve pool index - UNIV3_FEE_USDC_WETH, - UNIV3_FEE_WETH_COLL, - uniV3Router - ); - - bool lst = _collToken != WETH; - if (lst) { - gasCompZapper = new GasCompZapper(_addressesRegistry, flashLoanProvider, hybridExchange); - } else { - wethZapper = new WETHZapper(_addressesRegistry, flashLoanProvider, hybridExchange); - } - leverageZapper = _deployHybridLeverageZapper(_addressesRegistry, flashLoanProvider, hybridExchange, lst); } - function _deployHybridLeverageZapper( - IAddressesRegistry _addressesRegistry, - IFlashLoanProvider _flashLoanProvider, - IExchange _hybridExchange, - bool _lst - ) internal returns (ILeverageZapper) { - ILeverageZapper leverageZapperHybrid; - if (_lst) { - leverageZapperHybrid = new LeverageLSTZapper(_addressesRegistry, _flashLoanProvider, _hybridExchange); - } else { - leverageZapperHybrid = new LeverageWETHZapper(_addressesRegistry, _flashLoanProvider, _hybridExchange); - } - - return leverageZapperHybrid; - } - - function _deployCurvePool(IBoldToken _boldToken, IERC20Metadata _otherToken) + function _computeCreate2Address(bytes memory creationCode, address _addressesRegistry) internal - returns (ICurveStableswapNGPool) + view + returns (address) { - if (block.chainid == 31337) { - // local - return ICurveStableswapNGPool(address(0)); - } - - // deploy Curve StableswapNG pool - address[] memory coins = new address[](2); - coins[BOLD_TOKEN_INDEX] = address(_boldToken); - coins[OTHER_TOKEN_INDEX] = address(_otherToken); - uint8[] memory assetTypes = new uint8[](2); // 0: standard - bytes4[] memory methodIds = new bytes4[](2); - address[] memory oracles = new address[](2); - - ICurveStableswapNGPool curvePool = curveStableswapFactory.deploy_plain_pool({ - name: string.concat("BOLD/", _otherToken.symbol(), " Pool"), - symbol: string.concat("BOLD", _otherToken.symbol()), - coins: coins, - A: 100, - fee: 4000000, - offpeg_fee_multiplier: 20000000000, - ma_exp_time: 866, - implementation_id: 0, - asset_types: assetTypes, - method_ids: methodIds, - oracles: oracles - }); - - return curvePool; - } - - function _provideFlashloanLiquidity(ERC20Faucet _collToken, ERC20Faucet _monkeyBalls) internal { - uint256[] memory amountsIn = new uint256[](2); - amountsIn[0] = 1_000_000 ether; - amountsIn[1] = 1_000_000 ether; - - _collToken.mint(deployer, amountsIn[0]); - _monkeyBalls.mint(deployer, amountsIn[1]); - - IERC20[] memory tokens = new IERC20[](2); - (tokens[0], tokens[1]) = - address(_collToken) < address(_monkeyBalls) ? (_collToken, _monkeyBalls) : (_monkeyBalls, _collToken); - - uint256[] memory normalizedWeights = new uint256[](2); - normalizedWeights[0] = 0.5 ether; - normalizedWeights[1] = 0.5 ether; - - IWeightedPool pool = balancerFactorySepolia.create({ - name: string.concat(_collToken.name(), "-", _monkeyBalls.name()), - symbol: string.concat("bpt", _collToken.symbol(), _monkeyBalls.symbol()), - tokens: tokens, - normalizedWeights: normalizedWeights, - rateProviders: new IRateProvider[](2), // all zeroes - swapFeePercentage: 0.000001 ether, // 0.0001%, which is the minimum allowed - owner: deployer, - salt: bytes32("NaCl") - }); - - _collToken.approve(address(balancerVault), amountsIn[0]); - _monkeyBalls.approve(address(balancerVault), amountsIn[1]); - - balancerVault.joinPool( - pool.getPoolId(), - deployer, - deployer, - IVault.JoinPoolRequest({ - assets: tokens, - maxAmountsIn: amountsIn, - userData: abi.encode(IWeightedPool.JoinKind.INIT, amountsIn), - fromInternalBalance: false - }) - ); - } - - function _mintBold(uint256 _boldAmount, uint256 _price, LiquityContracts memory _contracts) internal { - uint256 collAmount = _boldAmount * 2 ether / _price; // CR of ~200% - - ERC20Faucet(address(_contracts.collToken)).mint(deployer, collAmount); - WETHTester(payable(address(WETH))).mint(deployer, ETH_GAS_COMPENSATION); - - if (_contracts.collToken == WETH) { - WETH.approve(address(_contracts.borrowerOperations), collAmount + ETH_GAS_COMPENSATION); - } else { - _contracts.collToken.approve(address(_contracts.borrowerOperations), collAmount); - WETH.approve(address(_contracts.borrowerOperations), ETH_GAS_COMPENSATION); - } - - _contracts.borrowerOperations.openTrove({ - _owner: deployer, - _ownerIndex: lastTroveIndex++, - _ETHAmount: collAmount, - _boldAmount: _boldAmount, - _upperHint: 0, - _lowerHint: 0, - _annualInterestRate: 0.05 ether, - _maxUpfrontFee: type(uint256).max, - _addManager: address(0), - _removeManager: address(0), - _receiver: address(0) - }); - } - - struct ProvideUniV3LiquidityVars { - uint256 token2Amount; - address[2] tokens; - uint256[2] amounts; - uint256 price; - int24 tickLower; - int24 tickUpper; - } - - // _price should be _token1 / _token2 - function _provideUniV3Liquidity( - ERC20Faucet _token1, - ERC20Faucet _token2, - uint256 _token1Amount, - uint256 _price, - uint24 _fee - ) internal { - ProvideUniV3LiquidityVars memory vars; - // tokens and amounts - vars.token2Amount = _token1Amount * DECIMAL_PRECISION / _price; - - if (address(_token1) < address(_token2)) { - vars.tokens[0] = address(_token1); - vars.tokens[1] = address(_token2); - vars.amounts[0] = _token1Amount; - vars.amounts[1] = vars.token2Amount; - // inverse price if token1 goes first - vars.price = DECIMAL_PRECISION * DECIMAL_PRECISION / _price; - } else { - vars.tokens[0] = address(_token2); - vars.tokens[1] = address(_token1); - vars.amounts[0] = vars.token2Amount; - vars.amounts[1] = _token1Amount; - vars.price = _price; - } - - //console2.log(priceToSqrtPriceX96(vars.price), "_priceToSqrtPrice(price)"); - uniV3PositionManagerSepolia.createAndInitializePoolIfNecessary( - vars.tokens[0], vars.tokens[1], _fee, priceToSqrtPriceX96(vars.price) - ); - - // mint and approve - _token1.mint(deployer, _token1Amount); - _token2.mint(deployer, vars.token2Amount); - _token1.approve(address(uniV3PositionManagerSepolia), _token1Amount); - _token2.approve(address(uniV3PositionManagerSepolia), vars.token2Amount); - - // mint new position - address uniV3PoolAddress = uniswapV3FactorySepolia.getPool(vars.tokens[0], vars.tokens[1], _fee); - int24 TICK_SPACING = IUniswapV3Pool(uniV3PoolAddress).tickSpacing(); - ( /* uint256 finalSqrtPriceX96 */ , int24 tick,,,,,) = IUniswapV3Pool(uniV3PoolAddress).slot0(); - //console2.log(finalSqrtPriceX96, "finalSqrtPriceX96"); - vars.tickLower = (tick - 60) / TICK_SPACING * TICK_SPACING; - vars.tickUpper = (tick + 60) / TICK_SPACING * TICK_SPACING; - - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - token0: vars.tokens[0], - token1: vars.tokens[1], - fee: _fee, - tickLower: vars.tickLower, - tickUpper: vars.tickUpper, - amount0Desired: vars.amounts[0], - amount1Desired: vars.amounts[1], - amount0Min: 0, - amount1Min: 0, - recipient: deployer, - deadline: block.timestamp + 600 minutes - }); - - uniV3PositionManagerSepolia.mint(params); - //(finalSqrtPriceX96, tick,,,,,) = IUniswapV3Pool(uniV3PoolAddress).slot0(); - //console2.log(finalSqrtPriceX96, "finalSqrtPriceX96"); - - /* - console2.log("--"); - console2.log(_token1.name()); - console2.log(address(_token1), "address(_token1)"); - console2.log(_token1Amount, "_token1Amount"); - console2.log(_token1.balanceOf(uniV3PoolAddress), "token1.balanceOf(pool)"); - console2.log(_token2.name()); - console2.log(address(_token2), "address(_token2)"); - console2.log(vars.token2Amount, "token2Amount"); - console2.log(_token2.balanceOf(uniV3PoolAddress), "token2.balanceOf(pool)"); - */ - } - - function _priceToSqrtPrice(uint256 _price) public pure returns (uint160) { - return uint160(Math.sqrt((_price << 192) / DECIMAL_PRECISION)); - } - - function _provideCurveLiquidity(IBoldToken _boldToken, LiquityContracts memory _contracts) internal { - ICurveStableswapNGPool usdcCurvePool = - HybridCurveUniV3Exchange(address(_contracts.leverageZapper.exchange())).curvePool(); - // Add liquidity to USDC-BOLD - //uint256 usdcAmount = 1e15; // 1B with 6 decimals - //boldAmount = usdcAmount * 1e12; // from 6 to 18 decimals - uint256 usdcAmount = 1e27; - uint256 boldAmount = usdcAmount; - - // mint - ERC20Faucet(address(USDC)).mint(deployer, usdcAmount); - (uint256 price,) = _contracts.priceFeed.fetchPrice(); - _mintBold(boldAmount, price, _contracts); - // approve - USDC.approve(address(usdcCurvePool), usdcAmount); - _boldToken.approve(address(usdcCurvePool), boldAmount); - - uint256[] memory amountsDynamic = new uint256[](2); - amountsDynamic[0] = boldAmount; - amountsDynamic[1] = usdcAmount; - // add liquidity - usdcCurvePool.add_liquidity(amountsDynamic, 0); - } - - function formatAmount(uint256 amount, uint256 decimals, uint256 digits) internal pure returns (string memory) { - if (digits > decimals) { - digits = decimals; - } - - uint256 scaled = amount / (10 ** (decimals - digits)); - string memory whole = Strings.toString(scaled / (10 ** digits)); - - if (digits == 0) { - return whole; - } - - string memory fractional = Strings.toString(scaled % (10 ** digits)); - for (uint256 i = bytes(fractional).length; i < digits; i++) { - fractional = string.concat("0", fractional); - } - return string.concat(whole, ".", fractional); + return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry))); } function _getBranchContractsJson(LiquityContracts memory c) internal view returns (string memory) { @@ -1111,12 +408,7 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, string.concat('"metadataNFT":"', address(c.metadataNFT).toHexString(), '",'), string.concat('"priceFeed":"', address(c.priceFeed).toHexString(), '",'), string.concat('"gasPool":"', address(c.gasPool).toHexString(), '",'), - string.concat('"interestRouter":"', address(c.interestRouter).toHexString(), '",'), - string.concat('"wethZapper":"', address(c.wethZapper).toHexString(), '",') - ), - string.concat( - string.concat('"gasCompZapper":"', address(c.gasCompZapper).toHexString(), '",'), - string.concat('"leverageZapper":"', address(c.leverageZapper).toHexString(), '"') // no comma + string.concat('"interestRouter":"', address(c.interestRouter).toHexString(), '",') ) ), "}" @@ -1139,30 +431,22 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, ); } - function _getManifestJson(DeploymentResult memory deployed, string memory _governanceManifest) - internal - view - returns (string memory) - { - string[] memory branches = new string[](deployed.contractsArray.length); + function _getManifestJson(DeploymentResult memory deployed) internal view returns (string memory) { + string[] memory branches = new string[](1); - // Poor man's .map() - for (uint256 i = 0; i < branches.length; ++i) { - branches[i] = _getBranchContractsJson(deployed.contractsArray[i]); - } + branches[0] = _getBranchContractsJson(deployed.contracts); return string.concat( "{", - string.concat( - string.concat('"constants":', _getDeploymentConstants(), ","), - string.concat('"collateralRegistry":"', address(deployed.collateralRegistry).toHexString(), '",'), - string.concat('"boldToken":"', address(deployed.boldToken).toHexString(), '",'), - string.concat('"hintHelpers":"', address(deployed.hintHelpers).toHexString(), '",'), - string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",'), - string.concat('"exchangeHelpers":"', address(deployed.exchangeHelpers).toHexString(), '",'), - string.concat('"branches":[', branches.join(","), "],"), - string.concat('"governance":', _governanceManifest, "") // no comma - ), + string.concat('"constants":', _getDeploymentConstants(), ","), + string.concat('"collateralRegistry":"', address(deployed.collateralRegistry).toHexString(), '",'), + string.concat('"boldToken":"', address(deployed.stableToken).toHexString(), '",'), + string.concat('"hintHelpers":"', address(deployed.hintHelpers).toHexString(), '",'), + string.concat('"stableTokenV3Impl":"', address(deployed.stableTokenV3Impl).toHexString(), '",'), + string.concat('"stabilityPoolImpl":"', address(deployed.stabilityPoolImpl).toHexString(), '",'), + string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",'), + string.concat('"fpmm":"', address(deployed.fpmm).toHexString(), '",'), + string.concat('"branches":[', branches.join(","), "]"), "}" ); } diff --git a/contracts/script/DeployOnlyExchangeHelpers.s.sol b/contracts/script/DeployOnlyExchangeHelpers.s.sol deleted file mode 100644 index 06385edd1..000000000 --- a/contracts/script/DeployOnlyExchangeHelpers.s.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -import {Script} from "forge-std/Script.sol"; - -import "src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGPool.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol"; - -import "forge-std/console2.sol"; - -contract DeployOnlyExchangeHelpers is Script { - IERC20 constant USDC = IERC20(0xc4f4dE29be4d05EA0644dfebb44a87a48E3BfcCE); - IWETH constant WETH = IWETH(0xbCDdC15adbe087A75526C0b7273Fcdd27bE9dD18); - ICurveStableswapNGPool constant usdcCurvePool = ICurveStableswapNGPool(0xdCD2D012C1A4fc509763657ED24b83c8Fe6cf756); - - uint128 constant BOLD_TOKEN_INDEX = 0; - uint128 constant USDC_INDEX = 1; - - uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05% - uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01% - IQuoterV2 constant uniV3QuoterSepolia = IQuoterV2(0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3); - - address deployer; - - function run() external { - if (vm.envBytes("DEPLOYER").length == 20) { - // address - deployer = vm.envAddress("DEPLOYER"); - vm.startBroadcast(deployer); - } else { - // private key - uint256 privateKey = vm.envUint("DEPLOYER"); - deployer = vm.addr(privateKey); - vm.startBroadcast(privateKey); - } - - console2.log(deployer, "deployer"); - console2.log(deployer.balance, "deployer balance"); - - IExchangeHelpers exchangeHelpers = new HybridCurveUniV3ExchangeHelpers( - USDC, - WETH, - usdcCurvePool, - USDC_INDEX, // USDC Curve pool index - BOLD_TOKEN_INDEX, // BOLD Curve pool index - UNIV3_FEE_USDC_WETH, - UNIV3_FEE_WETH_COLL, - uniV3QuoterSepolia - ); - console2.log(address(exchangeHelpers), "exchangeHelpers"); - } -} diff --git a/contracts/script/DeploySomeCurvePools.s.sol b/contracts/script/DeploySomeCurvePools.s.sol deleted file mode 100644 index 81ad6f857..000000000 --- a/contracts/script/DeploySomeCurvePools.s.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; -import {ICurveStableSwapFactoryNG} from "test/Interfaces/Curve/ICurveStableSwapFactoryNG.sol"; -import {ERC20Faucet} from "test/TestContracts/ERC20Faucet.sol"; - -contract DeploySomeCurvePools is Script { - using Strings for *; - - ICurveStableSwapFactoryNG constant factory = ICurveStableSwapFactoryNG(0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf); - - function run() external { - vm.startBroadcast(); - - for (uint256 i = 1; i <= 3; ++i) { - address[] memory coins = new address[](2); - uint8[] memory assetTypes = new uint8[](2); - bytes4[] memory methodIds = new bytes4[](2); - address[] memory oracles = new address[](2); - - coins[0] = address( - new ERC20Faucet({ - _name: string.concat("Coin #", i.toString(), ".1"), - _symbol: string.concat("COIN", i.toString(), "1"), - _tapAmount: 0, - _tapPeriod: 0 - }) - ); - - coins[1] = address( - new ERC20Faucet({ - _name: string.concat("Coin #", i.toString(), ".2"), - _symbol: string.concat("COIN", i.toString(), "2"), - _tapAmount: 0, - _tapPeriod: 0 - }) - ); - - factory.deploy_plain_pool({ - _name: string.concat("Fancy Pool #", i.toString()), - _symbol: string.concat(string.concat("POOL", i.toString())), - _coins: coins, - _A: 100, - _fee: 4000000, - _offpeg_fee_multiplier: 20000000000, - _ma_exp_time: 866, - _implementation_idx: 0, - _asset_types: assetTypes, - _method_ids: methodIds, - _oracles: oracles - }); - } - } -} diff --git a/contracts/script/GenerateStakingRewards.s.sol b/contracts/script/GenerateStakingRewards.s.sol deleted file mode 100644 index 0c9ac184a..000000000 --- a/contracts/script/GenerateStakingRewards.s.sol +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {Math} from "openzeppelin-contracts/contracts/utils/math/Math.sol"; -import {IBorrowerOperationsV1} from "test/Interfaces/LiquityV1/IBorrowerOperationsV1.sol"; -import {IPriceFeedV1} from "test/Interfaces/LiquityV1/IPriceFeedV1.sol"; -import {ISortedTrovesV1} from "test/Interfaces/LiquityV1/ISortedTrovesV1.sol"; -import {ITroveManagerV1} from "test/Interfaces/LiquityV1/ITroveManagerV1.sol"; - -IBorrowerOperationsV1 constant borrowerOperations = IBorrowerOperationsV1(0x24179CD81c9e782A4096035f7eC97fB8B783e007); -IPriceFeedV1 constant priceFeed = IPriceFeedV1(0x4c517D4e2C851CA76d7eC94B805269Df0f2201De); -ISortedTrovesV1 constant sortedTroves = ISortedTrovesV1(0x8FdD3fbFEb32b28fb73555518f8b361bCeA741A6); -ITroveManagerV1 constant troveManager = ITroveManagerV1(0xA39739EF8b0231DbFA0DcdA07d7e29faAbCf4bb2); - -contract Runner { - function _revert(bytes memory revertData) internal pure { - assembly { - revert(add(32, revertData), mload(revertData)) - } - } - - function run() external payable { - uint256 borrowedLusd = 1_000_000 ether; - uint256 redeemedLusd = 1_000 ether; - - uint256 price = priceFeed.fetchPrice(); - address lastTrove = sortedTroves.getLast(); - - if (troveManager.getCurrentICR(lastTrove, price) < 1.1 ether) { - troveManager.liquidateTroves(50); - lastTrove = sortedTroves.getLast(); - require(troveManager.getCurrentICR(lastTrove, price) >= 1.1 ether, "too much to liquidate, try again"); - } - - uint256 borrowingRate = troveManager.getBorrowingRateWithDecay(); - uint256 borrowingFee = borrowedLusd * borrowingRate / 1 ether; - uint256 debt = borrowedLusd + borrowingFee + 200 ether; - - uint256 coll = Math.ceilDiv(debt * 1.1 ether, price); - require(address(this).balance >= coll, "balance < coll"); - - borrowerOperations.openTrove{value: coll}({ - _LUSDAmount: borrowedLusd, - _maxFeePercentage: borrowingRate, - _upperHint: lastTrove, - _lowerHint: address(0) - }); - - require(sortedTroves.getLast() == address(this), "last Trove != new Trove"); - - uint256 redeemedColl = redeemedLusd * 1 ether / price; - uint256 balanceBefore = address(this).balance; - - troveManager.redeemCollateral({ - _LUSDamount: redeemedLusd, - _maxFeePercentage: 1 ether, - _maxIterations: 1, - _firstRedemptionHint: address(this), - _upperPartialRedemptionHint: lastTrove, - _lowerPartialRedemptionHint: lastTrove, - _partialRedemptionHintNICR: (coll - redeemedColl) * 100 ether / (debt - redeemedLusd) - }); - - uint256 redemptionFee = redeemedColl * troveManager.getBorrowingRateWithDecay() / 1 ether; - require(address(this).balance - balanceBefore == redeemedColl - redemptionFee, "coll received != expected"); - - (bool success, bytes memory returnData) = msg.sender.call{value: address(this).balance}(""); - if (!success) _revert(returnData); - } - - receive() external payable {} -} - -contract GenerateStakingRewards is Script { - function run() external { - vm.startBroadcast(); - - Runner runner = new Runner(); - runner.run{value: msg.sender.balance * 9 / 10}(); - } -} diff --git a/contracts/script/Interfaces/Balancer/IVault.sol b/contracts/script/Interfaces/Balancer/IVault.sol deleted file mode 100644 index d30f9e62c..000000000 --- a/contracts/script/Interfaces/Balancer/IVault.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -interface IVault { - struct JoinPoolRequest { - IERC20[] assets; - uint256[] maxAmountsIn; - bytes userData; - bool fromInternalBalance; - } - - function joinPool(bytes32 poolId, address sender, address recipient, JoinPoolRequest memory request) external; -} diff --git a/contracts/script/Interfaces/Balancer/IWeightedPool.sol b/contracts/script/Interfaces/Balancer/IWeightedPool.sol deleted file mode 100644 index 8f4ca38ca..000000000 --- a/contracts/script/Interfaces/Balancer/IWeightedPool.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -interface IRateProvider {} - -interface IWeightedPoolFactory { - function create( - string memory name, - string memory symbol, - IERC20[] memory tokens, - uint256[] memory normalizedWeights, - IRateProvider[] memory rateProviders, - uint256 swapFeePercentage, - address owner, - bytes32 salt - ) external returns (IWeightedPool); -} - -interface IWeightedPool { - enum JoinKind { - INIT - } - - function getPoolId() external view returns (bytes32); -} diff --git a/contracts/script/LiquidateTrove.s.sol b/contracts/script/LiquidateTrove.s.sol deleted file mode 100644 index 6d0411ae4..000000000 --- a/contracts/script/LiquidateTrove.s.sol +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; -import {IAddressesRegistry} from "src/Interfaces/IAddressesRegistry.sol"; -import {ICollateralRegistry} from "src/Interfaces/ICollateralRegistry.sol"; -import {LatestTroveData} from "src/Types/LatestTroveData.sol"; -import {ITroveManager} from "src/Interfaces/ITroveManager.sol"; -import {IPriceFeedTestnet} from "test/TestContracts/Interfaces/IPriceFeedTestnet.sol"; - -contract LiquidateTrove is Script { - using Strings for uint256; - - function run() external { - vm.startBroadcast(); - - IAddressesRegistry addressesRegistry; - try vm.envAddress("ADDRESSES_REGISTRY") returns (address value) { - addressesRegistry = IAddressesRegistry(value); - } catch { - uint256 i = vm.envUint("BRANCH"); - string memory manifestJson = vm.readFile("deployment-manifest.json"); - addressesRegistry = IAddressesRegistry( - vm.parseJsonAddress(manifestJson, string.concat(".branches[", i.toString(), "].addressesRegistry")) - ); - } - vm.label(address(addressesRegistry), "AddressesRegistry"); - - ITroveManager troveManager = addressesRegistry.troveManager(); - vm.label(address(troveManager), "TroveManager"); - IPriceFeedTestnet priceFeed = IPriceFeedTestnet(address(addressesRegistry.priceFeed())); - vm.label(address(priceFeed), "PriceFeedTestnet"); - - uint256 troveId = vm.envUint("TROVE_ID"); - LatestTroveData memory trove = troveManager.getLatestTroveData(troveId); - - uint256 originalPrice = priceFeed.getPrice(); - uint256 liquidationPrice = (addressesRegistry.MCR() - 0.01 ether) * trove.entireDebt / trove.entireColl; - priceFeed.setPrice(liquidationPrice); - - uint256[] memory troveIds = new uint256[](1); - troveIds[0] = troveId; - troveManager.batchLiquidateTroves(troveIds); - - priceFeed.setPrice(originalPrice); - } -} diff --git a/contracts/script/OpenTroves.s.sol b/contracts/script/OpenTroves.s.sol deleted file mode 100644 index 6d92754fb..000000000 --- a/contracts/script/OpenTroves.s.sol +++ /dev/null @@ -1,178 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import {Clones} from "openzeppelin-contracts/contracts/proxy/Clones.sol"; -import {ERC20Faucet} from "test/TestContracts/ERC20Faucet.sol"; -import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; -import {ICollateralRegistry} from "src/Interfaces/ICollateralRegistry.sol"; -import {IHintHelpers} from "src/Interfaces/IHintHelpers.sol"; -import {ISortedTroves} from "src/Interfaces/ISortedTroves.sol"; -import {ITroveManager} from "src/Interfaces/ITroveManager.sol"; -import {ITroveNFT} from "src/Interfaces/ITroveNFT.sol"; - -import { - ETH_GAS_COMPENSATION, - MAX_ANNUAL_INTEREST_RATE, - MIN_ANNUAL_INTEREST_RATE, - MIN_INTEREST_RATE_CHANGE_PERIOD -} from "src/Dependencies/Constants.sol"; - -function sqrt(uint256 y) pure returns (uint256 z) { - if (y > 3) { - z = y; - uint256 x = y / 2 + 1; - while (x < z) { - z = x; - x = (y / x + x) / 2; - } - } else if (y != 0) { - z = 1; - } -} - -contract Proxy { - function tap(ERC20Faucet faucet) external { - faucet.tap(); - faucet.transfer(msg.sender, faucet.balanceOf(address(this))); - } - - function sweepTrove(ITroveNFT nft, uint256 troveId) external { - nft.transferFrom(address(this), msg.sender, troveId); - } -} - -contract OpenTroves is Script { - struct BranchContracts { - ERC20Faucet collateral; - ITroveManager troveManager; - ISortedTroves sortedTroves; - IBorrowerOperations borrowerOperations; - ITroveNFT nft; - } - - function _findHints(IHintHelpers hintHelpers, BranchContracts memory c, uint256 branch, uint256 interestRate) - internal - view - returns (uint256 upperHint, uint256 lowerHint) - { - // Find approx hint (off-chain) - (uint256 approxHint,,) = hintHelpers.getApproxHint({ - _collIndex: branch, - _interestRate: interestRate, - _numTrials: sqrt(100 * c.troveManager.getTroveIdsCount()), - _inputRandomSeed: block.timestamp - }); - - // Find concrete insert position (off-chain) - (upperHint, lowerHint) = c.sortedTroves.findInsertPosition(interestRate, approxHint, approxHint); - } - - function run() external { - vm.startBroadcast(); - - string memory manifestJson; - try vm.readFile("deployment-manifest.json") returns (string memory content) { - manifestJson = content; - } catch {} - - ICollateralRegistry collateralRegistry; - try vm.envAddress("COLLATERAL_REGISTRY") returns (address value) { - collateralRegistry = ICollateralRegistry(value); - } catch { - collateralRegistry = ICollateralRegistry(vm.parseJsonAddress(manifestJson, ".collateralRegistry")); - } - vm.label(address(collateralRegistry), "CollateralRegistry"); - - IHintHelpers hintHelpers; - try vm.envAddress("HINT_HELPERS") returns (address value) { - hintHelpers = IHintHelpers(value); - } catch { - hintHelpers = IHintHelpers(vm.parseJsonAddress(manifestJson, ".hintHelpers")); - } - vm.label(address(hintHelpers), "HintHelpers"); - - address proxyImplementation = address(new Proxy()); - vm.label(proxyImplementation, "ProxyImplementation"); - - ERC20Faucet weth = ERC20Faucet(address(collateralRegistry.getToken(0))); // branch #0 is WETH - uint256 numBranches = collateralRegistry.totalCollaterals(); - - for (uint256 branch = 0; branch < numBranches; ++branch) { - BranchContracts memory c; - c.collateral = ERC20Faucet(address(collateralRegistry.getToken(branch))); - vm.label(address(c.collateral), "ERC20Faucet"); - c.troveManager = collateralRegistry.getTroveManager(branch); - vm.label(address(c.troveManager), "TroveManager"); - c.sortedTroves = c.troveManager.sortedTroves(); - vm.label(address(c.sortedTroves), "SortedTroves"); - c.borrowerOperations = c.troveManager.borrowerOperations(); - vm.label(address(c.borrowerOperations), "BorrowerOperations"); - c.nft = c.troveManager.troveNFT(); - vm.label(address(c.nft), "TroveNFT"); - - if (c.borrowerOperations.getInterestBatchManager(msg.sender).maxInterestRate == 0) { - // Register ourselves as batch manager, if we haven't - c.borrowerOperations.registerBatchManager({ - minInterestRate: uint128(MIN_ANNUAL_INTEREST_RATE), - maxInterestRate: uint128(MAX_ANNUAL_INTEREST_RATE), - currentInterestRate: 0.025 ether, - fee: 0.001 ether, - minInterestRateChangePeriod: MIN_INTEREST_RATE_CHANGE_PERIOD - }); - } - - for (uint256 i = 1; i <= 4; ++i) { - Proxy proxy = Proxy(Clones.clone(proxyImplementation)); - vm.label(address(proxy), "Proxy"); - - proxy.tap(c.collateral); - uint256 ethAmount = c.collateral.tapAmount() / 2; - - if (branch == 0) { - // collateral == WETH - c.collateral.approve(address(c.borrowerOperations), ethAmount + ETH_GAS_COMPENSATION); - } else { - proxy.tap(weth); - c.collateral.approve(address(c.borrowerOperations), ethAmount); - weth.approve(address(c.borrowerOperations), ETH_GAS_COMPENSATION); - } - - uint256 interestRate = i * 0.01 ether; - (uint256 upperHint, uint256 lowerHint) = _findHints(hintHelpers, c, branch, interestRate); - - uint256 troveId = c.borrowerOperations.openTrove({ - _owner: address(proxy), - _ownerIndex: 0, - _ETHAmount: ethAmount, - _boldAmount: 2_000 ether, - _upperHint: upperHint, - _lowerHint: lowerHint, - _annualInterestRate: interestRate, - _maxUpfrontFee: type(uint256).max, // we don't care about fee slippage - _addManager: address(0), - _removeManager: address(0), - _receiver: address(0) - }); - - proxy.sweepTrove(c.nft, troveId); - c.collateral.transfer(address(0xdead), c.collateral.balanceOf(msg.sender)); - if (branch != 0) weth.transfer(address(0xdead), weth.balanceOf(msg.sender)); - - if (i % 2 == 0) { - interestRate = c.troveManager.getLatestBatchData(msg.sender).annualInterestRate; - (upperHint, lowerHint) = _findHints(hintHelpers, c, branch, interestRate); - - // Have every 2nd Trove delegate to us - c.borrowerOperations.setInterestBatchManager({ - _troveId: troveId, - _newBatchManager: msg.sender, - _upperHint: upperHint, - _lowerHint: lowerHint, - _maxUpfrontFee: type(uint256).max // we don't care about fee slippage - }); - } - } - } - } -} diff --git a/contracts/script/ProvideCurveLiquidity.s.sol b/contracts/script/ProvideCurveLiquidity.s.sol deleted file mode 100644 index 554a41ba7..000000000 --- a/contracts/script/ProvideCurveLiquidity.s.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import {UseDeployment} from "test/Utils/UseDeployment.sol"; - -contract ProvideCurveLiquidity is Script, UseDeployment { - function run() external { - vm.startBroadcast(); - _loadDeploymentFromManifest("deployment-manifest.json"); - - uint256 boldAmount = 200_000 ether; - uint256 usdcAmount = boldAmount * 10 ** usdc.decimals() / 10 ** boldToken.decimals(); - - uint256[] memory amounts = new uint256[](2); - (amounts[0], amounts[1]) = curveUsdcBold.coins(0) == BOLD ? (boldAmount, usdcAmount) : (usdcAmount, boldAmount); - - boldToken.approve(address(curveUsdcBold), boldAmount); - usdc.approve(address(curveUsdcBold), usdcAmount); - curveUsdcBold.add_liquidity(amounts, 0); - } -} diff --git a/contracts/script/ProvideUniV3Liquidity.s.sol b/contracts/script/ProvideUniV3Liquidity.s.sol deleted file mode 100644 index 73f5d79a9..000000000 --- a/contracts/script/ProvideUniV3Liquidity.s.sol +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import "openzeppelin-contracts/contracts/utils/math/Math.sol"; - -import {ERC20Faucet} from "test/TestContracts/ERC20Faucet.sol"; -import {WETHTester} from "test/TestContracts/WETHTester.sol"; - -import "src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Pool.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Factory.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol"; - -import "forge-std/console2.sol"; - -contract ProvideUniV3Liquidity is Script { - uint256 constant DECIMAL_PRECISION = 1e18; - - uint24 constant UNIV3_FEE = 0.3e4; - uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05% - uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01% - ISwapRouter constant uniV3RouterSepolia = ISwapRouter(0x65669fE35312947050C450Bd5d36e6361F85eC12); - IQuoterV2 constant uniV3QuoterSepolia = IQuoterV2(0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3); - IUniswapV3Factory constant uniswapV3FactorySepolia = IUniswapV3Factory(0x0227628f3F023bb0B980b67D528571c95c6DaC1c); - INonfungiblePositionManager constant uniV3PositionManagerSepolia = - INonfungiblePositionManager(0x1238536071E1c677A632429e3655c799b22cDA52); - - WETHTester constant WETH = WETHTester(payable(0x3e8Bd35e898505EE0dD29277ee42eD92021C82aF)); - ERC20Faucet constant usdc = ERC20Faucet(0xF00ad39d0aC1A422DAB5A2EceBAa5268ea909aD4); - ERC20Faucet constant wstETH = ERC20Faucet(0xC5958986793086593871f207975053cf66d0B764); - ERC20Faucet constant rETH = ERC20Faucet(0x078c20A159eA4EdF8d029Fb21E6bd120455B4acc); - - address deployer; - - function run() external { - if (vm.envBytes("DEPLOYER").length == 20) { - // address - deployer = vm.envAddress("DEPLOYER"); - vm.startBroadcast(deployer); - } else { - // private key - uint256 privateKey = vm.envUint("DEPLOYER"); - deployer = vm.addr(privateKey); - vm.startBroadcast(privateKey); - } - - console2.log(deployer, "deployer"); - console2.log(deployer.balance, "deployer balance"); - - uint256 price = 2_000 ether; - - // WETH - console2.log("WETH"); - uint256 token1Amount = 1_000_000 ether; - _provideUniV3Liquidity(usdc, WETH, token1Amount, price, UNIV3_FEE_USDC_WETH); - - token1Amount = 1_000 ether; - - // wstETH - console2.log("wstETH"); - _provideUniV3Liquidity(WETH, wstETH, token1Amount, 1 ether, UNIV3_FEE_WETH_COLL); - - // rETH - console2.log("rETH"); - _provideUniV3Liquidity(WETH, rETH, token1Amount, 1 ether, UNIV3_FEE_WETH_COLL); - } - - // _price should be _token1 / _token2 - function _provideUniV3Liquidity( - ERC20Faucet _token1, - ERC20Faucet _token2, - uint256 _token1Amount, - uint256 _price, - uint24 _fee - ) internal { - // tokens and amounts - uint256 token2Amount = _token1Amount * DECIMAL_PRECISION / _price; - address[2] memory tokens; - uint256[2] memory amounts; - - uint256 price; - if (address(_token1) < address(_token2)) { - tokens[0] = address(_token1); - tokens[1] = address(_token2); - amounts[0] = _token1Amount; - amounts[1] = token2Amount; - // inverse price if token1 goes first - price = DECIMAL_PRECISION * DECIMAL_PRECISION / _price; - } else { - tokens[0] = address(_token2); - tokens[1] = address(_token1); - amounts[0] = token2Amount; - amounts[1] = _token1Amount; - price = _price; - } - - uniV3PositionManagerSepolia.createAndInitializePoolIfNecessary( - tokens[0], - tokens[1], - _fee, - _priceToSqrtPrice(price) // sqrtPriceX96 - ); - - // mint and approve - _token1.mint(deployer, _token1Amount); - _token2.mint(deployer, token2Amount); - _token1.approve(address(uniV3PositionManagerSepolia), _token1Amount); - _token2.approve(address(uniV3PositionManagerSepolia), token2Amount); - - // mint new position - address uniV3PoolAddress = uniswapV3FactorySepolia.getPool(tokens[0], tokens[1], _fee); - int24 TICK_SPACING = IUniswapV3Pool(uniV3PoolAddress).tickSpacing(); - (, int24 tick,,,,,) = IUniswapV3Pool(uniV3PoolAddress).slot0(); - int24 tickLower = (tick - 6000) / TICK_SPACING * TICK_SPACING; - int24 tickUpper = (tick + 6000) / TICK_SPACING * TICK_SPACING; - - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - token0: tokens[0], - token1: tokens[1], - fee: _fee, - tickLower: tickLower, - tickUpper: tickUpper, - amount0Desired: amounts[0], - amount1Desired: amounts[1], - amount0Min: 0, - amount1Min: 0, - recipient: deployer, - deadline: block.timestamp + 600 minutes - }); - - uniV3PositionManagerSepolia.mint(params); - - console2.log("--"); - console2.log(_token1.name()); - console2.log(address(_token1), "address(_token1)"); - console2.log(_token1Amount, "_token1Amount"); - console2.log(_token1.balanceOf(uniV3PoolAddress), "token1.balanceOf(pool)"); - console2.log(_token2.name()); - console2.log(address(_token2), "address(_token2)"); - console2.log(token2Amount, "token2Amount"); - console2.log(_token2.balanceOf(uniV3PoolAddress), "token2.balanceOf(pool)"); - } - - function _priceToSqrtPrice(uint256 _price) public pure returns (uint160) { - return uint160(Math.sqrt((_price << 192) / DECIMAL_PRECISION)); - } -} diff --git a/contracts/script/RedeemCollateral.s.sol b/contracts/script/RedeemCollateral.s.sol deleted file mode 100644 index baecd7267..000000000 --- a/contracts/script/RedeemCollateral.s.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {console} from "forge-std/console.sol"; -import {Script} from "forge-std/Script.sol"; -import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; -import {StringFormatting} from "test/Utils/StringFormatting.sol"; -import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; -import {ICollateralRegistry} from "src/Interfaces/ICollateralRegistry.sol"; -import {DECIMAL_PRECISION} from "src/Dependencies/Constants.sol"; - -contract RedeemCollateral is Script { - using Strings for *; - using StringFormatting for *; - - function run() external { - vm.startBroadcast(); - - string memory manifestJson; - try vm.readFile("deployment-manifest.json") returns (string memory content) { - manifestJson = content; - } catch {} - - ICollateralRegistry collateralRegistry; - try vm.envAddress("COLLATERAL_REGISTRY") returns (address value) { - collateralRegistry = ICollateralRegistry(value); - } catch { - collateralRegistry = ICollateralRegistry(vm.parseJsonAddress(manifestJson, ".collateralRegistry")); - } - vm.label(address(collateralRegistry), "CollateralRegistry"); - - IBoldToken boldToken = IBoldToken(collateralRegistry.boldToken()); - vm.label(address(boldToken), "BoldToken"); - - uint256 boldBefore = boldToken.balanceOf(msg.sender); - uint256[] memory collBefore = new uint256[](collateralRegistry.totalCollaterals()); - for (uint256 i = 0; i < collBefore.length; ++i) { - collBefore[i] = collateralRegistry.getToken(i).balanceOf(msg.sender); - } - - uint256 attemptedBoldAmount = vm.envUint("AMOUNT") * DECIMAL_PRECISION; - console.log("Attempting to redeem (BOLD):", attemptedBoldAmount.decimal()); - - uint256 maxFeePct = collateralRegistry.getRedemptionRateForRedeemedAmount(attemptedBoldAmount); - collateralRegistry.redeemCollateral(attemptedBoldAmount, 10, maxFeePct); - - uint256 actualBoldAmount = boldBefore - boldToken.balanceOf(msg.sender); - console.log("Actually redeemed (BOLD):", actualBoldAmount.decimal()); - - uint256[] memory collAmount = new uint256[](collBefore.length); - for (uint256 i = 0; i < collBefore.length; ++i) { - collAmount[i] = collateralRegistry.getToken(i).balanceOf(msg.sender) - collBefore[i]; - console.log("Received coll", string.concat("#", i.toString(), ":"), collAmount[i].decimal()); - } - } -} diff --git a/contracts/script/RedeployWETHZappers.s.sol b/contracts/script/RedeployWETHZappers.s.sol deleted file mode 100644 index 01ebaf966..000000000 --- a/contracts/script/RedeployWETHZappers.s.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import {console} from "forge-std/console.sol"; -import {BaseZapper} from "src/Zappers/BaseZapper.sol"; -import {WETHZapper} from "src/Zappers/WETHZapper.sol"; -import {LeverageWETHZapper} from "src/Zappers/LeverageWETHZapper.sol"; -import {UseDeployment} from "test/Utils/UseDeployment.sol"; -import {StringEquality} from "test/Utils/StringEquality.sol"; - -contract RedeployWETHZappers is Script, UseDeployment { - using StringEquality for string; - - function run() external { - vm.startBroadcast(); - _loadDeploymentFromManifest("addresses/1.json"); - - BranchContracts storage wethBranch = branches[0]; - require(wethBranch.collToken.symbol().eq("WETH"), "Wrong branch"); - - BaseZapper oldWETHZapper = BaseZapper(address(wethBranch.zapper)); - BaseZapper oldLeverageWETHZapper = BaseZapper(address(wethBranch.leverageZapper)); - - WETHZapper newWETHZapper = new WETHZapper({ - _addressesRegistry: wethBranch.addressesRegistry, - _flashLoanProvider: oldWETHZapper.flashLoanProvider(), - _exchange: oldWETHZapper.exchange() - }); - - LeverageWETHZapper newLeverageWETHZapper = new LeverageWETHZapper({ - _addressesRegistry: wethBranch.addressesRegistry, - _flashLoanProvider: oldLeverageWETHZapper.flashLoanProvider(), - _exchange: oldLeverageWETHZapper.exchange() - }); - - console.log("newWETHZapper: ", address(newWETHZapper)); - console.log("newLeverageWETHZapper:", address(newLeverageWETHZapper)); - } -} diff --git a/contracts/script/bold-vanity.ts b/contracts/script/bold-vanity.ts deleted file mode 100644 index 5983b3feb..000000000 --- a/contracts/script/bold-vanity.ts +++ /dev/null @@ -1,45 +0,0 @@ -import assert from "assert"; - -import { - type ByteArray, - bytesToHex, - concatBytes, - getAddress, - hexToBytes, - keccak256, - padBytes, - stringToBytes, -} from "viem"; - -import BoldToken from "../out/BoldToken.sol/BoldToken.json"; - -const DEPLOYER = "0xbEC25C5590e89596BDE2DfCdc71579E66858772c"; -const SALT_PREFIX = "beBOLD"; -const CREATE2_DEPLOYER = "0x4e59b44847b379578588920cA78FbF26c0B4956C"; -const CREATE2_PREFIX = concatBytes([hexToBytes("0xFF"), hexToBytes(CREATE2_DEPLOYER)]); - -const computeCreate2Address = (salt: ByteArray, initCodeHash: ByteArray): ByteArray => - keccak256(concatBytes([CREATE2_PREFIX, salt, initCodeHash]), "bytes").slice(12); - -const startsWith = (str: string, prefix: T): str is `${T}${string}` => str.startsWith(prefix); -assert(startsWith(BoldToken.bytecode.object, "0x")); - -const boldInitCodeHash = keccak256( - concatBytes([ - hexToBytes(BoldToken.bytecode.object), - padBytes(hexToBytes(DEPLOYER)), - ]), - "bytes", -); - -for (let i = 0;; ++i) { - const saltStr = `${SALT_PREFIX}${i}`; - const salt = keccak256(stringToBytes(saltStr), "bytes"); - const boldAddress = computeCreate2Address(salt, boldInitCodeHash); - - if (boldAddress[0] === 0xb0 && boldAddress[1] === 0x1d /*&& boldAddress[18] === 0xb0 && boldAddress[19] === 0x1d*/) { - console.log("Salt found:", saltStr); - console.log("BOLD address:", getAddress(bytesToHex(boldAddress))); - break; - } -} diff --git a/contracts/script/coverage.sh b/contracts/script/coverage.sh deleted file mode 100755 index a465e49d1..000000000 --- a/contracts/script/coverage.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -# Foundry coverage -forge coverage --report lcov --report-file lcov_foundry.info - -# # Hardhat coverage -# NODE_OPTIONS="--max-old-space-size=16384" npx hardhat coverage - -# # Remove path from contract names in Hardhat -# sed -i "s/SF:.*src/SF:src/g" coverage/lcov.info - -# # Merge coverage reports -# lcov \ -# --rc lcov_branch_coverage=1 \ -# --add-tracefile lcov_foundry.info \ -# --add-tracefile coverage/lcov.info \ -# --output-file lcov_merged.info - -# Instead of merge -cp lcov_foundry.info lcov_merged.info - -lcov --remove lcov_merged.info -o lcov_merged.info \ - 'test/*' \ - 'script/*' \ - 'src/Dependencies/Ownable.sol' \ - 'src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol' \ - 'src/NFTMetadata/*' \ - 'src/MultiTroveGetter.sol' \ - 'src/HintHelpers.sol' - -genhtml lcov_merged.info --output-directory coverage diff --git a/contracts/script/governance.ts b/contracts/script/governance.ts deleted file mode 100644 index f08e6cf59..000000000 --- a/contracts/script/governance.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { foundry } from "viem/chains"; -import { z } from "zod"; -import { fs } from "zx"; - -import { createTestClient, getContract, maxUint256, publicActions, walletActions, webSocket, zeroAddress } from "viem"; - -import { - abiBoldToken, - abiBorrowerOperations, - abiBribeInitiative, - abiERC20Faucet, - abiGovernanceProxy, - abiPriceFeedTestnet, - abiWETHTester, - bytecodeBribeInitiative, - bytecodeGovernanceProxy, -} from "../abi"; - -const ANVIL_ACCOUNT_0 = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; - -const startsWith = (prefix: T) => (x: string): x is `${T}${string}` => x.startsWith(prefix); - -const Address = z.string().refine(startsWith("0x")); - -const DeploymentManifest = z.object({ - boldToken: Address, - - constants: z.object({ - ETH_GAS_COMPENSATION: z.string(), - MIN_ANNUAL_INTEREST_RATE: z.string(), - MIN_DEBT: z.string(), - }), - - branches: z.object({ - collToken: Address, - borrowerOperations: Address, - priceFeed: Address, - }).array(), - - governance: z.object({ - governance: Address, - LQTYToken: Address, - }), -}); - -const deploymentManifest = DeploymentManifest.parse(fs.readJSONSync("deployment-manifest.json")); - -const ETH_GAS_COMPENSATION = BigInt(deploymentManifest.constants.ETH_GAS_COMPENSATION); -const MIN_ANNUAL_INTEREST_RATE = BigInt(deploymentManifest.constants.MIN_ANNUAL_INTEREST_RATE); -const MIN_DEBT = BigInt(deploymentManifest.constants.MIN_DEBT); - -const client = createTestClient({ - mode: "anvil", - chain: foundry, - account: ANVIL_ACCOUNT_0, - transport: webSocket(), -}) - .extend(publicActions) - .extend(walletActions); - -const lqtyToken = getContract({ - address: deploymentManifest.governance.LQTYToken, - abi: abiERC20Faucet, - client, -}); - -const boldToken = getContract({ - address: deploymentManifest.boldToken, - abi: abiBoldToken, - client, -}); - -const collToken = getContract({ - address: deploymentManifest.branches[0].collToken, - abi: abiWETHTester, - client, -}); - -const borrowerOperations = getContract({ - address: deploymentManifest.branches[0].borrowerOperations, - abi: abiBorrowerOperations, - client, -}); - -const priceFeed = getContract({ - address: deploymentManifest.branches[0].priceFeed, - abi: abiPriceFeedTestnet, - client, -}); - -const mineOnce = () => { - const futureBlockTimestamp = new Promise((resolve) => { - const stopWatching = client.watchBlocks({ - onBlock: (block) => { - stopWatching(); - resolve(block.timestamp); - }, - }); - }); - - client.mine({ blocks: 1 }); - return futureBlockTimestamp; -}; - -const waitForSuccess = async (hash: `0x${string}`) => { - const receipt = await client.waitForTransactionReceipt({ hash }); - if (receipt.status === "reverted") throw Object.assign(new Error("Transaction reverted"), { receipt }); - return receipt; -}; - -const waitForContractAddress = async (hash: `0x${string}`) => { - const receipt = await waitForSuccess(hash); - if (receipt.contractAddress == null) throw Object.assign(new Error("No contract address"), { receipt }); - return receipt.contractAddress; -}; - -const mintBold = async (to: `0x${string}`, amount: bigint) => { - const price = await priceFeed.read.getPrice(); - const collAmount = amount * BigInt(2e18) / price; - - await collToken.write.mint([client.account.address, collAmount + ETH_GAS_COMPENSATION]).then(waitForSuccess); - await collToken.write.approve([borrowerOperations.address, collAmount + ETH_GAS_COMPENSATION]).then(waitForSuccess); - - await borrowerOperations.write.openTrove([ - to, // _owner - 0n, // _ownerIndex - collAmount, // _ETHAmount - amount, // _boldAmount - 0n, // _upperHint - 0n, // _lowerHint - MIN_ANNUAL_INTEREST_RATE, // _annualInterestRate - maxUint256, // _maxUpfrontFee - zeroAddress, // _addManager - zeroAddress, // _removeManager - zeroAddress, // _receiver - ]).then(waitForSuccess); - - await boldToken.write.transfer([to, amount]).then(waitForSuccess); -}; - -const main = async () => { - const governanceProxy = getContract({ - client, - abi: abiGovernanceProxy, - address: await client.deployContract({ - abi: abiGovernanceProxy, - bytecode: bytecodeGovernanceProxy, - args: [deploymentManifest.governance.governance], - }).then(waitForContractAddress), - }); - - const EPOCH_DURATION = await governanceProxy.read.EPOCH_DURATION(); - let epochStart = await governanceProxy.read.epochStart(); - - console.log("current epoch:", await governanceProxy.read.epoch()); - - const lqtyAmount = BigInt(1_000e18); - await lqtyToken.write.mint([governanceProxy.address, lqtyAmount]).then(waitForSuccess); - await governanceProxy.write.depositLQTY([lqtyAmount]).then(waitForSuccess); - await mintBold(governanceProxy.address, MIN_DEBT); - - // Warp to the beginning of the next epoch - epochStart += EPOCH_DURATION; - client.setNextBlockTimestamp({ timestamp: epochStart }); - await mineOnce(); - - const bribeInitiative = getContract({ - client, - abi: abiBribeInitiative, - address: await client.deployContract({ - abi: abiBribeInitiative, - bytecode: bytecodeBribeInitiative, - args: [deploymentManifest.governance.governance, boldToken.address, lqtyToken.address], - }).then(waitForContractAddress), - }); - - await governanceProxy.write.registerInitiative([bribeInitiative.address]).then(waitForSuccess); - - // Warp to the beginning of the next epoch - epochStart += EPOCH_DURATION; - client.setNextBlockTimestamp({ timestamp: epochStart }); - await mineOnce(); - - await governanceProxy.write.allocateLQTY([[], [bribeInitiative.address], [lqtyAmount], [0n]]).then(waitForSuccess); -}; - -main().then(() => { - // No way to gracefully close the WebSocket in viem? - process.exit(0); -}).catch((error) => { - console.error(error); - process.exit(1); -}); diff --git a/contracts/src/HintHelpers.sol b/contracts/src/HintHelpers.sol index 6cd6c2b99..6d2b722d5 100644 --- a/contracts/src/HintHelpers.sol +++ b/contracts/src/HintHelpers.sol @@ -3,9 +3,14 @@ pragma solidity 0.8.24; import "./Interfaces/ICollateralRegistry.sol"; +import "./Interfaces/IActivePool.sol"; +import "./Interfaces/ISortedTroves.sol"; import "./Dependencies/LiquityMath.sol"; import "./Dependencies/Constants.sol"; import "./Interfaces/IHintHelpers.sol"; +import "./Types/LatestTroveData.sol"; +import "./Types/TroveChange.sol"; +import "./Types/LatestBatchData.sol"; contract HintHelpers is IHintHelpers { string public constant NAME = "HintHelpers"; diff --git a/contracts/src/Interfaces/IFPMMFactory.sol b/contracts/src/Interfaces/IFPMMFactory.sol new file mode 100644 index 000000000..c3c184cb8 --- /dev/null +++ b/contracts/src/Interfaces/IFPMMFactory.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +interface IFPMMFactory { + /* ========================================== */ + /* ================= Events ================= */ + /* ========================================== */ + + /** + * @notice Emitted when a new FPMM is deployed. + * @param token0 The address of the first token + * @param token1 The address of the second token + * @param fpmmProxy The address of the deployed FPMM proxy + * @param fpmmImplementation The address of the deployed FPMM implementation + */ + event FPMMDeployed(address indexed token0, address indexed token1, address fpmmProxy, address fpmmImplementation); + + /** + * @notice Emitted when a new FPMM implementation is registered. + * @param implementation The address of the registered implementation + */ + event FPMMImplementationRegistered(address indexed implementation); + + /** + * @notice Emitted when a new FPMM implementation is unregistered. + * @param implementation The address of the unregistered implementation + */ + event FPMMImplementationUnregistered(address indexed implementation); + + /** + * @notice Emitted when the proxy admin is set. + * @param proxyAdmin The address of the new proxy admin + */ + event ProxyAdminSet(address indexed proxyAdmin); + + /** + * @notice Emitted when the sorted oracles address is set. + * @param sortedOracles The address of the new sorted oracles contract + */ + event SortedOraclesSet(address indexed sortedOracles); + + /** + * @notice Emitted when the breaker box address is set. + * @param breakerBox The address of the new breaker box contract + */ + event BreakerBoxSet(address indexed breakerBox); + + /** + * @notice Emitted when the governance address is set. + * @param governance The address of the new governance contract + */ + event GovernanceSet(address indexed governance); + + /* ======================================================== */ + /* ==================== View Functions ==================== */ + /* ======================================================== */ + + /** + * @notice Gets the address of the sorted oracles contract. + * @return The address of the sorted oracles contract + */ + function sortedOracles() external view returns (address); + + /** + * @notice Gets the address of the proxy admin contract. + * @return The address of the proxy admin contract + */ + function proxyAdmin() external view returns (address); + + /** + * @notice Gets the address of the breaker box contract. + * @return The address of the breaker box contract + */ + function breakerBox() external view returns (address); + + /** + * @notice Gets the address of the governance contract. + * @return The address of the governance contract + */ + function governance() external view returns (address); + + /** + * @notice Gets the address of the deployed FPMM for a token pair. + * @param token0 The address of the first token + * @param token1 The address of the second token + * @return The address of the deployed FPMM for the token pair + */ + function deployedFPMMs(address token0, address token1) external view returns (address); + + /** + * @notice Gets the list of deployed FPMM addresses. + * @return The list of deployed FPMM addresses + */ + function deployedFPMMAddresses() external view returns (address[] memory); + + /** + * @notice Checks if a FPMM implementation is registered. + * @param fpmmImplementation The address of the FPMM implementation + * @return True if the FPMM implementation is registered, false otherwise + */ + function isRegisteredImplementation(address fpmmImplementation) external view returns (bool); + + /** + * @notice Gets the list of registered FPMM implementations. + * @return The list of registered FPMM implementations + */ + function registeredImplementations() external view returns (address[] memory); + + /** + * @notice Gets the precomputed or current proxy address for a token pair. + * @param token0 The address of the first token + * @param token1 The address of the second token + * @return The address of the FPMM proxy for the token pair + */ + function getOrPrecomputeProxyAddress(address token0, address token1) external view returns (address); + + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ + + /** + * @notice Initializes the factory with required addresses. + * @param _sortedOracles The address of the sorted oracles contract + * @param _proxyAdmin The address of the proxy admin contract + * @param _breakerBox The address of the breaker box contract + * @param _governance The address of the governance contract + * @param _fpmmImplementation The address of the FPMM implementation + */ + function initialize( + address _sortedOracles, + address _proxyAdmin, + address _breakerBox, + address _governance, + address _fpmmImplementation + ) external; + + /** + * @notice Sets the address of the sorted oracles contract. + * @param _sortedOracles The new address of the sorted oracles contract + */ + function setSortedOracles(address _sortedOracles) external; + + /** + * @notice Sets the address of the proxy admin contract. + * @param _proxyAdmin The new address of the proxy admin contract + */ + function setProxyAdmin(address _proxyAdmin) external; + + /** + * @notice Sets the address of the breaker box contract. + * @param _breakerBox The new address of the breaker box contract + */ + function setBreakerBox(address _breakerBox) external; + + /** + * @notice Sets the address of the governance contract. + * @param _governance The new address of the governance contract + */ + function setGovernance(address _governance) external; + + /** + * @notice Registers a new FPMM implementation address. + * @param fpmmImplementation The FPMM implementation address to register + */ + function registerFPMMImplementation(address fpmmImplementation) external; + + /** + * @notice Unregisters a FPMM implementation address. + * @param fpmmImplementation The FPMM implementation address to unregister + * @param index The index of the FPMM implementation to unregister + */ + function unregisterFPMMImplementation(address fpmmImplementation, uint256 index) external; + + /** + * @notice Deploys a new FPMM for a token pair using the default parameters. + * @param fpmmImplementation The address of the FPMM implementation + * @param token0 The address of the first token + * @param token1 The address of the second token + * @param referenceRateFeedID The address of the reference rate feed + * @return proxy The address of the deployed FPMM proxy + */ + function deployFPMM(address fpmmImplementation, address token0, address token1, address referenceRateFeedID) + external + returns (address proxy); + + /** + * @notice Deploys a new FPMM for a token pair using custom parameters. + * @param fpmmImplementation The address of the FPMM implementation + * @param customSortedOracles The address of the custom sorted oracles contract + * @param customProxyAdmin The address of the custom proxy admin contract + * @param customBreakerBox The address of the custom breaker box contract + * @param customGovernance The address of the custom governance contract + * @param token0 The address of the first token + * @param token1 The address of the second token + * @param referenceRateFeedID The address of the reference rate feed + * @return proxy The address of the deployed FPMM proxy + */ + function deployFPMM( + address fpmmImplementation, + address customSortedOracles, + address customProxyAdmin, + address customBreakerBox, + address customGovernance, + address token0, + address token1, + address referenceRateFeedID + ) external returns (address proxy); +} diff --git a/contracts/src/Interfaces/IStabilityPool.sol b/contracts/src/Interfaces/IStabilityPool.sol index f3229b931..63bc0d4f4 100644 --- a/contracts/src/Interfaces/IStabilityPool.sol +++ b/contracts/src/Interfaces/IStabilityPool.sol @@ -7,6 +7,7 @@ import "./ILiquityBase.sol"; import "./IBoldToken.sol"; import "./ITroveManager.sol"; import "./IBoldRewardsReceiver.sol"; +import "./IAddressesRegistry.sol"; /* * The Stability Pool holds Bold tokens deposited by Stability Pool depositors. @@ -29,6 +30,8 @@ import "./IBoldRewardsReceiver.sol"; * */ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { + function initialize(IAddressesRegistry _addressesRegistry) external; + function boldToken() external view returns (IBoldToken); function troveManager() external view returns (ITroveManager); diff --git a/contracts/src/Interfaces/IStableTokenV3.sol b/contracts/src/Interfaces/IStableTokenV3.sol new file mode 100644 index 000000000..a0b7c1e37 --- /dev/null +++ b/contracts/src/Interfaces/IStableTokenV3.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +/** + * @title IStableTokenV3 + * @notice Interface for the StableTokenV3 contract. + */ +interface IStableTokenV3 { + /** + * @notice Initializes a StableTokenV3. + * @param _name The name of the stable token (English) + * @param _symbol A short symbol identifying the token (e.g. "cUSD") + * @param initialBalanceAddresses Array of addresses with an initial balance. + * @param initialBalanceValues Array of balance values corresponding to initialBalanceAddresses. + * @param _minters The addresses that are allowed to mint. + * @param _burners The addresses that are allowed to burn. + * @param _operators The addresses that are allowed to call the operator functions. + */ + function initialize( + string calldata _name, + string calldata _symbol, + address[] calldata initialBalanceAddresses, + uint256[] calldata initialBalanceValues, + address[] calldata _minters, + address[] calldata _burners, + address[] calldata _operators + ) external; + + /** + * @notice Initializes a StableTokenV3 contract + * when upgrading from StableTokenV2.sol. + * It sets the addresses of the minters, burners, and operators. + * @dev This function is only callable once. + * @param _minters The addresses that are allowed to mint. + * @param _burners The addresses that are allowed to burn. + * @param _operators The addresses that are allowed to call the operator functions. + */ + function initializeV3(address[] calldata _minters, address[] calldata _burners, address[] calldata _operators) + external; + + /** + * @notice Sets the operator role for an address. + * @param _operator The address of the operator. + * @param _isOperator The boolean value indicating if the address is an operator. + */ + function setOperator(address _operator, bool _isOperator) external; + + /** + * @notice Sets the minter role for an address. + * @param _minter The address of the minter. + * @param _isMinter The boolean value indicating if the address is a minter. + */ + function setMinter(address _minter, bool _isMinter) external; + + /** + * @notice Sets the burner role for an address. + * @param _burner The address of the burner. + * @param _isBurner The boolean value indicating if the address is a burner. + */ + function setBurner(address _burner, bool _isBurner) external; + + /** + * From openzeppelin's IERC20.sol + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * From openzeppelin's IERC20.sol + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * From openzeppelin's IERC20.sol + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * From openzeppelin's IERC20.sol + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * From openzeppelin's IERC20.sol + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * From openzeppelin's IERC20.sol + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + /** + * @notice Mints new StableToken and gives it to 'to'. + * @param to The account for which to mint tokens. + * @param value The amount of StableToken to mint. + */ + function mint(address to, uint256 value) external returns (bool); + + /** + * @notice Burns StableToken from the balance of msg.sender. + * @param value The amount of StableToken to burn. + */ + function burn(uint256 value) external returns (bool); + + /** + * From openzeppelin's IERC20PermitUpgradeable.sol + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; + + /** + * @notice Transfer token from a specified address to the stability pool. + * @param _sender The address to transfer from. + * @param _poolAddress The address of the pool to transfer to. + * @param _amount The amount to be transferred. + */ + function sendToPool(address _sender, address _poolAddress, uint256 _amount) external; + + /** + * @notice Transfer token to a specified address from the stability pool. + * @param _poolAddress The address of the pool to transfer from + * @param _receiver The address to transfer to. + * @param _amount The amount to be transferred. + */ + function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external; + + /** + * @notice Reserve balance for making payments for gas in this StableToken currency. + * @param from The account to reserve balance from + * @param value The amount of balance to reserve + * @dev Note that this function is called by the protocol when paying for tx fees in this + * currency. After the tx is executed, gas is refunded to the sender and credited to the + * various tx fee recipients via a call to `creditGasFees`. + */ + function debitGasFees(address from, uint256 value) external; + + /** + * @notice Alternative function to credit balance after making payments + * for gas in this StableToken currency. + * @param from The account to debit balance from + * @param feeRecipient Coinbase address + * @param gatewayFeeRecipient Gateway address + * @param communityFund Community fund address + * @param refund amount to be refunded by the VM + * @param tipTxFee Coinbase fee + * @param baseTxFee Community fund fee + * @param gatewayFee Gateway fee + * @dev Note that this function is called by the protocol when paying for tx fees in this + * currency. Before the tx is executed, gas is debited from the sender via a call to + * `debitGasFees`. + */ + function creditGasFees( + address from, + address feeRecipient, + address gatewayFeeRecipient, + address communityFund, + uint256 refund, + uint256 tipTxFee, + uint256 gatewayFee, + uint256 baseTxFee + ) external; +} diff --git a/contracts/src/MultiTroveGetter.sol b/contracts/src/MultiTroveGetter.sol index d9a6c47dd..8141eb18c 100644 --- a/contracts/src/MultiTroveGetter.sol +++ b/contracts/src/MultiTroveGetter.sol @@ -4,7 +4,10 @@ pragma solidity 0.8.24; import "./Interfaces/ICollateralRegistry.sol"; import "./Interfaces/IMultiTroveGetter.sol"; +import "./Interfaces/ISortedTroves.sol"; import "./Types/BatchId.sol"; +import "./Types/LatestTroveData.sol"; +import "./Types/LatestBatchData.sol"; /* Helper contract for grabbing Trove data for the front end. Not part of the core Liquity system. */ contract MultiTroveGetter is IMultiTroveGetter { diff --git a/contracts/src/TroveNFT.sol b/contracts/src/TroveNFT.sol index 4aac97eec..50644e7e4 100644 --- a/contracts/src/TroveNFT.sol +++ b/contracts/src/TroveNFT.sol @@ -7,6 +7,8 @@ import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.s import "./Interfaces/ITroveNFT.sol"; import "./Interfaces/IAddressesRegistry.sol"; +import "./Interfaces/IBoldToken.sol"; +import "./Types/LatestTroveData.sol"; import {IMetadataNFT} from "./NFTMetadata/MetadataNFT.sol"; import {ITroveManager} from "./Interfaces/ITroveManager.sol"; diff --git a/contracts/src/Zappers/BaseZapper.sol b/contracts/src/Zappers/BaseZapper.sol deleted file mode 100644 index c4f30ac33..000000000 --- a/contracts/src/Zappers/BaseZapper.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "../Interfaces/IWETH.sol"; -import "../Interfaces/IAddressesRegistry.sol"; -import "../Interfaces/IBorrowerOperations.sol"; -import "../Dependencies/AddRemoveManagers.sol"; -import "./LeftoversSweep.sol"; -import "./Interfaces/IFlashLoanProvider.sol"; -import "./Interfaces/IFlashLoanReceiver.sol"; -import "./Interfaces/IExchange.sol"; -import "./Interfaces/IZapper.sol"; - -abstract contract BaseZapper is AddRemoveManagers, LeftoversSweep, IFlashLoanReceiver, IZapper { - IBorrowerOperations public immutable borrowerOperations; // LST branch (i.e., not WETH as collateral) - ITroveManager public immutable troveManager; - IWETH public immutable WETH; - IBoldToken public immutable boldToken; - - IFlashLoanProvider public immutable flashLoanProvider; - IExchange public immutable exchange; - - constructor(IAddressesRegistry _addressesRegistry, IFlashLoanProvider _flashLoanProvider, IExchange _exchange) - AddRemoveManagers(_addressesRegistry) - { - borrowerOperations = _addressesRegistry.borrowerOperations(); - troveManager = _addressesRegistry.troveManager(); - boldToken = _addressesRegistry.boldToken(); - WETH = _addressesRegistry.WETH(); - - flashLoanProvider = _flashLoanProvider; - exchange = _exchange; - } - - function _getTroveIndex(address _sender, uint256 _ownerIndex) internal pure returns (uint256) { - return uint256(keccak256(abi.encode(_sender, _ownerIndex))); - } - - function _getTroveIndex(uint256 _ownerIndex) internal view returns (uint256) { - return _getTroveIndex(msg.sender, _ownerIndex); - } - - function _requireZapperIsReceiver(uint256 _troveId) internal view { - (, address receiver) = borrowerOperations.removeManagerReceiverOf(_troveId); - require(receiver == address(this), "BZ: Zapper is not receiver for this trove"); - } - - function _checkAdjustTroveManagers( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - bool _isDebtIncrease - ) internal view returns (address) { - address owner = troveNFT.ownerOf(_troveId); - address receiver = owner; - - if ((!_isCollIncrease && _collChange > 0) || _isDebtIncrease) { - receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner); - _requireZapperIsReceiver(_troveId); - } else { - // RemoveManager assumes AddManager, so if the former is set, there's no need to check the latter - _requireSenderIsOwnerOrAddManager(_troveId, owner); - // No need to check the type of trove change for two reasons: - // - If the check above fails, it means sender is not owner, nor AddManager, nor RemoveManager. - // An independent 3rd party should not be allowed here. - // - If it's not collIncrease or debtDecrease, _requireNonZeroAdjustment would revert - } - - return receiver; - } -} diff --git a/contracts/src/Zappers/GasCompZapper.sol b/contracts/src/Zappers/GasCompZapper.sol deleted file mode 100644 index 0792052ec..000000000 --- a/contracts/src/Zappers/GasCompZapper.sol +++ /dev/null @@ -1,324 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; - -import "./BaseZapper.sol"; -import "../Dependencies/Constants.sol"; - -contract GasCompZapper is BaseZapper { - using SafeERC20 for IERC20; - - IERC20 public immutable collToken; - - constructor(IAddressesRegistry _addressesRegistry, IFlashLoanProvider _flashLoanProvider, IExchange _exchange) - BaseZapper(_addressesRegistry, _flashLoanProvider, _exchange) - { - collToken = _addressesRegistry.collToken(); - require(address(WETH) != address(collToken), "GCZ: Wrong coll branch"); - - // Approve WETH to BorrowerOperations - WETH.approve(address(borrowerOperations), type(uint256).max); - // Approve coll to BorrowerOperations - collToken.approve(address(borrowerOperations), type(uint256).max); - // Approve Coll to exchange module (for closeTroveFromCollateral) - collToken.approve(address(_exchange), type(uint256).max); - } - - function openTroveWithRawETH(OpenTroveParams calldata _params) external payable returns (uint256) { - require(msg.value == ETH_GAS_COMPENSATION, "GCZ: Wrong ETH"); - require( - _params.batchManager == address(0) || _params.annualInterestRate == 0, - "GCZ: Cannot choose interest if joining a batch" - ); - - // Convert ETH to WETH - WETH.deposit{value: msg.value}(); - - // Pull coll - collToken.safeTransferFrom(msg.sender, address(this), _params.collAmount); - - uint256 troveId; - // Include sender in index - uint256 index = _getTroveIndex(_params.ownerIndex); - if (_params.batchManager == address(0)) { - troveId = borrowerOperations.openTrove( - _params.owner, - index, - _params.collAmount, - _params.boldAmount, - _params.upperHint, - _params.lowerHint, - _params.annualInterestRate, - _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - address(this), // add manager - address(this), // remove manager - address(this) // receiver for remove manager - ); - } else { - IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams memory - openTroveAndJoinInterestBatchManagerParams = IBorrowerOperations - .OpenTroveAndJoinInterestBatchManagerParams({ - owner: _params.owner, - ownerIndex: index, - collAmount: _params.collAmount, - boldAmount: _params.boldAmount, - upperHint: _params.upperHint, - lowerHint: _params.lowerHint, - interestBatchManager: _params.batchManager, - maxUpfrontFee: _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - addManager: address(this), // add manager - removeManager: address(this), // remove manager - receiver: address(this) // receiver for remove manager - }); - troveId = - borrowerOperations.openTroveAndJoinInterestBatchManager(openTroveAndJoinInterestBatchManagerParams); - } - - boldToken.transfer(msg.sender, _params.boldAmount); - - // Set add/remove managers - _setAddManager(troveId, _params.addManager); - _setRemoveManagerAndReceiver(troveId, _params.removeManager, _params.receiver); - - return troveId; - } - - function addColl(uint256 _troveId, uint256 _amount) external { - address owner = troveNFT.ownerOf(_troveId); - _requireSenderIsOwnerOrAddManager(_troveId, owner); - - IBorrowerOperations borrowerOperationsCached = borrowerOperations; - - // Pull coll - collToken.safeTransferFrom(msg.sender, address(this), _amount); - - borrowerOperationsCached.addColl(_troveId, _amount); - } - - function withdrawColl(uint256 _troveId, uint256 _amount) external { - address owner = troveNFT.ownerOf(_troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner); - _requireZapperIsReceiver(_troveId); - - borrowerOperations.withdrawColl(_troveId, _amount); - - // Send coll left - collToken.safeTransfer(receiver, _amount); - } - - function withdrawBold(uint256 _troveId, uint256 _boldAmount, uint256 _maxUpfrontFee) external { - address owner = troveNFT.ownerOf(_troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner); - _requireZapperIsReceiver(_troveId); - - borrowerOperations.withdrawBold(_troveId, _boldAmount, _maxUpfrontFee); - - // Send Bold - boldToken.transfer(receiver, _boldAmount); - } - - function repayBold(uint256 _troveId, uint256 _boldAmount) external { - address owner = troveNFT.ownerOf(_troveId); - _requireSenderIsOwnerOrAddManager(_troveId, owner); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensAndBalances(collToken, boldToken, initialBalances); - - // Pull Bold - boldToken.transferFrom(msg.sender, address(this), _boldAmount); - - borrowerOperations.repayBold(_troveId, _boldAmount); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - function adjustTrove( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - uint256 _maxUpfrontFee - ) external { - InitialBalances memory initialBalances; - address receiver = - _adjustTrovePre(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, initialBalances); - borrowerOperations.adjustTrove( - _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _maxUpfrontFee - ); - _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver, initialBalances); - } - - function adjustZombieTrove( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - uint256 _upperHint, - uint256 _lowerHint, - uint256 _maxUpfrontFee - ) external { - InitialBalances memory initialBalances; - address receiver = - _adjustTrovePre(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, initialBalances); - borrowerOperations.adjustZombieTrove( - _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _upperHint, _lowerHint, _maxUpfrontFee - ); - _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver, initialBalances); - } - - function _adjustTrovePre( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - InitialBalances memory _initialBalances - ) internal returns (address) { - address receiver = _checkAdjustTroveManagers(_troveId, _collChange, _isCollIncrease, _isDebtIncrease); - - // Set initial balances to make sure there are not lefovers - _setInitialTokensAndBalances(collToken, boldToken, _initialBalances); - - // Pull coll - if (_isCollIncrease) { - collToken.safeTransferFrom(msg.sender, address(this), _collChange); - } - - // Pull Bold - if (!_isDebtIncrease) { - boldToken.transferFrom(msg.sender, address(this), _boldChange); - } - - return receiver; - } - - function _adjustTrovePost( - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - address _receiver, - InitialBalances memory _initialBalances - ) internal { - // Send coll left - if (!_isCollIncrease) { - collToken.safeTransfer(_receiver, _collChange); - } - - // Send Bold - if (_isDebtIncrease) { - boldToken.transfer(_receiver, _boldChange); - } - - // return leftovers to user - _returnLeftovers(_initialBalances); - } - - function closeTroveToRawETH(uint256 _troveId) external { - address owner = troveNFT.ownerOf(_troveId); - address payable receiver = payable(_requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner)); - _requireZapperIsReceiver(_troveId); - - // pull Bold for repayment - LatestTroveData memory trove = troveManager.getLatestTroveData(_troveId); - boldToken.transferFrom(msg.sender, address(this), trove.entireDebt); - - borrowerOperations.closeTrove(_troveId); - - // Send coll left - collToken.safeTransfer(receiver, trove.entireColl); - - // Send gas compensation - WETH.withdraw(ETH_GAS_COMPENSATION); - (bool success,) = receiver.call{value: ETH_GAS_COMPENSATION}(""); - require(success, "GCZ: Sending ETH failed"); - } - - function closeTroveFromCollateral(uint256 _troveId, uint256 _flashLoanAmount, uint256 _minExpectedCollateral) - external - override - { - address owner = troveNFT.ownerOf(_troveId); - address payable receiver = payable(_requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner)); - _requireZapperIsReceiver(_troveId); - - CloseTroveParams memory params = CloseTroveParams({ - troveId: _troveId, - flashLoanAmount: _flashLoanAmount, - minExpectedCollateral: _minExpectedCollateral, - receiver: receiver - }); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - initialBalances.tokens[0] = collToken; - initialBalances.tokens[1] = boldToken; - _setInitialBalancesAndReceiver(initialBalances, receiver); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - collToken, _flashLoanAmount, IFlashLoanProvider.Operation.CloseTrove, abi.encode(params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - function receiveFlashLoanOnCloseTroveFromCollateral( - CloseTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external { - require(msg.sender == address(flashLoanProvider), "GCZ: Caller not FlashLoan provider"); - - LatestTroveData memory trove = troveManager.getLatestTroveData(_params.troveId); - uint256 collLeft = trove.entireColl - _params.flashLoanAmount; - require(collLeft >= _params.minExpectedCollateral, "GCZ: Not enough collateral received"); - - // Swap Coll from flash loan to Bold, so we can repay and close trove - // We swap the flash loan minus the flash loan fee - exchange.swapToBold(_effectiveFlashLoanAmount, trove.entireDebt); - - // We asked for a min of entireDebt in swapToBold call above, so we don’t check again here: - //uint256 receivedBoldAmount = exchange.swapToBold(_effectiveFlashLoanAmount, trove.entireDebt); - //require(receivedBoldAmount >= trove.entireDebt, "GCZ: Not enough BOLD obtained to repay"); - - borrowerOperations.closeTrove(_params.troveId); - - // Send coll back to return flash loan - collToken.safeTransfer(address(flashLoanProvider), _params.flashLoanAmount); - - // Send coll left - collToken.safeTransfer(_params.receiver, collLeft); - - // Send gas compensation - WETH.withdraw(ETH_GAS_COMPENSATION); - (bool success,) = _params.receiver.call{value: ETH_GAS_COMPENSATION}(""); - require(success, "GCZ: Sending ETH failed"); - } - - receive() external payable {} - - // Unimplemented flash loan receive functions for leverage - function receiveFlashLoanOnOpenLeveragedTrove( - ILeverageZapper.OpenLeveragedTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external virtual override {} - function receiveFlashLoanOnLeverUpTrove( - ILeverageZapper.LeverUpTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external virtual override {} - function receiveFlashLoanOnLeverDownTrove( - ILeverageZapper.LeverDownTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external virtual override {} -} diff --git a/contracts/src/Zappers/Interfaces/IExchange.sol b/contracts/src/Zappers/Interfaces/IExchange.sol deleted file mode 100644 index 2203b78eb..000000000 --- a/contracts/src/Zappers/Interfaces/IExchange.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -interface IExchange { - function swapFromBold(uint256 _boldAmount, uint256 _minCollAmount) external; - - function swapToBold(uint256 _collAmount, uint256 _minBoldAmount) external returns (uint256); -} diff --git a/contracts/src/Zappers/Interfaces/IExchangeHelpers.sol b/contracts/src/Zappers/Interfaces/IExchangeHelpers.sol deleted file mode 100644 index fc60e0296..000000000 --- a/contracts/src/Zappers/Interfaces/IExchangeHelpers.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -interface IExchangeHelpers { - function getCollFromBold(uint256 _boldAmount, IERC20 _collToken, uint256 _desiredCollAmount) - external /* view */ - returns (uint256, uint256); -} diff --git a/contracts/src/Zappers/Interfaces/IFlashLoanProvider.sol b/contracts/src/Zappers/Interfaces/IFlashLoanProvider.sol deleted file mode 100644 index ab84a723d..000000000 --- a/contracts/src/Zappers/Interfaces/IFlashLoanProvider.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import "./ILeverageZapper.sol"; -import "./IFlashLoanReceiver.sol"; - -interface IFlashLoanProvider { - enum Operation { - OpenTrove, - CloseTrove, - LeverUpTrove, - LeverDownTrove - } - - function receiver() external view returns (IFlashLoanReceiver); - - function makeFlashLoan(IERC20 _token, uint256 _amount, Operation _operation, bytes calldata userData) external; -} diff --git a/contracts/src/Zappers/Interfaces/IFlashLoanReceiver.sol b/contracts/src/Zappers/Interfaces/IFlashLoanReceiver.sol deleted file mode 100644 index 5d66e490b..000000000 --- a/contracts/src/Zappers/Interfaces/IFlashLoanReceiver.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "./IZapper.sol"; -import "./ILeverageZapper.sol"; - -interface IFlashLoanReceiver { - function receiveFlashLoanOnOpenLeveragedTrove( - ILeverageZapper.OpenLeveragedTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external; - function receiveFlashLoanOnLeverUpTrove( - ILeverageZapper.LeverUpTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external; - function receiveFlashLoanOnLeverDownTrove( - ILeverageZapper.LeverDownTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external; - function receiveFlashLoanOnCloseTroveFromCollateral( - IZapper.CloseTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external; -} diff --git a/contracts/src/Zappers/Interfaces/ILeverageZapper.sol b/contracts/src/Zappers/Interfaces/ILeverageZapper.sol deleted file mode 100644 index b1cc8df17..000000000 --- a/contracts/src/Zappers/Interfaces/ILeverageZapper.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "./IZapper.sol"; - -interface ILeverageZapper is IZapper { - struct OpenLeveragedTroveParams { - address owner; - uint256 ownerIndex; - uint256 collAmount; - uint256 flashLoanAmount; - uint256 boldAmount; - uint256 upperHint; - uint256 lowerHint; - uint256 annualInterestRate; - address batchManager; - uint256 maxUpfrontFee; - address addManager; - address removeManager; - address receiver; - } - - struct LeverUpTroveParams { - uint256 troveId; - uint256 flashLoanAmount; - uint256 boldAmount; - uint256 maxUpfrontFee; - } - - struct LeverDownTroveParams { - uint256 troveId; - uint256 flashLoanAmount; - uint256 minBoldAmount; - } - - function openLeveragedTroveWithRawETH(OpenLeveragedTroveParams calldata _params) external payable; - - function leverUpTrove(LeverUpTroveParams calldata _params) external; - - function leverDownTrove(LeverDownTroveParams calldata _params) external; - - function leverageRatioToCollateralRatio(uint256 _inputRatio) external pure returns (uint256); -} diff --git a/contracts/src/Zappers/Interfaces/IZapper.sol b/contracts/src/Zappers/Interfaces/IZapper.sol deleted file mode 100644 index 4d5ec13c4..000000000 --- a/contracts/src/Zappers/Interfaces/IZapper.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "./IFlashLoanProvider.sol"; -import "./IExchange.sol"; - -interface IZapper { - struct OpenTroveParams { - address owner; - uint256 ownerIndex; - uint256 collAmount; - uint256 boldAmount; - uint256 upperHint; - uint256 lowerHint; - uint256 annualInterestRate; - address batchManager; - uint256 maxUpfrontFee; - address addManager; - address removeManager; - address receiver; - } - - struct CloseTroveParams { - uint256 troveId; - uint256 flashLoanAmount; - uint256 minExpectedCollateral; - address receiver; - } - - function flashLoanProvider() external view returns (IFlashLoanProvider); - - function exchange() external view returns (IExchange); - - function openTroveWithRawETH(OpenTroveParams calldata _params) external payable returns (uint256); - - function closeTroveFromCollateral(uint256 _troveId, uint256 _flashLoanAmount, uint256 _minExpectedCollateral) - external; -} diff --git a/contracts/src/Zappers/LeftoversSweep.sol b/contracts/src/Zappers/LeftoversSweep.sol deleted file mode 100644 index ea464d82e..000000000 --- a/contracts/src/Zappers/LeftoversSweep.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; - -import "../Interfaces/IBoldToken.sol"; - -contract LeftoversSweep { - using SafeERC20 for IERC20; - - struct InitialBalances { - IERC20[4] tokens; // paving the way for completely dynamic routes - uint256[4] balances; - address receiver; - } - - function _setInitialTokensAndBalances( - IERC20 _collToken, - IBoldToken _boldToken, - InitialBalances memory _initialBalances - ) internal view { - _setInitialTokensBalancesAndReceiver(_collToken, _boldToken, _initialBalances, msg.sender); - } - - function _setInitialTokensBalancesAndReceiver( - IERC20 _collToken, - IBoldToken _boldToken, - InitialBalances memory _initialBalances, - address _receiver - ) internal view { - _initialBalances.tokens[0] = _collToken; - _initialBalances.tokens[1] = _boldToken; - _setInitialBalancesAndReceiver(_initialBalances, _receiver); - } - - function _setInitialBalances(InitialBalances memory _initialBalances) internal view { - _setInitialBalancesAndReceiver(_initialBalances, msg.sender); - } - - function _setInitialBalancesAndReceiver(InitialBalances memory _initialBalances, address _receiver) internal view { - for (uint256 i = 0; i < _initialBalances.tokens.length; i++) { - if (address(_initialBalances.tokens[i]) == address(0)) break; - - _initialBalances.balances[i] = _initialBalances.tokens[i].balanceOf(address(this)); - } - _initialBalances.receiver = _receiver; - } - - function _returnLeftovers(InitialBalances memory _initialBalances) internal { - for (uint256 i = 0; i < _initialBalances.tokens.length; i++) { - if (address(_initialBalances.tokens[i]) == address(0)) break; - - uint256 currentBalance = _initialBalances.tokens[i].balanceOf(address(this)); - if (currentBalance > _initialBalances.balances[i]) { - _initialBalances.tokens[i].safeTransfer( - _initialBalances.receiver, currentBalance - _initialBalances.balances[i] - ); - } - } - } -} diff --git a/contracts/src/Zappers/LeverageLSTZapper.sol b/contracts/src/Zappers/LeverageLSTZapper.sol deleted file mode 100644 index 71782fdac..000000000 --- a/contracts/src/Zappers/LeverageLSTZapper.sol +++ /dev/null @@ -1,206 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; - -import "./GasCompZapper.sol"; -import "../Dependencies/Constants.sol"; - -contract LeverageLSTZapper is GasCompZapper, ILeverageZapper { - using SafeERC20 for IERC20; - - constructor(IAddressesRegistry _addressesRegistry, IFlashLoanProvider _flashLoanProvider, IExchange _exchange) - GasCompZapper(_addressesRegistry, _flashLoanProvider, _exchange) - { - // Approval of WETH and Coll to BorrowerOperations is done in parent GasCompZapper - // Approve Bold to exchange module (Coll is approved in parent GasCompZapper) - boldToken.approve(address(_exchange), type(uint256).max); - } - - function openLeveragedTroveWithRawETH(OpenLeveragedTroveParams memory _params) external payable { - require(msg.value == ETH_GAS_COMPENSATION, "LZ: Wrong ETH"); - require( - _params.batchManager == address(0) || _params.annualInterestRate == 0, - "LZ: Cannot choose interest if joining a batch" - ); - - // Include the original sender in the index, so it is included in the final troveId - _params.ownerIndex = _getTroveIndex(msg.sender, _params.ownerIndex); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensAndBalances(collToken, boldToken, initialBalances); - - // Convert ETH to WETH - WETH.deposit{value: msg.value}(); - - // Pull own coll - collToken.safeTransferFrom(msg.sender, address(this), _params.collAmount); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - collToken, _params.flashLoanAmount, IFlashLoanProvider.Operation.OpenTrove, abi.encode(_params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Callback from the flash loan provider - function receiveFlashLoanOnOpenLeveragedTrove( - OpenLeveragedTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external override { - require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider"); - - uint256 totalCollAmount = _params.collAmount + _effectiveFlashLoanAmount; - // We compute boldAmount off-chain for efficiency - - // Open trove - uint256 troveId; - if (_params.batchManager == address(0)) { - troveId = borrowerOperations.openTrove( - _params.owner, - _params.ownerIndex, - totalCollAmount, - _params.boldAmount, - _params.upperHint, - _params.lowerHint, - _params.annualInterestRate, - _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - address(this), // add manager - address(this), // remove manager - address(this) // receiver for remove manager - ); - } else { - IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams memory - openTroveAndJoinInterestBatchManagerParams = IBorrowerOperations - .OpenTroveAndJoinInterestBatchManagerParams({ - owner: _params.owner, - ownerIndex: _params.ownerIndex, - collAmount: totalCollAmount, - boldAmount: _params.boldAmount, - upperHint: _params.upperHint, - lowerHint: _params.lowerHint, - interestBatchManager: _params.batchManager, - maxUpfrontFee: _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - addManager: address(this), // add manager - removeManager: address(this), // remove manager - receiver: address(this) // receiver for remove manager - }); - troveId = - borrowerOperations.openTroveAndJoinInterestBatchManager(openTroveAndJoinInterestBatchManagerParams); - } - - // Set add/remove managers - _setAddManager(troveId, _params.addManager); - _setRemoveManagerAndReceiver(troveId, _params.removeManager, _params.receiver); - - // Swap Bold to Coll - exchange.swapFromBold(_params.boldAmount, _params.flashLoanAmount); - - // Send coll back to return flash loan - collToken.safeTransfer(address(flashLoanProvider), _params.flashLoanAmount); - } - - function leverUpTrove(LeverUpTroveParams calldata _params) external { - address owner = troveNFT.ownerOf(_params.troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_params.troveId, owner); - _requireZapperIsReceiver(_params.troveId); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensBalancesAndReceiver(collToken, boldToken, initialBalances, receiver); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - collToken, _params.flashLoanAmount, IFlashLoanProvider.Operation.LeverUpTrove, abi.encode(_params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Callback from the flash loan provider - function receiveFlashLoanOnLeverUpTrove(LeverUpTroveParams calldata _params, uint256 _effectiveFlashLoanAmount) - external - override - { - require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider"); - - // Adjust trove - // With the received coll from flash loan, we increase both the trove coll and debt - borrowerOperations.adjustTrove( - _params.troveId, - _effectiveFlashLoanAmount, // flash loan amount minus fee - true, // _isCollIncrease - _params.boldAmount, - true, // _isDebtIncrease - _params.maxUpfrontFee - ); - - // Swap Bold to Coll - // No need to use a min: if the obtained amount is not enough, the flash loan return below won’t be enough - // And the flash loan provider will revert after this function exits - // The frontend should calculate in advance the `_params.boldAmount` needed for this to work - exchange.swapFromBold(_params.boldAmount, _params.flashLoanAmount); - - // Send coll back to return flash loan - collToken.safeTransfer(address(flashLoanProvider), _params.flashLoanAmount); - } - - function leverDownTrove(LeverDownTroveParams calldata _params) external { - address owner = troveNFT.ownerOf(_params.troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_params.troveId, owner); - _requireZapperIsReceiver(_params.troveId); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensBalancesAndReceiver(collToken, boldToken, initialBalances, receiver); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - collToken, _params.flashLoanAmount, IFlashLoanProvider.Operation.LeverDownTrove, abi.encode(_params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Callback from the flash loan provider - function receiveFlashLoanOnLeverDownTrove(LeverDownTroveParams calldata _params, uint256 _effectiveFlashLoanAmount) - external - override - { - require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider"); - - // Swap Coll from flash loan to Bold, so we can repay and downsize trove - // We swap the flash loan minus the flash loan fee - // The frontend should calculate in advance the `_params.minBoldAmount` to achieve the desired leverage ratio - // (with some slippage tolerance) - uint256 receivedBoldAmount = exchange.swapToBold(_effectiveFlashLoanAmount, _params.minBoldAmount); - - // Adjust trove - borrowerOperations.adjustTrove( - _params.troveId, - _params.flashLoanAmount, - false, // _isCollIncrease - receivedBoldAmount, - false, // _isDebtIncrease - 0 - ); - - // Send coll back to return flash loan - collToken.safeTransfer(address(flashLoanProvider), _params.flashLoanAmount); - } - - // As formulas are symmetrical, it can be used in both ways - function leverageRatioToCollateralRatio(uint256 _inputRatio) external pure returns (uint256) { - return _inputRatio * DECIMAL_PRECISION / (_inputRatio - DECIMAL_PRECISION); - } -} diff --git a/contracts/src/Zappers/LeverageWETHZapper.sol b/contracts/src/Zappers/LeverageWETHZapper.sol deleted file mode 100644 index 7e6097d82..000000000 --- a/contracts/src/Zappers/LeverageWETHZapper.sol +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./WETHZapper.sol"; -import "../Dependencies/Constants.sol"; -import "./Interfaces/ILeverageZapper.sol"; - -contract LeverageWETHZapper is WETHZapper, ILeverageZapper { - constructor(IAddressesRegistry _addressesRegistry, IFlashLoanProvider _flashLoanProvider, IExchange _exchange) - WETHZapper(_addressesRegistry, _flashLoanProvider, _exchange) - { - // Approval of coll (WETH) to BorrowerOperations is done in parent WETHZapper - // Approve Bold to exchange module (Coll is approved in parent WETHZapper) - boldToken.approve(address(_exchange), type(uint256).max); - } - - function openLeveragedTroveWithRawETH(OpenLeveragedTroveParams memory _params) external payable { - require(msg.value == ETH_GAS_COMPENSATION + _params.collAmount, "LZ: Wrong amount of ETH"); - require( - _params.batchManager == address(0) || _params.annualInterestRate == 0, - "LZ: Cannot choose interest if joining a batch" - ); - - // Include the original sender in the index, so it is included in the final troveId - _params.ownerIndex = _getTroveIndex(msg.sender, _params.ownerIndex); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensAndBalances(WETH, boldToken, initialBalances); - - // Convert ETH to WETH - WETH.deposit{value: msg.value}(); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - WETH, _params.flashLoanAmount, IFlashLoanProvider.Operation.OpenTrove, abi.encode(_params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Callback from the flash loan provider - function receiveFlashLoanOnOpenLeveragedTrove( - OpenLeveragedTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external override { - require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider"); - - uint256 totalCollAmount = _params.collAmount + _effectiveFlashLoanAmount; - // We compute boldAmount off-chain for efficiency - - uint256 troveId; - // Open trove - if (_params.batchManager == address(0)) { - troveId = borrowerOperations.openTrove( - _params.owner, - _params.ownerIndex, - totalCollAmount, - _params.boldAmount, - _params.upperHint, - _params.lowerHint, - _params.annualInterestRate, - _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - address(this), // add manager - address(this), // remove manager - address(this) // receiver for remove manager - ); - } else { - IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams memory - openTroveAndJoinInterestBatchManagerParams = IBorrowerOperations - .OpenTroveAndJoinInterestBatchManagerParams({ - owner: _params.owner, - ownerIndex: _params.ownerIndex, - collAmount: totalCollAmount, - boldAmount: _params.boldAmount, - upperHint: _params.upperHint, - lowerHint: _params.lowerHint, - interestBatchManager: _params.batchManager, - maxUpfrontFee: _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - addManager: address(this), // add manager - removeManager: address(this), // remove manager - receiver: address(this) // receiver for remove manager - }); - troveId = - borrowerOperations.openTroveAndJoinInterestBatchManager(openTroveAndJoinInterestBatchManagerParams); - } - - // Set add/remove managers - _setAddManager(troveId, _params.addManager); - _setRemoveManagerAndReceiver(troveId, _params.removeManager, _params.receiver); - - // Swap Bold to Coll - exchange.swapFromBold(_params.boldAmount, _params.flashLoanAmount); - - // Send coll back to return flash loan - WETH.transfer(address(flashLoanProvider), _params.flashLoanAmount); - // WETH reverts on failure: https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code - } - - function leverUpTrove(LeverUpTroveParams calldata _params) external { - address owner = troveNFT.ownerOf(_params.troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_params.troveId, owner); - _requireZapperIsReceiver(_params.troveId); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensBalancesAndReceiver(WETH, boldToken, initialBalances, receiver); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - WETH, _params.flashLoanAmount, IFlashLoanProvider.Operation.LeverUpTrove, abi.encode(_params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Callback from the flash loan provider - function receiveFlashLoanOnLeverUpTrove(LeverUpTroveParams calldata _params, uint256 _effectiveFlashLoanAmount) - external - override - { - require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider"); - - // Adjust trove - // With the received coll from flash loan, we increase both the trove coll and debt - borrowerOperations.adjustTrove( - _params.troveId, - _effectiveFlashLoanAmount, // flash loan amount minus fee - true, // _isCollIncrease - _params.boldAmount, - true, // _isDebtIncrease - _params.maxUpfrontFee - ); - - // Swap Bold to Coll - // No need to use a min: if the obtained amount is not enough, the flash loan return below won’t be enough - // And the flash loan provider will revert after this function exits - // The frontend should calculate in advance the `_params.boldAmount` needed for this to work - exchange.swapFromBold(_params.boldAmount, _params.flashLoanAmount); - - // Send coll back to return flash loan - WETH.transfer(address(flashLoanProvider), _params.flashLoanAmount); - } - - function leverDownTrove(LeverDownTroveParams calldata _params) external { - address owner = troveNFT.ownerOf(_params.troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_params.troveId, owner); - _requireZapperIsReceiver(_params.troveId); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensBalancesAndReceiver(WETH, boldToken, initialBalances, receiver); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - WETH, _params.flashLoanAmount, IFlashLoanProvider.Operation.LeverDownTrove, abi.encode(_params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Callback from the flash loan provider - function receiveFlashLoanOnLeverDownTrove(LeverDownTroveParams calldata _params, uint256 _effectiveFlashLoanAmount) - external - override - { - require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider"); - - // Swap Coll from flash loan to Bold, so we can repay and downsize trove - // We swap the flash loan minus the flash loan fee - // The frontend should calculate in advance the `_params.minBoldAmount` to achieve the desired leverage ratio - // (with some slippage tolerance) - uint256 receivedBoldAmount = exchange.swapToBold(_effectiveFlashLoanAmount, _params.minBoldAmount); - - // Adjust trove - borrowerOperations.adjustTrove( - _params.troveId, - _params.flashLoanAmount, - false, // _isCollIncrease - receivedBoldAmount, - false, // _isDebtIncrease - 0 - ); - - // Send coll back to return flash loan - WETH.transfer(address(flashLoanProvider), _params.flashLoanAmount); - } - - // As formulas are symmetrical, it can be used in both ways - function leverageRatioToCollateralRatio(uint256 _inputRatio) external pure returns (uint256) { - return _inputRatio * DECIMAL_PRECISION / (_inputRatio - DECIMAL_PRECISION); - } -} diff --git a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveFactory.sol b/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveFactory.sol deleted file mode 100644 index 78e17829b..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveFactory.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "./ICurvePool.sol"; - -interface ICurveFactory { - function deploy_pool( - string memory name, - string memory symbol, - address[2] memory coins, - uint256 implementation_id, - uint256 A, - uint256 gamma, - uint256 mid_fee, - uint256 out_fee, - uint256 fee_gamma, - uint256 allowed_extra_profit, - uint256 adjustment_step, - uint256 ma_exp_time, - uint256 initial_price - ) external returns (ICurvePool); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurvePool.sol b/contracts/src/Zappers/Modules/Exchanges/Curve/ICurvePool.sol deleted file mode 100644 index 05d00c918..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurvePool.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -interface ICurvePool { - function add_liquidity(uint256[2] memory amounts, uint256 min_mint_amount) external returns (uint256); - //function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy, bool use_eth, address receiver) external returns (uint256 output); - function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external returns (uint256 output); - function get_dy(uint256 i, uint256 j, uint256 dx) external view returns (uint256 dy); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGFactory.sol b/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGFactory.sol deleted file mode 100644 index 0c689da0b..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGFactory.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "./ICurveStableswapNGPool.sol"; - -interface ICurveStableswapNGFactory { - /* - function deploy_plain_pool( - string memory name, - string memory symbol, - address[2] memory coins, - uint256 A, - uint256 fee, - uint256 asset_type, - uint256 implementation_id - ) external returns (ICurvePool); - */ - function deploy_plain_pool( - string memory name, - string memory symbol, - address[] memory coins, - uint256 A, - uint256 fee, - uint256 offpeg_fee_multiplier, - uint256 ma_exp_time, - uint256 implementation_id, - uint8[] memory asset_types, - bytes4[] memory method_ids, - address[] memory oracles - ) external returns (ICurveStableswapNGPool); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGPool.sol b/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGPool.sol deleted file mode 100644 index 96ecb529b..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGPool.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -interface ICurveStableswapNGPool { - function add_liquidity(uint256[] memory amounts, uint256 min_mint_amount) external returns (uint256); - function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external returns (uint256 output); - function get_dx(int128 i, int128 j, uint256 dy) external view returns (uint256 dx); - function get_dy(int128 i, int128 j, uint256 dx) external view returns (uint256 dy); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/CurveExchange.sol b/contracts/src/Zappers/Modules/Exchanges/CurveExchange.sol deleted file mode 100644 index a0e24efd8..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/CurveExchange.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; - -import "../../../Interfaces/IBoldToken.sol"; -import "./Curve/ICurvePool.sol"; -import "../../Interfaces/IExchange.sol"; - -contract CurveExchange is IExchange { - using SafeERC20 for IERC20; - - IERC20 public immutable collToken; - IBoldToken public immutable boldToken; - ICurvePool public immutable curvePool; - uint256 public immutable COLL_TOKEN_INDEX; - uint256 public immutable BOLD_TOKEN_INDEX; - - constructor( - IERC20 _collToken, - IBoldToken _boldToken, - ICurvePool _curvePool, - uint256 _collIndex, - uint256 _boldIndex - ) { - collToken = _collToken; - boldToken = _boldToken; - curvePool = _curvePool; - COLL_TOKEN_INDEX = _collIndex; - BOLD_TOKEN_INDEX = _boldIndex; - } - - function swapFromBold(uint256 _boldAmount, uint256 _minCollAmount) external { - ICurvePool curvePoolCached = curvePool; - uint256 initialBoldBalance = boldToken.balanceOf(address(this)); - boldToken.transferFrom(msg.sender, address(this), _boldAmount); - boldToken.approve(address(curvePoolCached), _boldAmount); - - uint256 output = curvePoolCached.exchange(BOLD_TOKEN_INDEX, COLL_TOKEN_INDEX, _boldAmount, _minCollAmount); - collToken.safeTransfer(msg.sender, output); - - uint256 currentBoldBalance = boldToken.balanceOf(address(this)); - if (currentBoldBalance > initialBoldBalance) { - boldToken.transfer(msg.sender, currentBoldBalance - initialBoldBalance); - } - } - - function swapToBold(uint256 _collAmount, uint256 _minBoldAmount) external returns (uint256) { - ICurvePool curvePoolCached = curvePool; - uint256 initialCollBalance = collToken.balanceOf(address(this)); - collToken.safeTransferFrom(msg.sender, address(this), _collAmount); - collToken.approve(address(curvePoolCached), _collAmount); - - uint256 output = curvePoolCached.exchange(COLL_TOKEN_INDEX, BOLD_TOKEN_INDEX, _collAmount, _minBoldAmount); - boldToken.transfer(msg.sender, output); - - uint256 currentCollBalance = collToken.balanceOf(address(this)); - if (currentCollBalance > initialCollBalance) { - collToken.safeTransfer(msg.sender, currentCollBalance - initialCollBalance); - } - - return output; - } -} diff --git a/contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3Exchange.sol b/contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3Exchange.sol deleted file mode 100644 index cc66ec6c7..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3Exchange.sol +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.18; - -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import "openzeppelin-contracts/contracts/utils/math/Math.sol"; - -import "../../../Interfaces/IWETH.sol"; -import "../../LeftoversSweep.sol"; -// Curve -import "./Curve/ICurveStableswapNGPool.sol"; -// UniV3 -import "./UniswapV3/ISwapRouter.sol"; - -import "../../Interfaces/IExchange.sol"; - -// import "forge-std/console2.sol"; - -contract HybridCurveUniV3Exchange is LeftoversSweep, IExchange { - using SafeERC20 for IERC20; - - IERC20 public immutable collToken; - IBoldToken public immutable boldToken; - IERC20 public immutable USDC; - IWETH public immutable WETH; - - // Curve - ICurveStableswapNGPool public immutable curvePool; - uint128 public immutable USDC_INDEX; - uint128 public immutable BOLD_TOKEN_INDEX; - - // Uniswap - uint24 public immutable feeUsdcWeth; - uint24 public immutable feeWethColl; - ISwapRouter public immutable uniV3Router; - - constructor( - IERC20 _collToken, - IBoldToken _boldToken, - IERC20 _usdc, - IWETH _weth, - // Curve - ICurveStableswapNGPool _curvePool, - uint128 _usdcIndex, - uint128 _boldIndex, - // UniV3 - uint24 _feeUsdcWeth, - uint24 _feeWethColl, - ISwapRouter _uniV3Router - ) { - collToken = _collToken; - boldToken = _boldToken; - USDC = _usdc; - WETH = _weth; - - // Curve - curvePool = _curvePool; - USDC_INDEX = _usdcIndex; - BOLD_TOKEN_INDEX = _boldIndex; - - // Uniswap - feeUsdcWeth = _feeUsdcWeth; - feeWethColl = _feeWethColl; - uniV3Router = _uniV3Router; - } - - // Bold -> USDC on Curve; then USDC -> WETH, and optionally WETH -> Coll, on UniV3 - function swapFromBold(uint256 _boldAmount, uint256 _minCollAmount) external { - InitialBalances memory initialBalances; - _setHybridExchangeInitialBalances(initialBalances); - - // Curve - boldToken.transferFrom(msg.sender, address(this), _boldAmount); - boldToken.approve(address(curvePool), _boldAmount); - - uint256 curveUsdcAmount = curvePool.exchange(int128(BOLD_TOKEN_INDEX), int128(USDC_INDEX), _boldAmount, 0); - - // Uniswap - USDC.approve(address(uniV3Router), curveUsdcAmount); - - // See: https://docs.uniswap.org/contracts/v3/guides/swaps/multihop-swaps - bytes memory path; - if (address(WETH) == address(collToken)) { - path = abi.encodePacked(USDC, feeUsdcWeth, WETH); - } else { - path = abi.encodePacked(USDC, feeUsdcWeth, WETH, feeWethColl, collToken); - } - - ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: path, - recipient: msg.sender, - deadline: block.timestamp, - amountIn: curveUsdcAmount, - amountOutMinimum: _minCollAmount - }); - - // Executes the swap. - uniV3Router.exactInput(params); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Optionally Coll -> WETH, and WETH -> USDC on UniV3; then USDC -> Bold on Curve - function swapToBold(uint256 _collAmount, uint256 _minBoldAmount) external returns (uint256) { - InitialBalances memory initialBalances; - _setHybridExchangeInitialBalances(initialBalances); - - // Uniswap - collToken.safeTransferFrom(msg.sender, address(this), _collAmount); - collToken.approve(address(uniV3Router), _collAmount); - - // See: https://docs.uniswap.org/contracts/v3/guides/swaps/multihop-swaps - bytes memory path; - if (address(WETH) == address(collToken)) { - path = abi.encodePacked(WETH, feeUsdcWeth, USDC); - } else { - path = abi.encodePacked(collToken, feeWethColl, WETH, feeUsdcWeth, USDC); - } - - ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: path, - recipient: address(this), - deadline: block.timestamp, - amountIn: _collAmount, - amountOutMinimum: 0 - }); - - // Executes the swap. - uint256 uniV3UsdcAmount = uniV3Router.exactInput(params); - - // Curve - USDC.approve(address(curvePool), uniV3UsdcAmount); - - uint256 boldAmount = - curvePool.exchange(int128(USDC_INDEX), int128(BOLD_TOKEN_INDEX), uniV3UsdcAmount, _minBoldAmount); - boldToken.transfer(msg.sender, boldAmount); - - // return leftovers to user - _returnLeftovers(initialBalances); - - return boldAmount; - } - - function _setHybridExchangeInitialBalances(InitialBalances memory initialBalances) internal view { - initialBalances.tokens[0] = boldToken; - initialBalances.tokens[1] = USDC; - initialBalances.tokens[2] = WETH; - if (address(WETH) != address(collToken)) { - initialBalances.tokens[3] = collToken; - } - _setInitialBalances(initialBalances); - } -} diff --git a/contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol b/contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol deleted file mode 100644 index ac55d1efe..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.18; - -import "../../../Interfaces/IWETH.sol"; -// Curve -import "./Curve/ICurveStableswapNGPool.sol"; -// UniV3 -import "./UniswapV3/IQuoterV2.sol"; - -import "../../Interfaces/IExchangeHelpers.sol"; - -contract HybridCurveUniV3ExchangeHelpers is IExchangeHelpers { - uint256 private constant DECIMAL_PRECISION = 1e18; - - //HybridCurveUniV3Exchange public immutable exchange; - - IERC20 public immutable USDC; - IWETH public immutable WETH; - - // Curve - ICurveStableswapNGPool public immutable curvePool; - uint128 public immutable USDC_INDEX; - uint128 public immutable BOLD_TOKEN_INDEX; - - // Uniswap - uint24 public immutable feeUsdcWeth; - uint24 public immutable feeWethColl; - IQuoterV2 public immutable uniV3Quoter; - - /* - constructor(HybridCurveUniV3Exchange _exchange) { - exchange = _exchange; - - USDC = _exchange.USDC(); - WETH = _exchange.WETH(); - - curvePool = _exchange.curvePool(); - USDC_INDEX = _exchange.USDC_INDEX(); - BOLD_TOKEN_INDEX = _exchange.BOLD_INDEX(); - - // Uniswap - feeUsdcWeth = _exchange.feeUsdcWeth(); - feeWethColl = _exchange.feeWethColl(); - uniV3Quoter = _exchange.uniV3Quoter(); - } - */ - - constructor( - IERC20 _usdc, - IWETH _weth, - // Curve - ICurveStableswapNGPool _curvePool, - uint128 _usdcIndex, - uint128 _boldIndex, - // UniV3 - uint24 _feeUsdcWeth, - uint24 _feeWethColl, - IQuoterV2 _uniV3Quoter - ) { - USDC = _usdc; - WETH = _weth; - - // Curve - curvePool = _curvePool; - USDC_INDEX = _usdcIndex; - BOLD_TOKEN_INDEX = _boldIndex; - - // Uniswap - feeUsdcWeth = _feeUsdcWeth; - feeWethColl = _feeWethColl; - uniV3Quoter = _uniV3Quoter; - } - - function getCollFromBold(uint256 _boldAmount, IERC20 _collToken, uint256 _desiredCollAmount) - external /* view */ - returns (uint256 collAmount, uint256 deviation) - { - // BOLD -> USDC - uint256 curveUsdcAmount = curvePool.get_dy(int128(BOLD_TOKEN_INDEX), int128(USDC_INDEX), _boldAmount); - - // USDC -> Coll - bytes memory path; - if (address(WETH) == address(_collToken)) { - path = abi.encodePacked(USDC, feeUsdcWeth, WETH); - } else { - path = abi.encodePacked(USDC, feeUsdcWeth, WETH, feeWethColl, _collToken); - } - - (collAmount,,,) = uniV3Quoter.quoteExactInput(path, curveUsdcAmount); - - if (_desiredCollAmount > 0 && collAmount <= _desiredCollAmount) { - deviation = DECIMAL_PRECISION - collAmount * DECIMAL_PRECISION / _desiredCollAmount; - } - - return (collAmount, deviation); - } -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniV3Exchange.sol b/contracts/src/Zappers/Modules/Exchanges/UniV3Exchange.sol deleted file mode 100644 index 0f64a9c42..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniV3Exchange.sol +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import "openzeppelin-contracts/contracts/utils/math/Math.sol"; - -import "../../LeftoversSweep.sol"; -import "../../../Interfaces/IBoldToken.sol"; -import "./UniswapV3/ISwapRouter.sol"; -import "./UniswapV3/UniPriceConverter.sol"; -import "../../Interfaces/IExchange.sol"; -import {DECIMAL_PRECISION} from "../../../Dependencies/Constants.sol"; - -contract UniV3Exchange is LeftoversSweep, UniPriceConverter, IExchange { - using SafeERC20 for IERC20; - - IERC20 public immutable collToken; - IBoldToken public immutable boldToken; - uint24 public immutable fee; - ISwapRouter public immutable uniV3Router; - - constructor(IERC20 _collToken, IBoldToken _boldToken, uint24 _fee, ISwapRouter _uniV3Router) { - collToken = _collToken; - boldToken = _boldToken; - fee = _fee; - uniV3Router = _uniV3Router; - } - - function swapFromBold(uint256 _boldAmount, uint256 _minCollAmount) external { - ISwapRouter uniV3RouterCached = uniV3Router; - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensAndBalances(collToken, boldToken, initialBalances); - - boldToken.transferFrom(msg.sender, address(this), _boldAmount); - boldToken.approve(address(uniV3RouterCached), _boldAmount); - - ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams({ - tokenIn: address(boldToken), - tokenOut: address(collToken), - fee: fee, - recipient: msg.sender, - deadline: block.timestamp, - amountOut: _minCollAmount, - amountInMaximum: _boldAmount, - sqrtPriceLimitX96: 0 // See: https://ethereum.stackexchange.com/a/156018/9205 - }); - - uniV3RouterCached.exactOutputSingle(params); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - function swapToBold(uint256 _collAmount, uint256 _minBoldAmount) external returns (uint256) { - ISwapRouter uniV3RouterCached = uniV3Router; - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensAndBalances(collToken, boldToken, initialBalances); - - collToken.safeTransferFrom(msg.sender, address(this), _collAmount); - collToken.approve(address(uniV3RouterCached), _collAmount); - - ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ - tokenIn: address(collToken), - tokenOut: address(boldToken), - fee: fee, - recipient: msg.sender, - deadline: block.timestamp, - amountIn: _collAmount, - amountOutMinimum: _minBoldAmount, - sqrtPriceLimitX96: 0 // See: https://ethereum.stackexchange.com/a/156018/9205 - }); - - uint256 amountOut = uniV3RouterCached.exactInputSingle(params); - - // return leftovers to user - _returnLeftovers(initialBalances); - - return amountOut; - } - - function priceToSqrtPrice(IBoldToken _boldToken, IERC20 _collToken, uint256 _price) public pure returns (uint160) { - // inverse price if Bold goes first - uint256 price = _zeroForOne(_boldToken, _collToken) ? DECIMAL_PRECISION * DECIMAL_PRECISION / _price : _price; - return priceToSqrtPriceX96(price); - } - - // See: https://github.com/Uniswap/v3-periphery/blob/main/contracts/lens/QuoterV2.sol#L207C9-L207C60 - function _zeroForOne(IBoldToken _boldToken, IERC20 _collToken) internal pure returns (bool) { - return address(_boldToken) < address(_collToken); - } -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol deleted file mode 100644 index 251c88020..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.7.5; -pragma abicoder v2; - -import "./IPoolInitializer.sol"; - -/// @title Non-fungible token for positions -/// @notice Wraps Uniswap V3 positions in a non-fungible token interface which allows for them to be transferred -/// and authorized. -interface INonfungiblePositionManager is IPoolInitializer { - /// @notice Emitted when liquidity is increased for a position NFT - /// @dev Also emitted when a token is minted - /// @param tokenId The ID of the token for which liquidity was increased - /// @param liquidity The amount by which liquidity for the NFT position was increased - /// @param amount0 The amount of token0 that was paid for the increase in liquidity - /// @param amount1 The amount of token1 that was paid for the increase in liquidity - event IncreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); - /// @notice Emitted when liquidity is decreased for a position NFT - /// @param tokenId The ID of the token for which liquidity was decreased - /// @param liquidity The amount by which liquidity for the NFT position was decreased - /// @param amount0 The amount of token0 that was accounted for the decrease in liquidity - /// @param amount1 The amount of token1 that was accounted for the decrease in liquidity - event DecreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); - /// @notice Emitted when tokens are collected for a position NFT - /// @dev The amounts reported may not be exactly equivalent to the amounts transferred, due to rounding behavior - /// @param tokenId The ID of the token for which underlying tokens were collected - /// @param recipient The address of the account that received the collected tokens - /// @param amount0 The amount of token0 owed to the position that was collected - /// @param amount1 The amount of token1 owed to the position that was collected - event Collect(uint256 indexed tokenId, address recipient, uint256 amount0, uint256 amount1); - - /// @notice Returns the position information associated with a given token ID. - /// @dev Throws if the token ID is not valid. - /// @param tokenId The ID of the token that represents the position - /// @return nonce The nonce for permits - /// @return operator The address that is approved for spending - /// @return token0 The address of the token0 for a specific pool - /// @return token1 The address of the token1 for a specific pool - /// @return fee The fee associated with the pool - /// @return tickLower The lower end of the tick range for the position - /// @return tickUpper The higher end of the tick range for the position - /// @return liquidity The liquidity of the position - /// @return feeGrowthInside0LastX128 The fee growth of token0 as of the last action on the individual position - /// @return feeGrowthInside1LastX128 The fee growth of token1 as of the last action on the individual position - /// @return tokensOwed0 The uncollected amount of token0 owed to the position as of the last computation - /// @return tokensOwed1 The uncollected amount of token1 owed to the position as of the last computation - function positions(uint256 tokenId) - external - view - returns ( - uint96 nonce, - address operator, - address token0, - address token1, - uint24 fee, - int24 tickLower, - int24 tickUpper, - uint128 liquidity, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128, - uint128 tokensOwed0, - uint128 tokensOwed1 - ); - - struct MintParams { - address token0; - address token1; - uint24 fee; - int24 tickLower; - int24 tickUpper; - uint256 amount0Desired; - uint256 amount1Desired; - uint256 amount0Min; - uint256 amount1Min; - address recipient; - uint256 deadline; - } - - /// @notice Creates a new position wrapped in a NFT - /// @dev Call this when the pool does exist and is initialized. Note that if the pool is created but not initialized - /// a method does not exist, i.e. the pool is assumed to be initialized. - /// @param params The params necessary to mint a position, encoded as `MintParams` in calldata - /// @return tokenId The ID of the token that represents the minted position - /// @return liquidity The amount of liquidity for this position - /// @return amount0 The amount of token0 - /// @return amount1 The amount of token1 - function mint(MintParams calldata params) - external - payable - returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); - - struct IncreaseLiquidityParams { - uint256 tokenId; - uint256 amount0Desired; - uint256 amount1Desired; - uint256 amount0Min; - uint256 amount1Min; - uint256 deadline; - } - - /// @notice Increases the amount of liquidity in a position, with tokens paid by the `msg.sender` - /// @param params tokenId The ID of the token for which liquidity is being increased, - /// amount0Desired The desired amount of token0 to be spent, - /// amount1Desired The desired amount of token1 to be spent, - /// amount0Min The minimum amount of token0 to spend, which serves as a slippage check, - /// amount1Min The minimum amount of token1 to spend, which serves as a slippage check, - /// deadline The time by which the transaction must be included to effect the change - /// @return liquidity The new liquidity amount as a result of the increase - /// @return amount0 The amount of token0 to acheive resulting liquidity - /// @return amount1 The amount of token1 to acheive resulting liquidity - function increaseLiquidity(IncreaseLiquidityParams calldata params) - external - payable - returns (uint128 liquidity, uint256 amount0, uint256 amount1); - - struct DecreaseLiquidityParams { - uint256 tokenId; - uint128 liquidity; - uint256 amount0Min; - uint256 amount1Min; - uint256 deadline; - } - - /// @notice Decreases the amount of liquidity in a position and accounts it to the position - /// @param params tokenId The ID of the token for which liquidity is being decreased, - /// amount The amount by which liquidity will be decreased, - /// amount0Min The minimum amount of token0 that should be accounted for the burned liquidity, - /// amount1Min The minimum amount of token1 that should be accounted for the burned liquidity, - /// deadline The time by which the transaction must be included to effect the change - /// @return amount0 The amount of token0 accounted to the position's tokens owed - /// @return amount1 The amount of token1 accounted to the position's tokens owed - function decreaseLiquidity(DecreaseLiquidityParams calldata params) - external - payable - returns (uint256 amount0, uint256 amount1); - - struct CollectParams { - uint256 tokenId; - address recipient; - uint128 amount0Max; - uint128 amount1Max; - } - - /// @notice Collects up to a maximum amount of fees owed to a specific position to the recipient - /// @param params tokenId The ID of the NFT for which tokens are being collected, - /// recipient The account that should receive the tokens, - /// amount0Max The maximum amount of token0 to collect, - /// amount1Max The maximum amount of token1 to collect - /// @return amount0 The amount of fees collected in token0 - /// @return amount1 The amount of fees collected in token1 - function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1); - - /// @notice Burns a token ID, which deletes it from the NFT contract. The token must have 0 liquidity and all tokens - /// must be collected first. - /// @param tokenId The ID of the token that is being burned - function burn(uint256 tokenId) external payable; -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IPoolInitializer.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IPoolInitializer.sol deleted file mode 100644 index c2146992d..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IPoolInitializer.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.7.5; -pragma abicoder v2; - -/// @title Creates and initializes V3 Pools -/// @notice Provides a method for creating and initializing a pool, if necessary, for bundling with other methods that -/// require the pool to exist. -interface IPoolInitializer { - /// @notice Creates a new pool if it does not exist, then initializes if not initialized - /// @dev This method can be bundled with others via IMulticall for the first action (e.g. mint) performed against a pool - /// @param token0 The contract address of token0 of the pool - /// @param token1 The contract address of token1 of the pool - /// @param fee The fee amount of the v3 pool for the specified token pair - /// @param sqrtPriceX96 The initial square root price of the pool as a Q64.96 value - /// @return pool Returns the pool address based on the pair of tokens and fee, will return the newly created pool address if necessary - function createAndInitializePoolIfNecessary(address token0, address token1, uint24 fee, uint160 sqrtPriceX96) - external - payable - returns (address pool); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol deleted file mode 100644 index 7f41cb084..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.7.5; -pragma abicoder v2; - -/// @title QuoterV2 Interface -/// @notice Supports quoting the calculated amounts from exact input or exact output swaps. -/// @notice For each pool also tells you the number of initialized ticks crossed and the sqrt price of the pool after the swap. -/// @dev These functions are not marked view because they rely on calling non-view functions and reverting -/// to compute the result. They are also not gas efficient and should not be called on-chain. -interface IQuoterV2 { - /// @notice Returns the amount out received for a given exact input swap without executing the swap - /// @param path The path of the swap, i.e. each token pair and the pool fee - /// @param amountIn The amount of the first token to swap - /// @return amountOut The amount of the last token that would be received - /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path - /// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path - /// @return gasEstimate The estimate of the gas that the swap consumes - function quoteExactInput(bytes memory path, uint256 amountIn) - external - returns ( - uint256 amountOut, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksCrossedList, - uint256 gasEstimate - ); - - struct QuoteExactInputSingleParams { - address tokenIn; - address tokenOut; - uint256 amountIn; - uint24 fee; - uint160 sqrtPriceLimitX96; - } - - /// @notice Returns the amount out received for a given exact input but for a swap of a single pool - /// @param params The params for the quote, encoded as `QuoteExactInputSingleParams` - /// tokenIn The token being swapped in - /// tokenOut The token being swapped out - /// fee The fee of the token pool to consider for the pair - /// amountIn The desired input amount - /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap - /// @return amountOut The amount of `tokenOut` that would be received - /// @return sqrtPriceX96After The sqrt price of the pool after the swap - /// @return initializedTicksCrossed The number of initialized ticks that the swap crossed - /// @return gasEstimate The estimate of the gas that the swap consumes - function quoteExactInputSingle(QuoteExactInputSingleParams memory params) - external - returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate); - - /// @notice Returns the amount in required for a given exact output swap without executing the swap - /// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order - /// @param amountOut The amount of the last token to receive - /// @return amountIn The amount of first token required to be paid - /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path - /// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path - /// @return gasEstimate The estimate of the gas that the swap consumes - function quoteExactOutput(bytes memory path, uint256 amountOut) - external - returns ( - uint256 amountIn, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksCrossedList, - uint256 gasEstimate - ); - - struct QuoteExactOutputSingleParams { - address tokenIn; - address tokenOut; - uint256 amount; - uint24 fee; - uint160 sqrtPriceLimitX96; - } - - /// @notice Returns the amount in required to receive the given exact output amount but for a swap of a single pool - /// @param params The params for the quote, encoded as `QuoteExactOutputSingleParams` - /// tokenIn The token being swapped in - /// tokenOut The token being swapped out - /// fee The fee of the token pool to consider for the pair - /// amountOut The desired output amount - /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap - /// @return amountIn The amount required as the input for the swap in order to receive `amountOut` - /// @return sqrtPriceX96After The sqrt price of the pool after the swap - /// @return initializedTicksCrossed The number of initialized ticks that the swap crossed - /// @return gasEstimate The estimate of the gas that the swap consumes - function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params) - external - returns (uint256 amountIn, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol deleted file mode 100644 index beb300450..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.7.5; -pragma abicoder v2; - -/// @title Router token swapping functionality -/// @notice Functions for swapping tokens via Uniswap V3 -interface ISwapRouter { - struct ExactInputSingleParams { - address tokenIn; - address tokenOut; - uint24 fee; - address recipient; - uint256 deadline; - uint256 amountIn; - uint256 amountOutMinimum; - uint160 sqrtPriceLimitX96; - } - - /// @notice Swaps `amountIn` of one token for as much as possible of another token - /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata - /// @return amountOut The amount of the received token - function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); - - struct ExactInputParams { - bytes path; - address recipient; - uint256 deadline; - uint256 amountIn; - uint256 amountOutMinimum; - } - - /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path - /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata - /// @return amountOut The amount of the received token - function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); - - struct ExactOutputSingleParams { - address tokenIn; - address tokenOut; - uint24 fee; - address recipient; - uint256 deadline; - uint256 amountOut; - uint256 amountInMaximum; - uint160 sqrtPriceLimitX96; - } - - /// @notice Swaps as little as possible of one token for `amountOut` of another token - /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata - /// @return amountIn The amount of the input token - function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); - - struct ExactOutputParams { - bytes path; - address recipient; - uint256 deadline; - uint256 amountOut; - uint256 amountInMaximum; - } - - /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed) - /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata - /// @return amountIn The amount of the input token - function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Factory.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Factory.sol deleted file mode 100644 index 1ce33c793..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Factory.sol +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.5.0; - -interface IUniswapV3Factory { - /// @notice Emitted when the owner of the factory is changed - /// @param oldOwner The owner before the owner was changed - /// @param newOwner The owner after the owner was changed - event OwnerChanged(address indexed oldOwner, address indexed newOwner); - - /// @notice Emitted when a pool is created - /// @param token0 The first token of the pool by address sort order - /// @param token1 The second token of the pool by address sort order - /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip - /// @param tickSpacing The minimum number of ticks between initialized ticks - /// @param pool The address of the created pool - event PoolCreated( - address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool - ); - - /// @notice Emitted when a new fee amount is enabled for pool creation via the factory - /// @param fee The enabled fee, denominated in hundredths of a bip - /// @param tickSpacing The minimum number of ticks between initialized ticks for pools created with the given fee - event FeeAmountEnabled(uint24 indexed fee, int24 indexed tickSpacing); - - /// @notice Returns the current owner of the factory - /// @dev Can be changed by the current owner via setOwner - /// @return The address of the factory owner - function owner() external view returns (address); - - /// @notice Returns the tick spacing for a given fee amount, if enabled, or 0 if not enabled - /// @dev A fee amount can never be removed, so this value should be hard coded or cached in the calling context - /// @param fee The enabled fee, denominated in hundredths of a bip. Returns 0 in case of unenabled fee - /// @return The tick spacing - function feeAmountTickSpacing(uint24 fee) external view returns (int24); - - /// @notice Returns the pool address for a given pair of tokens and a fee, or address 0 if it does not exist - /// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order - /// @param tokenA The contract address of either token0 or token1 - /// @param tokenB The contract address of the other token - /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip - /// @return pool The pool address - function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool); - - /// @notice Creates a pool for the given two tokens and fee - /// @param tokenA One of the two tokens in the desired pool - /// @param tokenB The other of the two tokens in the desired pool - /// @param fee The desired fee for the pool - /// @dev tokenA and tokenB may be passed in either order: token0/token1 or token1/token0. tickSpacing is retrieved - /// from the fee. The call will revert if the pool already exists, the fee is invalid, or the token arguments - /// are invalid. - /// @return pool The address of the newly created pool - function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool); - - /// @notice Updates the owner of the factory - /// @dev Must be called by the current owner - /// @param _owner The new owner of the factory - function setOwner(address _owner) external; - - /// @notice Enables a fee amount with the given tickSpacing - /// @dev Fee amounts may never be removed once enabled - /// @param fee The fee amount to enable, denominated in hundredths of a bip (i.e. 1e-6) - /// @param tickSpacing The spacing between ticks to be enforced for all pools created with the given fee amount - function enableFeeAmount(uint24 fee, int24 tickSpacing) external; -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Pool.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Pool.sol deleted file mode 100644 index 85dbdea6d..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Pool.sol +++ /dev/null @@ -1,271 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.5.0; - -interface IUniswapV3Pool { - /// @notice The contract that deployed the pool, which must adhere to the IUniswapV3Factory interface - /// @return The contract address - function factory() external view returns (address); - - /// @notice The first of the two tokens of the pool, sorted by address - /// @return The token contract address - function token0() external view returns (address); - - /// @notice The second of the two tokens of the pool, sorted by address - /// @return The token contract address - function token1() external view returns (address); - - /// @notice The pool's fee in hundredths of a bip, i.e. 1e-6 - /// @return The fee - function fee() external view returns (uint24); - - /// @notice The pool tick spacing - /// @dev Ticks can only be used at multiples of this value, minimum of 1 and always positive - /// e.g.: a tickSpacing of 3 means ticks can be initialized every 3rd tick, i.e., ..., -6, -3, 0, 3, 6, ... - /// This value is an int24 to avoid casting even though it is always positive. - /// @return The tick spacing - function tickSpacing() external view returns (int24); - - /// @notice The maximum amount of position liquidity that can use any tick in the range - /// @dev This parameter is enforced per tick to prevent liquidity from overflowing a uint128 at any point, and - /// also prevents out-of-range liquidity from being used to prevent adding in-range liquidity to a pool - /// @return The max amount of liquidity per tick - function maxLiquidityPerTick() external view returns (uint128); - - /// @notice The 0th storage slot in the pool stores many values, and is exposed as a single method to save gas - /// when accessed externally. - /// @return sqrtPriceX96 The current price of the pool as a sqrt(token1/token0) Q64.96 value - /// tick The current tick of the pool, i.e. according to the last tick transition that was run. - /// This value may not always be equal to SqrtTickMath.getTickAtSqrtRatio(sqrtPriceX96) if the price is on a tick - /// boundary. - /// observationIndex The index of the last oracle observation that was written, - /// observationCardinality The current maximum number of observations stored in the pool, - /// observationCardinalityNext The next maximum number of observations, to be updated when the observation. - /// feeProtocol The protocol fee for both tokens of the pool. - /// Encoded as two 4 bit values, where the protocol fee of token1 is shifted 4 bits and the protocol fee of token0 - /// is the lower 4 bits. Used as the denominator of a fraction of the swap fee, e.g. 4 means 1/4th of the swap fee. - /// unlocked Whether the pool is currently locked to reentrancy - function slot0() - external - view - returns ( - uint160 sqrtPriceX96, - int24 tick, - uint16 observationIndex, - uint16 observationCardinality, - uint16 observationCardinalityNext, - uint8 feeProtocol, - bool unlocked - ); - - /// @notice The fee growth as a Q128.128 fees of token0 collected per unit of liquidity for the entire life of the pool - /// @dev This value can overflow the uint256 - function feeGrowthGlobal0X128() external view returns (uint256); - - /// @notice The fee growth as a Q128.128 fees of token1 collected per unit of liquidity for the entire life of the pool - /// @dev This value can overflow the uint256 - function feeGrowthGlobal1X128() external view returns (uint256); - - /// @notice The amounts of token0 and token1 that are owed to the protocol - /// @dev Protocol fees will never exceed uint128 max in either token - function protocolFees() external view returns (uint128 token0, uint128 token1); - - /// @notice The currently in range liquidity available to the pool - /// @dev This value has no relationship to the total liquidity across all ticks - function liquidity() external view returns (uint128); - - /// @notice Look up information about a specific tick in the pool - /// @param tick The tick to look up - /// @return liquidityGross the total amount of position liquidity that uses the pool either as tick lower or - /// tick upper, - /// liquidityNet how much liquidity changes when the pool price crosses the tick, - /// feeGrowthOutside0X128 the fee growth on the other side of the tick from the current tick in token0, - /// feeGrowthOutside1X128 the fee growth on the other side of the tick from the current tick in token1, - /// tickCumulativeOutside the cumulative tick value on the other side of the tick from the current tick - /// secondsPerLiquidityOutsideX128 the seconds spent per liquidity on the other side of the tick from the current tick, - /// secondsOutside the seconds spent on the other side of the tick from the current tick, - /// initialized Set to true if the tick is initialized, i.e. liquidityGross is greater than 0, otherwise equal to false. - /// Outside values can only be used if the tick is initialized, i.e. if liquidityGross is greater than 0. - /// In addition, these values are only relative and must be used only in comparison to previous snapshots for - /// a specific position. - function ticks(int24 tick) - external - view - returns ( - uint128 liquidityGross, - int128 liquidityNet, - uint256 feeGrowthOutside0X128, - uint256 feeGrowthOutside1X128, - int56 tickCumulativeOutside, - uint160 secondsPerLiquidityOutsideX128, - uint32 secondsOutside, - bool initialized - ); - - /// @notice Returns 256 packed tick initialized boolean values. See TickBitmap for more information - function tickBitmap(int16 wordPosition) external view returns (uint256); - - /// @notice Returns the information about a position by the position's key - /// @param key The position's key is a hash of a preimage composed by the owner, tickLower and tickUpper - /// @return _liquidity The amount of liquidity in the position, - /// Returns feeGrowthInside0LastX128 fee growth of token0 inside the tick range as of the last mint/burn/poke, - /// Returns feeGrowthInside1LastX128 fee growth of token1 inside the tick range as of the last mint/burn/poke, - /// Returns tokensOwed0 the computed amount of token0 owed to the position as of the last mint/burn/poke, - /// Returns tokensOwed1 the computed amount of token1 owed to the position as of the last mint/burn/poke - function positions(bytes32 key) - external - view - returns ( - uint128 _liquidity, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128, - uint128 tokensOwed0, - uint128 tokensOwed1 - ); - - /// @notice Returns data about a specific observation index - /// @param index The element of the observations array to fetch - /// @dev You most likely want to use #observe() instead of this method to get an observation as of some amount of time - /// ago, rather than at a specific index in the array. - /// @return blockTimestamp The timestamp of the observation, - /// Returns tickCumulative the tick multiplied by seconds elapsed for the life of the pool as of the observation timestamp, - /// Returns secondsPerLiquidityCumulativeX128 the seconds per in range liquidity for the life of the pool as of the observation timestamp, - /// Returns initialized whether the observation has been initialized and the values are safe to use - function observations(uint256 index) - external - view - returns ( - uint32 blockTimestamp, - int56 tickCumulative, - uint160 secondsPerLiquidityCumulativeX128, - bool initialized - ); - /// @notice Returns the cumulative tick and liquidity as of each timestamp `secondsAgo` from the current block timestamp - /// @dev To get a time weighted average tick or liquidity-in-range, you must call this with two values, one representing - /// the beginning of the period and another for the end of the period. E.g., to get the last hour time-weighted average tick, - /// you must call it with secondsAgos = [3600, 0]. - /// @dev The time weighted average tick represents the geometric time weighted average price of the pool, in - /// log base sqrt(1.0001) of token1 / token0. The TickMath library can be used to go from a tick value to a ratio. - /// @param secondsAgos From how long ago each cumulative tick and liquidity value should be returned - /// @return tickCumulatives Cumulative tick values as of each `secondsAgos` from the current block timestamp - /// @return secondsPerLiquidityCumulativeX128s Cumulative seconds per liquidity-in-range value as of each `secondsAgos` from the current block - /// timestamp - function observe(uint32[] calldata secondsAgos) - external - view - returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s); - - /// @notice Returns a snapshot of the tick cumulative, seconds per liquidity and seconds inside a tick range - /// @dev Snapshots must only be compared to other snapshots, taken over a period for which a position existed. - /// I.e., snapshots cannot be compared if a position is not held for the entire period between when the first - /// snapshot is taken and the second snapshot is taken. - /// @param tickLower The lower tick of the range - /// @param tickUpper The upper tick of the range - /// @return tickCumulativeInside The snapshot of the tick accumulator for the range - /// @return secondsPerLiquidityInsideX128 The snapshot of seconds per liquidity for the range - /// @return secondsInside The snapshot of seconds per liquidity for the range - function snapshotCumulativesInside(int24 tickLower, int24 tickUpper) - external - view - returns (int56 tickCumulativeInside, uint160 secondsPerLiquidityInsideX128, uint32 secondsInside); - - /// @notice Sets the initial price for the pool - /// @dev Price is represented as a sqrt(amountToken1/amountToken0) Q64.96 value - /// @param sqrtPriceX96 the initial sqrt price of the pool as a Q64.96 - function initialize(uint160 sqrtPriceX96) external; - - /// @notice Adds liquidity for the given recipient/tickLower/tickUpper position - /// @dev The caller of this method receives a callback in the form of IUniswapV3MintCallback#uniswapV3MintCallback - /// in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends - /// on tickLower, tickUpper, the amount of liquidity, and the current price. - /// @param recipient The address for which the liquidity will be created - /// @param tickLower The lower tick of the position in which to add liquidity - /// @param tickUpper The upper tick of the position in which to add liquidity - /// @param amount The amount of liquidity to mint - /// @param data Any data that should be passed through to the callback - /// @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in the callback - /// @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in the callback - function mint(address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data) - external - returns (uint256 amount0, uint256 amount1); - - /// @notice Collects tokens owed to a position - /// @dev Does not recompute fees earned, which must be done either via mint or burn of any amount of liquidity. - /// Collect must be called by the position owner. To withdraw only token0 or only token1, amount0Requested or - /// amount1Requested may be set to zero. To withdraw all tokens owed, caller may pass any value greater than the - /// actual tokens owed, e.g. type(uint128).max. Tokens owed may be from accumulated swap fees or burned liquidity. - /// @param recipient The address which should receive the fees collected - /// @param tickLower The lower tick of the position for which to collect fees - /// @param tickUpper The upper tick of the position for which to collect fees - /// @param amount0Requested How much token0 should be withdrawn from the fees owed - /// @param amount1Requested How much token1 should be withdrawn from the fees owed - /// @return amount0 The amount of fees collected in token0 - /// @return amount1 The amount of fees collected in token1 - function collect( - address recipient, - int24 tickLower, - int24 tickUpper, - uint128 amount0Requested, - uint128 amount1Requested - ) external returns (uint128 amount0, uint128 amount1); - - /// @notice Burn liquidity from the sender and account tokens owed for the liquidity to the position - /// @dev Can be used to trigger a recalculation of fees owed to a position by calling with an amount of 0 - /// @dev Fees must be collected separately via a call to #collect - /// @param tickLower The lower tick of the position for which to burn liquidity - /// @param tickUpper The upper tick of the position for which to burn liquidity - /// @param amount How much liquidity to burn - /// @return amount0 The amount of token0 sent to the recipient - /// @return amount1 The amount of token1 sent to the recipient - function burn(int24 tickLower, int24 tickUpper, uint128 amount) - external - returns (uint256 amount0, uint256 amount1); - - /// @notice Swap token0 for token1, or token1 for token0 - /// @dev The caller of this method receives a callback in the form of IUniswapV3SwapCallback#uniswapV3SwapCallback - /// @param recipient The address to receive the output of the swap - /// @param zeroForOne The direction of the swap, true for token0 to token1, false for token1 to token0 - /// @param amountSpecified The amount of the swap, which implicitly configures the swap as exact input (positive), or exact output (negative) - /// @param sqrtPriceLimitX96 The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this - /// value after the swap. If one for zero, the price cannot be greater than this value after the swap - /// @param data Any data to be passed through to the callback - /// @return amount0 The delta of the balance of token0 of the pool, exact when negative, minimum when positive - /// @return amount1 The delta of the balance of token1 of the pool, exact when negative, minimum when positive - function swap( - address recipient, - bool zeroForOne, - int256 amountSpecified, - uint160 sqrtPriceLimitX96, - bytes calldata data - ) external returns (int256 amount0, int256 amount1); - - /// @notice Receive token0 and/or token1 and pay it back, plus a fee, in the callback - /// @dev The caller of this method receives a callback in the form of IUniswapV3FlashCallback#uniswapV3FlashCallback - /// @dev Can be used to donate underlying tokens pro-rata to currently in-range liquidity providers by calling - /// with 0 amount{0,1} and sending the donation amount(s) from the callback - /// @param recipient The address which will receive the token0 and token1 amounts - /// @param amount0 The amount of token0 to send - /// @param amount1 The amount of token1 to send - /// @param data Any data to be passed through to the callback - function flash(address recipient, uint256 amount0, uint256 amount1, bytes calldata data) external; - - /// @notice Increase the maximum number of price and liquidity observations that this pool will store - /// @dev This method is no-op if the pool already has an observationCardinalityNext greater than or equal to - /// the input observationCardinalityNext. - /// @param observationCardinalityNext The desired minimum number of observations for the pool to store - function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external; - - /// @notice Set the denominator of the protocol's % share of the fees - /// @param feeProtocol0 new protocol fee for token0 of the pool - /// @param feeProtocol1 new protocol fee for token1 of the pool - function setFeeProtocol(uint8 feeProtocol0, uint8 feeProtocol1) external; - - /// @notice Collect the protocol fee accrued to the pool - /// @param recipient The address to which collected protocol fees should be sent - /// @param amount0Requested The maximum amount of token0 to send, can be 0 to collect fees in only token1 - /// @param amount1Requested The maximum amount of token1 to send, can be 0 to collect fees in only token0 - /// @return amount0 The protocol fee collected in token0 - /// @return amount1 The protocol fee collected in token1 - function collectProtocol(address recipient, uint128 amount0Requested, uint128 amount1Requested) - external - returns (uint128 amount0, uint128 amount1); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol deleted file mode 100644 index a418baa85..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/utils/math/Math.sol"; - -import {DECIMAL_PRECISION} from "../../../../Dependencies/Constants.sol"; - -contract UniPriceConverter { - function priceToSqrtPriceX96(uint256 _price) public pure returns (uint160 sqrtPriceX96) { - // overflow vs precision - if (_price > (1 << 64)) { - // ~18.4e18 - sqrtPriceX96 = uint160(Math.sqrt(_price / DECIMAL_PRECISION) << 96); - } else { - sqrtPriceX96 = uint160(Math.sqrt((_price << 192) / DECIMAL_PRECISION)); - } - } - - function sqrtPriceX96ToPrice(uint160 _sqrtPriceX96) public pure returns (uint256 price) { - //price = uint256(_sqrtPriceX96) * uint256(_sqrtPriceX96) * DECIMAL_PRECISION / (1 << 192); - uint256 squaredPrice = uint256(_sqrtPriceX96) * uint256(_sqrtPriceX96); - // overflow vs precision - if (squaredPrice > 115e57) { - // max uint256 / 1e18 - price = ((squaredPrice >> 96) * DECIMAL_PRECISION) >> 96; - } else { - price = (squaredPrice * DECIMAL_PRECISION) >> 192; - } - } -} diff --git a/contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IFlashLoanRecipient.sol b/contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IFlashLoanRecipient.sol deleted file mode 100644 index bcf5b5db8..000000000 --- a/contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IFlashLoanRecipient.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pragma solidity >=0.7.0 <0.9.0; - -// Inspired by Aave Protocol's IFlashLoanReceiver. - -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -interface IFlashLoanRecipient { - /** - * @dev When `flashLoan` is called on the Vault, it invokes the `receiveFlashLoan` hook on the recipient. - * - * At the time of the call, the Vault will have transferred `amounts` for `tokens` to the recipient. Before this - * call returns, the recipient must have transferred `amounts` plus `feeAmounts` for each token back to the - * Vault, or else the entire flash loan will revert. - * - * `userData` is the same value passed in the `IVault.flashLoan` call. - */ - function receiveFlashLoan( - IERC20[] memory tokens, - uint256[] memory amounts, - uint256[] memory feeAmounts, - bytes memory userData - ) external; -} diff --git a/contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IVault.sol b/contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IVault.sol deleted file mode 100644 index 5a89098ca..000000000 --- a/contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IVault.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import "./IFlashLoanRecipient.sol"; - -pragma solidity >=0.7.0 <0.9.0; - -/** - * @dev Full external interface for the Vault core contract - no external or public methods exist in the contract that - * don't override one of these declarations. - */ -interface IVault { - // Flash Loans - - /** - * @dev Performs a 'flash loan', sending tokens to `recipient`, executing the `receiveFlashLoan` hook on it, - * and then reverting unless the tokens plus a proportional protocol fee have been returned. - * - * The `tokens` and `amounts` arrays must have the same length, and each entry in these indicates the loan amount - * for each token contract. `tokens` must be sorted in ascending order. - * - * The 'userData' field is ignored by the Vault, and forwarded as-is to `recipient` as part of the - * `receiveFlashLoan` call. - * - * Emits `FlashLoan` events. - */ - function flashLoan( - IFlashLoanRecipient recipient, - IERC20[] memory tokens, - uint256[] memory amounts, - bytes memory userData - ) external; - - /** - * @dev Emitted for each individual flash loan performed by `flashLoan`. - */ - event FlashLoan(IFlashLoanRecipient indexed recipient, IERC20 indexed token, uint256 amount, uint256 feeAmount); - - /** - * @dev Returns the Vault's WETH instance. - */ - //function WETH() external view returns (IWETH); - // solhint-disable-previous-line func-name-mixedcase -} diff --git a/contracts/src/Zappers/Modules/FlashLoans/BalancerFlashLoan.sol b/contracts/src/Zappers/Modules/FlashLoans/BalancerFlashLoan.sol deleted file mode 100644 index 7fdb73c69..000000000 --- a/contracts/src/Zappers/Modules/FlashLoans/BalancerFlashLoan.sol +++ /dev/null @@ -1,120 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import "./Balancer/vault/IVault.sol"; -import "./Balancer/vault/IFlashLoanRecipient.sol"; - -import "../../Interfaces/ILeverageZapper.sol"; -import "../../Interfaces/IFlashLoanReceiver.sol"; -import "../../Interfaces/IFlashLoanProvider.sol"; - -contract BalancerFlashLoan is IFlashLoanRecipient, IFlashLoanProvider { - using SafeERC20 for IERC20; - - IVault private constant vault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - IFlashLoanReceiver public receiver; - - function makeFlashLoan(IERC20 _token, uint256 _amount, Operation _operation, bytes calldata _params) external { - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = _token; - uint256[] memory amounts = new uint256[](1); - amounts[0] = _amount; - - // Data for the callback receiveFlashLoan - bytes memory userData; - if (_operation == Operation.OpenTrove) { - ILeverageZapper.OpenLeveragedTroveParams memory openTroveParams = - abi.decode(_params, (ILeverageZapper.OpenLeveragedTroveParams)); - userData = abi.encode(_operation, openTroveParams); - } else if (_operation == Operation.LeverUpTrove) { - ILeverageZapper.LeverUpTroveParams memory leverUpTroveParams = - abi.decode(_params, (ILeverageZapper.LeverUpTroveParams)); - userData = abi.encode(_operation, leverUpTroveParams); - } else if (_operation == Operation.LeverDownTrove) { - ILeverageZapper.LeverDownTroveParams memory leverDownTroveParams = - abi.decode(_params, (ILeverageZapper.LeverDownTroveParams)); - userData = abi.encode(_operation, leverDownTroveParams); - } else if (_operation == Operation.CloseTrove) { - IZapper.CloseTroveParams memory closeTroveParams = abi.decode(_params, (IZapper.CloseTroveParams)); - userData = abi.encode(_operation, closeTroveParams); - } else { - revert("LZ: Wrong Operation"); - } - - // This will be used by the callback below no - receiver = IFlashLoanReceiver(msg.sender); - - vault.flashLoan(this, tokens, amounts, userData); - } - - function receiveFlashLoan( - IERC20[] calldata tokens, - uint256[] calldata amounts, - uint256[] calldata feeAmounts, - bytes calldata userData - ) external override { - require(msg.sender == address(vault), "Caller is not Vault"); - require(address(receiver) != address(0), "Flash loan not properly initiated"); - - // Cache and reset receiver, to comply with CEI pattern, as some callbacks in zappers do raw calls - // It’s not necessary, as Balancer flash loans are protected against re-entrancy - // But it’s safer, specially if someone tries to reuse this code, and more gas efficient - IFlashLoanReceiver receiverCached = receiver; - receiver = IFlashLoanReceiver(address(0)); - - // decode and operation - Operation operation = abi.decode(userData[0:32], (Operation)); - - if (operation == Operation.OpenTrove) { - // Open - // decode params - ILeverageZapper.OpenLeveragedTroveParams memory openTroveParams = - abi.decode(userData[32:], (ILeverageZapper.OpenLeveragedTroveParams)); - // Flash loan minus fees - uint256 effectiveFlashLoanAmount = amounts[0] - feeAmounts[0]; - // We send only effective flash loan, keeping fees here - tokens[0].safeTransfer(address(receiverCached), effectiveFlashLoanAmount); - // Zapper callback - receiverCached.receiveFlashLoanOnOpenLeveragedTrove(openTroveParams, effectiveFlashLoanAmount); - } else if (operation == Operation.LeverUpTrove) { - // Lever up - // decode params - ILeverageZapper.LeverUpTroveParams memory leverUpTroveParams = - abi.decode(userData[32:], (ILeverageZapper.LeverUpTroveParams)); - // Flash loan minus fees - uint256 effectiveFlashLoanAmount = amounts[0] - feeAmounts[0]; - // We send only effective flash loan, keeping fees here - tokens[0].safeTransfer(address(receiverCached), effectiveFlashLoanAmount); - // Zapper callback - receiverCached.receiveFlashLoanOnLeverUpTrove(leverUpTroveParams, effectiveFlashLoanAmount); - } else if (operation == Operation.LeverDownTrove) { - // Lever down - // decode params - ILeverageZapper.LeverDownTroveParams memory leverDownTroveParams = - abi.decode(userData[32:], (ILeverageZapper.LeverDownTroveParams)); - // Flash loan minus fees - uint256 effectiveFlashLoanAmount = amounts[0] - feeAmounts[0]; - // We send only effective flash loan, keeping fees here - tokens[0].safeTransfer(address(receiverCached), effectiveFlashLoanAmount); - // Zapper callback - receiverCached.receiveFlashLoanOnLeverDownTrove(leverDownTroveParams, effectiveFlashLoanAmount); - } else if (operation == Operation.CloseTrove) { - // Close trove - // decode params - IZapper.CloseTroveParams memory closeTroveParams = abi.decode(userData[32:], (IZapper.CloseTroveParams)); - // Flash loan minus fees - uint256 effectiveFlashLoanAmount = amounts[0] - feeAmounts[0]; - // We send only effective flash loan, keeping fees here - tokens[0].safeTransfer(address(receiverCached), effectiveFlashLoanAmount); - // Zapper callback - receiverCached.receiveFlashLoanOnCloseTroveFromCollateral(closeTroveParams, effectiveFlashLoanAmount); - } else { - revert("LZ: Wrong Operation"); - } - - // Return flash loan - tokens[0].safeTransfer(address(vault), amounts[0] + feeAmounts[0]); - } -} diff --git a/contracts/src/Zappers/WETHZapper.sol b/contracts/src/Zappers/WETHZapper.sol deleted file mode 100644 index 520575bd8..000000000 --- a/contracts/src/Zappers/WETHZapper.sol +++ /dev/null @@ -1,319 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./BaseZapper.sol"; -import "../Dependencies/Constants.sol"; - -contract WETHZapper is BaseZapper { - constructor(IAddressesRegistry _addressesRegistry, IFlashLoanProvider _flashLoanProvider, IExchange _exchange) - BaseZapper(_addressesRegistry, _flashLoanProvider, _exchange) - { - require(address(WETH) == address(_addressesRegistry.collToken()), "WZ: Wrong coll branch"); - - // Approve coll to BorrowerOperations - WETH.approve(address(borrowerOperations), type(uint256).max); - // Approve Coll to exchange module (for closeTroveFromCollateral) - WETH.approve(address(_exchange), type(uint256).max); - } - - function openTroveWithRawETH(OpenTroveParams calldata _params) external payable returns (uint256) { - require(msg.value > ETH_GAS_COMPENSATION, "WZ: Insufficient ETH"); - require( - _params.batchManager == address(0) || _params.annualInterestRate == 0, - "WZ: Cannot choose interest if joining a batch" - ); - - // Convert ETH to WETH - WETH.deposit{value: msg.value}(); - - uint256 troveId; - // Include sender in index - uint256 index = _getTroveIndex(_params.ownerIndex); - if (_params.batchManager == address(0)) { - troveId = borrowerOperations.openTrove( - _params.owner, - index, - msg.value - ETH_GAS_COMPENSATION, - _params.boldAmount, - _params.upperHint, - _params.lowerHint, - _params.annualInterestRate, - _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - address(this), // add manager - address(this), // remove manager - address(this) // receiver for remove manager - ); - } else { - IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams memory - openTroveAndJoinInterestBatchManagerParams = IBorrowerOperations - .OpenTroveAndJoinInterestBatchManagerParams({ - owner: _params.owner, - ownerIndex: index, - collAmount: msg.value - ETH_GAS_COMPENSATION, - boldAmount: _params.boldAmount, - upperHint: _params.upperHint, - lowerHint: _params.lowerHint, - interestBatchManager: _params.batchManager, - maxUpfrontFee: _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - addManager: address(this), // add manager - removeManager: address(this), // remove manager - receiver: address(this) // receiver for remove manager - }); - troveId = - borrowerOperations.openTroveAndJoinInterestBatchManager(openTroveAndJoinInterestBatchManagerParams); - } - - boldToken.transfer(msg.sender, _params.boldAmount); - - // Set add/remove managers - _setAddManager(troveId, _params.addManager); - _setRemoveManagerAndReceiver(troveId, _params.removeManager, _params.receiver); - - return troveId; - } - - function addCollWithRawETH(uint256 _troveId) external payable { - address owner = troveNFT.ownerOf(_troveId); - _requireSenderIsOwnerOrAddManager(_troveId, owner); - // Convert ETH to WETH - WETH.deposit{value: msg.value}(); - - borrowerOperations.addColl(_troveId, msg.value); - } - - function withdrawCollToRawETH(uint256 _troveId, uint256 _amount) external { - address owner = troveNFT.ownerOf(_troveId); - address payable receiver = payable(_requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner)); - _requireZapperIsReceiver(_troveId); - - borrowerOperations.withdrawColl(_troveId, _amount); - - // Convert WETH to ETH - WETH.withdraw(_amount); - (bool success,) = receiver.call{value: _amount}(""); - require(success, "WZ: Sending ETH failed"); - } - - function withdrawBold(uint256 _troveId, uint256 _boldAmount, uint256 _maxUpfrontFee) external { - address owner = troveNFT.ownerOf(_troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner); - _requireZapperIsReceiver(_troveId); - - borrowerOperations.withdrawBold(_troveId, _boldAmount, _maxUpfrontFee); - - // Send Bold - boldToken.transfer(receiver, _boldAmount); - } - - function repayBold(uint256 _troveId, uint256 _boldAmount) external { - address owner = troveNFT.ownerOf(_troveId); - _requireSenderIsOwnerOrAddManager(_troveId, owner); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensAndBalances(WETH, boldToken, initialBalances); - - // Pull Bold - boldToken.transferFrom(msg.sender, address(this), _boldAmount); - - borrowerOperations.repayBold(_troveId, _boldAmount); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - function adjustTroveWithRawETH( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - uint256 _maxUpfrontFee - ) external payable { - InitialBalances memory initialBalances; - address payable receiver = - _adjustTrovePre(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, initialBalances); - borrowerOperations.adjustTrove( - _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _maxUpfrontFee - ); - _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver, initialBalances); - } - - function adjustZombieTroveWithRawETH( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - uint256 _upperHint, - uint256 _lowerHint, - uint256 _maxUpfrontFee - ) external payable { - InitialBalances memory initialBalances; - address payable receiver = - _adjustTrovePre(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, initialBalances); - borrowerOperations.adjustZombieTrove( - _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _upperHint, _lowerHint, _maxUpfrontFee - ); - _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver, initialBalances); - } - - function _adjustTrovePre( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - InitialBalances memory _initialBalances - ) internal returns (address payable) { - if (_isCollIncrease) { - require(_collChange == msg.value, "WZ: Wrong coll amount"); - } else { - require(msg.value == 0, "WZ: Not adding coll, no ETH should be received"); - } - - address payable receiver = - payable(_checkAdjustTroveManagers(_troveId, _collChange, _isCollIncrease, _isDebtIncrease)); - - // Set initial balances to make sure there are not lefovers - _setInitialTokensAndBalances(WETH, boldToken, _initialBalances); - - // ETH -> WETH - if (_isCollIncrease) { - WETH.deposit{value: _collChange}(); - } - - // Pull Bold - if (!_isDebtIncrease) { - boldToken.transferFrom(msg.sender, address(this), _boldChange); - } - - return receiver; - } - - function _adjustTrovePost( - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - address payable _receiver, - InitialBalances memory _initialBalances - ) internal { - // Send Bold - if (_isDebtIncrease) { - boldToken.transfer(_receiver, _boldChange); - } - - // return BOLD leftovers to user (trying to repay more than possible) - uint256 currentBoldBalance = boldToken.balanceOf(address(this)); - if (currentBoldBalance > _initialBalances.balances[1]) { - boldToken.transfer(_initialBalances.receiver, currentBoldBalance - _initialBalances.balances[1]); - } - // There shouldn’t be Collateral leftovers, everything sent should end up in the trove - // But ETH and WETH balance can be non-zero if someone accidentally send it to this contract - - // WETH -> ETH - if (!_isCollIncrease && _collChange > 0) { - WETH.withdraw(_collChange); - (bool success,) = _receiver.call{value: _collChange}(""); - require(success, "WZ: Sending ETH failed"); - } - } - - function closeTroveToRawETH(uint256 _troveId) external { - address owner = troveNFT.ownerOf(_troveId); - address payable receiver = payable(_requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner)); - _requireZapperIsReceiver(_troveId); - - // pull Bold for repayment - LatestTroveData memory trove = troveManager.getLatestTroveData(_troveId); - boldToken.transferFrom(msg.sender, address(this), trove.entireDebt); - - borrowerOperations.closeTrove(_troveId); - - WETH.withdraw(trove.entireColl + ETH_GAS_COMPENSATION); - (bool success,) = receiver.call{value: trove.entireColl + ETH_GAS_COMPENSATION}(""); - require(success, "WZ: Sending ETH failed"); - } - - function closeTroveFromCollateral(uint256 _troveId, uint256 _flashLoanAmount, uint256 _minExpectedCollateral) - external - override - { - address owner = troveNFT.ownerOf(_troveId); - address payable receiver = payable(_requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner)); - _requireZapperIsReceiver(_troveId); - - CloseTroveParams memory params = CloseTroveParams({ - troveId: _troveId, - flashLoanAmount: _flashLoanAmount, - minExpectedCollateral: _minExpectedCollateral, - receiver: receiver - }); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - initialBalances.tokens[0] = WETH; - initialBalances.tokens[1] = boldToken; - _setInitialBalancesAndReceiver(initialBalances, receiver); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - WETH, _flashLoanAmount, IFlashLoanProvider.Operation.CloseTrove, abi.encode(params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - function receiveFlashLoanOnCloseTroveFromCollateral( - CloseTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external { - require(msg.sender == address(flashLoanProvider), "WZ: Caller not FlashLoan provider"); - - LatestTroveData memory trove = troveManager.getLatestTroveData(_params.troveId); - uint256 collLeft = trove.entireColl - _params.flashLoanAmount; - require(collLeft >= _params.minExpectedCollateral, "WZ: Not enough collateral received"); - - // Swap Coll from flash loan to Bold, so we can repay and close trove - // We swap the flash loan minus the flash loan fee - exchange.swapToBold(_effectiveFlashLoanAmount, trove.entireDebt); - - // We asked for a min of entireDebt in swapToBold call above, so we don’t check again here: - // uint256 receivedBoldAmount = exchange.swapToBold(_effectiveFlashLoanAmount, trove.entireDebt); - //require(receivedBoldAmount >= trove.entireDebt, "WZ: Not enough BOLD obtained to repay"); - - borrowerOperations.closeTrove(_params.troveId); - - // Send coll back to return flash loan - WETH.transfer(address(flashLoanProvider), _params.flashLoanAmount); - - uint256 ethToSendBack = collLeft + ETH_GAS_COMPENSATION; - // Send coll left and gas compensation - WETH.withdraw(ethToSendBack); - (bool success,) = _params.receiver.call{value: ethToSendBack}(""); - require(success, "WZ: Sending ETH failed"); - } - - receive() external payable {} - - // Unimplemented flash loan receive functions for leverage - function receiveFlashLoanOnOpenLeveragedTrove( - ILeverageZapper.OpenLeveragedTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external virtual override {} - function receiveFlashLoanOnLeverUpTrove( - ILeverageZapper.LeverUpTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external virtual override {} - function receiveFlashLoanOnLeverDownTrove( - ILeverageZapper.LeverDownTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external virtual override {} -} diff --git a/contracts/src/tokens/StableTokenV3.sol b/contracts/src/tokens/StableTokenV3.sol new file mode 100644 index 000000000..0ead98372 --- /dev/null +++ b/contracts/src/tokens/StableTokenV3.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable gas-custom-errors +pragma solidity 0.8.24; + +import {ERC20PermitUpgradeable} from "./patched/ERC20PermitUpgradeable.sol"; +import {ERC20Upgradeable} from "./patched/ERC20Upgradeable.sol"; + +import {IStableTokenV3} from "../interfaces/IStableTokenV3.sol"; + +/** + * @title ERC20 token with minting and burning permissiones to a minter and burner roles. + * Direct transfers between the protocol and the user are done by the operator role. + */ +contract StableTokenV3 is ERC20PermitUpgradeable, IStableTokenV3 { + /* ========================================================= */ + /* ==================== State Variables ==================== */ + /* ========================================================= */ + + // Deprecated storage slots for backwards compatibility with StableTokenV2 + // solhint-disable-next-line var-name-mixedcase + address public deprecated_validators_storage_slot__; + // solhint-disable-next-line var-name-mixedcase + address public deprecated_broker_storage_slot__; + // solhint-disable-next-line var-name-mixedcase + address public deprecated_exchange_storage_slot__; + + // Mapping of allowed addresses that can mint + mapping(address => bool) public isMinter; + // Mapping of allowed addresses that can burn + mapping(address => bool) public isBurner; + // Mapping of allowed addresses that can call the operator functions + // These functions are used to do direct transfers between the protocol and the user + // This will be the StabilityPools + mapping(address => bool) public isOperator; + + /* ========================================================= */ + /* ======================== Events ========================= */ + /* ========================================================= */ + + event MinterUpdated(address indexed minter, bool isMinter); + event BurnerUpdated(address indexed burner, bool isBurner); + event OperatorUpdated(address indexed operator, bool isOperator); + + /* ========================================================= */ + /* ====================== Modifiers ======================== */ + /* ========================================================= */ + + /// @dev legacy helper to ensure only the vm can call this function + modifier onlyVm() { + require(msg.sender == address(0), "Only VM can call"); + _; + } + + /// @dev Restricts a function so it can only be executed by an address that's allowed to mint. + modifier onlyMinter() { + address sender = _msgSender(); + require(isMinter[sender], "StableTokenV3: not allowed to mint"); + _; + } + + /// @dev Restricts a function so it can only be executed by an address that's allowed to burn. + modifier onlyBurner() { + address sender = _msgSender(); + require(isBurner[sender], "StableTokenV3: not allowed to burn"); + _; + } + + /// @dev Restricts a function so it can only be executed by the operator role. + modifier onlyOperator() { + address sender = _msgSender(); + require(isOperator[sender], "StableTokenV3: not allowed to call only by operator"); + _; + } + + /* ========================================================= */ + /* ====================== Constructor ====================== */ + /* ========================================================= */ + + /** + * @notice The constructor for the StableTokenV3 contract. + * @dev Should be called with disable=true in deployments when + * it's accessed through a Proxy. + * Call this with disable=false during testing, when used + * without a proxy. + * @param disable Set to true to run `_disableInitializers()` inherited from + * openzeppelin-contracts-upgradeable/Initializable.sol + */ + constructor(bool disable) { + if (disable) { + _disableInitializers(); + } + } + + /// @inheritdoc IStableTokenV3 + function initialize( + // slither-disable-start shadowing-local + string memory _name, + string memory _symbol, + // slither-disable-end shadowing-local + address[] memory initialBalanceAddresses, + uint256[] memory initialBalanceValues, + address[] memory _minters, + address[] memory _burners, + address[] memory _operators + ) external reinitializer(3) { + __ERC20_init_unchained(_name, _symbol); + __ERC20Permit_init(_symbol); + _transferOwnership(_msgSender()); + + require(initialBalanceAddresses.length == initialBalanceValues.length, "Array length mismatch"); + for (uint256 i = 0; i < initialBalanceAddresses.length; i += 1) { + _mint(initialBalanceAddresses[i], initialBalanceValues[i]); + } + for (uint256 i = 0; i < _minters.length; i += 1) { + _setMinter(_minters[i], true); + } + for (uint256 i = 0; i < _burners.length; i += 1) { + _setBurner(_burners[i], true); + } + for (uint256 i = 0; i < _operators.length; i += 1) { + _setOperator(_operators[i], true); + } + } + + /// @inheritdoc IStableTokenV3 + function initializeV3(address[] memory _minters, address[] memory _burners, address[] memory _operators) + public + reinitializer(3) + onlyOwner + { + for (uint256 i = 0; i < _minters.length; i += 1) { + _setMinter(_minters[i], true); + } + for (uint256 i = 0; i < _burners.length; i += 1) { + _setBurner(_burners[i], true); + } + for (uint256 i = 0; i < _operators.length; i += 1) { + _setOperator(_operators[i], true); + } + } + + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ + + /// @inheritdoc IStableTokenV3 + function setOperator(address _operator, bool _isOperator) external onlyOwner { + _setOperator(_operator, _isOperator); + } + + /// @inheritdoc IStableTokenV3 + function setMinter(address _minter, bool _isMinter) external onlyOwner { + _setMinter(_minter, _isMinter); + } + + /// @inheritdoc IStableTokenV3 + function setBurner(address _burner, bool _isBurner) external onlyOwner { + _setBurner(_burner, _isBurner); + } + + /// @inheritdoc IStableTokenV3 + function mint(address to, uint256 value) external onlyMinter returns (bool) { + _mint(to, value); + return true; + } + + /// @inheritdoc IStableTokenV3 + function burn(uint256 value) external onlyBurner returns (bool) { + _burn(msg.sender, value); + return true; + } + + /// @inheritdoc IStableTokenV3 + function sendToPool(address _sender, address _poolAddress, uint256 _amount) external onlyOperator { + _transfer(_sender, _poolAddress, _amount); + } + + /// @inheritdoc IStableTokenV3 + function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external onlyOperator { + _transfer(_poolAddress, _receiver, _amount); + } + + /// @inheritdoc IStableTokenV3 + function transferFrom(address from, address to, uint256 amount) + public + override(ERC20Upgradeable, IStableTokenV3) + returns (bool) + { + return ERC20Upgradeable.transferFrom(from, to, amount); + } + + /// @inheritdoc IStableTokenV3 + function transfer(address to, uint256 amount) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { + return ERC20Upgradeable.transfer(to, amount); + } + + /// @inheritdoc IStableTokenV3 + function balanceOf(address account) public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { + return ERC20Upgradeable.balanceOf(account); + } + + /// @inheritdoc IStableTokenV3 + function approve(address spender, uint256 amount) + public + override(ERC20Upgradeable, IStableTokenV3) + returns (bool) + { + return ERC20Upgradeable.approve(spender, amount); + } + + /// @inheritdoc IStableTokenV3 + function allowance(address owner, address spender) + public + view + override(ERC20Upgradeable, IStableTokenV3) + returns (uint256) + { + return ERC20Upgradeable.allowance(owner, spender); + } + + /// @inheritdoc IStableTokenV3 + function totalSupply() public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { + return ERC20Upgradeable.totalSupply(); + } + + /// @inheritdoc IStableTokenV3 + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + public + override(ERC20PermitUpgradeable, IStableTokenV3) + { + ERC20PermitUpgradeable.permit(owner, spender, value, deadline, v, r, s); + } + + /// @inheritdoc IStableTokenV3 + function debitGasFees(address from, uint256 value) external onlyVm { + _burn(from, value); + } + + /// @inheritdoc IStableTokenV3 + function creditGasFees( + address from, + address feeRecipient, + address gatewayFeeRecipient, + address communityFund, + uint256 refund, + uint256 tipTxFee, + uint256 gatewayFee, + uint256 baseTxFee + ) external onlyVm { + // slither-disable-next-line uninitialized-local + uint256 amountToBurn; + _mint(from, refund + tipTxFee + gatewayFee + baseTxFee); + + if (feeRecipient != address(0)) { + _transfer(from, feeRecipient, tipTxFee); + } else if (tipTxFee > 0) { + amountToBurn += tipTxFee; + } + + if (gatewayFeeRecipient != address(0)) { + _transfer(from, gatewayFeeRecipient, gatewayFee); + } else if (gatewayFee > 0) { + amountToBurn += gatewayFee; + } + + if (communityFund != address(0)) { + _transfer(from, communityFund, baseTxFee); + } else if (baseTxFee > 0) { + amountToBurn += baseTxFee; + } + + if (amountToBurn > 0) { + _burn(from, amountToBurn); + } + } + + /* =========================================================== */ + /* ==================== Private Functions ==================== */ + /* =========================================================== */ + + function _setOperator(address _operator, bool _isOperator) internal { + isOperator[_operator] = _isOperator; + emit OperatorUpdated(_operator, _isOperator); + } + + function _setMinter(address _minter, bool _isMinter) internal { + isMinter[_minter] = _isMinter; + emit MinterUpdated(_minter, _isMinter); + } + + function _setBurner(address _burner, bool _isBurner) internal { + isBurner[_burner] = _isBurner; + emit BurnerUpdated(_burner, _isBurner); + } +} diff --git a/contracts/src/tokens/patched/ERC20PermitUpgradeable.sol b/contracts/src/tokens/patched/ERC20PermitUpgradeable.sol new file mode 100644 index 000000000..71246fcb0 --- /dev/null +++ b/contracts/src/tokens/patched/ERC20PermitUpgradeable.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +// solhint-disable gas-custom-errors +// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/extensions/draft-ERC20Permit.sol) +/* + * 🔥 MentoLabs: This is a copied file from v4.8.0 of OZ-Upgradable, + * and only changes the import of ERC20Upgradeable to be the local one + * which is modified in order to keep storage variables consistent + * with the pervious implementation of StableToken. + * See ./README.md for more details. + */ + +pragma solidity ^0.8.0; + +import "./ERC20Upgradeable.sol"; + +import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/draft-IERC20PermitUpgradeable.sol"; +import "openzeppelin-contracts-upgradeable/contracts/utils/cryptography/ECDSAUpgradeable.sol"; +import "openzeppelin-contracts-upgradeable/contracts/utils/cryptography/EIP712Upgradeable.sol"; +import "openzeppelin-contracts-upgradeable/contracts/utils/CountersUpgradeable.sol"; + +/** + * @dev Implementation of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ + * + * @custom:storage-size 51 + */ +abstract contract ERC20PermitUpgradeable is ERC20Upgradeable, IERC20PermitUpgradeable, EIP712Upgradeable { + using CountersUpgradeable for CountersUpgradeable.Counter; + + mapping(address => CountersUpgradeable.Counter) private _nonces; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private constant _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + /** + * @dev In previous versions `_PERMIT_TYPEHASH` was declared as `immutable`. + * However, to ensure consistency with the upgradeable transpiler, we will continue + * to reserve a slot. + * @custom:oz-renamed-from _PERMIT_TYPEHASH + */ + // slither-disable-start constable-states + // solhint-disable-next-line var-name-mixedcase + bytes32 private _PERMIT_TYPEHASH_DEPRECATED_SLOT; + + // slither-disable-end constable-states + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC20 token name. + */ + // solhint-disable-next-line func-name-mixedcase + function __ERC20Permit_init(string memory name) internal onlyInitializing { + __EIP712_init_unchained(name, "1"); + } + + // solhint-disable-next-line func-name-mixedcase + function __ERC20Permit_init_unchained(string memory) internal onlyInitializing {} + + /** + * @dev See {IERC20Permit-permit}. + */ + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + public + virtual + override + { + require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSAUpgradeable.recover(hash, v, r, s); + require(signer == owner, "ERC20Permit: invalid signature"); + + _approve(owner, spender, value); + } + + /** + * @dev See {IERC20Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev See {IERC20Permit-DOMAIN_SEPARATOR}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + CountersUpgradeable.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; +} diff --git a/contracts/src/tokens/patched/ERC20Upgradeable.sol b/contracts/src/tokens/patched/ERC20Upgradeable.sol new file mode 100644 index 000000000..895261e8c --- /dev/null +++ b/contracts/src/tokens/patched/ERC20Upgradeable.sol @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: MIT +// solhint-disable gas-custom-errors +// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/ERC20.sol) +/* + * 🔥 MentoLabs: This is a copied file from v4.8.0 of OZ-Upgradable, which only changes + * the ordering of storage variables to keep it consistent with the existing + * StableToken, so this can act as a new implementation for the proxy. + * See ./README.md for more details. + */ + +pragma solidity ^0.8.0; + +import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; +import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; +import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "openzeppelin-contracts/contracts/access/Ownable.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20Upgradeable is Ownable, Initializable, IERC20Upgradeable, IERC20MetadataUpgradeable { + // solhint-disable var-name-mixedcase + address private __deprecated_registry_storage_slot__; + string private _name; + string private _symbol; + uint8 private __deprecated_decimals_storage_slot__; + + mapping(address => uint256) private _balances; + uint256 private _totalSupply; + mapping(address => mapping(address => uint256)) private _allowances; + + uint256[4] private __deprecated_inflationState_storage_slot__; + bytes32 private __deprecated_exchangeRegistryId_storage_slot__; + + // solhint-enable var-name-mixedcase + + /** + * @dev Sets the values for {name} and {symbol}. + * + * The default value of {decimals} is 18. To select a different value for + * {decimals} you should overload it. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + + // solhint-disable-next-line func-name-mixedcase + function __ERC20_init(string memory name_, string memory symbol_) internal onlyInitializing { + __ERC20_init_unchained(name_, symbol_); + } + + // solhint-disable-next-line func-name-mixedcase + function __ERC20_init_unchained(string memory name_, string memory symbol_) internal onlyInitializing { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the value {ERC20} uses, unless this function is + * overridden; + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual override returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address to, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + * - the caller must have allowance for ``from``'s tokens of at least + * `amount`. + */ + function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, allowance(owner, spender) + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + address owner = _msgSender(); + uint256 currentAllowance = allowance(owner, spender); + require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + unchecked { + _approve(owner, spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @dev Moves `amount` of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + */ + function _transfer(address from, address to, uint256 amount) internal virtual { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(from, to, amount); + + uint256 fromBalance = _balances[from]; + require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[from] = fromBalance - amount; + // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by + // decrementing then incrementing. + _balances[to] += amount; + } + + emit Transfer(from, to, amount); + + _afterTokenTransfer(from, to, amount); + } + + /** + * @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += amount; + unchecked { + // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. + _balances[account] += amount; + } + emit Transfer(address(0), account, amount); + + _afterTokenTransfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + // Overflow not possible: amount <= accountBalance <= totalSupply. + _totalSupply -= amount; + } + + emit Transfer(account, address(0), amount); + + _afterTokenTransfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `amount`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require(currentAllowance >= amount, "ERC20: insufficient allowance"); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {} + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[40] private __gap; +} diff --git a/contracts/src/tokens/patched/README.md b/contracts/src/tokens/patched/README.md new file mode 100644 index 000000000..de82c3080 --- /dev/null +++ b/contracts/src/tokens/patched/README.md @@ -0,0 +1,75 @@ +### Patched OpenZeppelin contracts + +The only way we can migrate our StableToken implementation to a modern one is if we can keep the same storage layout in the proxy. +Sadly our tokens aren't compatible with the OZ ERC20 out of the box. We can use the `bin/storage-show.sh` command to get a quick view of the storage layout: + +``` +> ./bin/storage-show.sh StableToken +0 0 _owner t_address +0 20 initialized t_bool +1 0 registry t_contract(IRegistry)4167 +2 0 name_ t_string_storage +3 0 symbol_ t_string_storage +4 0 decimals_ t_uint8 +5 0 balances t_mapping(t_address,t_uint256) +6 0 totalSupply_ t_uint256 +7 0 allowed t_mapping(t_address,t_mapping(t_address,t_uint256)) +8 0 inflationState t_struct(InflationState)10264_storage +12 0 exchangeRegistryId t_bytes32 +⏎ +``` + +To make this work I copied the contracts from version `v4.8.0` here and modified the order of storage variables in order to get to something that's compatible: + +``` +> ./bin/storage-show.sh ERC20Upgradeable +0 0 _owner t_address +0 20 _initialized t_uint8 +0 21 _initializing t_bool +1 0 __deprecated_registry_storage_slot__ t_address +2 0 _name t_string_storage +3 0 _symbol t_string_storage +4 0 __deprecated_decimals_storage_slot__ t_uint8 +5 0 _balances t_mapping(t_address,t_uint256) +6 0 _totalSupply t_uint256 +7 0 _allowances t_mapping(t_address,t_mapping(t_address,t_uint256)) +8 0 __gap t_array(t_uint256)45_storage +``` + +> Note: The columns of the table are: slot, offset, name, type + +The `initialized` bool upgrades nicely to the new `Initializable` structure in more recent OZ - it was designed this way. +We reserve some deprecated slots, and make sure the others match up 1:1. The name being different is not an issue. + +And which can then be used in `ERC20Permit` and `StableTokenV2` to finally come up with this: + +``` +> ./bin/storage-show.sh StableTokenV2 +0 0 _owner t_address +0 20 _initialized t_uint8 +0 21 _initializing t_bool +1 0 __deprecated_registry_storage_slot__ t_address +2 0 _name t_string_storage +3 0 _symbol t_string_storage +4 0 __deprecated_decimals_storage_slot__ t_uint8 +5 0 _balances t_mapping(t_address,t_uint256) +6 0 _totalSupply t_uint256 +7 0 _allowances t_mapping(t_address,t_mapping(t_address,t_uint256)) +8 0 __deeprecated_inflationState_storage_slot__ t_array(t_uint256)4_storage +12 0 __deprecated_exchangeRegistryId_storage_slot__ t_bytes32 +13 0 __gap t_array(t_uint256)40_storage +53 0 _HASHED_NAME t_bytes32 +54 0 _HASHED_VERSION t_bytes32 +55 0 __gap t_array(t_uint256)50_storage +105 0 _nonces t_mapping(t_address,t_struct(Counter)51157_storage) +106 0 _PERMIT_TYPEHASH_DEPRECATED_SLOT t_bytes32 +107 0 __gap t_array(t_uint256)49_storage +156 0 validators t_address +157 0 broker t_address +158 0 exchange t_address +``` + +In this new implementation we also remove the dependency on the `Registry` and introduce the 3 new storage variables to store addresses for the dependencies directly. +See the `test/integration/TokenUpgrade.t.sol` for a simulation of switching the implementation of an existing live token in a forked environment. + +In the future, if we want to migrate to newer versions of this we can go through the same process of copying the files and patching them to keep the storage layout consistent. diff --git a/contracts/test/AnchoredInvariantsTest.t.sol b/contracts/test/AnchoredInvariantsTest.t.sol index 026b5d716..31d3157cb 100644 --- a/contracts/test/AnchoredInvariantsTest.t.sol +++ b/contracts/test/AnchoredInvariantsTest.t.sol @@ -26,7 +26,7 @@ contract AnchoredInvariantsTest is Logging, BaseInvariantTest, BaseMultiCollater p[3] = TestDeployer.TroveManagerParams(1.6 ether, 1.25 ether, 0.1 ether, 1.01 ether, 0.05 ether, 0.1 ether); TestDeployer deployer = new TestDeployer(); Contracts memory contracts; - (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth,) + (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth) = deployer.deployAndConnectContractsMultiColl(p); setupContracts(contracts); diff --git a/contracts/test/AnchoredSPInvariantsTest.t.sol b/contracts/test/AnchoredSPInvariantsTest.t.sol index 328ae9d32..c99b30124 100644 --- a/contracts/test/AnchoredSPInvariantsTest.t.sol +++ b/contracts/test/AnchoredSPInvariantsTest.t.sol @@ -1,51 +1,37 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; +import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; + import "./TestContracts/DevTestSetup.sol"; -import {SPInvariantsTestHandler} from "./TestContracts/SPInvariantsTestHandler.t.sol"; +import {BaseInvariantTest} from "./TestContracts/BaseInvariantTest.sol"; +import {BaseMultiCollateralTest} from "./TestContracts/BaseMultiCollateralTest.sol"; +import {AdjustedTroveProperties, InvariantsTestHandler} from "./TestContracts/InvariantsTestHandler.t.sol"; import {Logging} from "./Utils/Logging.sol"; +import {TroveId} from "./Utils/TroveId.sol"; -contract AnchoredSPInvariantsTest is DevTestSetup { +contract AnchoredInvariantsTest is Logging, BaseInvariantTest, BaseMultiCollateralTest, TroveId { + using Strings for uint256; using StringFormatting for uint256; - struct Actor { - string label; - address account; - } - - SPInvariantsTestHandler handler; - - address constant adam = 0x1111111111111111111111111111111111111111; - address constant barb = 0x2222222222222222222222222222222222222222; - address constant carl = 0x3333333333333333333333333333333333333333; - address constant dana = 0x4444444444444444444444444444444444444444; - address constant eric = 0x5555555555555555555555555555555555555555; - address constant fran = 0x6666666666666666666666666666666666666666; - address constant gabe = 0x7777777777777777777777777777777777777777; - address constant hope = 0x8888888888888888888888888888888888888888; - - Actor[] actors; + InvariantsTestHandler handler; function setUp() public override { super.setUp(); + TestDeployer.TroveManagerParams[] memory p = new TestDeployer.TroveManagerParams[](4); + p[0] = TestDeployer.TroveManagerParams(1.5 ether, 1.1 ether, 0.1 ether, 1.01 ether, 0.05 ether, 0.1 ether); + p[1] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 0.1 ether, 1.01 ether, 0.05 ether, 0.1 ether); + p[2] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 0.1 ether, 1.01 ether, 0.05 ether, 0.1 ether); + p[3] = TestDeployer.TroveManagerParams(1.6 ether, 1.25 ether, 0.1 ether, 1.01 ether, 0.05 ether, 0.1 ether); TestDeployer deployer = new TestDeployer(); - (TestDeployer.LiquityContractsDev memory contracts,, IBoldToken boldToken, HintHelpers hintHelpers,,,) = - deployer.deployAndConnectContracts(); - stabilityPool = contracts.stabilityPool; - - handler = new SPInvariantsTestHandler( - SPInvariantsTestHandler.Contracts({ - boldToken: boldToken, - borrowerOperations: contracts.borrowerOperations, - collateralToken: contracts.collToken, - priceFeed: contracts.priceFeed, - stabilityPool: contracts.stabilityPool, - troveManager: contracts.troveManager, - collSurplusPool: contracts.pools.collSurplusPool - }), - hintHelpers - ); + Contracts memory contracts; + (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth) + = deployer.deployAndConnectContractsMultiColl(p); + setupContracts(contracts); + + handler = new InvariantsTestHandler({contracts: contracts, assumeNoExpectedFailures: true}); + vm.label(address(handler), "handler"); actors.push(Actor("adam", adam)); actors.push(Actor("barb", barb)); @@ -58,1196 +44,1072 @@ contract AnchoredSPInvariantsTest is DevTestSetup { for (uint256 i = 0; i < actors.length; ++i) { vm.label(actors[i].account, actors[i].label); } - - vm.label(address(handler), "handler"); - } - - function invariant_allFundsClaimable() internal view { - uint256 stabilityPoolColl = stabilityPool.getCollBalance(); - uint256 stabilityPoolBold = stabilityPool.getTotalBoldDeposits(); - uint256 yieldGainsOwed = stabilityPool.getYieldGainsOwed(); - - uint256 claimableColl = 0; - uint256 claimableBold = 0; - uint256 sumYieldGains = 0; - - for (uint256 i = 0; i < actors.length; ++i) { - claimableColl += stabilityPool.getDepositorCollGain(actors[i].account); - claimableBold += stabilityPool.getCompoundedBoldDeposit(actors[i].account); - sumYieldGains += stabilityPool.getDepositorYieldGain(actors[i].account); - //info("+sumYieldGains: ", sumYieldGains.decimal()); - } - - info("stabilityPoolColl: ", stabilityPoolColl.decimal()); - info("claimableColl: ", claimableColl.decimal()); - info("stabilityPoolBold: ", stabilityPoolBold.decimal()); - info("claimableBold: ", claimableBold.decimal()); - info("yieldGainsOwed: ", yieldGainsOwed.decimal()); - info("sumYieldGains: ", sumYieldGains.decimal()); - for (uint256 i = 0; i < actors.length; ++i) { - info( - actors[i].label, - ": ", - stabilityPool.getDepositorYieldGain(actors[i].account).decimal() - ); - } - info(""); - assertApproxEqAbsDecimal(stabilityPoolColl, claimableColl, 0.00001 ether, 18, "SP Coll !~ claimable Coll"); - assertApproxEqAbsDecimal(stabilityPoolBold, claimableBold, 0.001 ether, 18, "SP BOLD !~ claimable BOLD"); - assertApproxEqAbsDecimal(yieldGainsOwed, sumYieldGains, 0.001 ether, 18, "SP yieldGainsOwed !~= sum(yieldGain)"); - - //assertGe(stabilityPoolBold, claimableBold, "Not enough deposits for all depositors"); - //assertGe(stabilityPoolColl, claimableColl, "Not enough collateral for all depositors"); - //assertGe(yieldGainsOwed, sumYieldGains, "Not enough yield gains for all depositors"); } - function testUnclaimableDeposit() external { - // coll = 581.807407427107718655 ether, debt = 77_574.320990281029153872 ether - vm.prank(hope); - handler.openTrove(77_566.883069986646872666 ether); - - // coll = 735.070487541934665757 ether, debt = 98_009.398338924622100814 ether - vm.prank(barb); - handler.openTrove(98_000.001078547227161224 ether); - - vm.prank(hope); - handler.provideToSp(0.000001023636824878 ether, false); - - // totalBoldDeposits = 0.000001023636824878 ether - - // pulling `deposited` from fixture - vm.prank(gabe); - handler.provideToSp(98_009.398338924622100814 ether, false); - - // totalBoldDeposits = 98_009.398339948258925692 ether - - // coll = 735.070479452054794532 ether, debt = 98_009.397260273972604207 ether - vm.prank(carl); - handler.openTrove(98_000.000000000000001468 ether); - - // coll = 60.195714636403445628 ether, debt = 8_026.095284853792750331 ether - vm.prank(fran); - handler.openTrove(8_025.325733071169487504 ether); - - // coll = 15.001438356164383562 ether, debt = 2_000.191780821917808222 ether + function testWrongYield() external { vm.prank(adam); - handler.openTrove(2_000.000000000000000003 ether); + handler.addMeToUrgentRedemptionBatch(); vm.prank(adam); - handler.liquidateMe(); - - // totalBoldDeposits = 96_009.20655912634111747 ether - // P = 0.979591836959510777 ether - - vm.prank(carl); - handler.provideToSp(0.000000000001265034 ether, false); + handler.registerBatchManager( + 0, + 0.257486338754888547 ether, + 0.580260126400716372 ether, + 0.474304801140122485 ether, + 0.84978254245815657 ether, + 2121012 + ); - // totalBoldDeposits = 96_009.206559126342382504 ether + vm.prank(eric); + handler.registerBatchManager( + 2, + 0.995000000000011223 ether, + 0.999999999997818617 ether, + 0.999999999561578875 ether, + 0.000000000000010359 ether, + 5174410 + ); vm.prank(fran); - handler.liquidateMe(); + handler.warp(3_662_052); - // totalBoldDeposits = 87_983.111274272549632173 ether - // P = 0.89770075895273572 ether + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); - // pulling `deposited` from fixture vm.prank(hope); - handler.provideToSp(2_000.191780821917810223 ether, false); - - // totalBoldDeposits = 89_983.303055094467442396 ether - - // coll = 568.201183566424575581 ether, debt = 75_760.157808856610077362 ether - vm.prank(dana); - handler.openTrove(75_752.893832735662822023 ether); - - vm.prank(dana); - handler.liquidateMe(); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 14_223.145246237857365034 ether - // P = 0.141894416505527947 ether - - invariant_allFundsClaimable(); - } + vm.prank(barb); + handler.addMeToLiquidationBatch(); - function testUnclaimableDeposit2() external { - // coll = 735.140965662413210774 ether, debt = 98_018.795421655094769748 ether - vm.prank(dana); - handler.openTrove(98_009.397260273972607992 ether); + // upper hint: 0 + // lower hint: 0 + // upfront fee: 1_246.586073354248297808 ether + vm.prank(hope); + handler.openTrove( + 0, 99_999.999999999999999997 ether, 2.251600954885856105 ether, 0.650005595391858041 ether, 8768, 0 + ); - // pulling `deposited` from fixture vm.prank(adam); - handler.provideToSp(9.398161381122161756 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 9.398161381122161756 ether - - // coll = 735.070479452054794705 ether, debt = 98_009.39726027397262726 ether vm.prank(eric); - handler.openTrove(98_000.000000000000024518 ether); - - // pulling `deposited` from fixture - vm.prank(gabe); - handler.provideToSp(98_009.39726027397262726 ether, false); - - // totalBoldDeposits = 98_018.795421655094789016 ether + handler.addMeToLiquidationBatch(); vm.prank(hope); - handler.provideToSp(89_926.427447073294525543 ether, false); + handler.warp(9_396_472); - // totalBoldDeposits = 187_945.222868728389314559 ether - - // coll = 695.649439428737938566 ether, debt = 92_753.258590498391808747 ether vm.prank(gabe); - handler.openTrove(92_744.365295196112729445 ether); + handler.addMeToUrgentRedemptionBatch(); - vm.prank(dana); - handler.provideToSp(12_389.101939905632219407 ether, false); - - // totalBoldDeposits = 200_334.324808634021533966 ether + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); - // pulling `deposited` from fixture vm.prank(dana); - handler.provideToSp(9_589.959513437908897281 ether, false); - - // totalBoldDeposits = 209_924.284322071930431247 ether - - // coll = 358.727589004694716678 ether, debt = 47_830.345200625962223645 ether - vm.prank(hope); - handler.openTrove(47_825.759168924832445192 ether); - - // coll = 387.627141578823956719 ether, debt = 51_683.618877176527562444 ether - vm.prank(barb); - handler.openTrove(51_678.663388906358459579 ether); - - vm.prank(barb); - handler.liquidateMe(); - - // totalBoldDeposits = 158_240.665444895402868803 ether - // P = 0.753798761091013085 ether - - vm.prank(barb); - handler.provideToSp(0.000000000000008548 ether, false); - - // totalBoldDeposits = 158_240.665444895402877351 ether + handler.registerBatchManager( + 2, + 0.995000000000011139 ether, + 0.998635073564148166 ether, + 0.996010156573547401 ether, + 0.000000000000011577 ether, + 9078342 + ); vm.prank(carl); - handler.provideToSp(4_591.158415534017479187 ether, false); - - // totalBoldDeposits = 162_831.823860429420356538 ether - - vm.prank(dana); - handler.provideToSp(86_019.232581553804992428 ether, false); - - // totalBoldDeposits = 248_851.056441983225348966 ether + handler.registerBatchManager( + 1, 0.995000004199127012 ether, 1 ether, 0.999139502777974999 ether, 0.059938454189132239 ether, 1706585 + ); + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 897.541972815058774421 ether vm.prank(gabe); - handler.provideToSp(83_736.829497136058174833 ether, false); + handler.provideToSP(0, 58_897.613356828171795189 ether, false); + } - // totalBoldDeposits = 332_587.885939119283523799 ether + function testRedeemUnderflow() external { + vm.prank(fran); + handler.warp(18_162); - // coll = 735.082255482320817013 ether, debt = 98_010.967397642775601628 ether vm.prank(carl); - handler.openTrove(98_001.569986822121425601 ether); - - vm.prank(gabe); - handler.liquidateMe(); - - // totalBoldDeposits = 239_834.627348620891715052 ether - // P = 0.543576758520929938 ether - - // pulling `deposited` from fixture - vm.prank(barb); - handler.provideToSp(47_830.345200625962271476 ether, false); + handler.registerBatchManager( + 0, + 0.995000001857124003 ether, + 0.999999628575220679 ether, + 0.999925530120657388 ether, + 0.249999999999999999 ether, + 12664 + ); - // totalBoldDeposits = 287_664.972549246853986528 ether + vm.prank(hope); + handler.addMeToLiquidationBatch(); - // coll = 735.070479452054794659 ether, debt = 98_009.397260273972621122 ether vm.prank(fran); - handler.openTrove(98_000.000000000000018381 ether); - - // coll = 735.070479452054794701 ether, debt = 98_009.397260273972626757 ether - vm.prank(barb); - handler.openTrove(98_000.000000000000024015 ether); + handler.addMeToUrgentRedemptionBatch(); - vm.prank(barb); - handler.liquidateMe(); - - // totalBoldDeposits = 189_655.575288972881359771 ether - // P = 0.358376488932287869 ether + vm.prank(fran); + handler.addMeToUrgentRedemptionBatch(); - // coll = 482.348723040733119578 ether, debt = 64_313.163072097749277015 ether vm.prank(gabe); - handler.openTrove(64_306.996647761662542251 ether); + handler.addMeToUrgentRedemptionBatch(); - vm.prank(eric); - handler.liquidateMe(); - - // totalBoldDeposits = 91_646.178028698908732511 ether - // P = 0.173176219343645801 ether - - vm.prank(hope); - handler.liquidateMe(); + vm.prank(dana); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 43_815.832828072946508866 ether - // P = 0.082795163309295299 ether + vm.prank(eric); + handler.warp(4_641_555); vm.prank(adam); - handler.provideToSp(156_013.932831544173758454 ether, false); - - // totalBoldDeposits = 199_829.76565961712026732 ether - - vm.prank(barb); - handler.provideToSp(149_177.525713798558063626 ether, false); - - // totalBoldDeposits = 349_007.291373415678330946 ether - - // coll = 212.375283704302188719 ether, debt = 28_316.704493906958495745 ether - vm.prank(barb); - handler.openTrove(28_313.989453822345394132 ether); - - vm.prank(fran); - handler.liquidateMe(); - - // totalBoldDeposits = 250_997.894113141705709824 ether - // P = 0.059544348061060947 ether + handler.addMeToUrgentRedemptionBatch(); - // pulling `deposited` from fixture vm.prank(dana); - handler.provideToSp(22_796.354529886855570135 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 273_794.248643028561279959 ether + vm.prank(gabe); + handler.addMeToLiquidationBatch(); - // pulling `deposited` from fixture vm.prank(fran); - handler.provideToSp(3_365.431868318464668052 ether, false); - - // totalBoldDeposits = 277_159.680511347025948011 ether - - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(6_369.148148716152063935 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 283_528.828660063178011946 ether - - // coll = 56.130386313173824619 ether, debt = 7_484.051508423176615827 ether vm.prank(hope); - handler.openTrove(7_483.333928457434122145 ether); - - // coll = 409.098077325686901872 ether, debt = 54_546.41031009158691615 ether - vm.prank(fran); - handler.openTrove(54_541.180333895186007903 ether); + handler.registerBatchManager( + 0, + 0.739903753088089514 ether, + 0.780288740735740819 ether, + 0.767858707410717411 ether, + 0.000000000000022941 ether, + 21644 + ); - // coll = 549.477014983678353472 ether, debt = 73_263.601997823780462917 ether + // upper hint: 80084422859880547211683076133703299733277748156566366325829078699459944778998 + // lower hint: 104346312485569601582594868672255666718935311025283394307913733247512361320190 + // upfront fee: 290.81243876303301812 ether vm.prank(adam); - handler.openTrove(73_256.577394511977944484 ether); - - // pulling `deposited` from fixture - vm.prank(fran); - handler.provideToSp(98_010.967397642775601628 ether, false); - - // totalBoldDeposits = 381_539.796057705953613574 ether - - vm.prank(dana); - handler.provideToSp(13_294.811494145641399612 ether, false); - - // totalBoldDeposits = 394_834.607551851595013186 ether + handler.openTrove( + 3, 39_503.887731534058892956 ether, 1.6863644596244192 ether, 0.38385567397413886 ether, 1, 7433679 + ); vm.prank(adam); - handler.liquidateMe(); + handler.addMeToUrgentRedemptionBatch(); - // totalBoldDeposits = 321_571.005554027814550269 ether - // P = 0.048495586543891854 ether + vm.prank(hope); + handler.warp(23_201); - vm.prank(gabe); - handler.liquidateMe(); + vm.prank(carl); + handler.warp(18_593_995); + + // redemption rate: 0.195871664252157123 ether + // redeemed BOLD: 15_191.361299840412827416 ether + // redeemed Troves: [ + // [], + // [], + // [], + // [adam], + // ] + vm.prank(carl); + handler.redeemCollateral(15_191.361299840412827416 ether, 0); + + // redemption rate: 0.195871664252157123 ether + // redeemed BOLD: 0.000000000000006302 ether + // redeemed Troves: [ + // [], + // [], + // [], + // [adam], + // ] + vm.prank(dana); + handler.redeemCollateral(0.000000000000006302 ether, 1); - // totalBoldDeposits = 257_257.842481930065273254 ether - // P = 0.038796625779998194 ether + vm.prank(hope); + handler.registerBatchManager( + 1, + 0.822978751289802582 ether, + 0.835495454680029657 ether, + 0.833312890646159679 ether, + 0.422857251385135959 ether, + 29470036 + ); vm.prank(gabe); - handler.provideToSp(2_881.242711585620903523 ether, false); - - // totalBoldDeposits = 260_139.085193515686176777 ether + handler.addMeToUrgentRedemptionBatch(); vm.prank(barb); - handler.liquidateMe(); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 231_822.380699608727681032 ether - // P = 0.034573528790341043 ether - - invariant_allFundsClaimable(); - } - - function testUnderflow() external { - // coll = 735.070479452054794521 ether, debt = 98_009.39726027397260276 ether vm.prank(gabe); - handler.openTrove(98_000.000000000000000021 ether); - - // pulling `deposited` from fixture - vm.prank(carl); - handler.provideToSp(9.397260273972700749 ether, false); - - // totalBoldDeposits = 9.397260273972700749 ether - + handler.warp(31); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 0 ether + // pendingInterest: 0.012686316538387649 ether vm.prank(carl); - handler.provideToSp(0.000000000000019902 ether, false); - - // totalBoldDeposits = 9.397260273972720651 ether - - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(12.028493150685049425 ether, false); - - // totalBoldDeposits = 21.425753424657770076 ether - - // pulling `deposited` from fixture - vm.prank(fran); - handler.provideToSp(24.05698630137009885 ether, false); - - // totalBoldDeposits = 45.482739726027868926 ether - - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(48.11397260274029571 ether, false); - - // totalBoldDeposits = 93.596712328768164636 ether - - vm.prank(hope); - handler.provideToSp(22_378.224492402901169486 ether, false); - - // totalBoldDeposits = 22_471.821204731669334122 ether - - // coll = 670.171237313523994506 ether, debt = 89_356.164975136532600748 ether - vm.prank(adam); - handler.openTrove(89_347.597397303914417174 ether); - - vm.prank(adam); - handler.provideToSp(66_119.976516875041256465 ether, false); - - // totalBoldDeposits = 88_591.797721606710590587 ether - - // coll = 735.07047945205479454 ether, debt = 98_009.39726027397260532 ether - vm.prank(hope); - handler.openTrove(98_000.000000000000002581 ether); + handler.provideToSP(3, 0.000000000000021916 ether, false); + // upper hint: 0 + // lower hint: 39695913545351040647077841548061220386885435874215782275463606055905069661493 + // upfront fee: 0 ether vm.prank(carl); - handler.provideToSp(99_018.369068280498073463 ether, false); - - // totalBoldDeposits = 187_610.16678988720866405 ether - - // coll = 727.153278118134034577 ether, debt = 96_953.770415751204610242 ether - vm.prank(eric); - handler.openTrove(96_944.474370263645082632 ether); + handler.setBatchManagerAnnualInterestRate(0, 0.998884384586837808 ether, 15539582, 63731457); - vm.prank(eric); - handler.liquidateMe(); - - // totalBoldDeposits = 90_656.396374136004053808 ether - // P = 0.483216863591758585 ether - - // coll = 170.932345052439560746 ether, debt = 22_790.979340325274766094 ether - //vm.prank(barb); - //handler.openTrove(22_788.794113492474117891 ether); + vm.prank(gabe); + handler.registerBatchManager( + 0, + 0.351143076054309979 ether, + 0.467168361632094569 ether, + 0.433984569464653931 ether, + 0.000000000000000026 ether, + 16482089 + ); vm.prank(adam); - handler.liquidateMe(); - - // totalBoldDeposits = 1_300.23139899947145306 ether - // P = 0.006930495405697588 ether - - console2.log("-9"); - invariant_allFundsClaimable(); - - // coll = 735.070479452054794697 ether, debt = 98_009.397260273972626152 ether - vm.prank(fran); - handler.openTrove(98_000.000000000000023411 ether); - - console2.log("-8"); - invariant_allFundsClaimable(); + handler.registerBatchManager( + 3, + 0.995000000000006201 ether, + 0.996462074472343849 ether, + 0.995351673013151748 ether, + 0.045759837128294745 ether, + 10150905 + ); - // coll = 735.070479452056938533 ether, debt = 98_009.397260274258470952 ether vm.prank(dana); - handler.openTrove(98_000.000000000285840803 ether); - - console2.log("-7"); - invariant_allFundsClaimable(); - - // coll = 735.073432425759024971 ether, debt = 98_009.790990101203329407 ether - vm.prank(adam); - handler.openTrove(98_000.393692075935773922 ether); - - console2.log("-6"); - invariant_allFundsClaimable(); - - // pulling `deposited` from fixture - vm.prank(adam); - handler.provideToSp(98_009.39726027397270077 ether, false); - - // totalBoldDeposits = 99_309.62865927344415383 ether - - console2.log("-5"); - invariant_allFundsClaimable(); - - // pulling `deposited` from fixture - vm.prank(fran); - handler.provideToSp(98_009.39726027397270077 ether, false); - - // totalBoldDeposits = 197_319.0259195474168546 ether + handler.warp(23_299); - console2.log("-4"); - invariant_allFundsClaimable(); - - // pulling `deposited` from fixture - vm.prank(fran); - handler.provideToSp(98_009.790990101203427417 ether, false); - - // totalBoldDeposits = 295_328.816909648620282017 ether - - console2.log("-3"); - invariant_allFundsClaimable(); - - vm.prank(eric); - handler.provideToSp(89_618.132493028108872257 ether, false); - - // totalBoldDeposits = 384_946.949402676729154274 ether - - console2.log("-2"); - invariant_allFundsClaimable(); - - // pulling `deposited` from fixture vm.prank(carl); - handler.provideToSp(22_790.979340325274788885 ether, false); - - // totalBoldDeposits = 407_737.928743002003943159 ether - - console2.log("-1"); - invariant_allFundsClaimable(); - - vm.prank(hope); - handler.liquidateMe(); - - // totalBoldDeposits = 309_728.531482728031337839 ether - // P = 0.005264587896132409 ether - - console2.log("0"); - invariant_allFundsClaimable(); - - vm.prank(hope); - handler.provideToSp(36_648.420465084212639386 ether, false); - // [FAIL. Reason: panic: arithmetic underflow or overflow (0x11)] test_XXX() (gas: 9712433) + handler.warp(13_319_679); + + // redemption rate: 0.246264103698059017 ether + // redeemed BOLD: 16_223.156659761268542045 ether + // redeemed Troves: [ + // [], + // [], + // [], + // [adam], + // ] + vm.prank(eric); + handler.redeemCollateral(16_223.156659761268542045 ether, 0); } - function testNotEnoughYieldToClaim() external { - // coll = 531.374961037517928877 ether, debt = 70_849.994805002390516918 ether - vm.prank(barb); - handler.openTrove(70_843.201621285280969428 ether); - - // pulling `deposited` from fixture + function testWrongYieldPrecision() external { vm.prank(carl); - handler.provideToSp(6.79318371710954749 ether, false); - - // totalBoldDeposits = 6.79318371710954749 ether - - vm.prank(barb); - handler.provideToSp(54_410.610723992018269811 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 54_417.403907709127817301 ether - - vm.prank(hope); - handler.provideToSp(5_146.80395069777790841 ether, false); - - // totalBoldDeposits = 59_564.207858406905725711 ether - - // coll = 531.425914800905088131 ether, debt = 70_856.788640120678417378 ether - vm.prank(hope); - handler.openTrove(70_849.994805002390516918 ether); - - invariant_allFundsClaimable(); - } - - function testNotEnoughYieldToClaim2() external { - // coll = 735.071211554227610995 ether, debt = 98_009.494873897014799279 ether - vm.prank(barb); - handler.openTrove(98_000.097604263729236202 ether); + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); - // pulling `deposited` from fixture vm.prank(barb); - handler.provideToSp(9.397269633285661087 ether, false); + handler.warp(19_326); - // totalBoldDeposits = 9.397269633285661087 ether + vm.prank(carl); + handler.addMeToUrgentRedemptionBatch(); - // coll = 296.145816189419348496 ether, debt = 39_486.108825255913132743 ether - vm.prank(hope); - handler.openTrove(39_482.322849092301542185 ether); + vm.prank(dana); + handler.registerBatchManager( + 3, + 0.30820256993275862 ether, + 0.691797430067250243 ether, + 0.383672204747583321 ether, + 0.000000000000018015 ether, + 11403 + ); - // coll = 690.364688805446856156 ether, debt = 92_048.625174059580820766 ether vm.prank(eric); - handler.openTrove(92_039.79943986671688901 ether); + handler.registerBatchManager( + 3, + 0.018392910495297323 ether, + 0.98160708950470919 ether, + 0.963214179009414206 ether, + 0.000000000000019546 ether, + 13319597 + ); vm.prank(fran); - handler.provideToSp(0.000000000000021173 ether, false); + handler.warp(354); - // totalBoldDeposits = 9.39726963328568226 ether + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); - // coll = 735.070479452054794549 ether, debt = 98_009.397260273972606523 ether - vm.prank(carl); - handler.openTrove(98_000.000000000000003783 ether); + vm.prank(eric); + handler.warp(15_305_108); - // coll = 515.861846232288564631 ether, debt = 68_781.579497638475284021 ether - vm.prank(adam); - handler.openTrove(68_774.984636098027527957 ether); + // upper hint: 84669063888545001427406517193344625874395507444463583314999084271619652858036 + // lower hint: 69042136817699606427763587628766179145825895354994492055731203083594873444699 + // upfront fee: 1_702.831959251916404109 ether + vm.prank(fran); + handler.openTrove( + 1, 99_999.999999999999999998 ether, 1.883224555937797003 ether, 0.887905235895642125 ether, 4164477, 39 + ); - // coll = 735.070528577820199656 ether, debt = 98_009.403810376026620703 ether vm.prank(dana); - handler.openTrove(98_000.006549474022262404 ether); + handler.warp(996); - // pulling `deposited` from fixture vm.prank(eric); - handler.provideToSp(98_009.397260273972704533 ether, false); - - // totalBoldDeposits = 98_018.794529907258386793 ether + handler.addMeToUrgentRedemptionBatch(); vm.prank(barb); - handler.liquidateMe(); + handler.warp(4_143_017); - // totalBoldDeposits = 9.299656010243587514 ether - // P = 0.000094876253629155 ether - - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(98_009.397260273972704533 ether, false); - - // totalBoldDeposits = 98_018.696916284216292047 ether + vm.prank(fran); + handler.addMeToLiquidationBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 0 ether + // pendingInterest: 0 ether + vm.prank(adam); + handler.provideToSP(0, 0.000000000000011094 ether, true); vm.prank(carl); - handler.liquidateMe(); - - // totalBoldDeposits = 9.299656010243685524 ether - // P = 0.000000009001512467 ether - - // coll = 439.615230826832584704 ether, debt = 58_615.364110244344627095 ether - vm.prank(fran); - handler.openTrove(58_609.743997806198827208 ether); + handler.addMeToUrgentRedemptionBatch(); - // pulling `deposited` from fixture + // upper hint: 0 + // lower hint: 0 + // upfront fee: 1_513.428916567114728229 ether vm.prank(barb); - handler.provideToSp(68_781.579497638475284021 ether, false); - - // totalBoldDeposits = 68_790.879153648718969545 ether - - // pulling `deposited` from fixture - vm.prank(gabe); - handler.provideToSp(39_486.108825255913132743 ether, false); - - // totalBoldDeposits = 108_276.987978904632102288 ether + handler.openTrove( + 2, + 79_311.063107967331806055 ether, + 1.900000000000001559 ether, + 0.995000000000007943 ether, + 3270556590, + 1229144376 + ); - // pulling `deposited` from fixture vm.prank(fran); - handler.provideToSp(98_009.403810376026620703 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 206_286.391789280658722991 ether + // price: 221.052631578948441462 ether + vm.prank(dana); + handler.setPrice(2, 2.100000000000011917 ether); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 1_226.039010661379810958 ether + // pendingInterest: 11_866.268348193546380256 ether + vm.prank(carl); + handler.provideToSP(1, 0.027362680048399155 ether, false); - vm.prank(fran); - handler.provideToSp(67_852.887887440994776149 ether, false); + // upper hint: 0 + // lower hint: 109724453348421969168156614404527408958334892291486496459024204968877369036377 + // upfront fee: 9.807887080131946403 ether + vm.prank(eric); + handler.openTrove( + 3, 30_260.348082017558572105 ether, 1.683511222023706186 ether, 0.016900375815455486 ether, 108, 14159 + ); - // totalBoldDeposits = 274_139.27967672165349914 ether + vm.prank(carl); + handler.addMeToUrgentRedemptionBatch(); vm.prank(adam); - handler.provideToSp(78_134.913037086847714575 ether, false); - - // totalBoldDeposits = 352_274.192713808501213715 ether - - vm.prank(hope); - handler.liquidateMe(); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 312_788.083888552588080972 ether - // P = 0.000000007992540739 ether - - // pulling `deposited` from fixture vm.prank(adam); - handler.provideToSp(401.55682795488209173 ether, false); + handler.addMeToUrgentRedemptionBatch(); + + // redemption rate: 0.1474722457669512 ether + // redeemed BOLD: 64_016.697525751186019703 ether + // redeemed Troves: [ + // [], + // [fran], + // [barb], + // [eric], + // ] + vm.prank(dana); + handler.redeemCollateral(64_016.697525751186019705 ether, 0); - // totalBoldDeposits = 313_189.640716507470172702 ether + // upper hint: 102052496222650354016228296600262737092032771006947291868573062530791731100756 + // lower hint: 0 + vm.prank(eric); + handler.applyMyPendingDebt(3, 2542, 468); - // coll = 735.070479452054794648 ether, debt = 98_009.397260273972619611 ether vm.prank(gabe); - handler.openTrove(98_000.00000000000001687 ether); + handler.warp(20_216); - vm.prank(adam); - handler.liquidateMe(); - - // totalBoldDeposits = 244_408.061218868994888681 ether - // P = 0.000000006237247764 ether - - // coll = 260.952813316840363413 ether, debt = 34_793.708442245381788334 ether vm.prank(carl); - handler.openTrove(34_790.372379140532696158 ether); - - vm.prank(dana); - handler.provideToSp(126_931.523034110680273609 ether, false); - - info(""); - info(" -------------- here it starts! ----------------"); - info(""); - - // totalBoldDeposits = 371_339.584252979675162290 ether - - vm.prank(fran); - handler.liquidateMe(); - - // totalBoldDeposits = 312_724.220142735330535195 ether - // P = 0.000000005252708102 ether - info(""); - info("P ratio: ", (5252708102 * DECIMAL_PRECISION / 6237247764).decimal()); - info( - "deposits ratio: ", - (312_724.220142735330535195 ether * DECIMAL_PRECISION / 371339584252979675162290).decimal() + handler.registerBatchManager( + 1, + 0.995000000000425732 ether, + 0.998288014105982235 ether, + 0.996095220733623871 ether, + 0.000000000000027477 ether, + 3299 ); - info(""); - - info(""); - info(" -------------- here it goes! ----------------"); - info(""); - - uint256 prevError = 99469643824625821462110 * DECIMAL_PRECISION / 371339584252979675162290; - uint256 pWithError = 5252708102 * DECIMAL_PRECISION + prevError; - uint256 newP = pWithError * 705655592866947642 / DECIMAL_PRECISION / DECIMAL_PRECISION; - info("prev error: ", prevError.decimal()); - info("P w prev error: ", pWithError.decimal()); - info("P * F: ", (pWithError * 705655592866947642 / DECIMAL_PRECISION).decimal()); - info("final P: ", newP.decimal()); + vm.prank(carl); + handler.addMeToLiquidationBatch(); + + // redemption rate: 0.108097849716691371 ether + // redeemed BOLD: 0.000151948988774207 ether + // redeemed Troves: [ + // [], + // [fran], + // [barb], + // [eric], + // ] + vm.prank(hope); + handler.redeemCollateral(0.000151948988774209 ether, 0); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 0 ether + // pendingInterest: 0 ether vm.prank(eric); - handler.liquidateMe(); - - // totalBoldDeposits = 220_675.594968675749714429 ether - // P = 0.000000003706602850 ether - info(""); - info("P ratio: ", (3706602850 * DECIMAL_PRECISION / 6237247764).decimal()); - info( - "deposits ratio: ", - (220_675.594968675749714429 ether * DECIMAL_PRECISION / 371339584252979675162290).decimal() - ); - info("P - 1 ratio: ", (3706602849 * DECIMAL_PRECISION / 6237247764).decimal()); - info(""); + handler.provideToSP(0, 76_740.446487959260685533 ether, true); - // coll = 735.070479452054794556 ether, debt = 98_009.397260273972607451 ether + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 9_803.032557027063219919 ether + // pendingInterest: 0 ether vm.prank(hope); - handler.openTrove(98_000.000000000000004711 ether); - - vm.prank(carl); - handler.liquidateMe(); + handler.provideToSP(1, 4.127947448768090932 ether, false); + } - // totalBoldDeposits = 185_881.886526430367926095 ether - // P = 0.000000003122186351 ether + function testSortedTroveSize() external { + uint256 i = 1; + TestDeployer.LiquityContractsDev memory c = branches[i]; - //uint256 depositsRatio = 185881886526430367926095 * DECIMAL_PRECISION / 371339584252979675162290; - //uint256 pRatio = 3122186351 * DECIMAL_PRECISION / 6237247764; - info(""); - info("P ratio: ", (3122186351 * DECIMAL_PRECISION / 6237247764).decimal()); - info("deposits ratio: ", (185881886526430367926095 * DECIMAL_PRECISION / 371339584252979675162290).decimal()); - info(""); + vm.prank(adam); + handler.addMeToLiquidationBatch(); - invariant_allFundsClaimable(); + vm.prank(barb); + handler.addMeToLiquidationBatch(); - // coll = 285.918279973646172899 ether, debt = 38_122.437329819489719827 ether vm.prank(adam); - handler.openTrove(38_118.782104138270981514 ether); - - invariant_allFundsClaimable(); - } + handler.addMeToUrgentRedemptionBatch(); - function testNotEnoughYieldToClaim3() external { - // coll = 500.857826775587502992 ether, debt = 66_781.043570078333732226 ether vm.prank(adam); - handler.openTrove(66_774.640522357011826983 ether); + handler.registerBatchManager( + 3, + 0.100944149373120884 ether, + 0.377922952132481818 ether, + 0.343424998629201343 ether, + 0.489955880173256455 ether, + 2070930 + ); - // coll = 455.998017386763941998 ether, debt = 60_799.73565156852559962 ether - vm.prank(fran); - handler.openTrove(60_793.906098928902280224 ether); + vm.prank(carl); + handler.addMeToLiquidationBatch(); + + vm.prank(carl); + handler.warp(9_303_785); - // coll = 15.001438356164383562 ether, debt = 2_000.191780821917808219 ether vm.prank(barb); - handler.openTrove(2_000 ether); + handler.registerBatchManager( + 1, + 0.301964103682871801 ether, + 0.756908371280377546 ether, + 0.540898165697757771 ether, + 0.000017102564306416 ether, + 27657915 + ); vm.prank(fran); - handler.provideToSp(0.127035053107027317 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 0.127035053107027317 ether + vm.prank(eric); + handler.addMeToUrgentRedemptionBatch(); - // coll = 390.048079659008251957 ether, debt = 52_006.410621201100260869 ether vm.prank(hope); - handler.openTrove(52_001.424183265718616619 ether); - - // coll = 736.681846984701599582 ether, debt = 98_224.246264626879944156 ether - vm.prank(dana); - handler.openTrove(98_214.828404368926759399 ether); + handler.addMeToLiquidationBatch(); - // pulling `deposited` from fixture - vm.prank(carl); - handler.provideToSp(52_006.410621201100260869 ether, false); - - // totalBoldDeposits = 52_006.537656254207288186 ether + // upper hint: 30979495632948298397104351002742564073201815129975103483277328125306028611241 + // lower hint: 36051278007718023196469061266077621121244014979449590376694871896669965056265 + // upfront fee: 118.231198854524639989 ether + vm.prank(gabe); + handler.openTrove( + 1, 7_591.289850943621327156 ether, 1.900000000017470971 ether, 0.812103428106344175 ether, 1121, 415425919 + ); - // coll = 735.070479452054794533 ether, debt = 98_009.397260273972604297 ether + // redemption rate: 0.005000000000000004 ether + // redeemed BOLD: 0.000000000000071705 ether + // redeemed Troves: [ + // [], + // [gabe], + // [], + // [], + // ] + vm.prank(hope); + handler.redeemCollateral(0.000000000000071705 ether, 1); + + // redemption rate: 0.387443853477360594 ether + // redeemed BOLD: 5_896.917877499258624384 ether + // redeemed Troves: [ + // [], + // [gabe], + // [], + // [], + // ] vm.prank(gabe); - handler.openTrove(98_000.000000000000001558 ether); + handler.redeemCollateral(5_896.917877499258624384 ether, 1); vm.prank(eric); - handler.provideToSp(98_018.79542165509478359 ether, false); - - // totalBoldDeposits = 150_025.333077909302071776 ether + handler.warp(11_371_761); vm.prank(gabe); - handler.liquidateMe(); - - // totalBoldDeposits = 52_015.935817635329467479 ether - // P = 0.346714349839990399 ether + handler.registerBatchManager( + 0, + 0.23834235868248997 ether, + 0.761711006198436234 ether, + 0.523368647516059893 ether, + 0.761688376122671962 ether, + 31535998 + ); vm.prank(hope); - handler.liquidateMe(); - - // totalBoldDeposits = 9.52519643422920661 ether - // P = 0.000063490586814979 ether + handler.registerBatchManager( + 2, + 0.036127532604869915 ether, + 0.999999999999999999 ether, + 0.963882428861225203 ether, + 0.848537401570757863 ether, + 29802393 + ); - // coll = 297.885541044368726255 ether, debt = 39_718.072139249163500539 ether - vm.prank(hope); - handler.openTrove(39_714.263922160737128486 ether); + vm.prank(eric); + handler.addMeToUrgentRedemptionBatch(); + // batch manager: hope + // upper hint: 111996671338791781291582287523793567344508255320483065919810498665837663289426 + // lower hint: 37857035535383668733402580992354953018471987882089934484705744026840633200601 + // upfront fee: 1_355.203530437779650125 ether vm.prank(carl); - handler.provideToSp(45_503.134640909581521244 ether, false); - - // totalBoldDeposits = 45_512.659837343810727854 ether - - vm.prank(dana); - handler.provideToSp(13_158.641347715694197298 ether, false); + handler.openTroveAndJoinInterestBatchManager( + 2, 73_312.036791249214758342 ether, 1.900020510596646286 ether, 40, 115, 737 + ); - // totalBoldDeposits = 58_671.301185059504925152 ether + vm.prank(barb); + handler.registerBatchManager( + 0, + 0.955741837871335122 ether, + 0.974535636428930833 ether, + 0.964294359297779033 ether, + 0.000000000000268875 ether, + 3335617 + ); + vm.prank(gabe); + handler.addMeToLiquidationBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 975.74654191520134809 ether vm.prank(fran); - handler.provideToSp(88_720.671803804390427542 ether, false); - - // totalBoldDeposits = 147_391.972988863895352694 ether - - // pulling `deposited` from fixture - vm.prank(barb); - handler.provideToSp(98_224.246264626880042381 ether, false); + handler.provideToSP(2, 12_633.808570846161076142 ether, true); - // totalBoldDeposits = 245_616.219253490775395075 ether + // batch manager: adam + // upper hint: 7512901306961997563120107574274771509748256751277397278816998908345777536679 + // lower hint: 27989025468780058605431608942843597971189459457295957311648808450848491056535 + // upfront fee: 166.681364294341638522 ether + vm.prank(carl); + handler.openTroveAndJoinInterestBatchManager( + 3, 25_307.541971224954454066 ether, 2.401194840294921108 ether, 142, 6432363, 25223 + ); - // pulling `deposited` from fixture - vm.prank(fran); - handler.provideToSp(2_000.191780821917808219 ether, false); + vm.prank(carl); + handler.addMeToUrgentRedemptionBatch(); - // totalBoldDeposits = 247_616.411034312693203294 ether + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); vm.prank(dana); - handler.liquidateMe(); + handler.warp(8_774_305); - // totalBoldDeposits = 149_392.164769685813259138 ether - // P = 0.000038305200237608 ether + vm.prank(adam); + handler.warp(3_835); - // coll = 16.056327434142920133 ether, debt = 2_140.84365788572268436 ether vm.prank(eric); - handler.openTrove(2_140.638391190677003004 ether); + handler.warp(9_078_180); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 0 ether + vm.prank(gabe); + handler.provideToSP(2, 5_179.259567321319728284 ether, true); - vm.prank(fran); - handler.liquidateMe(); + // price: 120.905132749610222778 ether + vm.prank(hope); + handler.setPrice(1, 2.100000000000002648 ether); - // totalBoldDeposits = 88_592.429118117287659518 ether - // P = 0.000022715721016141 ether + vm.prank(barb); + handler.lowerBatchManagementFee(1, 0.000008085711886436 ether); - // pulling `deposited` from fixture vm.prank(hope); - handler.provideToSp(16_730.704105575056759258 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 105_323.133223692344418776 ether + vm.prank(adam); + handler.addMeToLiquidationBatch(); - // pulling `deposited` from fixture - vm.prank(dana); - handler.provideToSp(2_755.636251036681598567 ether, false); + vm.prank(gabe); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 108_078.769474729026017343 ether + // price: 80.314880400478576408 ether + vm.prank(gabe); + handler.setPrice(1, 1.394988326842136963 ether); - // pulling `deposited` from fixture vm.prank(carl); - handler.provideToSp(528.278543964116931444 ether, false); - - // totalBoldDeposits = 108_607.048018693142948787 ether + handler.warp(1_849_907); - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(1_508.113441559160422894 ether, false); - - // totalBoldDeposits = 110_115.161460252303371681 ether - - vm.prank(barb); - handler.provideToSp(0.000000000000017716 ether, false); + // upper hint: 84800337471693920904250232874319843718400766719524250287777680170677855896573 + // lower hint: 0 + // upfront fee: 0 ether + // function: adjustZombieTrove() + vm.prank(gabe); + handler.adjustTrove( + 1, + uint8(AdjustedTroveProperties.onlyColl), + 29.524853479148084596 ether, + true, + 0 ether, + true, + 40, + 14, + 4554760 + ); - // totalBoldDeposits = 110_115.161460252303389397 ether + info("SortedTroves size: ", c.sortedTroves.getSize().toString()); + info("num troves: ", handler.numTroves(i).toString()); + info("num zombies: ", handler.numZombies(i).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); - // coll = 391.562210654678915577 ether, debt = 52_208.294753957188743495 ether + // upper hint: 0 + // lower hint: 74750724351164404027318726202729770837051588626953680774538886892291438048970 + // upfront fee: 773.037543760336600445 ether vm.prank(carl); - handler.openTrove(52_203.28895912549177853 ether); - - // coll = 694.424483959494241787 ether, debt = 92_589.931194599232238236 ether - vm.prank(fran); - handler.openTrove(92_581.05355932642011576 ether); - - vm.prank(fran); - handler.liquidateMe(); - - // totalBoldDeposits = 17_525.230265653071151161 ether - // P = 0.000003615289994393 ether - - info(""); - info("P ratio: ", (3615289994393 * DECIMAL_PRECISION / 22715721016141).decimal()); - info( - "deposits ratio: ", - (17_525.230265653071151161 ether * DECIMAL_PRECISION / 110_115.161460252303389397 ether).decimal() + handler.openTrove( + 0, 40_510.940914935073773948 ether, 2.063402456659389908 ether, 0.995000000000000248 ether, 0, 55487655 ); - info(""); - - vm.prank(eric); - handler.liquidateMe(); - - // totalBoldDeposits = 15_384.386607767348466801 ether - // P = 0.00000317365410496 ether - info(""); - info("P ratio: ", (3173654104960 * DECIMAL_PRECISION / 22715721016141).decimal()); - info( - "deposits ratio: ", - (15_384.386607767348466801 ether * DECIMAL_PRECISION / 110_115.161460252303389397 ether).decimal() + vm.prank(adam); + handler.registerBatchManager( + 0, + 0.541865737266494949 ether, + 0.672692246806001449 ether, + 0.650860934960147488 ether, + 0.070089828074852802 ether, + 29179158 ); - info(""); - // coll = 472.174813390591817645 ether, debt = 62_956.641785412242352578 ether vm.prank(fran); - handler.openTrove(62_950.605425987832560415 ether); + handler.registerBatchManager( + 1, + 0.566980989185701648 ether, + 0.86881504225021711 ether, + 0.702666683322997409 ether, + 0.667232273668645041 ether, + 7007521 + ); - // coll = 735.070479452054794524 ether, debt = 98_009.397260273972603184 ether vm.prank(dana); - handler.openTrove(98_000.000000000000000445 ether); - - invariant_allFundsClaimable(); + handler.addMeToUrgentRedemptionBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 1_129.588991574293634631 ether + vm.prank(barb); + handler.provideToSP(1, 0.000000000000000002 ether, false); + + info("SortedTroves size: ", c.sortedTroves.getSize().toString()); + info("num troves: ", handler.numTroves(i).toString()); + info("num zombies: ", handler.numZombies(i).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + + // redemption rate: 0.184202341360173417 ether + // redeemed BOLD: 66_462.494346928386331338 ether + // redeemed Troves: [ + // [carl], + // [gabe], + // [], + // [carl], + // ] + vm.prank(eric); + handler.redeemCollateral(66_462.49434692838633134 ether, 1); + + info("SortedTroves size: ", c.sortedTroves.getSize().toString()); + info("num troves: ", handler.numTroves(i).toString()); + info("num zombies: ", handler.numZombies(i).toString()); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + assertEq(c.sortedTroves.getSize(), handler.numTroves(i) - handler.numZombies(i), "Wrong SortedTroves size"); } - function testNotEnoughYieldToClaim4() external { - // coll = 735.070479452054794523 ether, debt = 98_009.397260273972602968 ether - vm.prank(dana); - handler.openTrove(98_000.000000000000000229 ether); - - // pulling `deposited` from fixture - vm.prank(hope); - handler.provideToSp(9.397260273972700749 ether, false); + function testAssertLastZombieTroveInABatchHasMoreThanMinDebt() external { + uint256 i = 1; + TestDeployer.LiquityContractsDev memory c = branches[i]; - // totalBoldDeposits = 9.397260273972700749 ether + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); - // coll = 735.070479452054794596 ether, debt = 98_009.397260273972612766 ether vm.prank(hope); - handler.openTrove(98_000.000000000000010026 ether); - - vm.prank(gabe); - handler.provideToSp(98_009.397260282669422181 ether, false); - - // totalBoldDeposits = 98_018.79452055664212293 ether - - vm.prank(dana); - handler.liquidateMe(); - - // totalBoldDeposits = 9.397260282669519962 ether - // P = 0.000095872024631956 ether - - // coll = 15.001438356164383562 ether, debt = 2_000.191780821917808222 ether - vm.prank(barb); - handler.openTrove(2_000.000000000000000003 ether); + handler.registerBatchManager( + 0, + 0.99500000000072184 ether, + 0.996944021609020651 ether, + 0.99533906344899454 ether, + 0.378970428480541887 ether, + 314055 + ); - // coll = 245.638216964337321336 ether, debt = 32_751.762261911642844722 ether vm.prank(dana); - handler.openTrove(32_748.621983091346414244 ether); + handler.warp(2_225_439); - // coll = 671.510441931192659665 ether, debt = 89_534.72559082568795521 ether - vm.prank(fran); - handler.openTrove(89_526.14089238395250771 ether); - - // coll = 735.070479452054794659 ether, debt = 98_009.397260273972621098 ether vm.prank(adam); - handler.openTrove(98_000.000000000000018357 ether); + handler.addMeToUrgentRedemptionBatch(); - vm.prank(adam); - handler.provideToSp(0.000000000000021956 ether, false); + vm.prank(barb); + handler.addMeToUrgentRedemptionBatch(); - // totalBoldDeposits = 9.397260282669541918 ether + vm.prank(barb); + handler.registerBatchManager( + 2, + 0.995000000000009379 ether, + 0.999999999998128142 ether, + 0.997477804125778004 ether, + 0.000000001035389259 ether, + 10046 + ); - // coll = 376.789379643035011626 ether, debt = 50_238.583952404668216799 ether vm.prank(gabe); - handler.openTrove(50_233.767015841505332726 ether); - - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(50_238.583952404668216799 ether, false); + handler.registerBatchManager( + 2, + 0.346476084765605513 ether, + 0.346476084765605514 ether, + 0.346476084765605514 ether, + 0.000000000000000002 ether, + 27010346 + ); - // totalBoldDeposits = 50_247.981212687337758717 ether + vm.prank(fran); + handler.warp(19_697_329); vm.prank(gabe); - handler.liquidateMe(); - - // totalBoldDeposits = 9.397260282669541918 ether - // P = 0.0000000179297625 ether + handler.addMeToUrgentRedemptionBatch(); - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(89_534.72559082568795521 ether, false); - - // totalBoldDeposits = 89_544.122851108357497128 ether + vm.prank(fran); + handler.registerBatchManager( + 1, + 0.995000000000019257 ether, + 0.999999999996150378 ether, + 0.999999999226237651 ether, + 0.696688179568702502 ether, + 7641047 + ); - // coll = 391.027929559007118106 ether, debt = 52_137.057274534282414027 ether vm.prank(gabe); - handler.openTrove(52_132.058310038799241497 ether); + handler.warp(977_685); - vm.prank(dana); - handler.provideToSp(0.000000000000000001 ether, false); - - // totalBoldDeposits = 89_544.122851108357497129 ether - - vm.prank(barb); - handler.liquidateMe(); + vm.prank(gabe); + handler.addMeToUrgentRedemptionBatch(); - // totalBoldDeposits = 87_543.931070286439688907 ether - // P = 0.000000017529256443 ether + vm.prank(fran); + handler.addMeToLiquidationBatch(); - // coll = 362.672606823421941244 ether, debt = 48_356.347576456258832456 ether - vm.prank(eric); - handler.openTrove(48_351.711111007258136471 ether); + // batch manager: fran + // upper hint: 0 + // lower hint: 60678094901167127062962700790111047491633904950610080336398562382189456360809 + // upfront fee: 242.684833433337541236 ether + vm.prank(gabe); + handler.openTroveAndJoinInterestBatchManager( + 1, 12_654.280610244006254376 ether, 2.145058504746006382 ether, 182, 22444926, 124118903 + ); - invariant_allFundsClaimable(); - } + // redemption rate: 0.005 ether + // redeemed BOLD: 0.000000000000017162 ether + // redeemed Troves: [ + // [], + // [gabe], + // [], + // [], + // ] + vm.prank(hope); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + handler.redeemCollateral(0.000000000000017162 ether, 0); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + + // upper hint: 79178440845664423591903906560915994242429107602729190780850212197412640295587 + // lower hint: 0 + // upfront fee: 624.393448965513162837 ether + vm.prank(hope); + handler.openTrove( + 0, 32_721.264734011072612096 ether, 2.333153121000516764 ether, 0.995000000000109949 ether, 52719, 31482 + ); - function testCollGainsUnderflow3CollSkin() external { - // coll = 289.601984682301661608 ether, debt = 38_613.59795764022154772 ether + // price: 244.435094708283018275 ether vm.prank(dana); - handler.openTrove(38_609.895638880328913441 ether); + handler.setPrice(1, 2.621637893811990143 ether); - // pulling `deposited` from fixture vm.prank(carl); - handler.provideToSp(3.702318759892672893 ether, false); - - // totalBoldDeposits = 3.702318759892672893 ether - - // pulling `deposited` from fixture + handler.addMeToUrgentRedemptionBatch(); + + // redemption rate: 0.37622704640950591 ether + // redeemed BOLD: 34_333.025174298345667786 ether + // redeemed Troves: [ + // [hope], + // [gabe], + // [], + // [], + // ] vm.prank(carl); - handler.provideToSp(7.404637519785345786 ether, false); - - // totalBoldDeposits = 11.106956279678018679 ether - - vm.prank(carl); - handler.provideToSp(6_872.312325153568231613 ether, false); - - // totalBoldDeposits = 6_883.419281433246250292 ether - - // pulling `deposited` from fixture - vm.prank(carl); - handler.provideToSp(6_884.455930686016187896 ether, false); - - // totalBoldDeposits = 13_767.875212119262438188 ether + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + handler.redeemCollateral(34_333.025174298345667787 ether, 0); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); - // coll = 485.870086975795226011 ether, debt = 64_782.678263439363468128 ether - vm.prank(adam); - handler.openTrove(64_776.466821415392129157 ether); - - // coll = 735.070479841999818057 ether, debt = 98_009.39731226664240759 ether vm.prank(gabe); - handler.openTrove(98_000.000051987684684402 ether); + handler.addMeToLiquidationBatch(); vm.prank(adam); - handler.provideToSp(56_502.482327086364961955 ether, false); - - // totalBoldDeposits = 70_270.357539205627400143 ether - - // coll = 735.138660165061182493 ether, debt = 98_018.48802200815766572 ether - vm.prank(hope); - handler.openTrove(98_009.089890100887717583 ether); - - // coll = 735.070493012734388559 ether, debt = 98_009.399068364585141076 ether - vm.prank(barb); - handler.openTrove(98_000.001807917250610196 ether); - - vm.prank(dana); - handler.provideToSp(66_572.988267614156561955 ether, false); - - // totalBoldDeposits = 136_843.345806819783962098 ether - - vm.prank(adam); - handler.liquidateMe(); - - // totalBoldDeposits = 72_060.66754338042049397 ether - // P = 0.526592412064432101 ether - - // coll = 735.140965662413210843 ether, debt = 98_018.795421655094779019 ether - vm.prank(eric); - handler.openTrove(98_009.397260273972617262 ether); + handler.addMeToLiquidationBatch(); - // pulling `deposited` from fixture + // upper hint: hope + // lower hint: hope + // upfront fee: 45.851924869044942133 ether vm.prank(carl); - handler.provideToSp(64_782.678263439363532911 ether, false); - - // totalBoldDeposits = 136_843.345806819784026881 ether - - vm.prank(eric); - handler.liquidateMe(); - - // totalBoldDeposits = 38_824.550385164689247862 ether - // P = 0.149402322152386736 ether - - vm.prank(dana); - handler.liquidateMe(); - - // totalBoldDeposits = 210.952427524467700142 ether - // P = 0.000811774565916968 ether - - // coll = 735.237290720905222225 ether, debt = 98_031.638762787362963266 ether - vm.prank(adam); - handler.openTrove(98_022.239369971064368053 ether); - - vm.prank(adam); - handler.provideToSp(370_408.786768579111584211 ether, false); + handler.openTrove( + 0, 3_111.607048463492852195 ether, 1.16895262626418546 ether, 0.142852735597140811 ether, 1885973, 10937 + ); - // totalBoldDeposits = 370_619.739196103579284353 ether + // upper hint: 2646484967802154597987056038088487662712072023062744056283555991417410575365 + // lower hint: 20207836743015961388089283396921182522044498153231052202943306959004515414684 + // upfront fee: 0 ether + // function: addColl() + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + vm.prank(gabe); + handler.adjustTrove( + 1, uint8(AdjustedTroveProperties.onlyColl), 3.631424438531681645 ether, true, 0 ether, false, 86, 703, 9499 + ); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); vm.prank(barb); - handler.liquidateMe(); - - // totalBoldDeposits = 272_610.340127738994143277 ether - // P = 0.00059710295248084 ether - - // pulling `deposited` from fixture - vm.prank(dana); - handler.provideToSp(508.689744747380433717 ether, false); - - // totalBoldDeposits = 273_119.029872486374576994 ether - - vm.prank(eric); - handler.provideToSp(0.000000000000011519 ether, false); - - // totalBoldDeposits = 273_119.029872486374588513 ether - - vm.prank(dana); - handler.provideToSp(14_325.409601730288741627 ether, false); - - // totalBoldDeposits = 287_444.43947421666333014 ether + handler.lowerBatchManagementFee(2, 0.000000000204221707 ether); vm.prank(hope); - handler.liquidateMe(); - - // totalBoldDeposits = 189_425.95145220850566442 ether - // P = 0.000393490982450372 ether - - vm.prank(carl); - handler.provideToSp(656.318601037450927984 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 190_082.270053245956592404 ether + vm.prank(hope); + handler.addMeToLiquidationBatch(); - // coll = 735.070479452062891793 ether, debt = 98_009.39726027505223895 ether + vm.prank(hope); + handler.addMeToUrgentRedemptionBatch(); + + // redemption rate: 0.37622704640950591 ether + // redeemed BOLD: 0.000000000000005602 ether + // redeemed Troves: [ + // [carl], + // [gabe], + // [], + // [], + // ] vm.prank(carl); - handler.openTrove(98_000.000000001079532694 ether); - - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(38_613.597957640221586334 ether, false); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + handler.redeemCollateral(0.000000000000005603 ether, 1); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); - // totalBoldDeposits = 228_695.868010886178178738 ether + vm.prank(fran); + handler.addMeToUrgentRedemptionBatch(); vm.prank(dana); - handler.provideToSp(53_385.94712175149302094 ether, false); - - // totalBoldDeposits = 282_081.815132637671199678 ether - - vm.prank(carl); - handler.provideToSp(6_856.901188404296809837 ether, false); + handler.addMeToUrgentRedemptionBatch(); - // totalBoldDeposits = 288_938.716321041968009515 ether + vm.prank(dana); + handler.registerBatchManager( + 1, 0.995000000000001129 ether, 1 ether, 0.999999999999799729 ether, 0.000000000000000001 ether, 31535999 + ); - // coll = 15.001438356164383562 ether, debt = 2_000.191780821917808219 ether - vm.prank(fran); - handler.openTrove(2_000 ether); + // redemption rate: 0.718476929948594246 ether + // redeemed BOLD: 5_431.066474911544502914 ether + // redeemed Troves: [ + // [carl], + // [gabe], + // [], + // [], + // ] + vm.prank(barb); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + handler.redeemCollateral(10_313.397298437031513085 ether, 1); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe ent debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + info("gabe rec debt: ", c.troveManager.getTroveDebt(addressToTroveId(gabe)).decimal()); + info("lzti: ", c.troveManager.lastZombieTroveId().toString()); vm.prank(dana); - handler.provideToSp(5_881.506587694815077057 ether, false); + handler.warp(30_167_580); - // totalBoldDeposits = 294_820.222908736783086572 ether + info("gabe ent debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + info("gabe rec debt: ", c.troveManager.getTroveDebt(addressToTroveId(gabe)).decimal()); + info("lzti: ", c.troveManager.lastZombieTroveId().toString()); + vm.prank(gabe); + handler.registerBatchManager( + 1, + 0.995000000000002877 ether, + 0.999999999999430967 ether, + 0.996456350847225481 ether, + 0.000000001322368348 ether, + 14343 + ); + info("gabe ent debt 1: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + info("gabe rec debt 1: ", c.troveManager.getTroveDebt(addressToTroveId(gabe)).decimal()); - // coll = 735.072533183874248984 ether, debt = 98_009.671091183233197827 ether vm.prank(hope); - handler.openTrove(98_000.273804654019798669 ether); - - // pulling `deposited` from fixture - vm.prank(carl); - handler.provideToSp(98_018.795421655094877038 ether, false); - - // totalBoldDeposits = 392_839.01833039187796361 ether - - vm.prank(eric); - handler.provideToSp(1_186.940091321995741882 ether, false); - - // totalBoldDeposits = 394_025.958421713873705492 ether - - vm.prank(adam); - handler.provideToSp(0.001983727284992749 ether, false); - - invariant_allFundsClaimable(); - } - - function testSPYieldBigDispropRedeem() external { - // coll = 490_098_347_574_376_811.735209341223553774 ether, debt = 65_346_446_343_250_241_564.694578829807169845 ether + handler.addMeToLiquidationBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 0 ether vm.prank(barb); - handler.openTrove(65_340_180_846_456_745_712.365995789115062922 ether); + handler.provideToSP(3, 1_933.156398582065633891 ether, false); - // coll = 750_071_917_808_219_163.080753424657534401 ether, debt = 100_009_589_041_095_888_410.767123287671253375 ether - vm.prank(gabe); - handler.openTrove(99_999_999_999_999_998_000.000000000000020497 ether); - - // coll = 502_539_092_456_032_564.492320686560399734 ether, debt = 67_005_212_327_471_008_598.976091541386631089 ether vm.prank(hope); - handler.openTrove(66_998_787_786_176_443_734.508398955185448923 ether); + handler.addMeToUrgentRedemptionBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 6_368.077020894268536036 ether + vm.prank(hope); + handler.provideToSP(0, 6_184.412833814428802676 ether, true); - // pulling `deposited` from fixture vm.prank(carl); - handler.provideToSp(65_346_446_343_250_241_630.04102517305741141 ether, false); - - // totalBoldDeposits = 65_346_446_343_250_241_630.04102517305741141 ether - - vm.prank(eric); - handler.provideToSp(0.00000000000000052 ether, false); - - // totalBoldDeposits = 65_346_446_343_250_241_630.04102517305741193 ether + handler.addMeToLiquidationBatch(); + // upper hint: 81940996894813545005963650320412669449148720334632109303327864712326705297348 + // lower hint: carl + // upfront fee: 297.236383200558451701 ether vm.prank(barb); - handler.liquidateMe(); - - // totalBoldDeposits = 65.346446343250242085 ether - // P = 1_000_000_000.000000006962260387 ether + handler.openTrove( + 0, + 69_695.596747080749922615 ether, + 1.900000000000006402 ether, + 0.153255449436557929 ether, + 1498297936, + 1276315316 + ); - // coll = 53_550_456_698_134_716.302091267691508688 ether, debt = 7_140_060_893_084_628_840.27883569220115827 ether - vm.prank(eric); - handler.openTrove(7_139_376_295_357_676_734.290616044087341676 ether); + // upper hint: 0 + // lower hint: 30960623452289762463130736603892188849115197753010878244835568881362241800197 + // upfront fee: 56.245103106642574315 ether + // function: withdrawBold() + vm.prank(hope); + handler.adjustTrove( + 0, + uint8(AdjustedTroveProperties.onlyDebt), + 0 ether, + false, + 7_875.177407392532383015 ether, + true, + 5, + 16648, + 270 + ); - // pulling `deposited` from fixture - vm.prank(carl); - handler.provideToSp(67_005_212_327_471_008_598.976091541386631089 ether, false); + // batch manager: gabe + // upper hint: gabe + // lower hint: 0 + // upfront fee: 1_261.275141740191589507 ether + vm.prank(adam); + handler.openTroveAndJoinInterestBatchManager( + 1, 66_969.454138225567397381 ether, 2.984784797753777921 ether, 4294967294, 1, 52 + ); + info("gabe ent debt 2: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + info("gabe rec debt 2: ", c.troveManager.getTroveDebt(addressToTroveId(gabe)).decimal()); - // totalBoldDeposits = 67_005_212_327_471_008_664.322537884636873174 ether + // batch manager: hope + // upper hint: 0 + // lower hint: barb + // upfront fee: 1_272.067039116734276271 ether + vm.prank(eric); + handler.openTroveAndJoinInterestBatchManager( + 0, 96_538.742068715532219745 ether, 2.762063859567414329 ether, 0, 61578232, 336273331 + ); + // initial deposit: 6_184.412833814428802676 ether + // compounded deposit: 6_184.412833814428802676 ether + // yield gain: 7_538.471959199501948711 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 0 ether vm.prank(hope); - handler.liquidateMe(); - - // totalBoldDeposits = 65.346446343250242085 ether - // P = 975_244_224_641_600_008.705577453466833391 ether + handler.provideToSP(0, 0.000000001590447554 ether, true); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 0 ether + vm.prank(fran); + handler.provideToSP(3, 180_836.387435487377369461 ether, true); - // coll = 570_376_835_580_790_313.880152031558999999 ether, debt = 76_050_244_744_105_375_184.020270874533333148 ether vm.prank(fran); - handler.openTrove(76_042_952_954_096_078_299.799742132137100824 ether); + handler.addMeToLiquidationBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 0 ether + vm.prank(eric); + handler.provideToSP(2, 0.000000000000000012 ether, true); - // Very extreme edge case. It gets fixed with SCALE_SPAN = 3 - // invariant_allFundsClaimable(); + vm.prank(carl); + handler.addMeToUrgentRedemptionBatch(); + + // redemption rate: 0.00500000000000102 ether + // redeemed BOLD: 0.000000000536305094 ether + // redeemed Troves: [ + // [barb], + // [gabe], + // [], + // [], + // ] + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe ent debt e: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + info("gabe rec debt e: ", c.troveManager.getTroveDebt(addressToTroveId(gabe)).decimal()); + info("lzti: ", c.troveManager.lastZombieTroveId().toString()); + vm.prank(barb); + handler.redeemCollateral(0.000000000536305095 ether, 3); } -} +} \ No newline at end of file diff --git a/contracts/test/E2E.t.sol b/contracts/test/E2E.t.sol deleted file mode 100644 index 542864a5a..000000000 --- a/contracts/test/E2E.t.sol +++ /dev/null @@ -1,468 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {MockStakingV1} from "V2-gov/test/mocks/MockStakingV1.sol"; -import {CurveV2GaugeRewards} from "V2-gov/src/CurveV2GaugeRewards.sol"; -import {Ownable} from "src/Dependencies/Ownable.sol"; -import {ICurveStableSwapNG} from "./Interfaces/Curve/ICurveStableSwapNG.sol"; -import {ILiquidityGaugeV6} from "./Interfaces/Curve/ILiquidityGaugeV6.sol"; -import "./Utils/E2EHelpers.sol"; - -function coalesce(address a, address b) pure returns (address) { - return a != address(0) ? a : b; -} - -contract E2ETest is E2EHelpers { - using SideEffectFreeGetPrice for IPriceFeedV1; - - struct Initiative { - address addr; - ILiquidityGaugeV6 gauge; // optional - } - - address[] ownables; - - function _addCurveLiquidity( - address liquidityProvider, - ICurveStableSwapNG pool, - uint256 coin0Amount, - address coin0, - uint256 coin1Amount, - address coin1 - ) internal { - uint256[] memory amounts = new uint256[](2); - (amounts[0], amounts[1]) = pool.coins(0) == coin0 ? (coin0Amount, coin1Amount) : (coin1Amount, coin0Amount); - - deal(coin0, liquidityProvider, coin0Amount); - deal(coin1, liquidityProvider, coin1Amount); - - vm.startPrank(liquidityProvider); - IERC20(coin0).approve(address(pool), coin0Amount); - IERC20(coin1).approve(address(pool), coin1Amount); - pool.add_liquidity(amounts, 0); - vm.stopPrank(); - } - - function _depositIntoCurveGauge(address liquidityProvider, ILiquidityGaugeV6 gauge, uint256 amount) internal { - vm.startPrank(liquidityProvider); - gauge.lp_token().approve(address(gauge), amount); - gauge.deposit(amount); - vm.stopPrank(); - } - - function _claimRewardsFromCurveGauge(address liquidityProvider, ILiquidityGaugeV6 gauge) internal { - vm.prank(liquidityProvider); - gauge.claim_rewards(); - } - - function _mainnet_V1_openTroveAtTail(address owner, uint256 lusdAmount) internal returns (uint256 borrowingFee) { - uint256 price = mainnet_V1_priceFeed.getPrice(); - address lastTrove = mainnet_V1_sortedTroves.getLast(); - assertGeDecimal(mainnet_V1_troveManager.getCurrentICR(lastTrove, price), 1.1 ether, 18, "last ICR < MCR"); - - uint256 borrowingRate = mainnet_V1_troveManager.getBorrowingRateWithDecay(); - borrowingFee = lusdAmount * borrowingRate / 1 ether; - uint256 debt = lusdAmount + borrowingFee + 200 ether; - uint256 collAmount = Math.ceilDiv(debt * 1.1 ether, price); - deal(owner, collAmount); - - vm.startPrank(owner); - mainnet_V1_borrowerOperations.openTrove{value: collAmount}({ - _LUSDAmount: lusdAmount, - _maxFeePercentage: borrowingRate, - _upperHint: lastTrove, - _lowerHint: address(0) - }); - vm.stopPrank(); - - assertEq(mainnet_V1_sortedTroves.getLast(), owner, "last Trove != new Trove"); - } - - function _mainnet_V1_redeemCollateralFromTroveAtTail(address redeemer, uint256 lusdAmount) - internal - returns (uint256 redemptionFee) - { - address lastTrove = mainnet_V1_sortedTroves.getLast(); - address prevTrove = mainnet_V1_sortedTroves.getPrev(lastTrove); - (uint256 lastTroveDebt, uint256 lastTroveColl,,) = mainnet_V1_troveManager.getEntireDebtAndColl(lastTrove); - assertLeDecimal(lusdAmount, lastTroveDebt - 2_000 ether, 18, "lusdAmount > redeemable from last Trove"); - - uint256 price = mainnet_V1_priceFeed.getPrice(); - uint256 collAmount = lusdAmount * 1 ether / price; - uint256 balanceBefore = redeemer.balance; - - vm.startPrank(redeemer); - mainnet_V1_troveManager.redeemCollateral({ - _LUSDamount: lusdAmount, - _maxFeePercentage: 1 ether, - _maxIterations: 1, - _firstRedemptionHint: lastTrove, - _upperPartialRedemptionHint: prevTrove, - _lowerPartialRedemptionHint: prevTrove, - _partialRedemptionHintNICR: (lastTroveColl - collAmount) * 100 ether / (lastTroveDebt - lusdAmount) - }); - vm.stopPrank(); - - redemptionFee = collAmount * mainnet_V1_troveManager.getBorrowingRateWithDecay() / 1 ether; - assertEqDecimal(redeemer.balance - balanceBefore, collAmount - redemptionFee, 18, "coll received != expected"); - } - - function _generateStakingRewards() internal returns (uint256 lusdAmount, uint256 ethAmount) { - if (block.chainid == 1) { - address stakingRewardGenerator = makeAddr("stakingRewardGenerator"); - lusdAmount = _mainnet_V1_openTroveAtTail(stakingRewardGenerator, 1e6 ether); - ethAmount = _mainnet_V1_redeemCollateralFromTroveAtTail(stakingRewardGenerator, 1_000 ether); - } else { - // Testnet - lusdAmount = 10_000 ether; - ethAmount = 1 ether; - - MockStakingV1 stakingV1 = MockStakingV1(address(governance.stakingV1())); - address owner = stakingV1.owner(); - - deal(LUSD, owner, lusdAmount); - deal(owner, ethAmount); - - vm.startPrank(owner); - lusd.approve(address(stakingV1), lusdAmount); - stakingV1.mock_addLUSDGain(lusdAmount); - stakingV1.mock_addETHGain{value: ethAmount}(); - vm.stopPrank(); - } - } - - function test_OwnershipRenounced() external { - ownables.push(address(boldToken)); - - for (uint256 i = 0; i < branches.length; ++i) { - ownables.push(address(branches[i].addressesRegistry)); - } - - for (uint256 i = 0; i < ownables.length; ++i) { - assertEq( - Ownable(ownables[i]).owner(), - address(0), - string.concat("Ownership of ", vm.getLabel(ownables[i]), " should have been renounced") - ); - } - - ILiquidityGaugeV6[2] memory gauges = [curveUsdcBoldGauge, curveLusdBoldGauge]; - - for (uint256 i = 0; i < gauges.length; ++i) { - if (address(gauges[i]) == address(0)) continue; - address gaugeManager = gauges[i].manager(); - assertEq(gaugeManager, address(0), "Gauge manager role should have been renounced"); - } - } - - function _epoch(uint256 n) internal view returns (uint256) { - return EPOCH_START + (n - 1) * EPOCH_DURATION; - } - - function test_Initially_NewInitiativeCannotBeRegistered() external { - vm.skip(governance.epoch() > 2); - - address registrant = makeAddr("registrant"); - address newInitiative = makeAddr("newInitiative"); - - _openTrove(0, registrant, 0, Math.max(REGISTRATION_FEE, MIN_DEBT)); - - uint256 epoch2 = _epoch(2); - if (block.timestamp < epoch2) vm.warp(epoch2); - - vm.startPrank(registrant); - { - boldToken.approve(address(governance), REGISTRATION_FEE); - vm.expectRevert("Governance: registration-not-yet-enabled"); - governance.registerInitiative(newInitiative); - } - vm.stopPrank(); - } - - function test_AfterOneEpoch_NewInitiativeCanBeRegistered() external { - vm.skip(governance.epoch() > 2); - - address registrant = makeAddr("registrant"); - address newInitiative = makeAddr("newInitiative"); - - _openTrove(0, registrant, 0, Math.max(REGISTRATION_FEE, MIN_DEBT)); - - uint256 epoch3 = _epoch(3); - if (block.timestamp < epoch3) vm.warp(epoch3); - - vm.startPrank(registrant); - { - boldToken.approve(address(governance), REGISTRATION_FEE); - governance.registerInitiative(newInitiative); - } - vm.stopPrank(); - } - - function test_E2E() external { - // Test assumes that all Stability Pools are empty in the beginning - for (uint256 i = 0; i < branches.length; ++i) { - vm.skip(branches[i].stabilityPool.getTotalBoldDeposits() != 0); - } - - uint256 repaid; - uint256 borrowed = boldToken.totalSupply() - boldToken.balanceOf(address(governance)); - - for (uint256 i = 0; i < branches.length; ++i) { - borrowed -= boldToken.balanceOf(address(branches[i].stabilityPool)); - } - - if (block.chainid == 1) { - assertEqDecimal(borrowed, 0, 18, "Mainnet deployment script should not have borrowed anything"); - assertNotEq(address(curveUsdcBoldGauge), address(0), "Mainnet should have USDC-BOLD gauge"); - assertNotEq(address(curveUsdcBoldInitiative), address(0), "Mainnet should have USDC-BOLD initiative"); - assertNotEq(address(curveLusdBold), address(0), "Mainnet should have LUSD-BOLD pool"); - assertNotEq(address(curveLusdBoldGauge), address(0), "Mainnet should have LUSD-BOLD gauge"); - assertNotEq(address(curveLusdBoldInitiative), address(0), "Mainnet should have LUSD-BOLD initiative"); - assertNotEq(address(defiCollectiveInitiative), address(0), "Mainnet should have DeFi Collective initiative"); - } - - address borrower = providerOf[BOLD] = makeAddr("borrower"); - - for (uint256 j = 0; j < 5; ++j) { - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - borrowed += _openTrove(i, borrower, j, 20_000 ether); - } - } - - address liquidityProvider = makeAddr("liquidityProvider"); - { - skip(5 minutes); - - uint256 boldAmount = boldToken.balanceOf(borrower) * 2 / 5; - uint256 usdcAmount = boldAmount * 10 ** usdc.decimals() / 10 ** boldToken.decimals(); - uint256 lusdAmount = boldAmount; - - _addCurveLiquidity(liquidityProvider, curveUsdcBold, boldAmount, BOLD, usdcAmount, USDC); - - if (address(curveLusdBold) != address(0)) { - _addCurveLiquidity(liquidityProvider, curveLusdBold, boldAmount, BOLD, lusdAmount, LUSD); - } - - if (address(curveUsdcBoldGauge) != address(0)) { - _depositIntoCurveGauge( - liquidityProvider, curveUsdcBoldGauge, curveUsdcBold.balanceOf(liquidityProvider) - ); - } - - if (address(curveLusdBoldGauge) != address(0)) { - _depositIntoCurveGauge( - liquidityProvider, curveLusdBoldGauge, curveLusdBold.balanceOf(liquidityProvider) - ); - } - } - - address stabilityDepositor = makeAddr("stabilityDepositor"); - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - _provideToSP(i, stabilityDepositor, boldToken.balanceOf(borrower) / (branches.length - i)); - } - - address leverageSeeker = makeAddr("leverageSeeker"); - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - borrowed += _openLeveragedTrove(i, leverageSeeker, 0, 10_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - borrowed += _leverUpTrove(i, leverageSeeker, 0, 1_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - repaid += _leverDownTrove(i, leverageSeeker, 0, 1_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - repaid += _closeTroveFromCollateral(i, leverageSeeker, 0, true); - } - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - repaid += _closeTroveFromCollateral(i, borrower, 0, false); - } - - skip(5 minutes); - - Initiative[] memory initiatives = new Initiative[](initialInitiatives.length); - for (uint256 i = 0; i < initiatives.length; ++i) { - initiatives[i].addr = initialInitiatives[i]; - if (initialInitiatives[i] == address(curveUsdcBoldInitiative)) initiatives[i].gauge = curveUsdcBoldGauge; - if (initialInitiatives[i] == address(curveLusdBoldInitiative)) initiatives[i].gauge = curveLusdBoldGauge; - } - - address staker = makeAddr("staker"); - { - uint256 lqtyStake = 30_000 ether; - _depositLQTY(staker, lqtyStake); - - skip(5 minutes); - - (uint256 lusdAmount, uint256 ethAmount) = _generateStakingRewards(); - uint256 totalLQTYStaked = governance.stakingV1().totalLQTYStaked(); - - skip(5 minutes); - - vm.prank(staker); - governance.claimFromStakingV1(staker); - - assertApproxEqAbsDecimal( - lusd.balanceOf(staker), lusdAmount * lqtyStake / totalLQTYStaked, 1e5, 18, "LUSD reward" - ); - assertApproxEqAbsDecimal(staker.balance, ethAmount * lqtyStake / totalLQTYStaked, 1e5, 18, "ETH reward"); - - skip(5 minutes); - - if (initiatives.length > 0) { - // Voting on initial initiatives opens in epoch #2 - uint256 votingStart = _epoch(2); - if (block.timestamp < votingStart) vm.warp(votingStart); - - _allocateLQTY_begin(staker); - - for (uint256 i = 0; i < initiatives.length; ++i) { - _allocateLQTY_vote(initiatives[i].addr, int256(lqtyStake / initiatives.length)); - } - - _allocateLQTY_end(); - } - } - - skip(EPOCH_DURATION); - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - _claimFromSP(i, stabilityDepositor); - } - - uint256 interest = boldToken.totalSupply() + repaid - borrowed; - uint256 spShareOfInterest = boldToken.balanceOf(stabilityDepositor); - uint256 governanceShareOfInterest = boldToken.balanceOf(address(governance)); - - assertApproxEqRelDecimal( - interest, - spShareOfInterest + governanceShareOfInterest, - 1e-16 ether, - 18, - "Stability depositor and Governance should have received the interest" - ); - - if (initiatives.length > 0) { - uint256 initiativeShareOfInterest; - - for (uint256 i = 0; i < initiatives.length; ++i) { - governance.claimForInitiative(initiatives[i].addr); - initiativeShareOfInterest += - boldToken.balanceOf(coalesce(address(initiatives[i].gauge), initiatives[i].addr)); - } - - assertApproxEqRelDecimal( - governanceShareOfInterest, - initiativeShareOfInterest, - 1e-15 ether, - 18, - "Initiatives should have received the interest from Governance" - ); - - uint256 numGauges; - uint256 maxGaugeDuration; - - for (uint256 i = 0; i < initiatives.length; ++i) { - if (address(initiatives[i].gauge) != address(0)) { - maxGaugeDuration = Math.max(maxGaugeDuration, CurveV2GaugeRewards(initiatives[i].addr).duration()); - ++numGauges; - } - } - - skip(maxGaugeDuration); - - if (numGauges > 0) { - uint256 gaugeShareOfInterest; - - for (uint256 i = 0; i < initiatives.length; ++i) { - if (address(initiatives[i].gauge) != address(0)) { - gaugeShareOfInterest += boldToken.balanceOf(address(initiatives[i].gauge)); - _claimRewardsFromCurveGauge(liquidityProvider, initiatives[i].gauge); - } - } - - assertApproxEqRelDecimal( - boldToken.balanceOf(liquidityProvider), - gaugeShareOfInterest, - 1e-13 ether, - 18, - "Liquidity provider should have earned the rewards from the Curve gauges" - ); - } - } - } - - // This can be used to check that everything's still working as expected in a live testnet deployment - function test_Borrowing_InExistingDeployment() external { - for (uint256 i = 0; i < branches.length; ++i) { - vm.skip(branches[i].troveManager.getTroveIdsCount() == 0); - } - - address borrower = makeAddr("borrower"); - - for (uint256 i = 0; i < branches.length; ++i) { - _openTrove(i, borrower, 0, 10_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - _closeTroveFromCollateral(i, borrower, 0, false); - } - - address leverageSeeker = makeAddr("leverageSeeker"); - - for (uint256 i = 0; i < branches.length; ++i) { - _openLeveragedTrove(i, leverageSeeker, 0, 10_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - _leverUpTrove(i, leverageSeeker, 0, 1_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - _leverDownTrove(i, leverageSeeker, 0, 1_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - _closeTroveFromCollateral(i, leverageSeeker, 0, true); - } - } - - function test_ManagerOfCurveGauge_UnlessRenounced_CanReassignRewardDistributor() external { - vm.skip(address(curveUsdcBoldGauge) == address(0)); - - address manager = curveUsdcBoldGauge.manager(); - vm.skip(manager == address(0)); - vm.label(manager, "manager"); - - address newRewardDistributor = makeAddr("newRewardDistributor"); - uint256 rewardAmount = 10_000 ether; - _openTrove(0, newRewardDistributor, 0, rewardAmount); - - vm.startPrank(newRewardDistributor); - boldToken.approve(address(curveUsdcBoldGauge), rewardAmount); - vm.expectRevert(); - curveUsdcBoldGauge.deposit_reward_token(BOLD, rewardAmount, 7 days); - vm.stopPrank(); - - vm.prank(manager); - curveUsdcBoldGauge.set_reward_distributor(BOLD, newRewardDistributor); - - vm.startPrank(newRewardDistributor); - curveUsdcBoldGauge.deposit_reward_token(BOLD, rewardAmount, 7 days); - vm.stopPrank(); - } -} diff --git a/contracts/test/InitiativeSmarDEX.t.sol b/contracts/test/InitiativeSmarDEX.t.sol deleted file mode 100644 index b01b1d60e..000000000 --- a/contracts/test/InitiativeSmarDEX.t.sol +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import "./Utils/E2EHelpers.sol"; -import "forge-std/console2.sol"; - -address constant INITIATIVE_ADDRESS = 0xa58FBe38AAB33b9fadEf1B5Aaff4D7bC27C43aD4; -address constant GOVERNANCE_WHALE = 0xF30da4E4e7e20Dbf5fBE9adCD8699075D62C60A4; - -// Start an anvil node or set up a proper RPC URL -// FOUNDRY_PROFILE=e2e E2E_RPC_URL="http://localhost:8545" forge test --mc InitiativeSmarDEX -vvv - -contract InitiativeSmarDEX is E2EHelpers { - address public NEW_LQTY_WHALE = 0xF977814e90dA44bFA03b6295A0616a897441aceC; - - function setUp() public override { - super.setUp(); - vm.label(NEW_LQTY_WHALE, "LQTY_WHALE"); - providerOf[LQTY] = NEW_LQTY_WHALE; - } - - function testInitiativeRegistrationAndClaim() external { - address borrower = makeAddr("borrower"); - address registrant = GOVERNANCE_WHALE; - - // Open trove and transfer BOLD to registrant - uint256 donationAmount = 10_000 ether; - _openTrove(0, borrower, 0, Math.max(REGISTRATION_FEE, MIN_DEBT) + donationAmount); - vm.startPrank(borrower); - boldToken.transfer(registrant, REGISTRATION_FEE); - vm.stopPrank(); - - // Stake LQTY and accumulate voting power - address staker = makeAddr("staker"); - uint256 lqtyStake = 3_000_000 ether; - _depositLQTY(staker, lqtyStake); - - skip(30 days); - - assertEq(governance.registeredInitiatives(INITIATIVE_ADDRESS), 0, "Initiative should not be registered"); - - // Register initiative - vm.startPrank(registrant); - boldToken.approve(address(governance), REGISTRATION_FEE); - governance.registerInitiative(INITIATIVE_ADDRESS); - vm.stopPrank(); - - assertGt(governance.registeredInitiatives(INITIATIVE_ADDRESS), 0, "Initiative should be registered"); - - skip(7 days); - - // Allocate to initiative - _allocateLQTY_begin(staker); - _allocateLQTY_vote(INITIATIVE_ADDRESS, int256(lqtyStake)); // TODO - _allocateLQTY_end(); - - // Donate - vm.startPrank(borrower); - boldToken.transfer(address(governance), donationAmount); - vm.stopPrank(); - - skip(7 days); - - /* - console2.log(boldToken.balanceOf(address(governance)), "boldToken.balanceOf(address(governance))"); - console2.log(governance.getLatestVotingThreshold(), "voting threshold"); - (Governance.VoteSnapshot memory voteSnapshot, Governance.InitiativeVoteSnapshot memory initiativeVoteSnapshot) = - governance.snapshotVotesForInitiative(INITIATIVE_ADDRESS); - console2.log(initiativeVoteSnapshot.votes, "initiative Votes"); - (Governance.InitiativeStatus status, uint256 lastEpochClaim, uint256 claimableAmount) = - governance.getInitiativeState(INITIATIVE_ADDRESS); - console2.log(uint256(status), "uint(status)"); - console2.log(lastEpochClaim, "lastEpochClaim"); - console2.log(claimableAmount, "claimableAmount"); - */ - // Claim for initiative - governance.claimForInitiative(INITIATIVE_ADDRESS); - //console2.log(boldToken.balanceOf(INITIATIVE_ADDRESS), "boldToken.balanceOf(INITIATIVE_ADDRESS)"); - assertGt(boldToken.balanceOf(INITIATIVE_ADDRESS), 0, "Initiative should have received incentives"); - } -} diff --git a/contracts/test/Invariants.t.sol b/contracts/test/Invariants.t.sol index 15ff652fb..6269f2718 100644 --- a/contracts/test/Invariants.t.sol +++ b/contracts/test/Invariants.t.sol @@ -86,7 +86,7 @@ contract InvariantsTest is Assertions, Logging, BaseInvariantTest, BaseMultiColl TestDeployer deployer = new TestDeployer(); Contracts memory contracts; - (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth,) + (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth) = deployer.deployAndConnectContractsMultiColl(p); setupContracts(contracts); diff --git a/contracts/test/SPInvariants.t.sol b/contracts/test/SPInvariants.t.sol index 310401b97..250ef549e 100644 --- a/contracts/test/SPInvariants.t.sol +++ b/contracts/test/SPInvariants.t.sol @@ -17,7 +17,7 @@ abstract contract SPInvariantsBase is Assertions, BaseInvariantTest { super.setUp(); TestDeployer deployer = new TestDeployer(); - (TestDeployer.LiquityContractsDev memory contracts,, IBoldToken boldToken, HintHelpers hintHelpers,,,) = + (TestDeployer.LiquityContractsDev memory contracts,, IBoldToken boldToken, HintHelpers hintHelpers,,) = deployer.deployAndConnectContracts(); stabilityPool = contracts.stabilityPool; diff --git a/contracts/test/TestContracts/BaseMultiCollateralTest.sol b/contracts/test/TestContracts/BaseMultiCollateralTest.sol index b7c1b1d48..6824306aa 100644 --- a/contracts/test/TestContracts/BaseMultiCollateralTest.sol +++ b/contracts/test/TestContracts/BaseMultiCollateralTest.sol @@ -6,6 +6,7 @@ import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; import {ICollateralRegistry} from "src/Interfaces/ICollateralRegistry.sol"; import {IWETH} from "src/Interfaces/IWETH.sol"; import {HintHelpers} from "src/HintHelpers.sol"; +import {MultiTroveGetter} from "src/MultiTroveGetter.sol"; import {TestDeployer} from "./Deployment.t.sol"; contract BaseMultiCollateralTest { @@ -14,6 +15,7 @@ contract BaseMultiCollateralTest { ICollateralRegistry collateralRegistry; IBoldToken boldToken; HintHelpers hintHelpers; + MultiTroveGetter multiTroveGetter; TestDeployer.LiquityContractsDev[] branches; } diff --git a/contracts/test/TestContracts/BaseTest.sol b/contracts/test/TestContracts/BaseTest.sol index 75708d222..507878b04 100644 --- a/contracts/test/TestContracts/BaseTest.sol +++ b/contracts/test/TestContracts/BaseTest.sol @@ -16,9 +16,6 @@ import "./PriceFeedTestnet.sol"; import "src/Interfaces/IInterestRouter.sol"; import "src/GasPool.sol"; import "src/HintHelpers.sol"; -import "src/Zappers/WETHZapper.sol"; -import "src/Zappers/GasCompZapper.sol"; -import "src/Zappers/LeverageLSTZapper.sol"; import {mulDivCeil} from "../Utils/Math.sol"; import {Logging} from "../Utils/Logging.sol"; import {StringFormatting} from "../Utils/StringFormatting.sol"; @@ -55,10 +52,6 @@ contract BaseTest is TestAccounts, Logging, TroveId { IERC20 collToken; HintHelpers hintHelpers; IWETH WETH; // used for gas compensation - WETHZapper wethZapper; - GasCompZapper gasCompZapper; - ILeverageZapper leverageZapperCurve; - ILeverageZapper leverageZapperUniV3; // Structs for use in test where we need to bi-pass "stack-too-deep" errors struct ABCDEF { diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index 27425e7f0..1f591e905 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -22,22 +22,7 @@ import "src/CollateralRegistry.sol"; import "./MockInterestRouter.sol"; import "./PriceFeedTestnet.sol"; import "./MetadataDeployment.sol"; -import "src/Zappers/WETHZapper.sol"; -import "src/Zappers/GasCompZapper.sol"; -import "src/Zappers/LeverageLSTZapper.sol"; -import "src/Zappers/LeverageWETHZapper.sol"; -import "src/Zappers/Modules/FlashLoans/BalancerFlashLoan.sol"; -import "src/Zappers/Interfaces/IFlashLoanProvider.sol"; -import "src/Zappers/Interfaces/IExchange.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurveFactory.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGFactory.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurvePool.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGPool.sol"; -import "src/Zappers/Modules/Exchanges/CurveExchange.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol"; -import "src/Zappers/Modules/Exchanges/UniV3Exchange.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol"; -import "src/Zappers/Modules/Exchanges/HybridCurveUniV3Exchange.sol"; + import {WETHTester} from "./WETHTester.sol"; import {ERC20Faucet} from "./ERC20Faucet.sol"; @@ -55,22 +40,6 @@ contract TestDeployer is MetadataDeployment { IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); IWETH constant WETH_MAINNET = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - // Curve - ICurveFactory constant curveFactory = ICurveFactory(0x98EE851a00abeE0d95D08cF4CA2BdCE32aeaAF7F); - ICurveStableswapNGFactory constant curveStableswapFactory = - ICurveStableswapNGFactory(0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf); - uint128 constant BOLD_TOKEN_INDEX = 0; - uint256 constant COLL_TOKEN_INDEX = 1; - uint128 constant USDC_INDEX = 1; - - // UniV3 - ISwapRouter constant uniV3Router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - INonfungiblePositionManager constant uniV3PositionManager = - INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); - uint24 constant UNIV3_FEE = 3000; // 0.3% - uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05% - uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01% - bytes32 constant SALT = keccak256("LiquityV2"); struct LiquityContractsDevPools { @@ -109,14 +78,6 @@ contract TestDeployer is MetadataDeployment { IERC20Metadata collToken; } - struct Zappers { - WETHZapper wethZapper; - GasCompZapper gasCompZapper; - ILeverageZapper leverageZapperCurve; - ILeverageZapper leverageZapperUniV3; - ILeverageZapper leverageZapperHybrid; - } - struct LiquityContractAddresses { address activePool; address borrowerOperations; @@ -158,7 +119,6 @@ contract TestDeployer is MetadataDeployment { IBoldToken boldToken; HintHelpers hintHelpers; MultiTroveGetter multiTroveGetter; - Zappers[] zappersArray; } struct DeploymentVarsMainnet { @@ -179,12 +139,11 @@ contract TestDeployer is MetadataDeployment { IPriceFeed priceFeed; IBoldToken boldToken; ICollateralRegistry collateralRegistry; - IWETH weth; + IERC20Metadata gasToken; IAddressesRegistry addressesRegistry; address troveManagerAddress; IHintHelpers hintHelpers; IMultiTroveGetter multiTroveGetter; - ICurveStableswapNGPool usdcCurvePool; } struct ExternalAddresses { @@ -206,6 +165,10 @@ contract TestDeployer is MetadataDeployment { return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry)); } + function getBytecode(bytes memory _creationCode, bool _disable) public pure returns (bytes memory) { + return abi.encodePacked(_creationCode, abi.encode(_disable)); + } + function getAddress(address _deployer, bytes memory _bytecode, bytes32 _salt) public pure returns (address) { bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), _deployer, _salt, keccak256(_bytecode))); @@ -221,8 +184,7 @@ contract TestDeployer is MetadataDeployment { IBoldToken boldToken, HintHelpers hintHelpers, MultiTroveGetter multiTroveGetter, - IWETH WETH, // for gas compensation - Zappers memory zappers + IWETH WETH // for gas compensation ) { return deployAndConnectContracts(TroveManagerParams(150e16, 110e16, 10e16, 110e16, 5e16, 10e16)); @@ -236,20 +198,17 @@ contract TestDeployer is MetadataDeployment { IBoldToken boldToken, HintHelpers hintHelpers, MultiTroveGetter multiTroveGetter, - IWETH WETH, // for gas compensation - Zappers memory zappers + IWETH WETH // for gas compensation ) { LiquityContractsDev[] memory contractsArray; TroveManagerParams[] memory troveManagerParamsArray = new TroveManagerParams[](1); - Zappers[] memory zappersArray; troveManagerParamsArray[0] = troveManagerParams; - (contractsArray, collateralRegistry, boldToken, hintHelpers, multiTroveGetter, WETH, zappersArray) = + (contractsArray, collateralRegistry, boldToken, hintHelpers, multiTroveGetter, WETH) = deployAndConnectContractsMultiColl(troveManagerParamsArray); contracts = contractsArray[0]; - zappers = zappersArray[0]; } function deployAndConnectContractsMultiColl(TroveManagerParams[] memory troveManagerParamsArray) @@ -260,8 +219,7 @@ contract TestDeployer is MetadataDeployment { IBoldToken boldToken, HintHelpers hintHelpers, MultiTroveGetter multiTroveGetter, - IWETH WETH, // for gas compensation - Zappers[] memory zappersArray + IWETH WETH // for gas compensation ) { // used for gas compensation and as collateral of the first branch @@ -269,7 +227,7 @@ contract TestDeployer is MetadataDeployment { 100 ether, // _tapAmount 1 days // _tapPeriod ); - (contractsArray, collateralRegistry, boldToken, hintHelpers, multiTroveGetter, zappersArray) = + (contractsArray, collateralRegistry, boldToken, hintHelpers, multiTroveGetter) = deployAndConnectContracts(troveManagerParamsArray, WETH); } @@ -292,8 +250,7 @@ contract TestDeployer is MetadataDeployment { ICollateralRegistry collateralRegistry, IBoldToken boldToken, HintHelpers hintHelpers, - MultiTroveGetter multiTroveGetter, - Zappers[] memory zappersArray + MultiTroveGetter multiTroveGetter ) { DeploymentVarsDev memory vars; @@ -305,7 +262,6 @@ contract TestDeployer is MetadataDeployment { assert(address(boldToken) == vars.boldTokenAddress); contractsArray = new LiquityContractsDev[](vars.numCollaterals); - zappersArray = new Zappers[](vars.numCollaterals); vars.collaterals = new IERC20Metadata[](vars.numCollaterals); vars.addressesRegistries = new IAddressesRegistry[](vars.numCollaterals); vars.troveManagers = new ITroveManager[](vars.numCollaterals); @@ -334,7 +290,7 @@ contract TestDeployer is MetadataDeployment { hintHelpers = new HintHelpers(collateralRegistry); multiTroveGetter = new MultiTroveGetter(collateralRegistry); - (contractsArray[0], zappersArray[0]) = _deployAndConnectCollateralContractsDev( + contractsArray[0] = _deployAndConnectCollateralContractsDev( _WETH, boldToken, collateralRegistry, @@ -347,7 +303,7 @@ contract TestDeployer is MetadataDeployment { // Deploy the remaining branches with LST collateral for (vars.i = 1; vars.i < vars.numCollaterals; vars.i++) { - (contractsArray[vars.i], zappersArray[vars.i]) = _deployAndConnectCollateralContractsDev( + contractsArray[vars.i] = _deployAndConnectCollateralContractsDev( vars.collaterals[vars.i], boldToken, collateralRegistry, @@ -386,12 +342,12 @@ contract TestDeployer is MetadataDeployment { IERC20Metadata _collToken, IBoldToken _boldToken, ICollateralRegistry _collateralRegistry, - IWETH _weth, + IERC20Metadata _gasToken, IAddressesRegistry _addressesRegistry, address _troveManagerAddress, IHintHelpers _hintHelpers, IMultiTroveGetter _multiTroveGetter - ) internal returns (LiquityContractsDev memory contracts, Zappers memory zappers) { + ) internal returns (LiquityContractsDev memory contracts) { LiquityContractAddresses memory addresses; contracts.collToken = _collToken; @@ -417,9 +373,9 @@ contract TestDeployer is MetadataDeployment { addresses.troveNFT = getAddress( address(this), getBytecode(type(TroveNFT).creationCode, address(contracts.addressesRegistry)), SALT ); - addresses.stabilityPool = getAddress( - address(this), getBytecode(type(StabilityPool).creationCode, address(contracts.addressesRegistry)), SALT - ); + bytes32 stabilityPoolSalt = keccak256(abi.encodePacked(address(contracts.addressesRegistry))); + addresses.stabilityPool = + getAddress(address(this), getBytecode(type(StabilityPool).creationCode, bool(false)), stabilityPoolSalt); addresses.activePool = getAddress( address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry)), SALT ); @@ -438,7 +394,6 @@ contract TestDeployer is MetadataDeployment { // Deploy contracts IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ - collToken: _collToken, borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), troveManager: ITroveManager(addresses.troveManager), troveNFT: ITroveNFT(addresses.troveNFT), @@ -455,14 +410,15 @@ contract TestDeployer is MetadataDeployment { multiTroveGetter: _multiTroveGetter, collateralRegistry: _collateralRegistry, boldToken: _boldToken, - WETH: _weth + collToken: _collToken, + gasToken: _gasToken }); contracts.addressesRegistry.setAddresses(addressVars); contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry); contracts.troveManager = new TroveManagerTester{salt: SALT}(contracts.addressesRegistry); contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); - contracts.stabilityPool = new StabilityPool{salt: SALT}(contracts.addressesRegistry); + contracts.stabilityPool = new StabilityPool{salt: stabilityPoolSalt}(false); contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry); contracts.pools.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); contracts.pools.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); @@ -479,6 +435,8 @@ contract TestDeployer is MetadataDeployment { assert(address(contracts.pools.collSurplusPool) == addresses.collSurplusPool); assert(address(contracts.sortedTroves) == addresses.sortedTroves); + contracts.stabilityPool.initialize(contracts.addressesRegistry); + // Connect contracts _boldToken.setBranchAddresses( address(contracts.troveManager), @@ -486,18 +444,6 @@ contract TestDeployer is MetadataDeployment { address(contracts.borrowerOperations), address(contracts.activePool) ); - - // deploy zappers - _deployZappers( - contracts.addressesRegistry, - contracts.collToken, - _boldToken, - _weth, - contracts.priceFeed, - ICurveStableswapNGPool(address(0)), - false, - zappers - ); } // Creates individual PriceFeed contracts based on oracle addresses. @@ -523,7 +469,6 @@ contract TestDeployer is MetadataDeployment { // Colls: WETH, WSTETH, RETH vars.numCollaterals = 3; result.contractsArray = new LiquityContracts[](vars.numCollaterals); - result.zappersArray = new Zappers[](vars.numCollaterals); vars.collaterals = new IERC20Metadata[](vars.numCollaterals); vars.addressesRegistries = new IAddressesRegistry[](vars.numCollaterals); vars.troveManagers = new ITroveManager[](vars.numCollaterals); @@ -560,8 +505,6 @@ contract TestDeployer is MetadataDeployment { result.hintHelpers = new HintHelpers(result.collateralRegistry); result.multiTroveGetter = new MultiTroveGetter(result.collateralRegistry); - ICurveStableswapNGPool usdcCurvePool = _deployCurveBoldUsdcPool(result.boldToken, true); - // Deploy each set of core contracts for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { DeploymentParamsMainnet memory params; @@ -569,13 +512,12 @@ contract TestDeployer is MetadataDeployment { params.collToken = vars.collaterals[vars.i]; params.boldToken = result.boldToken; params.collateralRegistry = result.collateralRegistry; - params.weth = WETH; + params.gasToken = vars.collaterals[0]; params.addressesRegistry = vars.addressesRegistries[vars.i]; params.troveManagerAddress = address(vars.troveManagers[vars.i]); params.hintHelpers = result.hintHelpers; params.multiTroveGetter = result.multiTroveGetter; - params.usdcCurvePool = usdcCurvePool; - (result.contractsArray[vars.i], result.zappersArray[vars.i]) = + result.contractsArray[vars.i] = _deployAndConnectCollateralContractsMainnet(params, result.externalAddresses, vars.oracleParams); } @@ -605,7 +547,7 @@ contract TestDeployer is MetadataDeployment { DeploymentParamsMainnet memory _params, ExternalAddresses memory _externalAddresses, OracleParams memory _oracleParams - ) internal returns (LiquityContracts memory contracts, Zappers memory zappers) { + ) internal returns (LiquityContracts memory contracts) { LiquityContractAddresses memory addresses; contracts.collToken = _params.collToken; contracts.interestRouter = new MockInterestRouter(); @@ -629,9 +571,7 @@ contract TestDeployer is MetadataDeployment { addresses.troveNFT = getAddress( address(this), getBytecode(type(TroveNFT).creationCode, address(contracts.addressesRegistry)), SALT ); - addresses.stabilityPool = getAddress( - address(this), getBytecode(type(StabilityPool).creationCode, address(contracts.addressesRegistry)), SALT - ); + addresses.stabilityPool = getAddress(address(this), getBytecode(type(StabilityPool).creationCode, false), SALT); addresses.activePool = getAddress( address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry)), SALT ); @@ -653,7 +593,6 @@ contract TestDeployer is MetadataDeployment { // Deploy contracts IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ - collToken: _params.collToken, borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), troveManager: ITroveManager(addresses.troveManager), troveNFT: ITroveNFT(addresses.troveNFT), @@ -670,14 +609,15 @@ contract TestDeployer is MetadataDeployment { multiTroveGetter: _params.multiTroveGetter, collateralRegistry: _params.collateralRegistry, boldToken: _params.boldToken, - WETH: _params.weth + collToken: _params.collToken, + gasToken: _params.gasToken }); contracts.addressesRegistry.setAddresses(addressVars); contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry); contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry); contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); - contracts.stabilityPool = new StabilityPool{salt: SALT}(contracts.addressesRegistry); + contracts.stabilityPool = new StabilityPool{salt: SALT}(false); contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry); contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); @@ -701,18 +641,6 @@ contract TestDeployer is MetadataDeployment { address(contracts.borrowerOperations), address(contracts.activePool) ); - - // deploy zappers - _deployZappers( - contracts.addressesRegistry, - contracts.collToken, - _params.boldToken, - _params.weth, - contracts.priceFeed, - _params.usdcCurvePool, - true, - zappers - ); } function _deployPriceFeed( @@ -750,226 +678,4 @@ contract TestDeployer is MetadataDeployment { _borrowerOperationsAddress ); } - - function _deployZappers( - IAddressesRegistry _addressesRegistry, - IERC20 _collToken, - IBoldToken _boldToken, - IWETH _weth, - IPriceFeed _priceFeed, - ICurveStableswapNGPool _usdcCurvePool, - bool _mainnet, - Zappers memory zappers // result - ) internal { - IFlashLoanProvider flashLoanProvider = new BalancerFlashLoan(); - IExchange curveExchange = _deployCurveExchange(_collToken, _boldToken, _priceFeed, _mainnet); - - // TODO: Deploy base zappers versions with Uni V3 exchange - bool lst = _collToken != _weth; - if (lst) { - zappers.gasCompZapper = new GasCompZapper(_addressesRegistry, flashLoanProvider, curveExchange); - } else { - zappers.wethZapper = new WETHZapper(_addressesRegistry, flashLoanProvider, curveExchange); - } - - if (_mainnet) { - _deployLeverageZappers( - _addressesRegistry, - _collToken, - _boldToken, - _priceFeed, - flashLoanProvider, - curveExchange, - _usdcCurvePool, - lst, - zappers - ); - } - } - - function _deployCurveExchange(IERC20 _collToken, IBoldToken _boldToken, IPriceFeed _priceFeed, bool _mainnet) - internal - returns (IExchange) - { - if (!_mainnet) return new CurveExchange(_collToken, _boldToken, ICurvePool(address(0)), 1, 0); - - (uint256 price,) = _priceFeed.fetchPrice(); - - // deploy Curve Twocrypto NG pool - address[2] memory coins; - coins[BOLD_TOKEN_INDEX] = address(_boldToken); - coins[COLL_TOKEN_INDEX] = address(_collToken); - ICurvePool curvePool = curveFactory.deploy_pool( - "LST-Bold pool", - "LBLD", - coins, - 0, // implementation id - 400000, // A - 145000000000000, // gamma - 26000000, // mid_fee - 45000000, // out_fee - 230000000000000, // fee_gamma - 2000000000000, // allowed_extra_profit - 146000000000000, // adjustment_step - 600, // ma_exp_time - price // initial_price - ); - - IExchange curveExchange = new CurveExchange(_collToken, _boldToken, curvePool, 1, 0); - - return curveExchange; - } - - function _deployLeverageZappers( - IAddressesRegistry _addressesRegistry, - IERC20 _collToken, - IBoldToken _boldToken, - IPriceFeed _priceFeed, - IFlashLoanProvider _flashLoanProvider, - IExchange _curveExchange, - ICurveStableswapNGPool _usdcCurvePool, - bool _lst, - Zappers memory zappers // result - ) internal { - zappers.leverageZapperCurve = - _deployCurveLeverageZapper(_addressesRegistry, _flashLoanProvider, _curveExchange, _lst); - zappers.leverageZapperUniV3 = - _deployUniV3LeverageZapper(_addressesRegistry, _collToken, _boldToken, _priceFeed, _flashLoanProvider, _lst); - zappers.leverageZapperHybrid = _deployHybridLeverageZapper( - _addressesRegistry, _collToken, _boldToken, _flashLoanProvider, _usdcCurvePool, _lst - ); - } - - function _deployCurveLeverageZapper( - IAddressesRegistry _addressesRegistry, - IFlashLoanProvider _flashLoanProvider, - IExchange _curveExchange, - bool _lst - ) internal returns (ILeverageZapper) { - ILeverageZapper leverageZapperCurve; - if (_lst) { - leverageZapperCurve = new LeverageLSTZapper(_addressesRegistry, _flashLoanProvider, _curveExchange); - } else { - leverageZapperCurve = new LeverageWETHZapper(_addressesRegistry, _flashLoanProvider, _curveExchange); - } - - return leverageZapperCurve; - } - - struct UniV3Vars { - IExchange uniV3Exchange; - uint256 price; - address[2] tokens; - } - - function _deployUniV3LeverageZapper( - IAddressesRegistry _addressesRegistry, - IERC20 _collToken, - IBoldToken _boldToken, - IPriceFeed _priceFeed, - IFlashLoanProvider _flashLoanProvider, - bool _lst - ) internal returns (ILeverageZapper) { - UniV3Vars memory vars; - vars.uniV3Exchange = new UniV3Exchange(_collToken, _boldToken, UNIV3_FEE, uniV3Router); - ILeverageZapper leverageZapperUniV3; - if (_lst) { - leverageZapperUniV3 = new LeverageLSTZapper(_addressesRegistry, _flashLoanProvider, vars.uniV3Exchange); - } else { - leverageZapperUniV3 = new LeverageWETHZapper(_addressesRegistry, _flashLoanProvider, vars.uniV3Exchange); - } - - // Create Uni V3 pool - (vars.price,) = _priceFeed.fetchPrice(); - if (address(_boldToken) < address(_collToken)) { - //console2.log("b < c"); - vars.tokens[0] = address(_boldToken); - vars.tokens[1] = address(_collToken); - } else { - //console2.log("c < b"); - vars.tokens[0] = address(_collToken); - vars.tokens[1] = address(_boldToken); - } - uniV3PositionManager.createAndInitializePoolIfNecessary( - vars.tokens[0], // token0, - vars.tokens[1], // token1, - UNIV3_FEE, // fee, - UniV3Exchange(address(vars.uniV3Exchange)).priceToSqrtPrice(_boldToken, _collToken, vars.price) // sqrtPriceX96 - ); - - return leverageZapperUniV3; - } - - function _deployHybridLeverageZapper( - IAddressesRegistry _addressesRegistry, - IERC20 _collToken, - IBoldToken _boldToken, - IFlashLoanProvider _flashLoanProvider, - ICurveStableswapNGPool _usdcCurvePool, - bool _lst - ) internal returns (ILeverageZapper) { - IExchange hybridExchange = new HybridCurveUniV3Exchange( - _collToken, - _boldToken, - USDC, - WETH_MAINNET, - _usdcCurvePool, - USDC_INDEX, // USDC Curve pool index - BOLD_TOKEN_INDEX, // BOLD Curve pool index - UNIV3_FEE_USDC_WETH, - UNIV3_FEE_WETH_COLL, - uniV3Router - ); - - ILeverageZapper leverageZapperHybrid; - if (_lst) { - leverageZapperHybrid = new LeverageLSTZapper(_addressesRegistry, _flashLoanProvider, hybridExchange); - } else { - leverageZapperHybrid = new LeverageWETHZapper(_addressesRegistry, _flashLoanProvider, hybridExchange); - } - - return leverageZapperHybrid; - } - - function _deployCurveBoldUsdcPool(IBoldToken _boldToken, bool _mainnet) internal returns (ICurveStableswapNGPool) { - if (!_mainnet) return ICurveStableswapNGPool(address(0)); - - // deploy Curve Stableswap pool - /* - address[2] memory coins; - coins[BOLD_TOKEN_INDEX] = address(_boldToken); - coins[USDC_INDEX] = address(USDC); - ICurvePool curvePool = curveStableswapFactory.deploy_plain_pool( - "USDC-Bold pool", - "USDCBOLD", - coins, - 4000, // A - 0, // asset type: USD - 1000000, // fee - 0 // implementation id - ); - */ - // deploy Curve StableswapNG pool - address[] memory coins = new address[](2); - coins[BOLD_TOKEN_INDEX] = address(_boldToken); - coins[USDC_INDEX] = address(USDC); - uint8[] memory assetTypes = new uint8[](2); // 0: standard - bytes4[] memory methodIds = new bytes4[](2); - address[] memory oracles = new address[](2); - ICurveStableswapNGPool curvePool = curveStableswapFactory.deploy_plain_pool( - "USDC-BOLD", - "USDCBOLD", - coins, - 4000, // A - 1000000, // fee - 20000000000, // _offpeg_fee_multiplier - 865, // _ma_exp_time - 0, // implementation id - assetTypes, - methodIds, - oracles - ); - - return curvePool; - } } diff --git a/contracts/test/TestContracts/DevTestSetup.sol b/contracts/test/TestContracts/DevTestSetup.sol index 59194d4b6..83cc2bd9c 100644 --- a/contracts/test/TestContracts/DevTestSetup.sol +++ b/contracts/test/TestContracts/DevTestSetup.sol @@ -49,8 +49,7 @@ contract DevTestSetup is BaseTest { TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev memory contracts; - TestDeployer.Zappers memory zappers; - (contracts, collateralRegistry, boldToken, hintHelpers,, WETH, zappers) = deployer.deployAndConnectContracts(); + (contracts, collateralRegistry, boldToken, hintHelpers,, WETH) = deployer.deployAndConnectContracts(); addressesRegistry = contracts.addressesRegistry; collToken = contracts.collToken; activePool = contracts.activePool; @@ -65,10 +64,6 @@ contract DevTestSetup is BaseTest { troveNFT = contracts.troveNFT; metadataNFT = addressesRegistry.metadataNFT(); mockInterestRouter = contracts.interestRouter; - wethZapper = zappers.wethZapper; - gasCompZapper = zappers.gasCompZapper; - leverageZapperCurve = zappers.leverageZapperCurve; - leverageZapperUniV3 = zappers.leverageZapperUniV3; // Give some Coll to test accounts, and approve it to BorrowerOperations uint256 initialCollAmount = 10_000_000_000e18; diff --git a/contracts/test/Utils/E2EHelpers.sol b/contracts/test/Utils/E2EHelpers.sol deleted file mode 100644 index f9026c135..000000000 --- a/contracts/test/Utils/E2EHelpers.sol +++ /dev/null @@ -1,339 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {Vm} from "forge-std/Vm.sol"; -import {Test} from "forge-std/Test.sol"; -import {IERC20Metadata as IERC20} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import {Math} from "openzeppelin-contracts/contracts/utils/math/Math.sol"; - -import {Governance} from "V2-gov/src/Governance.sol"; -import {ILeverageZapper} from "src/Zappers/Interfaces/ILeverageZapper.sol"; -import {IZapper} from "src/Zappers/Interfaces/IZapper.sol"; -import {IPriceFeed} from "src/Interfaces/IPriceFeed.sol"; -import {IBorrowerOperationsV1} from "../Interfaces/LiquityV1/IBorrowerOperationsV1.sol"; -import {IPriceFeedV1} from "../Interfaces/LiquityV1/IPriceFeedV1.sol"; -import {ISortedTrovesV1} from "../Interfaces/LiquityV1/ISortedTrovesV1.sol"; -import {ITroveManagerV1} from "../Interfaces/LiquityV1/ITroveManagerV1.sol"; -import {ERC20Faucet} from "../TestContracts/ERC20Faucet.sol"; - -import {StringEquality} from "./StringEquality.sol"; -import {UseDeployment} from "./UseDeployment.sol"; -import {TroveId} from "./TroveId.sol"; - -uint256 constant PRICE_TOLERANCE = 0.05 ether; - -address constant ETH_WHALE = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; // Anvil account #1 -address constant WETH_WHALE = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; // Anvil account #2 -address constant WSTETH_WHALE = 0xd85351181b3F264ee0FDFa94518464d7c3DefaDa; -address constant RETH_WHALE = 0xE76af4a9A3E71681F4C9BE600A0BA8D9d249175b; -address constant USDC_WHALE = 0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341; -address constant LQTY_WHALE = 0xA78f19D7f331247212C6d9C0F27D3d9464D3604D; -address constant LUSD_WHALE = 0xcd6Eb888e76450eF584E8B51bB73c76ffBa21FF2; - -IBorrowerOperationsV1 constant mainnet_V1_borrowerOperations = - IBorrowerOperationsV1(0x24179CD81c9e782A4096035f7eC97fB8B783e007); -IPriceFeedV1 constant mainnet_V1_priceFeed = IPriceFeedV1(0x4c517D4e2C851CA76d7eC94B805269Df0f2201De); -ISortedTrovesV1 constant mainnet_V1_sortedTroves = ISortedTrovesV1(0x8FdD3fbFEb32b28fb73555518f8b361bCeA741A6); -ITroveManagerV1 constant mainnet_V1_troveManager = ITroveManagerV1(0xA39739EF8b0231DbFA0DcdA07d7e29faAbCf4bb2); - -contract SideEffectFreeGetPriceHelper { - function _revert(bytes memory revertData) internal pure { - assembly { - revert(add(32, revertData), mload(revertData)) - } - } - - function throwPrice(IPriceFeed priceFeed) external { - (uint256 price,) = priceFeed.fetchPrice(); - _revert(abi.encode(price)); - } - - function throwPriceV1(IPriceFeedV1 priceFeed) external { - _revert(abi.encode(priceFeed.fetchPrice())); - } -} - -library SideEffectFreeGetPrice { - Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - - // Random address - address private constant helperDeployer = 0x9C82588e2B9229168aDbb55E730e0d20c0581a3B; - - // Deterministic address of first contract deployed by `helperDeployer` - SideEffectFreeGetPriceHelper private constant helper = - SideEffectFreeGetPriceHelper(0xc583097AE39B039fA74bB5bd6479469290B7cDe5); - - function deploy() internal { - if (address(helper).code.length == 0) { - vm.prank(helperDeployer); - new SideEffectFreeGetPriceHelper(); - } - } - - function getPrice(IPriceFeed priceFeed) internal returns (uint256) { - deploy(); - - try helper.throwPrice(priceFeed) { - revert("SideEffectFreeGetPrice: throwPrice() should have reverted"); - } catch (bytes memory revertData) { - return abi.decode(revertData, (uint256)); - } - } - - function getPrice(IPriceFeedV1 priceFeed) internal returns (uint256) { - deploy(); - - try helper.throwPriceV1(priceFeed) { - revert("SideEffectFreeGetPrice: throwPriceV1() should have reverted"); - } catch (bytes memory revertData) { - return abi.decode(revertData, (uint256)); - } - } -} - -contract E2EHelpers is Test, UseDeployment, TroveId { - using SideEffectFreeGetPrice for IPriceFeed; - using StringEquality for string; - - mapping(address token => address) providerOf; - - address[] _allocateLQTY_initiativesToReset; - address[] _allocateLQTY_initiatives; - int256[] _allocateLQTY_votes; - int256[] _allocateLQTY_vetos; - - function setUp() public virtual { - vm.skip(vm.envOr("FOUNDRY_PROFILE", string("")).notEq("e2e")); - vm.createSelectFork(vm.envString("E2E_RPC_URL")); - _loadDeploymentFromManifest("deployment-manifest.json"); - - vm.label(ETH_WHALE, "ETH_WHALE"); - vm.label(WETH_WHALE, "WETH_WHALE"); - vm.label(WSTETH_WHALE, "WSTETH_WHALE"); - vm.label(RETH_WHALE, "RETH_WHALE"); - vm.label(USDC_WHALE, "USDC_WHALE"); - vm.label(LQTY_WHALE, "LQTY_WHALE"); - vm.label(LUSD_WHALE, "LUSD_WHALE"); - - providerOf[WETH] = WETH_WHALE; - providerOf[WSTETH] = WSTETH_WHALE; - providerOf[RETH] = RETH_WHALE; - providerOf[USDC] = USDC_WHALE; - providerOf[LQTY] = LQTY_WHALE; - providerOf[LUSD] = LUSD_WHALE; - - vm.prank(WETH_WHALE); - weth.deposit{value: WETH_WHALE.balance}(); - - // Testnet - if (block.chainid != 1) { - address[5] memory coins = [WSTETH, RETH, USDC, LQTY, LUSD]; - - for (uint256 i = 0; i < coins.length; ++i) { - ERC20Faucet faucet = ERC20Faucet(coins[i]); - vm.prank(faucet.owner()); - faucet.mint(providerOf[coins[i]], 1e6 ether); - } - } - } - - function deal(address to, uint256 give) internal virtual override { - if (to.balance < give) { - vm.prank(ETH_WHALE); - payable(to).transfer(give - to.balance); - } else { - vm.prank(to); - payable(ETH_WHALE).transfer(to.balance - give); - } - } - - function deal(address token, address to, uint256 give) internal virtual override { - uint256 balance = IERC20(token).balanceOf(to); - address provider = providerOf[token]; - - assertNotEq(provider, address(0), string.concat("No provider for ", IERC20(token).symbol())); - - if (balance < give) { - vm.prank(provider); - IERC20(token).transfer(to, give - balance); - } else { - vm.prank(to); - IERC20(token).transfer(provider, balance - give); - } - } - - function deal(address token, address to, uint256 give, bool) internal virtual override { - deal(token, to, give); - } - - function _openTrove(uint256 i, address owner, uint256 ownerIndex, uint256 boldAmount) internal returns (uint256) { - IZapper.OpenTroveParams memory p; - p.owner = owner; - p.ownerIndex = ownerIndex; - p.boldAmount = boldAmount; - p.collAmount = boldAmount * 2 ether / branches[i].priceFeed.getPrice(); - p.annualInterestRate = 0.05 ether; - p.maxUpfrontFee = hintHelpers.predictOpenTroveUpfrontFee(i, boldAmount, p.annualInterestRate); - - (uint256 collTokenAmount, uint256 value) = branches[i].collToken == weth - ? (0, p.collAmount + ETH_GAS_COMPENSATION) - : (p.collAmount, ETH_GAS_COMPENSATION); - - deal(owner, value); - deal(address(branches[i].collToken), owner, collTokenAmount); - - vm.startPrank(owner); - branches[i].collToken.approve(address(branches[i].zapper), collTokenAmount); - branches[i].zapper.openTroveWithRawETH{value: value}(p); - vm.stopPrank(); - - return boldAmount; - } - - function _closeTroveFromCollateral(uint256 i, address owner, uint256 ownerIndex, bool _leveraged) - internal - returns (uint256) - { - IZapper zapper; - if (_leveraged) { - zapper = branches[i].leverageZapper; - } else { - zapper = branches[i].zapper; - } - uint256 troveId = addressToTroveIdThroughZapper(address(zapper), owner, ownerIndex); - uint256 debt = branches[i].troveManager.getLatestTroveData(troveId).entireDebt; - uint256 coll = branches[i].troveManager.getLatestTroveData(troveId).entireColl; - uint256 flashLoanAmount = debt * (1 ether + PRICE_TOLERANCE) / branches[i].priceFeed.getPrice(); - - vm.startPrank(owner); - zapper.closeTroveFromCollateral({ - _troveId: troveId, - _flashLoanAmount: flashLoanAmount, - _minExpectedCollateral: coll - flashLoanAmount - }); - vm.stopPrank(); - - return debt; - } - - function _openLeveragedTrove(uint256 i, address owner, uint256 ownerIndex, uint256 boldAmount) - internal - returns (uint256) - { - uint256 price = branches[i].priceFeed.getPrice(); - - ILeverageZapper.OpenLeveragedTroveParams memory p; - p.owner = owner; - p.ownerIndex = ownerIndex; - p.boldAmount = boldAmount; - p.collAmount = boldAmount * 0.5 ether / price; - p.flashLoanAmount = boldAmount * (1 ether - PRICE_TOLERANCE) / price; - p.annualInterestRate = 0.1 ether; - p.maxUpfrontFee = hintHelpers.predictOpenTroveUpfrontFee(i, boldAmount, p.annualInterestRate); - - (uint256 collTokenAmount, uint256 value) = branches[i].collToken == weth - ? (0, p.collAmount + ETH_GAS_COMPENSATION) - : (p.collAmount, ETH_GAS_COMPENSATION); - - deal(owner, value); - deal(address(branches[i].collToken), owner, collTokenAmount); - - vm.startPrank(owner); - branches[i].collToken.approve(address(branches[i].leverageZapper), collTokenAmount); - branches[i].leverageZapper.openLeveragedTroveWithRawETH{value: value}(p); - vm.stopPrank(); - - return boldAmount; - } - - function _leverUpTrove(uint256 i, address owner, uint256 ownerIndex, uint256 boldAmount) - internal - returns (uint256) - { - uint256 troveId = addressToTroveIdThroughZapper(address(branches[i].leverageZapper), owner, ownerIndex); - - ILeverageZapper.LeverUpTroveParams memory p = ILeverageZapper.LeverUpTroveParams({ - troveId: troveId, - boldAmount: boldAmount, - flashLoanAmount: boldAmount * (1 ether - PRICE_TOLERANCE) / branches[i].priceFeed.getPrice(), - maxUpfrontFee: hintHelpers.predictAdjustTroveUpfrontFee(i, troveId, boldAmount) - }); - - vm.prank(owner); - branches[i].leverageZapper.leverUpTrove(p); - - return boldAmount; - } - - function _leverDownTrove(uint256 i, address owner, uint256 ownerIndex, uint256 boldAmount) - internal - returns (uint256) - { - uint256 troveId = addressToTroveIdThroughZapper(address(branches[i].leverageZapper), owner, ownerIndex); - uint256 debtBefore = branches[i].troveManager.getLatestTroveData(troveId).entireDebt; - - ILeverageZapper.LeverDownTroveParams memory p = ILeverageZapper.LeverDownTroveParams({ - troveId: troveId, - minBoldAmount: boldAmount, - flashLoanAmount: boldAmount * (1 ether + PRICE_TOLERANCE) / branches[i].priceFeed.getPrice() - }); - - vm.prank(owner); - branches[i].leverageZapper.leverDownTrove(p); - - return debtBefore - branches[i].troveManager.getLatestTroveData(troveId).entireDebt; - } - - function _provideToSP(uint256 i, address depositor, uint256 boldAmount) internal { - deal(BOLD, depositor, boldAmount); - vm.prank(depositor); - branches[i].stabilityPool.provideToSP(boldAmount, false); - } - - function _claimFromSP(uint256 i, address depositor) internal { - vm.prank(depositor); - branches[i].stabilityPool.withdrawFromSP(0, true); - } - - function _depositLQTY(address voter, uint256 amount) internal { - deal(LQTY, voter, amount); - - vm.startPrank(voter); - lqty.approve(governance.deriveUserProxyAddress(voter), amount); - governance.depositLQTY(amount); - vm.stopPrank(); - } - - function _allocateLQTY_begin(address voter) internal { - vm.startPrank(voter); - } - - function _allocateLQTY_reset(address initiative) internal { - _allocateLQTY_initiativesToReset.push(initiative); - } - - function _allocateLQTY_vote(address initiative, int256 lqtyAmount) internal { - _allocateLQTY_initiatives.push(initiative); - _allocateLQTY_votes.push(lqtyAmount); - _allocateLQTY_vetos.push(); - } - - function _allocateLQTY_veto(address initiative, int256 lqtyAmount) internal { - _allocateLQTY_initiatives.push(initiative); - _allocateLQTY_votes.push(); - _allocateLQTY_vetos.push(lqtyAmount); - } - - function _allocateLQTY_end() internal { - governance.allocateLQTY( - _allocateLQTY_initiativesToReset, _allocateLQTY_initiatives, _allocateLQTY_votes, _allocateLQTY_vetos - ); - - delete _allocateLQTY_initiativesToReset; - delete _allocateLQTY_initiatives; - delete _allocateLQTY_votes; - delete _allocateLQTY_vetos; - - vm.stopPrank(); - } -} diff --git a/contracts/test/Utils/UniPriceConverterLog.sol b/contracts/test/Utils/UniPriceConverterLog.sol deleted file mode 100644 index f99b8dc57..000000000 --- a/contracts/test/Utils/UniPriceConverterLog.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/utils/math/Math.sol"; - -import {UniPriceConverter} from "src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol"; -import {Logging} from "./Logging.sol"; -import {StringFormatting} from "./StringFormatting.sol"; -import {DECIMAL_PRECISION} from "src/Dependencies/Constants.sol"; - -import "forge-std/console2.sol"; - -contract UniPriceConverterLog is UniPriceConverter, Logging { - using StringFormatting for uint256; - - function priceToSqrtPrice(uint256 _price) public pure returns (uint256, uint256, uint256) { - uint256 sqrtPriceX96 = priceToSqrtPriceX96(_price); - - uint256 inversePrice = DECIMAL_PRECISION * DECIMAL_PRECISION / _price; - uint256 sqrtInversePriceX96 = priceToSqrtPriceX96(inversePrice); - - console2.log(""); - info("Price: ", _price.decimal()); - info("Uni sqrt Price: ", sqrtPriceX96.decimal()); - info("Inverse price: ", inversePrice.decimal()); - info("Uni sqrt inv Price: ", sqrtInversePriceX96.decimal()); - - console2.log("Price: ", _price); - console2.log("Uni sqrt Price: ", sqrtPriceX96); - console2.log("Inverse price: ", inversePrice); - console2.log("Uni sqrt inv Price: ", sqrtInversePriceX96); - - return (sqrtPriceX96, inversePrice, sqrtInversePriceX96); - } - - function sqrtPriceToPrice(uint160 _sqrtPriceX96) public pure returns (uint256 price) { - price = sqrtPriceX96ToPrice(_sqrtPriceX96); - - console2.log(""); - info("Uni sqrt Price: ", uint256(_sqrtPriceX96).decimal()); - info("Price: ", price.decimal()); - console2.log("Uni sqrt Price: ", uint256(_sqrtPriceX96)); - console2.log("Price: ", price); - } -} diff --git a/contracts/test/Utils/UseDeployment.sol b/contracts/test/Utils/UseDeployment.sol index 2c924f13d..5af44fd5f 100644 --- a/contracts/test/Utils/UseDeployment.sol +++ b/contracts/test/Utils/UseDeployment.sol @@ -8,8 +8,6 @@ import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; import {IUserProxy} from "V2-gov/src/interfaces/IUserProxy.sol"; import {CurveV2GaugeRewards} from "V2-gov/src/CurveV2GaugeRewards.sol"; import {Governance} from "V2-gov/src/Governance.sol"; -import {ILeverageZapper} from "src/Zappers/Interfaces/ILeverageZapper.sol"; -import {IZapper} from "src/Zappers/Interfaces/IZapper.sol"; import {IActivePool} from "src/Interfaces/IActivePool.sol"; import {IAddressesRegistry} from "src/Interfaces/IAddressesRegistry.sol"; import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; @@ -47,8 +45,6 @@ contract UseDeployment is CommonBase { IActivePool activePool; IDefaultPool defaultPool; IStabilityPool stabilityPool; - ILeverageZapper leverageZapper; - IZapper zapper; } address WETH; @@ -128,14 +124,7 @@ contract UseDeployment is CommonBase { sortedTroves: ISortedTroves(json.readAddress(string.concat(branch, ".sortedTroves"))), activePool: IActivePool(json.readAddress(string.concat(branch, ".activePool"))), defaultPool: IDefaultPool(json.readAddress(string.concat(branch, ".defaultPool"))), - stabilityPool: IStabilityPool(json.readAddress(string.concat(branch, ".stabilityPool"))), - leverageZapper: ILeverageZapper(json.readAddress(string.concat(branch, ".leverageZapper"))), - zapper: IZapper( - coalesce( - json.readAddress(string.concat(branch, ".wethZapper")), - json.readAddress(string.concat(branch, ".gasCompZapper")) - ) - ) + stabilityPool: IStabilityPool(json.readAddress(string.concat(branch, ".stabilityPool"))) }); vm.label(address(branches[i].priceFeed), "PriceFeed"); @@ -146,8 +135,6 @@ contract UseDeployment is CommonBase { vm.label(address(branches[i].activePool), "ActivePool"); vm.label(address(branches[i].defaultPool), "DefaultPool"); vm.label(address(branches[i].stabilityPool), "StabilityPool"); - vm.label(address(branches[i].leverageZapper), "LeverageZapper"); - vm.label(address(branches[i].zapper), "Zapper"); string memory collSymbol = branches[i].collToken.symbol(); if (collSymbol.eq("WETH")) { diff --git a/contracts/test/liquidationsLST.t.sol b/contracts/test/liquidationsLST.t.sol index 73b6f1361..c5cad93da 100644 --- a/contracts/test/liquidationsLST.t.sol +++ b/contracts/test/liquidationsLST.t.sol @@ -24,7 +24,7 @@ contract LiquidationsLSTTest is DevTestSetup { TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev memory contracts; - (contracts, collateralRegistry, boldToken,,,,) = deployer.deployAndConnectContracts( + (contracts, collateralRegistry, boldToken,,,) = deployer.deployAndConnectContracts( TestDeployer.TroveManagerParams(160e16, 120e16, 10e16, 120e16, 5e16, 10e16) ); collToken = contracts.collToken; diff --git a/contracts/test/multicollateral.t.sol b/contracts/test/multicollateral.t.sol index f7388dbaa..d07a428fd 100644 --- a/contracts/test/multicollateral.t.sol +++ b/contracts/test/multicollateral.t.sol @@ -75,7 +75,7 @@ contract MulticollateralTest is DevTestSetup { TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _contractsArray; - (_contractsArray, collateralRegistry, boldToken,,, WETH,) = + (_contractsArray, collateralRegistry, boldToken,,, WETH) = deployer.deployAndConnectContractsMultiColl(troveManagerParamsArray); // Unimplemented feature (...):Copying of type struct LiquityContracts memory[] memory to storage not yet supported. for (uint256 c = 0; c < NUM_COLLATERALS; c++) { @@ -794,7 +794,7 @@ contract CsBold013 is TestAccounts { TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _branches; - (_branches, collateralRegistry, boldToken, hintHelpers,, weth,) = + (_branches, collateralRegistry, boldToken, hintHelpers,, weth) = deployer.deployAndConnectContractsMultiColl(params); for (uint256 i = 0; i < _branches.length; ++i) { diff --git a/contracts/test/shutdown.t.sol b/contracts/test/shutdown.t.sol index 477dc5bbf..06c97a4a8 100644 --- a/contracts/test/shutdown.t.sol +++ b/contracts/test/shutdown.t.sol @@ -34,7 +34,7 @@ contract ShutdownTest is DevTestSetup { TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _contractsArray; - (_contractsArray, collateralRegistry, boldToken,,, WETH,) = + (_contractsArray, collateralRegistry, boldToken,,, WETH) = deployer.deployAndConnectContractsMultiColl(troveManagerParamsArray); // Unimplemented feature (...):Copying of type struct LiquityContracts memory[] memory to storage not yet supported. for (uint256 c = 0; c < NUM_COLLATERALS; c++) { diff --git a/contracts/test/stabilityPool.t.sol b/contracts/test/stabilityPool.t.sol index cf5738c19..ab60d273a 100644 --- a/contracts/test/stabilityPool.t.sol +++ b/contracts/test/stabilityPool.t.sol @@ -1846,13 +1846,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(10)), // 10th storage slot where P is stored + bytes32(uint256(60)), // 60th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 10 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); - assertEq(storedVal, _cheatP, "value of slot 10 is not set"); + // Confirm that storage slot 60 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(60)))); + assertEq(storedVal, _cheatP, "value of slot 60 is not set"); // Confirm that P specfically is set assertEq(stabilityPool.P(), _cheatP, "P is not set"); @@ -1891,13 +1891,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(10)), // 10th storage slot where P is stored + bytes32(uint256(60)), // 10th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 10 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); - assertEq(storedVal, _cheatP, "value of slot 10 is not set"); + // Confirm that storage slot 60 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(60)))); + assertEq(storedVal, _cheatP, "value of slot 60 is not set"); // Confirm that P specfically is set assertEq(stabilityPool.P(), _cheatP, "P is not set"); @@ -2023,13 +2023,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(10)), // 10th storage slot where P is stored + bytes32(uint256(60)), // 60th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 10 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); - assertEq(storedVal, _cheatP, "value of slot 10 is not set"); + // Confirm that storage slot 60 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(60)))); + assertEq(storedVal, _cheatP, "value of slot 60 is not set"); // Confirm that P specfically is set assertEq(stabilityPool.P(), _cheatP, "P is not set"); @@ -2158,13 +2158,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(10)), // 10th storage slot where P is stored + bytes32(uint256(60)), // 60th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 10 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); - assertEq(storedVal, _cheatP, "value of slot 10 is not set"); + // Confirm that storage slot 60 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(60)))); + assertEq(storedVal, _cheatP, "value of slot 60 is not set"); // Confirm that P specfically is set console2.log(stabilityPool.P(), "stabilityPool.P()"); console2.log(_cheatP, "_cheatP"); @@ -2201,13 +2201,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(10)), // 10th storage slot where P is stored + bytes32(uint256(60)), // 60th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 10 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); - assertEq(storedVal, _cheatP, "value of slot 10 is not set"); + // Confirm that storage slot 60 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(60)))); + assertEq(storedVal, _cheatP, "value of slot 60 is not set"); // Confirm that P specfically is set assertEq(stabilityPool.P(), _cheatP, "P is not set"); diff --git a/contracts/test/troveNFT.t.sol b/contracts/test/troveNFT.t.sol index 5a1287b4d..80eaf5f29 100644 --- a/contracts/test/troveNFT.t.sol +++ b/contracts/test/troveNFT.t.sol @@ -75,7 +75,7 @@ contract troveNFTTest is DevTestSetup { TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _contractsArray; - (_contractsArray, collateralRegistry, boldToken,,, WETH,) = + (_contractsArray, collateralRegistry, boldToken,,, WETH) = deployer.deployAndConnectContractsMultiColl(troveManagerParamsArray); // Unimplemented feature (...):Copying of type struct LiquityContracts memory[] memory to storage not yet supported. for (uint256 c = 0; c < NUM_COLLATERALS; c++) { diff --git a/contracts/test/zapperGasComp.t.sol b/contracts/test/zapperGasComp.t.sol deleted file mode 100644 index f78368fca..000000000 --- a/contracts/test/zapperGasComp.t.sol +++ /dev/null @@ -1,888 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.18; - -import "./TestContracts/DevTestSetup.sol"; -import "./TestContracts/WETH.sol"; -import "src/Zappers/GasCompZapper.sol"; - -contract ZapperGasCompTest is DevTestSetup { - function setUp() public override { - // Start tests at a non-zero timestamp - vm.warp(block.timestamp + 600); - - accounts = new Accounts(); - createAccounts(); - - (A, B, C, D, E, F, G) = ( - accountsList[0], - accountsList[1], - accountsList[2], - accountsList[3], - accountsList[4], - accountsList[5], - accountsList[6] - ); - - WETH = new WETH9(); - - TestDeployer.TroveManagerParams[] memory troveManagerParams = new TestDeployer.TroveManagerParams[](2); - troveManagerParams[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 10e16, 110e16, 5e16, 10e16); - troveManagerParams[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 10e16, 120e16, 5e16, 10e16); - - TestDeployer deployer = new TestDeployer(); - TestDeployer.LiquityContractsDev[] memory contractsArray; - TestDeployer.Zappers[] memory zappersArray; - (contractsArray, collateralRegistry, boldToken,,, zappersArray) = - deployer.deployAndConnectContracts(troveManagerParams, WETH); - - // Set price feeds - contractsArray[1].priceFeed.setPrice(2000e18); - - // Set first branch as default - addressesRegistry = contractsArray[1].addressesRegistry; - borrowerOperations = contractsArray[1].borrowerOperations; - troveManager = contractsArray[1].troveManager; - troveNFT = contractsArray[1].troveNFT; - collToken = contractsArray[1].collToken; - gasCompZapper = zappersArray[1].gasCompZapper; - - // Give some Collateral to test accounts - uint256 initialCollateralAmount = 10_000e18; - - // A to F - for (uint256 i = 0; i < 6; i++) { - // Give some raw ETH to test accounts - deal(accountsList[i], initialCollateralAmount); - // Give and approve some coll token to test accounts - deal(address(collToken), accountsList[i], initialCollateralAmount); - vm.startPrank(accountsList[i]); - collToken.approve(address(gasCompZapper), initialCollateralAmount); - vm.stopPrank(); - } - } - - function testCanOpenTrove() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - uint256 ethBalanceBefore = A.balance; - uint256 collBalanceBefore = collToken.balanceOf(A); - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - assertEq(troveNFT.ownerOf(troveId), A, "Wrong owner"); - assertGt(troveId, 0, "Trove id should be set"); - assertEq(troveManager.getTroveEntireColl(troveId), collAmount, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore - ETH_GAS_COMPENSATION, "ETH bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBefore - collAmount, "Coll bal mismatch"); - } - - function testCanOpenTroveWithBatchManager() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - uint256 ethBalanceBefore = A.balance; - uint256 collBalanceBefore = collToken.balanceOf(A); - - registerBatchManager(B); - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 0, - batchManager: B, - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - assertEq(troveNFT.ownerOf(troveId), A, "Wrong owner"); - assertGt(troveId, 0, "Trove id should be set"); - assertEq(troveManager.getTroveEntireColl(troveId), collAmount, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore - ETH_GAS_COMPENSATION, "ETH bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBefore - collAmount, "Coll bal mismatch"); - assertEq(borrowerOperations.interestBatchManagerOf(troveId), B, "Wrong batch manager"); - (,,,,,,,, address tmBatchManagerAddress,) = troveManager.Troves(troveId); - assertEq(tmBatchManagerAddress, B, "Wrong batch manager (TM)"); - } - - function testCanNotOpenTroveWithBatchManagerAndInterest() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - registerBatchManager(B); - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: B, - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - vm.expectRevert("GCZ: Cannot choose interest if joining a batch"); - gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - } - - function testCanAddColl() external { - uint256 collAmount1 = 10 ether; - uint256 boldAmount = 10000e18; - uint256 collAmount2 = 5 ether; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 collBalanceBefore = collToken.balanceOf(A); - vm.startPrank(A); - gasCompZapper.addColl(troveId, collAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), collAmount1 + collAmount2, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBefore - collAmount2, "Coll bal mismatch"); - } - - function testCanWithdrawColl() external { - uint256 collAmount1 = 10 ether; - uint256 boldAmount = 10000e18; - uint256 collAmount2 = 1 ether; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 collBalanceBefore = collToken.balanceOf(A); - vm.startPrank(A); - gasCompZapper.withdrawColl(troveId, collAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), collAmount1 - collAmount2, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBefore + collAmount2, "Coll bal mismatch"); - } - - function testCanontWithdrawCollIfZapperIsNotReceiver() external { - uint256 collAmount1 = 10 ether; - uint256 boldAmount = 10000e18; - uint256 collAmount2 = 1 ether; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Change receiver - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(gasCompZapper), B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - gasCompZapper.withdrawColl(troveId, collAmount2); - vm.stopPrank(); - } - - function testCanNotAddReceiverWithoutRemoveManager() external { - uint256 collAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // Try to add a receiver for the zapper without remove manager - vm.startPrank(A); - vm.expectRevert(AddRemoveManagers.EmptyManager.selector); - gasCompZapper.setRemoveManagerWithReceiver(troveId, address(0), B); - vm.stopPrank(); - } - - function testCanRepayBold() external { - uint256 collAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 collBalanceBeforeA = collToken.balanceOf(A); - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 collBalanceBeforeB = collToken.balanceOf(B); - - // Add a remove manager for the zapper, and send bold - vm.startPrank(A); - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - boldToken.transfer(B, boldAmount2); - vm.stopPrank(); - - // Approve and repay - vm.startPrank(B); - boldToken.approve(address(gasCompZapper), boldAmount2); - gasCompZapper.repayBold(troveId, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), collAmount, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 - boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA - boldAmount2, "A BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBeforeA, "A Coll bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(collToken.balanceOf(B), collBalanceBeforeB, "B Coll bal mismatch"); - } - - function testCanWithdrawBold() external { - uint256 collAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 collBalanceBeforeA = collToken.balanceOf(A); - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 collBalanceBeforeB = collToken.balanceOf(B); - - // Add a remove manager for the zapper - vm.startPrank(A); - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Withdraw bold - vm.startPrank(B); - gasCompZapper.withdrawBold(troveId, boldAmount2, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), collAmount, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 + boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBeforeA, "A Coll bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(collToken.balanceOf(B), collBalanceBeforeB, "B Coll bal mismatch"); - } - - function testCannotWithdrawBoldIfZapperIsNotReceiver() external { - uint256 collAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(gasCompZapper), B); - vm.stopPrank(); - - // Withdraw bold - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - gasCompZapper.withdrawBold(troveId, boldAmount2, boldAmount2); - vm.stopPrank(); - } - - function testCanAdjustTroveWithdrawCollAndBold() external { - uint256 collAmount1 = 10 ether; - uint256 collAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 collBalanceBeforeA = collToken.balanceOf(A); - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 collBalanceBeforeB = collToken.balanceOf(B); - - // Add a remove manager for the zapper - vm.startPrank(A); - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - gasCompZapper.adjustTrove(troveId, collAmount2, false, boldAmount2, true, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), collAmount1 - collAmount2, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 + boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBeforeA + collAmount2, "A Coll bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(collToken.balanceOf(B), collBalanceBeforeB, "B Coll bal mismatch"); - } - - function testCannotAdjustTroveWithdrawCollAndBoldIfZapperIsNotReceiver() external { - uint256 collAmount1 = 10 ether; - uint256 collAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(gasCompZapper), B); - vm.stopPrank(); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - gasCompZapper.adjustTrove(troveId, collAmount2, false, boldAmount2, true, boldAmount2); - vm.stopPrank(); - } - - function testCanAdjustTroveAddCollAndWithdrawBold() external { - uint256 collAmount1 = 10 ether; - uint256 collAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 collBalanceBeforeA = collToken.balanceOf(A); - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 collBalanceBeforeB = collToken.balanceOf(B); - - // Add a remove manager for the zapper - vm.startPrank(A); - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Adjust (add coll and withdraw Bold) - vm.startPrank(B); - gasCompZapper.adjustTrove(troveId, collAmount2, true, boldAmount2, true, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), collAmount1 + collAmount2, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 + boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBeforeA, "A Coll bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(collToken.balanceOf(B), collBalanceBeforeB - collAmount2, "B Coll bal mismatch"); - } - - function testCannotAdjustTroveAddCollAndWithdrawBoldIfZapperIsNotReceiver() external { - uint256 collAmount1 = 10 ether; - uint256 collAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(gasCompZapper), B); - vm.stopPrank(); - - // Adjust (add coll and withdraw Bold) - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - gasCompZapper.adjustTrove(troveId, collAmount2, true, boldAmount2, true, boldAmount2); - vm.stopPrank(); - } - - // TODO: more adjustment combinations - function testCanAdjustZombieTroveWithdrawCollAndBold() external { - uint256 collAmount1 = 10 ether; - uint256 collAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // Add a remove manager for the zapper - vm.startPrank(A); - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Redeem to make trove zombie - vm.startPrank(A); - collateralRegistry.redeemCollateral(boldAmount1 - boldAmount2, 10, 1e18); - vm.stopPrank(); - - uint256 troveCollBefore = troveManager.getTroveEntireColl(troveId); - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 collBalanceBeforeA = collToken.balanceOf(A); - uint256 collBalanceBeforeB = collToken.balanceOf(B); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - gasCompZapper.adjustZombieTrove(troveId, collAmount2, false, boldAmount2, true, 0, 0, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), troveCollBefore - collAmount2, "Trove coll mismatch"); - assertApproxEqAbs(troveManager.getTroveEntireDebt(troveId), 2 * boldAmount2, 2e18, "Trove debt mismatch"); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBeforeA + collAmount2, "A Coll bal mismatch"); - assertEq(boldToken.balanceOf(B), 0, "B BOLD bal mismatch"); - assertEq(collToken.balanceOf(B), collBalanceBeforeB, "B Coll bal mismatch"); - } - - function testCannotAdjustZombieTroveWithdrawCollAndBoldIfZapperIsNotReceiver() external { - uint256 collAmount1 = 10 ether; - uint256 collAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(gasCompZapper), B); - vm.stopPrank(); - - // Redeem to make trove zombie - vm.startPrank(A); - collateralRegistry.redeemCollateral(boldAmount1 - boldAmount2, 10, 1e18); - vm.stopPrank(); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - gasCompZapper.adjustZombieTrove(troveId, collAmount2, false, boldAmount2, true, 0, 0, boldAmount2); - vm.stopPrank(); - } - - function testCanCloseTrove() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - uint256 ethBalanceBefore = A.balance; - uint256 collBalanceBefore = collToken.balanceOf(A); - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // open a 2nd trove so we can close the 1st one, and send Bold to account for interest and fee - vm.startPrank(B); - deal(address(WETH), B, ETH_GAS_COMPENSATION); - WETH.approve(address(borrowerOperations), ETH_GAS_COMPENSATION); - deal(address(collToken), B, 100 ether); - collToken.approve(address(borrowerOperations), 100 ether); - borrowerOperations.openTrove( - B, - 0, // index, - 100 ether, // coll, - 10000e18, //boldAmount, - 0, // _upperHint - 0, // _lowerHint - MIN_ANNUAL_INTEREST_RATE, // annualInterestRate, - 10000e18, // upfrontFee - address(0), - address(0), - address(0) - ); - boldToken.transfer(A, troveManager.getTroveEntireDebt(troveId) - boldAmount); - vm.stopPrank(); - - vm.startPrank(A); - boldToken.approve(address(gasCompZapper), type(uint256).max); - gasCompZapper.closeTroveToRawETH(troveId); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), 0, "Coll mismatch"); - assertEq(troveManager.getTroveEntireDebt(troveId), 0, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), 0, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore, "ETH bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBefore, "Coll bal mismatch"); - } - - function testCannotCloseTroveIfZapperIsNotReceiver() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // open a 2nd trove so we can close the 1st one, and send Bold to account for interest and fee - vm.startPrank(B); - deal(address(WETH), B, ETH_GAS_COMPENSATION); - WETH.approve(address(borrowerOperations), ETH_GAS_COMPENSATION); - deal(address(collToken), B, 100 ether); - collToken.approve(address(borrowerOperations), 100 ether); - borrowerOperations.openTrove( - B, - 0, // index, - 100 ether, // coll, - 10000e18, //boldAmount, - 0, // _upperHint - 0, // _lowerHint - MIN_ANNUAL_INTEREST_RATE, // annualInterestRate, - 10000e18, // upfrontFee - address(0), - address(0), - address(0) - ); - boldToken.transfer(A, troveManager.getTroveEntireDebt(troveId) - boldAmount); - vm.stopPrank(); - - vm.startPrank(A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(gasCompZapper), C); - boldToken.approve(address(gasCompZapper), type(uint256).max); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - gasCompZapper.closeTroveToRawETH(troveId); - vm.stopPrank(); - } - - function testExcessRepaymentByAdjustGoesBackToUser() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 ethBalanceBefore = A.balance; - uint256 collBalanceBefore = collToken.balanceOf(A); - uint256 boldDebtBefore = troveManager.getTroveEntireDebt(troveId); - - // Adjust trove: remove 1 ETH and try to repay 9k (only will repay ~8k, up to MIN_DEBT) - vm.startPrank(A); - boldToken.approve(address(gasCompZapper), type(uint256).max); - gasCompZapper.adjustTrove(troveId, 1 ether, false, 9000e18, false, 0); - vm.stopPrank(); - - assertEq(boldToken.balanceOf(A), boldAmount + MIN_DEBT - boldDebtBefore, "BOLD bal mismatch"); - assertEq(boldToken.balanceOf(address(gasCompZapper)), 0, "Zapper BOLD bal should be zero"); - assertEq(A.balance, ethBalanceBefore, "ETH bal mismatch"); - assertEq(address(gasCompZapper).balance, 0, "Zapper ETH bal should be zero"); - assertEq(collToken.balanceOf(A), collBalanceBefore + 1 ether, "Coll bal mismatch"); - assertEq(collToken.balanceOf(address(gasCompZapper)), 0, "Zapper Coll bal should be zero"); - } - - function testExcessRepaymentByRepayGoesBackToUser() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldDebtBefore = troveManager.getTroveEntireDebt(troveId); - uint256 collBalanceBefore = collToken.balanceOf(A); - - // Adjust trove: try to repay 9k (only will repay ~8k, up to MIN_DEBT) - vm.startPrank(A); - boldToken.approve(address(gasCompZapper), type(uint256).max); - gasCompZapper.repayBold(troveId, 9000e18); - vm.stopPrank(); - - assertEq(boldToken.balanceOf(A), boldAmount + MIN_DEBT - boldDebtBefore, "BOLD bal mismatch"); - assertEq(boldToken.balanceOf(address(gasCompZapper)), 0, "Zapper BOLD bal should be zero"); - assertEq(address(gasCompZapper).balance, 0, "Zapper ETH bal should be zero"); - assertEq(collToken.balanceOf(A), collBalanceBefore, "Coll bal mismatch"); - assertEq(collToken.balanceOf(address(gasCompZapper)), 0, "Zapper Coll bal should be zero"); - } - - // TODO: tests for add/remove managers of zapper contract -} diff --git a/contracts/test/zapperLeverage.t.sol b/contracts/test/zapperLeverage.t.sol deleted file mode 100644 index d59bfccb0..000000000 --- a/contracts/test/zapperLeverage.t.sol +++ /dev/null @@ -1,2064 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.18; - -import "./TestContracts/DevTestSetup.sol"; -import "./TestContracts/WETH.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurvePool.sol"; -import "src/Zappers/Modules/Exchanges/CurveExchange.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Pool.sol"; -import "src/Zappers/Modules/Exchanges/UniV3Exchange.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Factory.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol"; -import "src/Zappers/Modules/Exchanges/HybridCurveUniV3Exchange.sol"; -import "src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol"; -import "src/Zappers/Interfaces/IFlashLoanProvider.sol"; -import "src/Zappers/Modules/FlashLoans/Balancer/vault/IVault.sol"; - -import "src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGFactory.sol"; - -contract ZapperLeverageMainnet is DevTestSetup { - using StringFormatting for uint256; - - IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); - - // Curve - uint128 constant BOLD_TOKEN_INDEX = 0; - uint256 constant COLL_TOKEN_INDEX = 1; - uint128 constant USDC_INDEX = 1; - - // UniV3 - INonfungiblePositionManager constant uniV3PositionManager = - INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); - IUniswapV3Factory constant uniswapV3Factory = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); - IQuoterV2 constant uniV3Quoter = IQuoterV2(0x61fFE014bA17989E743c5F6cB21bF9697530B21e); - ISwapRouter constant uniV3Router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - uint24 constant UNIV3_FEE = 3000; // 0.3% - uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05% - uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01% - - uint256 constant NUM_COLLATERALS = 3; - - IZapper[] baseZapperArray; - ILeverageZapper[] leverageZapperCurveArray; - ILeverageZapper[] leverageZapperUniV3Array; - ILeverageZapper[] leverageZapperHybridArray; - - HybridCurveUniV3ExchangeHelpers hybridCurveUniV3ExchangeHelpers; - - ICurveStableswapNGPool usdcCurvePool; - - TestDeployer.LiquityContracts[] contractsArray; - - struct OpenTroveVars { - uint256 price; - uint256 flashLoanAmount; - uint256 expectedBoldAmount; - uint256 maxNetDebt; - uint256 effectiveBoldAmount; - uint256 value; - uint256 troveId; - } - - struct LeverVars { - uint256 price; - uint256 currentCR; - uint256 currentLR; - uint256 currentCollAmount; - uint256 flashLoanAmount; - uint256 expectedBoldAmount; - uint256 maxNetDebtIncrease; - uint256 effectiveBoldAmount; - } - - struct TestVars { - uint256 collAmount; - uint256 initialLeverageRatio; - uint256 troveId; - uint256 initialDebt; - uint256 newLeverageRatio; - uint256 resultingCollateralRatio; - uint256 flashLoanAmount; - uint256 price; - uint256 boldBalanceBeforeA; - uint256 ethBalanceBeforeA; - uint256 collBalanceBeforeA; - uint256 boldBalanceBeforeZapper; - uint256 ethBalanceBeforeZapper; - uint256 collBalanceBeforeZapper; - uint256 boldBalanceBeforeExchange; - uint256 ethBalanceBeforeExchange; - uint256 collBalanceBeforeExchange; - } - - enum ExchangeType { - Curve, - UniV3, - HybridCurveUniV3 - } - - function setUp() public override { - uint256 forkBlock = 21328610; - - try vm.envString("MAINNET_RPC_URL") returns (string memory rpcUrl) { - vm.createSelectFork(rpcUrl, forkBlock); - } catch { - vm.skip(true); - } - - // Start tests at a non-zero timestamp - vm.warp(block.timestamp + 600); - - accounts = new Accounts(); - createAccounts(); - - (A, B, C, D, E, F, G) = ( - accountsList[0], - accountsList[1], - accountsList[2], - accountsList[3], - accountsList[4], - accountsList[5], - accountsList[6] - ); - - WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - - TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = - new TestDeployer.TroveManagerParams[](NUM_COLLATERALS); - troveManagerParamsArray[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 10e16, 110e16, 5e16, 10e16); - for (uint256 c = 0; c < NUM_COLLATERALS; c++) { - troveManagerParamsArray[c] = TestDeployer.TroveManagerParams(160e16, 120e16, 10e16, 120e16, 5e16, 10e16); - } - - TestDeployer deployer = new TestDeployer(); - TestDeployer.DeploymentResultMainnet memory result = - deployer.deployAndConnectContractsMainnet(troveManagerParamsArray); - collateralRegistry = result.collateralRegistry; - boldToken = result.boldToken; - // Record contracts - baseZapperArray.push(result.zappersArray[0].wethZapper); - for (uint256 c = 1; c < NUM_COLLATERALS; c++) { - baseZapperArray.push(result.zappersArray[c].gasCompZapper); - } - for (uint256 c = 0; c < NUM_COLLATERALS; c++) { - contractsArray.push(result.contractsArray[c]); - leverageZapperCurveArray.push(result.zappersArray[c].leverageZapperCurve); - leverageZapperUniV3Array.push(result.zappersArray[c].leverageZapperUniV3); - leverageZapperHybridArray.push(result.zappersArray[c].leverageZapperHybrid); - } - - // Bootstrap Curve pools - fundCurveV2Pools(result.contractsArray, result.zappersArray); - - // Bootstrap UniV3 pools - fundUniV3Pools(result.contractsArray); - - // Give some Collateral to test accounts - uint256 initialCollateralAmount = 10_000e18; - - // A to F - for (uint256 c = 0; c < NUM_COLLATERALS; c++) { - for (uint256 i = 0; i < 6; i++) { - // Give some raw ETH to test accounts - deal(accountsList[i], initialCollateralAmount); - // Give and approve some coll token to test accounts - deal(address(contractsArray[c].collToken), accountsList[i], initialCollateralAmount); - vm.startPrank(accountsList[i]); - contractsArray[c].collToken.approve(address(baseZapperArray[c]), initialCollateralAmount); - contractsArray[c].collToken.approve(address(leverageZapperCurveArray[c]), initialCollateralAmount); - contractsArray[c].collToken.approve(address(leverageZapperUniV3Array[c]), initialCollateralAmount); - contractsArray[c].collToken.approve(address(leverageZapperHybridArray[c]), initialCollateralAmount); - vm.stopPrank(); - } - } - - // exchange helpers - hybridCurveUniV3ExchangeHelpers = new HybridCurveUniV3ExchangeHelpers( - USDC, - WETH, - usdcCurvePool, - USDC_INDEX, // USDC Curve pool index - BOLD_TOKEN_INDEX, // BOLD Curve pool index - UNIV3_FEE_USDC_WETH, - UNIV3_FEE_WETH_COLL, - uniV3Quoter - ); - } - - function fundCurveV2Pools( - TestDeployer.LiquityContracts[] memory _contractsArray, - TestDeployer.Zappers[] memory _zappersArray - ) internal { - uint256 boldAmount; - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - (uint256 price,) = _contractsArray[i].priceFeed.fetchPrice(); - ICurvePool curvePool = CurveExchange(address(_zappersArray[i].leverageZapperCurve.exchange())).curvePool(); - - // Add liquidity - uint256 collAmount = 1000 ether; - boldAmount = collAmount * price / DECIMAL_PRECISION; - deal(address(_contractsArray[i].collToken), A, collAmount); - deal(address(boldToken), A, boldAmount); - vm.startPrank(A); - // approve - _contractsArray[i].collToken.approve(address(curvePool), collAmount); - boldToken.approve(address(curvePool), boldAmount); - uint256[2] memory amounts; - amounts[0] = boldAmount; - amounts[1] = collAmount; - curvePool.add_liquidity(amounts, 0); - vm.stopPrank(); - } - - // Add liquidity to USDC-BOLD - usdcCurvePool = HybridCurveUniV3Exchange(address(_zappersArray[0].leverageZapperHybrid.exchange())).curvePool(); - uint256 usdcAmount = 1e15; // 1B with 6 decimals - boldAmount = usdcAmount * 1e12; // from 6 to 18 decimals - deal(address(USDC), A, usdcAmount); - deal(address(boldToken), A, boldAmount); - vm.startPrank(A); - // approve - USDC.approve(address(usdcCurvePool), usdcAmount); - boldToken.approve(address(usdcCurvePool), boldAmount); - uint256[] memory amountsDynamic = new uint256[](2); - amountsDynamic[0] = boldAmount; - amountsDynamic[1] = usdcAmount; - // add liquidity - usdcCurvePool.add_liquidity(amountsDynamic, 0); - vm.stopPrank(); - } - - function fundUniV3Pools(TestDeployer.LiquityContracts[] memory _contractsArray) internal { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - (uint256 price,) = _contractsArray[i].priceFeed.fetchPrice(); - //console2.log(price, "price"); - - // tokens and amounts - uint256 collAmount = 1000 ether; - uint256 boldAmount = collAmount * price / DECIMAL_PRECISION; - address[2] memory tokens; - uint256[2] memory amounts; - if (address(boldToken) < address(_contractsArray[i].collToken)) { - //console2.log("b < c"); - tokens[0] = address(boldToken); - tokens[1] = address(_contractsArray[i].collToken); - amounts[0] = boldAmount; - amounts[1] = collAmount; - } else { - //console2.log("c < b"); - tokens[0] = address(_contractsArray[i].collToken); - tokens[1] = address(boldToken); - amounts[0] = collAmount; - amounts[1] = boldAmount; - } - - // Add liquidity - - vm.startPrank(A); - - // deal and approve - deal(address(_contractsArray[i].collToken), A, collAmount); - deal(address(boldToken), A, boldAmount); - _contractsArray[i].collToken.approve(address(uniV3PositionManager), collAmount); - boldToken.approve(address(uniV3PositionManager), boldAmount); - - // mint new position - address uniV3PoolAddress = - uniswapV3Factory.getPool(address(boldToken), address(_contractsArray[i].collToken), UNIV3_FEE); - //console2.log(uniV3PoolAddress, "uniV3PoolAddress"); - int24 TICK_SPACING = IUniswapV3Pool(uniV3PoolAddress).tickSpacing(); - (, int24 tick,,,,,) = IUniswapV3Pool(uniV3PoolAddress).slot0(); - int24 tickLower = (tick - 6000) / TICK_SPACING * TICK_SPACING; - int24 tickUpper = (tick + 6000) / TICK_SPACING * TICK_SPACING; - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - token0: tokens[0], - token1: tokens[1], - fee: UNIV3_FEE, - tickLower: tickLower, - tickUpper: tickUpper, - amount0Desired: amounts[0], - amount1Desired: amounts[1], - amount0Min: 0, - amount1Min: 0, - recipient: A, - deadline: block.timestamp - }); - - uniV3PositionManager.mint(params); - - vm.stopPrank(); - } - } - - // Implementing `onERC721Received` so this contract can receive custody of erc721 tokens - function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { - return this.onERC721Received.selector; - } - - struct OpenLeveragedTroveWithIndexParams { - ILeverageZapper leverageZapper; - IERC20 collToken; - uint256 index; - uint256 collAmount; - uint256 leverageRatio; - IPriceFeed priceFeed; - ExchangeType exchangeType; - uint256 branch; - address batchManager; - } - - function openLeveragedTroveWithIndex(OpenLeveragedTroveWithIndexParams memory _inputParams) - internal - returns (uint256, uint256) - { - OpenTroveVars memory vars; - (vars.price,) = _inputParams.priceFeed.fetchPrice(); - - // This should be done in the frontend - vars.flashLoanAmount = - _inputParams.collAmount * (_inputParams.leverageRatio - DECIMAL_PRECISION) / DECIMAL_PRECISION; - vars.expectedBoldAmount = vars.flashLoanAmount * vars.price / DECIMAL_PRECISION; - vars.maxNetDebt = vars.expectedBoldAmount * 105 / 100; // slippage - vars.effectiveBoldAmount = _getBoldAmountToSwap( - _inputParams.exchangeType, - _inputParams.branch, - vars.expectedBoldAmount, - vars.maxNetDebt, - vars.flashLoanAmount, - _inputParams.collToken - ); - - ILeverageZapper.OpenLeveragedTroveParams memory params = ILeverageZapper.OpenLeveragedTroveParams({ - owner: A, - ownerIndex: _inputParams.index, - collAmount: _inputParams.collAmount, - flashLoanAmount: vars.flashLoanAmount, - boldAmount: vars.effectiveBoldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: _inputParams.batchManager == address(0) ? 5e16 : 0, - batchManager: _inputParams.batchManager, - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - vars.value = _inputParams.branch > 0 ? ETH_GAS_COMPENSATION : _inputParams.collAmount + ETH_GAS_COMPENSATION; - _inputParams.leverageZapper.openLeveragedTroveWithRawETH{value: vars.value}(params); - vars.troveId = addressToTroveIdThroughZapper(address(_inputParams.leverageZapper), A, _inputParams.index); - vm.stopPrank(); - - return (vars.troveId, vars.effectiveBoldAmount); - } - - function _setInitialBalances(ILeverageZapper _leverageZapper, uint256 _branch, TestVars memory vars) - internal - view - { - vars.boldBalanceBeforeA = boldToken.balanceOf(A); - vars.ethBalanceBeforeA = A.balance; - vars.collBalanceBeforeA = contractsArray[_branch].collToken.balanceOf(A); - vars.boldBalanceBeforeZapper = boldToken.balanceOf(address(_leverageZapper)); - vars.ethBalanceBeforeZapper = address(_leverageZapper).balance; - vars.collBalanceBeforeZapper = contractsArray[_branch].collToken.balanceOf(address(_leverageZapper)); - vars.boldBalanceBeforeExchange = boldToken.balanceOf(address(_leverageZapper.exchange())); - vars.ethBalanceBeforeExchange = address(_leverageZapper.exchange()).balance; - vars.collBalanceBeforeExchange = - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper.exchange())); - } - - function testCanOpenTroveWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanOpenTrove(leverageZapperCurveArray[i], ExchangeType.Curve, i, address(0)); - } - } - - function testCanOpenTroveWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanOpenTrove(leverageZapperUniV3Array[i], ExchangeType.UniV3, i, address(0)); - } - } - - function testCanOpenTroveWithHybrid() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testCanOpenTrove(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i, address(0)); - } - } - - function testCanOpenTroveAndJoinBatchWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _registerBatchManager(B, i); - _testCanOpenTrove(leverageZapperCurveArray[i], ExchangeType.Curve, i, B); - } - } - - function testCanOpenTroveAndJoinBatchWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - if (i == 2) continue; // TODO!! - _registerBatchManager(B, i); - _testCanOpenTrove(leverageZapperUniV3Array[i], ExchangeType.UniV3, i, B); - } - } - - function testCanOpenTroveAndJoinBatchWithHybrid() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _registerBatchManager(B, i); - _testCanOpenTrove(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i, B); - } - } - - function _registerBatchManager(address _account, uint256 _branch) internal { - vm.startPrank(_account); - contractsArray[_branch].borrowerOperations.registerBatchManager( - uint128(1e16), uint128(20e16), uint128(5e16), uint128(25e14), MIN_INTEREST_RATE_CHANGE_PERIOD - ); - vm.stopPrank(); - } - - function _testCanOpenTrove( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch, - address _batchManager - ) internal { - TestVars memory vars; - vars.collAmount = 10 ether; - vars.newLeverageRatio = 2e18; - vars.resultingCollateralRatio = _leverageZapper.leverageRatioToCollateralRatio(vars.newLeverageRatio); - - _setInitialBalances(_leverageZapper, _branch, vars); - - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = vars.collAmount; - openTroveParams.leverageRatio = vars.newLeverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = _batchManager; - uint256 expectedMinNetDebt; - (vars.troveId, expectedMinNetDebt) = openLeveragedTroveWithIndex(openTroveParams); - - // Checks - (vars.price,) = contractsArray[_branch].priceFeed.fetchPrice(); - // owner - assertEq(contractsArray[_branch].troveNFT.ownerOf(vars.troveId), A, "Wrong owner"); - // troveId - assertGt(vars.troveId, 0, "Trove id should be set"); - // coll - assertEq( - getTroveEntireColl(contractsArray[_branch].troveManager, vars.troveId), - vars.collAmount * vars.newLeverageRatio / DECIMAL_PRECISION, - "Coll mismatch" - ); - // debt - uint256 expectedMaxNetDebt = expectedMinNetDebt * 105 / 100; - uint256 troveEntireDebt = getTroveEntireDebt(contractsArray[_branch].troveManager, vars.troveId); - assertGe(troveEntireDebt, expectedMinNetDebt, "Debt too low"); - assertLe(troveEntireDebt, expectedMaxNetDebt, "Debt too high"); - // CR - uint256 ICR = contractsArray[_branch].troveManager.getCurrentICR(vars.troveId, vars.price); - assertTrue(ICR >= vars.resultingCollateralRatio || vars.resultingCollateralRatio - ICR < 3e16, "Wrong CR"); - // token balances - assertEq(boldToken.balanceOf(A), vars.boldBalanceBeforeA, "BOLD bal mismatch"); - assertEq( - boldToken.balanceOf(address(_leverageZapper)), vars.boldBalanceBeforeZapper, "Zapper should not keep BOLD" - ); - assertEq( - boldToken.balanceOf(address(_leverageZapper.exchange())), - vars.boldBalanceBeforeExchange, - "Exchange should not keep BOLD" - ); - assertEq( - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper)), - vars.collBalanceBeforeZapper, - "Zapper should not keep Coll" - ); - assertEq( - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper.exchange())), - vars.collBalanceBeforeExchange, - "Exchange should not keep Coll" - ); - assertEq(address(_leverageZapper).balance, vars.ethBalanceBeforeZapper, "Zapper should not keep ETH"); - assertEq( - address(_leverageZapper.exchange()).balance, vars.ethBalanceBeforeExchange, "Exchange should not keep ETH" - ); - if (_branch > 0) { - // LST - assertEq(A.balance, vars.ethBalanceBeforeA - ETH_GAS_COMPENSATION, "ETH bal mismatch"); - assertGe( - contractsArray[_branch].collToken.balanceOf(A), - vars.collBalanceBeforeA - vars.collAmount, - "Coll bal mismatch" - ); - } else { - assertEq(A.balance, vars.ethBalanceBeforeA - ETH_GAS_COMPENSATION - vars.collAmount, "ETH bal mismatch"); - assertGe(contractsArray[_branch].collToken.balanceOf(A), vars.collBalanceBeforeA, "Coll bal mismatch"); - } - } - - function testOnlyFlashLoanProviderCanCallOpenTroveCallbackWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallOpenTroveCallback(leverageZapperCurveArray[i]); - } - } - - function testOnlyFlashLoanProviderCanCallOpenTroveCallbackWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallOpenTroveCallback(leverageZapperUniV3Array[i]); - } - } - - function _testOnlyFlashLoanProviderCanCallOpenTroveCallback(ILeverageZapper _leverageZapper) internal { - ILeverageZapper.OpenLeveragedTroveParams memory params = ILeverageZapper.OpenLeveragedTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 10 ether, - flashLoanAmount: 10 ether, - boldAmount: 10000e18, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - vm.expectRevert("LZ: Caller not FlashLoan provider"); - IFlashLoanReceiver(address(_leverageZapper)).receiveFlashLoanOnOpenLeveragedTrove(params, 10 ether); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(_leverageZapper.flashLoanProvider().receiver()), address(0), "Receiver should be zero"); - } - - // Lever up - - struct LeverUpParams { - ILeverageZapper leverageZapper; - IERC20 collToken; - uint256 troveId; - uint256 leverageRatio; - ITroveManager troveManager; - IPriceFeed priceFeed; - ExchangeType exchangeType; - uint256 branch; - } - - function _getLeverUpFlashLoanAndBoldAmount(LeverUpParams memory _params) internal returns (uint256, uint256) { - LeverVars memory vars; - (vars.price,) = _params.priceFeed.fetchPrice(); - vars.currentCR = _params.troveManager.getCurrentICR(_params.troveId, vars.price); - vars.currentLR = _params.leverageZapper.leverageRatioToCollateralRatio(vars.currentCR); - assertGt(_params.leverageRatio, vars.currentLR, "Leverage ratio should increase"); - vars.currentCollAmount = getTroveEntireColl(_params.troveManager, _params.troveId); - vars.flashLoanAmount = vars.currentCollAmount * _params.leverageRatio / vars.currentLR - vars.currentCollAmount; - vars.expectedBoldAmount = vars.flashLoanAmount * vars.price / DECIMAL_PRECISION; - vars.maxNetDebtIncrease = vars.expectedBoldAmount * 105 / 100; // slippage - // The actual bold we need, capped by the slippage above, to get flash loan amount - vars.effectiveBoldAmount = _getBoldAmountToSwap( - _params.exchangeType, - _params.branch, - vars.expectedBoldAmount, - vars.maxNetDebtIncrease, - vars.flashLoanAmount, - _params.collToken - ); - - return (vars.flashLoanAmount, vars.effectiveBoldAmount); - } - - function leverUpTrove(LeverUpParams memory _params) internal returns (uint256, uint256) { - // This should be done in the frontend - (uint256 flashLoanAmount, uint256 effectiveBoldAmount) = _getLeverUpFlashLoanAndBoldAmount(_params); - - ILeverageZapper.LeverUpTroveParams memory params = ILeverageZapper.LeverUpTroveParams({ - troveId: _params.troveId, - flashLoanAmount: flashLoanAmount, - boldAmount: effectiveBoldAmount, - maxUpfrontFee: 1000e18 - }); - vm.startPrank(A); - _params.leverageZapper.leverUpTrove(params); - vm.stopPrank(); - - return (flashLoanAmount, effectiveBoldAmount); - } - - function testCanLeverUpTroveWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanLeverUpTrove(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testCanLeverUpTroveWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanLeverUpTrove(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testCanLeverUpTroveWithHybrid() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testCanLeverUpTrove(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i); - } - } - - function _testCanLeverUpTrove(ILeverageZapper _leverageZapper, ExchangeType _exchangeType, uint256 _branch) - internal - { - TestVars memory vars; - vars.collAmount = 10 ether; - vars.initialLeverageRatio = 2e18; - - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = vars.collAmount; - openTroveParams.leverageRatio = vars.initialLeverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (vars.troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - vars.initialDebt = getTroveEntireDebt(contractsArray[_branch].troveManager, vars.troveId); - - vars.newLeverageRatio = 2.5e18; - vars.resultingCollateralRatio = _leverageZapper.leverageRatioToCollateralRatio(vars.newLeverageRatio); - - _setInitialBalances(_leverageZapper, _branch, vars); - - LeverUpParams memory params; - params.leverageZapper = _leverageZapper; - params.collToken = contractsArray[_branch].collToken; - params.troveId = vars.troveId; - params.leverageRatio = vars.newLeverageRatio; - params.troveManager = contractsArray[_branch].troveManager; - params.priceFeed = contractsArray[_branch].priceFeed; - params.exchangeType = _exchangeType; - params.branch = _branch; - uint256 expectedMinLeverUpNetDebt; - (vars.flashLoanAmount, expectedMinLeverUpNetDebt) = leverUpTrove(params); - - // Checks - (vars.price,) = contractsArray[_branch].priceFeed.fetchPrice(); - // coll - uint256 coll = getTroveEntireColl(contractsArray[_branch].troveManager, vars.troveId); - uint256 collExpected = vars.collAmount * vars.newLeverageRatio / DECIMAL_PRECISION; - assertTrue(coll >= collExpected || collExpected - coll <= 4e17, "Coll mismatch"); - // debt - uint256 expectedMinNetDebt = vars.initialDebt + expectedMinLeverUpNetDebt; - uint256 expectedMaxNetDebt = expectedMinNetDebt * 105 / 100; - uint256 troveEntireDebt = getTroveEntireDebt(contractsArray[_branch].troveManager, vars.troveId); - assertGe(troveEntireDebt, expectedMinNetDebt, "Debt too low"); - assertLe(troveEntireDebt, expectedMaxNetDebt, "Debt too high"); - // CR - uint256 ICR = contractsArray[_branch].troveManager.getCurrentICR(vars.troveId, vars.price); - assertTrue(ICR >= vars.resultingCollateralRatio || vars.resultingCollateralRatio - ICR < 2e16, "Wrong CR"); - // token balances - assertEq(boldToken.balanceOf(A), vars.boldBalanceBeforeA, "BOLD bal mismatch"); - assertEq(A.balance, vars.ethBalanceBeforeA, "ETH bal mismatch"); - assertGe(contractsArray[_branch].collToken.balanceOf(A), vars.collBalanceBeforeA, "Coll bal mismatch"); - assertEq( - boldToken.balanceOf(address(_leverageZapper)), vars.boldBalanceBeforeZapper, "Zapper should not keep BOLD" - ); - assertEq( - boldToken.balanceOf(address(_leverageZapper.exchange())), - vars.boldBalanceBeforeExchange, - "Exchange should not keep BOLD" - ); - assertEq( - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper)), - vars.collBalanceBeforeZapper, - "Zapper should not keep Coll" - ); - assertEq( - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper.exchange())), - vars.collBalanceBeforeExchange, - "Exchange should not keep Coll" - ); - assertEq(address(_leverageZapper).balance, vars.ethBalanceBeforeZapper, "Zapper should not keep ETH"); - assertEq( - address(_leverageZapper.exchange()).balance, vars.ethBalanceBeforeExchange, "Exchange should not keep ETH" - ); - - // Check receiver is back to zero - assertEq(address(_leverageZapper.flashLoanProvider().receiver()), address(0), "Receiver should be zero"); - } - - function testCannotLeverUpTroveWithCurveIfZapperIsNotReceiver() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotLeverUpTroveIfZapperIsNotReceiver(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testCannotLeverUpTroveWithUniV3IfZapperIsNotReceiver() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotLeverUpTroveIfZapperIsNotReceiver(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testCannotLeverUpTroveWithHybridIfZapperIsNotReceiver() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testCannotLeverUpTroveIfZapperIsNotReceiver(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i); - } - } - - function _testCannotLeverUpTroveIfZapperIsNotReceiver( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - TestVars memory vars; - vars.collAmount = 10 ether; - vars.initialLeverageRatio = 2e18; - - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = vars.collAmount; - openTroveParams.leverageRatio = vars.initialLeverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (vars.troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - vars.initialDebt = getTroveEntireDebt(contractsArray[_branch].troveManager, vars.troveId); - - vars.newLeverageRatio = 2.5e18; - vars.resultingCollateralRatio = _leverageZapper.leverageRatioToCollateralRatio(vars.newLeverageRatio); - - LeverUpParams memory getterParams; - getterParams.leverageZapper = _leverageZapper; - getterParams.collToken = contractsArray[_branch].collToken; - getterParams.troveId = vars.troveId; - getterParams.leverageRatio = vars.newLeverageRatio; - getterParams.troveManager = contractsArray[_branch].troveManager; - getterParams.priceFeed = contractsArray[_branch].priceFeed; - getterParams.exchangeType = _exchangeType; - getterParams.branch = _branch; - - // This should be done in the frontend - (uint256 flashLoanAmount, uint256 effectiveBoldAmount) = _getLeverUpFlashLoanAndBoldAmount(getterParams); - - ILeverageZapper.LeverUpTroveParams memory params = ILeverageZapper.LeverUpTroveParams({ - troveId: vars.troveId, - flashLoanAmount: flashLoanAmount, - boldAmount: effectiveBoldAmount, - maxUpfrontFee: 1000e18 - }); - vm.startPrank(A); - // Change receiver in BO - contractsArray[_branch].borrowerOperations.setRemoveManagerWithReceiver( - vars.troveId, address(_leverageZapper), C - ); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - _leverageZapper.leverUpTrove(params); - vm.stopPrank(); - } - - function testOnlyFlashLoanProviderCanCallLeverUpCallbackWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallLeverUpCallback(leverageZapperCurveArray[i]); - } - } - - function testOnlyFlashLoanProviderCanCallLeverUpCallbackWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallLeverUpCallback(leverageZapperUniV3Array[i]); - } - } - - function _testOnlyFlashLoanProviderCanCallLeverUpCallback(ILeverageZapper _leverageZapper) internal { - ILeverageZapper.LeverUpTroveParams memory params = ILeverageZapper.LeverUpTroveParams({ - troveId: addressToTroveIdThroughZapper(address(_leverageZapper), A), - flashLoanAmount: 10 ether, - boldAmount: 10000e18, - maxUpfrontFee: 1000e18 - }); - vm.startPrank(A); - vm.expectRevert("LZ: Caller not FlashLoan provider"); - IFlashLoanReceiver(address(_leverageZapper)).receiveFlashLoanOnLeverUpTrove(params, 10 ether); - vm.stopPrank(); - } - - function testOnlyOwnerOrManagerCanLeverUpWithCurveFromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromZapper(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testOnlyOwnerOrManagerCanLeverUpWithUniV3FromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromZapper(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testOnlyOwnerOrManagerCanLeverUpWithHybridFromZapper() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromZapper(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i); - } - } - - function _testOnlyOwnerOrManagerCanLeverUpFromZapper( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - LeverUpParams memory getterParams; - getterParams.leverageZapper = _leverageZapper; - getterParams.collToken = contractsArray[_branch].collToken; - getterParams.troveId = troveId; - getterParams.leverageRatio = 2.5e18; - getterParams.troveManager = contractsArray[_branch].troveManager; - getterParams.priceFeed = contractsArray[_branch].priceFeed; - getterParams.exchangeType = _exchangeType; - getterParams.branch = _branch; - (uint256 flashLoanAmount, uint256 effectiveBoldAmount) = _getLeverUpFlashLoanAndBoldAmount(getterParams); - - ILeverageZapper.LeverUpTroveParams memory params = ILeverageZapper.LeverUpTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - boldAmount: effectiveBoldAmount, - maxUpfrontFee: 1000e18 - }); - // B tries to lever up A’s trove - vm.startPrank(B); - vm.expectRevert(AddRemoveManagers.NotOwnerNorRemoveManager.selector); - _leverageZapper.leverUpTrove(params); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(_leverageZapper.flashLoanProvider().receiver()), address(0), "Receiver should be zero"); - } - - function testOnlyOwnerOrManagerCanLeverUpWithCurveFromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromBalancerFLProvider(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testOnlyOwnerOrManagerCanLeverUpWithUniV3FromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromBalancerFLProvider(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testOnlyOwnerOrManagerCanLeverUpWithHybridFromBalancerFLProvider() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromBalancerFLProvider( - leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i - ); - } - } - - function _testOnlyOwnerOrManagerCanLeverUpFromBalancerFLProvider( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 1; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - LeverUpParams memory getterParams; - getterParams.leverageZapper = _leverageZapper; - getterParams.collToken = contractsArray[_branch].collToken; - getterParams.troveId = troveId; - getterParams.leverageRatio = 2.5e18; - getterParams.troveManager = contractsArray[_branch].troveManager; - getterParams.priceFeed = contractsArray[_branch].priceFeed; - getterParams.exchangeType = _exchangeType; - getterParams.branch = _branch; - (uint256 flashLoanAmount, uint256 effectiveBoldAmount) = _getLeverUpFlashLoanAndBoldAmount(getterParams); - - // B tries to lever up A’s trove calling our flash loan provider module - ILeverageZapper.LeverUpTroveParams memory params = ILeverageZapper.LeverUpTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - boldAmount: effectiveBoldAmount, - maxUpfrontFee: 1000e18 - }); - IFlashLoanProvider flashLoanProvider = _leverageZapper.flashLoanProvider(); - vm.startPrank(B); - vm.expectRevert(); // reverts without data because it calls back B - flashLoanProvider.makeFlashLoan( - contractsArray[_branch].collToken, - flashLoanAmount, - IFlashLoanProvider.Operation.LeverUpTrove, - abi.encode(params) - ); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(flashLoanProvider.receiver()), address(0), "Receiver should be zero"); - } - - function testOnlyOwnerOrManagerCanLeverUpWithCurveFromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromBalancerVault(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testOnlyOwnerOrManagerCanLeverUpWithUniV3FromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromBalancerVault(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testOnlyOwnerOrManagerCanLeverUpWithHybridFromBalancerVault() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromBalancerVault( - leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i - ); - } - } - - function _testOnlyOwnerOrManagerCanLeverUpFromBalancerVault( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 2; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - // B tries to lever up A’s trove calling Balancer Vault directly - LeverUpParams memory getterParams; - getterParams.leverageZapper = _leverageZapper; - getterParams.collToken = contractsArray[_branch].collToken; - getterParams.troveId = troveId; - getterParams.leverageRatio = 2.5e18; - getterParams.troveManager = contractsArray[_branch].troveManager; - getterParams.priceFeed = contractsArray[_branch].priceFeed; - getterParams.exchangeType = _exchangeType; - getterParams.branch = _branch; - (uint256 flashLoanAmount, uint256 effectiveBoldAmount) = _getLeverUpFlashLoanAndBoldAmount(getterParams); - - ILeverageZapper.LeverUpTroveParams memory params = ILeverageZapper.LeverUpTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - boldAmount: effectiveBoldAmount, - maxUpfrontFee: 1000e18 - }); - IFlashLoanProvider flashLoanProvider = _leverageZapper.flashLoanProvider(); - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = contractsArray[_branch].collToken; - uint256[] memory amounts = new uint256[](1); - amounts[0] = flashLoanAmount; - bytes memory userData = abi.encode(address(_leverageZapper), IFlashLoanProvider.Operation.LeverUpTrove, params); - IVault vault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - vm.startPrank(B); - vm.expectRevert("Flash loan not properly initiated"); - vault.flashLoan(IFlashLoanRecipient(address(flashLoanProvider)), tokens, amounts, userData); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(flashLoanProvider.receiver()), address(0), "Receiver should be zero"); - } - - // Lever down - - function _getLeverDownFlashLoanAndBoldAmount( - ILeverageZapper _leverageZapper, - uint256 _troveId, - uint256 _leverageRatio, - ITroveManager _troveManager, - IPriceFeed _priceFeed - ) internal returns (uint256, uint256) { - (uint256 price,) = _priceFeed.fetchPrice(); - - uint256 currentCR = _troveManager.getCurrentICR(_troveId, price); - uint256 currentLR = _leverageZapper.leverageRatioToCollateralRatio(currentCR); - assertLt(_leverageRatio, currentLR, "Leverage ratio should decrease"); - uint256 currentCollAmount = getTroveEntireColl(_troveManager, _troveId); - uint256 flashLoanAmount = currentCollAmount - currentCollAmount * _leverageRatio / currentLR; - uint256 expectedBoldAmount = flashLoanAmount * price / DECIMAL_PRECISION; - uint256 minBoldDebt = expectedBoldAmount * 95 / 100; // slippage - - return (flashLoanAmount, minBoldDebt); - } - - function leverDownTrove( - ILeverageZapper _leverageZapper, - uint256 _troveId, - uint256 _leverageRatio, - ITroveManager _troveManager, - IPriceFeed _priceFeed - ) internal returns (uint256) { - // This should be done in the frontend - (uint256 flashLoanAmount, uint256 minBoldDebt) = - _getLeverDownFlashLoanAndBoldAmount(_leverageZapper, _troveId, _leverageRatio, _troveManager, _priceFeed); - - ILeverageZapper.LeverDownTroveParams memory params = ILeverageZapper.LeverDownTroveParams({ - troveId: _troveId, - flashLoanAmount: flashLoanAmount, - minBoldAmount: minBoldDebt - }); - vm.startPrank(A); - _leverageZapper.leverDownTrove(params); - vm.stopPrank(); - - return flashLoanAmount; - } - - function testCanLeverDownTroveWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanLeverDownTrove(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testCanLeverDownTroveWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanLeverDownTrove(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testCanLeverDownTroveWithHybrid() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testCanLeverDownTrove(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i); - } - } - - function _testCanLeverDownTrove(ILeverageZapper _leverageZapper, ExchangeType _exchangeType, uint256 _branch) - internal - { - TestVars memory vars; - vars.collAmount = 10 ether; - vars.initialLeverageRatio = 2e18; - - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = vars.collAmount; - openTroveParams.leverageRatio = vars.initialLeverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (vars.troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - vars.initialDebt = getTroveEntireDebt(contractsArray[_branch].troveManager, vars.troveId); - - vars.newLeverageRatio = 1.5e18; - vars.resultingCollateralRatio = _leverageZapper.leverageRatioToCollateralRatio(vars.newLeverageRatio); - - _setInitialBalances(_leverageZapper, _branch, vars); - - vars.flashLoanAmount = leverDownTrove( - _leverageZapper, - vars.troveId, - vars.newLeverageRatio, - contractsArray[_branch].troveManager, - contractsArray[_branch].priceFeed - ); - - // Checks - (vars.price,) = contractsArray[_branch].priceFeed.fetchPrice(); - // coll - uint256 coll = getTroveEntireColl(contractsArray[_branch].troveManager, vars.troveId); - uint256 collExpected = vars.collAmount * vars.newLeverageRatio / DECIMAL_PRECISION; - assertTrue(coll >= collExpected || collExpected - coll <= 22e16, "Coll mismatch"); - // debt - uint256 expectedMinNetDebt = - vars.initialDebt - vars.flashLoanAmount * vars.price / DECIMAL_PRECISION * 101 / 100; - uint256 expectedMaxNetDebt = expectedMinNetDebt * 105 / 100; - uint256 troveEntireDebt = getTroveEntireDebt(contractsArray[_branch].troveManager, vars.troveId); - assertGe(troveEntireDebt, expectedMinNetDebt, "Debt too low"); - assertLe(troveEntireDebt, expectedMaxNetDebt, "Debt too high"); - // CR - // When getting flashloan amount, we allow the min debt to deviate up to 5% - // That deviation can translate into CR, specially for UniV3 exchange which is the less efficient - // With UniV3, the quoter gives a price “too good”, meaning we exchange less, so the deleverage is lower - uint256 CRTolerance = _exchangeType == ExchangeType.UniV3 ? 9e16 : 17e15; - uint256 ICR = contractsArray[_branch].troveManager.getCurrentICR(vars.troveId, vars.price); - assertTrue( - ICR >= vars.resultingCollateralRatio || vars.resultingCollateralRatio - ICR < CRTolerance, "Wrong CR" - ); - // token balances - assertEq(boldToken.balanceOf(A), vars.boldBalanceBeforeA, "BOLD bal mismatch"); - assertEq(A.balance, vars.ethBalanceBeforeA, "ETH bal mismatch"); - assertGe(contractsArray[_branch].collToken.balanceOf(A), vars.collBalanceBeforeA, "Coll bal mismatch"); - assertEq( - boldToken.balanceOf(address(_leverageZapper)), vars.boldBalanceBeforeZapper, "Zapper should not keep BOLD" - ); - assertEq( - boldToken.balanceOf(address(_leverageZapper.exchange())), - vars.boldBalanceBeforeExchange, - "Exchange should not keep BOLD" - ); - assertEq( - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper)), - vars.collBalanceBeforeZapper, - "Zapper should not keep Coll" - ); - assertEq( - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper.exchange())), - vars.collBalanceBeforeExchange, - "Exchange should not keep Coll" - ); - assertEq(address(_leverageZapper).balance, vars.ethBalanceBeforeZapper, "Zapper should not keep ETH"); - assertEq( - address(_leverageZapper.exchange()).balance, vars.ethBalanceBeforeExchange, "Exchange should not keep ETH" - ); - - // Check receiver is back to zero - assertEq(address(_leverageZapper.flashLoanProvider().receiver()), address(0), "Receiver should be zero"); - } - - function testCannotLeverDownWithCurveFromZapperIfZapperIsNotReceiver() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotLeverDownFromZapperIfZapperIsNotReceiver(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testCannotLeverDownWithUniV3FromZapperIfZapperIsNotReceiver() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotLeverDownFromZapperIfZapperIsNotReceiver(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testCannotLeverDownWithHybridFromZapperIfZapperIsNotReceiver() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testCannotLeverDownFromZapperIfZapperIsNotReceiver( - leverageZapperUniV3Array[i], ExchangeType.HybridCurveUniV3, i - ); - } - } - - function _testCannotLeverDownFromZapperIfZapperIsNotReceiver( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - (uint256 flashLoanAmount, uint256 minBoldDebt) = _getLeverDownFlashLoanAndBoldAmount( - _leverageZapper, - troveId, - 1.5e18, // _leverageRatio, - contractsArray[_branch].troveManager, - contractsArray[_branch].priceFeed - ); - - ILeverageZapper.LeverDownTroveParams memory params = ILeverageZapper.LeverDownTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - minBoldAmount: minBoldDebt - }); - vm.startPrank(A); - // Change receiver in BO - contractsArray[_branch].borrowerOperations.setRemoveManagerWithReceiver(troveId, address(_leverageZapper), C); - - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - _leverageZapper.leverDownTrove(params); - vm.stopPrank(); - } - - function testOnlyFlashLoanProviderCanCallLeverDownCallbackWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallLeverDownCallback(leverageZapperCurveArray[i]); - } - } - - function testOnlyFlashLoanProviderCanCallLeverDownCallbackWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallLeverDownCallback(leverageZapperUniV3Array[i]); - } - } - - function _testOnlyFlashLoanProviderCanCallLeverDownCallback(ILeverageZapper _leverageZapper) internal { - ILeverageZapper.LeverDownTroveParams memory params = ILeverageZapper.LeverDownTroveParams({ - troveId: addressToTroveIdThroughZapper(address(_leverageZapper), A), - flashLoanAmount: 10 ether, - minBoldAmount: 10000e18 - }); - vm.startPrank(A); - vm.expectRevert("LZ: Caller not FlashLoan provider"); - IFlashLoanReceiver(address(_leverageZapper)).receiveFlashLoanOnLeverDownTrove(params, 10 ether); - vm.stopPrank(); - } - - function testOnlyOwnerOrManagerCanLeverDownWithCurveFromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromZapper(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testOnlyOwnerOrManagerCanLeverDownWithUniV3FromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromZapper(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testOnlyOwnerOrManagerCanLeverDownWithHybridFromZapper() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromZapper(leverageZapperUniV3Array[i], ExchangeType.HybridCurveUniV3, i); - } - } - - function _testOnlyOwnerOrManagerCanLeverDownFromZapper( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - // B tries to lever up A’s trove - (uint256 flashLoanAmount, uint256 minBoldDebt) = _getLeverDownFlashLoanAndBoldAmount( - _leverageZapper, - troveId, - 1.5e18, // _leverageRatio, - contractsArray[_branch].troveManager, - contractsArray[_branch].priceFeed - ); - - ILeverageZapper.LeverDownTroveParams memory params = ILeverageZapper.LeverDownTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - minBoldAmount: minBoldDebt - }); - vm.startPrank(B); - vm.expectRevert(AddRemoveManagers.NotOwnerNorRemoveManager.selector); - _leverageZapper.leverDownTrove(params); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(_leverageZapper.flashLoanProvider().receiver()), address(0), "Receiver should be zero"); - } - - function testOnlyOwnerOrManagerCanLeverDownWithCurveFromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromBalancerFLProvider( - leverageZapperCurveArray[i], ExchangeType.Curve, i - ); - } - } - - function testOnlyOwnerOrManagerCanLeverDownWithUniV3FromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromBalancerFLProvider( - leverageZapperUniV3Array[i], ExchangeType.UniV3, i - ); - } - } - - function testOnlyOwnerOrManagerCanLeverDownWithHybridFromBalancerFLProvider() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromBalancerFLProvider( - leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i - ); - } - } - - function _testOnlyOwnerOrManagerCanLeverDownFromBalancerFLProvider( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 1; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - // B tries to lever down A’s trove calling our flash loan provider module - (uint256 flashLoanAmount, uint256 minBoldDebt) = _getLeverDownFlashLoanAndBoldAmount( - _leverageZapper, - troveId, - 1.5e18, // _leverageRatio, - contractsArray[_branch].troveManager, - contractsArray[_branch].priceFeed - ); - - ILeverageZapper.LeverDownTroveParams memory params = ILeverageZapper.LeverDownTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - minBoldAmount: minBoldDebt - }); - IFlashLoanProvider flashLoanProvider = _leverageZapper.flashLoanProvider(); - vm.startPrank(B); - vm.expectRevert(); // reverts without data because it calls back B - flashLoanProvider.makeFlashLoan( - contractsArray[_branch].collToken, - flashLoanAmount, - IFlashLoanProvider.Operation.LeverDownTrove, - abi.encode(params) - ); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(flashLoanProvider.receiver()), address(0), "Receiver should be zero"); - } - - function testOnlyOwnerOrManagerCanLeverDownWithCurveFromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromBalancerVault(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testOnlyOwnerOrManagerCanLeverDownWithUniV3FromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromBalancerVault(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testOnlyOwnerOrManagerCanLeverDownWithHybridFromBalancerVault() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromBalancerVault( - leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i - ); - } - } - - function _testOnlyOwnerOrManagerCanLeverDownFromBalancerVault( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 2; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - // B tries to lever down A’s trove calling Balancer Vault directly - (uint256 flashLoanAmount, uint256 minBoldDebt) = _getLeverDownFlashLoanAndBoldAmount( - _leverageZapper, - troveId, - 1.5e18, // _leverageRatio, - contractsArray[_branch].troveManager, - contractsArray[_branch].priceFeed - ); - - ILeverageZapper.LeverDownTroveParams memory params = ILeverageZapper.LeverDownTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - minBoldAmount: minBoldDebt - }); - IFlashLoanProvider flashLoanProvider = _leverageZapper.flashLoanProvider(); - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = contractsArray[_branch].collToken; - uint256[] memory amounts = new uint256[](1); - amounts[0] = flashLoanAmount; - bytes memory userData = - abi.encode(address(_leverageZapper), IFlashLoanProvider.Operation.LeverDownTrove, params); - IVault vault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - vm.startPrank(B); - vm.expectRevert("Flash loan not properly initiated"); - vault.flashLoan(IFlashLoanRecipient(address(flashLoanProvider)), tokens, amounts, userData); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(flashLoanProvider.receiver()), address(0), "Receiver should be zero"); - } - - // Close trove - - function testCanCloseTroveWithBaseZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanCloseTrove(baseZapperArray[i], i); - } - } - - function testCanCloseTroveWithLeverageCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanCloseTrove(IZapper(leverageZapperCurveArray[i]), i); - } - } - - function testCanCloseTroveWithLeverageUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanCloseTrove(IZapper(leverageZapperUniV3Array[i]), i); - } - } - - function testCanCloseTroveWithLeverageHybrid() external { - for (uint256 i = 0; i < 3; i++) { - _testCanCloseTrove(IZapper(leverageZapperHybridArray[i]), i); - } - } - - function _getCloseFlashLoanAmount(uint256 _troveId, ITroveManager _troveManager, IPriceFeed _priceFeed) - internal - returns (uint256, uint256) - { - (uint256 price,) = _priceFeed.fetchPrice(); - - uint256 currentDebt = getTroveEntireDebt(_troveManager, _troveId); - uint256 currentColl = getTroveEntireColl(_troveManager, _troveId); - uint256 flashLoanAmount = currentDebt * DECIMAL_PRECISION / price * 105 / 100; // slippage - - return (flashLoanAmount, currentColl - flashLoanAmount); - } - - function closeTrove(IZapper _zapper, uint256 _troveId, ITroveManager _troveManager, IPriceFeed _priceFeed) - internal - { - // This should be done in the frontend - (uint256 flashLoanAmount, uint256 minExpectedCollateral) = - _getCloseFlashLoanAmount(_troveId, _troveManager, _priceFeed); - - vm.startPrank(A); - _zapper.closeTroveFromCollateral(_troveId, flashLoanAmount, minExpectedCollateral); - vm.stopPrank(); - } - - function openTrove( - IZapper _zapper, - address _account, - uint256 _index, - uint256 _collAmount, - uint256 _boldAmount, - bool _lst - ) internal returns (uint256) { - return openTrove(_zapper, _account, _index, _collAmount, _boldAmount, _lst, MIN_ANNUAL_INTEREST_RATE); - } - - function openTrove( - IZapper _zapper, - address _account, - uint256 _index, - uint256 _collAmount, - uint256 _boldAmount, - bool _lst, - uint256 _interestRate - ) internal returns (uint256) { - IZapper.OpenTroveParams memory openParams = IZapper.OpenTroveParams({ - owner: _account, - ownerIndex: _index, - collAmount: _collAmount, - boldAmount: _boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: _interestRate, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - - vm.startPrank(_account); - uint256 value = _lst ? ETH_GAS_COMPENSATION : _collAmount + ETH_GAS_COMPENSATION; - uint256 troveId = _zapper.openTroveWithRawETH{value: value}(openParams); - vm.stopPrank(); - - return troveId; - } - - function _testCanCloseTrove(IZapper _zapper, uint256 _branch) internal { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - bool lst = _branch > 0; - uint256 troveId = openTrove(_zapper, A, 0, collAmount, boldAmount, lst); - - // open a 2nd trove so we can close the 1st one - openTrove(_zapper, B, 0, 100 ether, 10000e18, lst); - - uint256 boldBalanceBefore = boldToken.balanceOf(A); - uint256 collBalanceBefore = contractsArray[_branch].collToken.balanceOf(A); - uint256 ethBalanceBefore = A.balance; - (uint256 price,) = contractsArray[_branch].priceFeed.fetchPrice(); - uint256 debtInColl = - getTroveEntireDebt(contractsArray[_branch].troveManager, troveId) * DECIMAL_PRECISION / price; - - // Close trove - closeTrove(_zapper, troveId, contractsArray[_branch].troveManager, contractsArray[_branch].priceFeed); - - assertEq(getTroveEntireColl(contractsArray[_branch].troveManager, troveId), 0, "Coll mismatch"); - assertEq(getTroveEntireDebt(contractsArray[_branch].troveManager, troveId), 0, "Debt mismatch"); - assertGe(boldToken.balanceOf(A), boldBalanceBefore, "BOLD bal should not decrease"); - assertLe(boldToken.balanceOf(A), boldBalanceBefore * 105 / 100, "BOLD bal can only increase by slippage margin"); - if (lst) { - assertGe(contractsArray[_branch].collToken.balanceOf(A), collBalanceBefore, "Coll bal should not decrease"); - assertApproxEqAbs( - contractsArray[_branch].collToken.balanceOf(A), - collBalanceBefore + collAmount - debtInColl, - 3e17, - "Coll bal mismatch" - ); - assertEq(A.balance, ethBalanceBefore + ETH_GAS_COMPENSATION, "ETH bal mismatch"); - } else { - assertEq(contractsArray[_branch].collToken.balanceOf(A), collBalanceBefore, "Coll bal mismatch"); - assertGe(A.balance, ethBalanceBefore, "ETH bal should not decrease"); - assertApproxEqAbs( - A.balance, ethBalanceBefore + collAmount + ETH_GAS_COMPENSATION - debtInColl, 3e17, "ETH bal mismatch" - ); - } - } - - function testCannotCloseTroveWithBaseZapperIfLessCollThanExpected() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotCloseTroveIfLessCollThanExpected(baseZapperArray[i], i); - } - } - - function testCannotCloseTroveWithLeverageCurveIfLessCollThanExpected() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotCloseTroveIfLessCollThanExpected(IZapper(leverageZapperCurveArray[i]), i); - } - } - - function testCannotCloseTroveWithLeverageUniV3IfLessCollThanExpected() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotCloseTroveIfLessCollThanExpected(IZapper(leverageZapperUniV3Array[i]), i); - } - } - - function testCannotCloseTroveWithLeverageHybridIfLessCollThanExpected() external { - for (uint256 i = 0; i < 3; i++) { - _testCannotCloseTroveIfLessCollThanExpected(IZapper(leverageZapperHybridArray[i]), i); - } - } - - function _testCannotCloseTroveIfLessCollThanExpected(IZapper _zapper, uint256 _branch) internal { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - bool lst = _branch > 0; - uint256 troveId = openTrove(_zapper, A, 0, collAmount, boldAmount, lst); - - // open a 2nd trove so we can close the 1st one - openTrove(_zapper, B, 0, 100 ether, 10000e18, lst); - - // Try to close trove - // This should be done in the frontend - (uint256 flashLoanAmount, uint256 minExpectedCollateral) = - _getCloseFlashLoanAmount(troveId, contractsArray[_branch].troveManager, contractsArray[_branch].priceFeed); - - string memory revertReason = lst ? "GCZ: Not enough collateral received" : "WZ: Not enough collateral received"; - vm.startPrank(A); - vm.expectRevert(bytes(revertReason)); - _zapper.closeTroveFromCollateral(troveId, flashLoanAmount, minExpectedCollateral * 2); - vm.stopPrank(); - } - - function testCannotCloseTroveIfFrontRunByRedemption() external { - // Make sure redemption rate is not 100% - vm.warp(block.timestamp + 18 hours); - - IZapper zapper = IZapper(leverageZapperHybridArray[0]); - - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - // open a 2nd trove so we can close the A's one, with higher interest so it doesn't get redeemed - openTrove(zapper, B, 0, 100 ether, 10000e18, false, 1e17); - - uint256 troveId = openTrove(zapper, A, 0, collAmount, boldAmount, false); - - // Try to close trove - // This should be done in the frontend - (uint256 flashLoanAmount, uint256 minExpectedCollateral) = - _getCloseFlashLoanAmount(troveId, contractsArray[0].troveManager, contractsArray[0].priceFeed); - - // Now attacker redeems from trove and increases Bold price - vm.startPrank(B); - // Redemption - collateralRegistry.redeemCollateral(10000e18, 0, 1e18); - uint256 troveDebt = getTroveEntireDebt(contractsArray[0].troveManager, troveId); - uint256 troveColl = getTroveEntireColl(contractsArray[0].troveManager, troveId); - assertLt(troveDebt, boldAmount, "Trove debt should have decreased"); - assertLt(troveColl, collAmount, "Trove coll should have decreased"); - - // Swap WETH to USDC to increase price - uint256 swapWETHAmount = 10000e18; - deal(address(WETH), B, swapWETHAmount); - WETH.approve(address(uniV3Router), swapWETHAmount); - bytes memory path = abi.encodePacked(WETH, UNIV3_FEE_USDC_WETH, USDC); - ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: path, - recipient: B, - deadline: block.timestamp, - amountIn: swapWETHAmount, - amountOutMinimum: 0 - }); - - uniV3Router.exactInput(params); - vm.stopPrank(); - - vm.startPrank(A); - vm.expectRevert("WZ: Not enough collateral received"); - zapper.closeTroveFromCollateral(troveId, flashLoanAmount, minExpectedCollateral); - vm.stopPrank(); - } - - function testOnlyFlashLoanProviderCanCallCloseTroveCallbackWithBaseZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallCloseTroveCallback(baseZapperArray[i], i); - } - } - - function testOnlyFlashLoanProviderCanCallCloseTroveCallbackWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallCloseTroveCallback(leverageZapperCurveArray[i], i); - } - } - - function testOnlyFlashLoanProviderCanCallCloseTroveCallbackWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallCloseTroveCallback(leverageZapperUniV3Array[i], i); - } - } - - function testOnlyFlashLoanProviderCanCallCloseTroveCallbackWithHybrid() external { - for (uint256 i = 0; i < 3; i++) { - _testOnlyFlashLoanProviderCanCallCloseTroveCallback(leverageZapperHybridArray[i], i); - } - } - - function _testOnlyFlashLoanProviderCanCallCloseTroveCallback(IZapper _zapper, uint256 _branch) internal { - IZapper.CloseTroveParams memory params = IZapper.CloseTroveParams({ - troveId: addressToTroveIdThroughZapper(address(_zapper), A), - flashLoanAmount: 10 ether, - minExpectedCollateral: 0, - receiver: address(0) // Set later - }); - - bool lst = _branch > 0; - string memory revertReason = lst ? "GCZ: Caller not FlashLoan provider" : "WZ: Caller not FlashLoan provider"; - vm.startPrank(A); - vm.expectRevert(bytes(revertReason)); - IFlashLoanReceiver(address(_zapper)).receiveFlashLoanOnCloseTroveFromCollateral(params, 10 ether); - vm.stopPrank(); - } - - function testOnlyOwnerOrManagerCanCloseTroveWithBaseZapperFromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromZapper(baseZapperArray[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithCurveFromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromZapper(leverageZapperCurveArray[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithUniV3FromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromZapper(leverageZapperUniV3Array[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithHybridFromZapper() external { - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromZapper(leverageZapperHybridArray[i], i); - } - } - - function _testOnlyOwnerOrManagerCanCloseTroveFromZapper(IZapper _zapper, uint256 _branch) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - bool lst = _branch > 0; - uint256 troveId = openTrove(_zapper, A, 0, collAmount, boldAmount, lst); - - // B tries to close A’s trove - (uint256 flashLoanAmount, uint256 minExpectedCollateral) = - _getCloseFlashLoanAmount(troveId, contractsArray[_branch].troveManager, contractsArray[_branch].priceFeed); - - vm.startPrank(B); - vm.expectRevert(AddRemoveManagers.NotOwnerNorRemoveManager.selector); - _zapper.closeTroveFromCollateral(troveId, flashLoanAmount, minExpectedCollateral); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(_zapper.flashLoanProvider().receiver()), address(0), "Receiver should be zero"); - } - - function testOnlyOwnerOrManagerCanCloseTroveWithBaseZapperFromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerFLProvider(baseZapperArray[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithCurveFromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerFLProvider(leverageZapperCurveArray[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithUniV3FromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerFLProvider(leverageZapperUniV3Array[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithHybridFromBalancerFLProvider() external { - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerFLProvider(leverageZapperHybridArray[i], i); - } - } - - function _testOnlyOwnerOrManagerCanCloseTroveFromBalancerFLProvider(IZapper _zapper, uint256 _branch) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - bool lst = _branch > 0; - uint256 troveId = openTrove(_zapper, A, 0, collAmount, boldAmount, lst); - - // B tries to close A’s trove calling our flash loan provider module - (uint256 flashLoanAmount, uint256 minExpectedCollateral) = - _getCloseFlashLoanAmount(troveId, contractsArray[_branch].troveManager, contractsArray[_branch].priceFeed); - - IZapper.CloseTroveParams memory params = IZapper.CloseTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - minExpectedCollateral: minExpectedCollateral, - receiver: address(0) // Set later - }); - IFlashLoanProvider flashLoanProvider = _zapper.flashLoanProvider(); - vm.startPrank(B); - vm.expectRevert(); // reverts without data because it calls back B - flashLoanProvider.makeFlashLoan( - contractsArray[_branch].collToken, - flashLoanAmount, - IFlashLoanProvider.Operation.CloseTrove, - abi.encode(params) - ); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(flashLoanProvider.receiver()), address(0), "Receiver should be zero"); - } - - function testOnlyOwnerOrManagerCanCloseTroveWithBaseZapperFromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerVault(baseZapperArray[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithCurveFromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerVault(leverageZapperCurveArray[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithUniV3FromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerVault(leverageZapperUniV3Array[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithHybridFromBalancerVault() external { - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerVault(leverageZapperHybridArray[i], i); - } - } - - function _testOnlyOwnerOrManagerCanCloseTroveFromBalancerVault(IZapper _zapper, uint256 _branch) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - bool lst = _branch > 0; - uint256 troveId = openTrove(_zapper, A, 0, collAmount, boldAmount, lst); - - // B tries to close A’s trove calling Balancer Vault directly - (uint256 flashLoanAmount, uint256 minExpectedCollateral) = - _getCloseFlashLoanAmount(troveId, contractsArray[_branch].troveManager, contractsArray[_branch].priceFeed); - - IFlashLoanProvider flashLoanProvider = _zapper.flashLoanProvider(); - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = contractsArray[_branch].collToken; - uint256[] memory amounts = new uint256[](1); - amounts[0] = flashLoanAmount; - bytes memory userData = abi.encode( - address(_zapper), IFlashLoanProvider.Operation.CloseTrove, troveId, flashLoanAmount, minExpectedCollateral - ); - IVault vault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - vm.startPrank(B); - vm.expectRevert("Flash loan not properly initiated"); - vault.flashLoan(IFlashLoanRecipient(address(flashLoanProvider)), tokens, amounts, userData); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(flashLoanProvider.receiver()), address(0), "Receiver should be zero"); - } - - function testApprovalIsNotReset() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testApprovalIsNotReset(leverageZapperCurveArray[i], ExchangeType.Curve, i); - _testApprovalIsNotReset(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - for (uint256 i = 0; i < 3; i++) { - _testApprovalIsNotReset(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i); - } - } - - function _testApprovalIsNotReset(ILeverageZapper _leverageZapper, ExchangeType _exchangeType, uint256 _branch) - internal - { - // Open non leveraged trove - openTrove(_leverageZapper, A, uint256(_exchangeType) * 2, 10 ether, 10000e18, _branch > 0); - - // Now try to open leveraged trove, it should still work - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = uint256(_exchangeType) * 2 + 1; - openTroveParams.collAmount = 10 ether; - openTroveParams.leverageRatio = 1.5 ether; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - assertGt(getTroveEntireColl(contractsArray[_branch].troveManager, troveId), 0); - assertGt(getTroveEntireDebt(contractsArray[_branch].troveManager, troveId), 0); - } - - // helper price functions - - // Helper to get the actual bold we need, capped by a max value, to get flash loan amount - function _getBoldAmountToSwap( - ExchangeType _exchangeType, - uint256 _branch, - uint256 _boldAmount, - uint256 _maxBoldAmount, - uint256 _minCollAmount, - IERC20 _collToken - ) internal returns (uint256) { - if (_exchangeType == ExchangeType.Curve) { - return _getBoldAmountToSwapCurve(_branch, _boldAmount, _maxBoldAmount, _minCollAmount); - } - - if (_exchangeType == ExchangeType.UniV3) { - return _getBoldAmountToSwapUniV3(_maxBoldAmount, _minCollAmount, _collToken); - } - - return _getBoldAmountToSwapHybrid(_maxBoldAmount, _minCollAmount, _collToken); - } - - function _getBoldAmountToSwapCurve( - uint256 _branch, - uint256 _boldAmount, - uint256 _maxBoldAmount, - uint256 _minCollAmount - ) internal view returns (uint256) { - ICurvePool curvePool = CurveExchange(address(leverageZapperCurveArray[_branch].exchange())).curvePool(); - - uint256 step = (_maxBoldAmount - _boldAmount) / 5; // In max 5 iterations we should reach the target, unless price is lower - uint256 dy; - // TODO: Optimizations: binary search, change the step depending on last dy, ... - // Or check if there’s any helper implemented anywhere - uint256 lastBoldAmount = _maxBoldAmount + step; - do { - lastBoldAmount -= step; - dy = curvePool.get_dy(BOLD_TOKEN_INDEX, COLL_TOKEN_INDEX, lastBoldAmount); - } while (dy > _minCollAmount && lastBoldAmount > step); - - uint256 boldAmountToSwap = dy >= _minCollAmount ? lastBoldAmount : lastBoldAmount + step; - require(boldAmountToSwap <= _maxBoldAmount, "Bold amount required too high"); - - return boldAmountToSwap; - } - - // See: https://docs.uniswap.org/contracts/v3/reference/periphery/interfaces/IQuoterV2 - // These functions are not marked view because they rely on calling non-view functions and reverting to compute the result. - // They are also not gas efficient and should not be called on-chain. - function _getBoldAmountToSwapUniV3(uint256 _maxBoldAmount, uint256 _minCollAmount, IERC20 _collToken) - internal /* view */ - returns (uint256) - { - IQuoterV2.QuoteExactOutputSingleParams memory params = IQuoterV2.QuoteExactOutputSingleParams({ - tokenIn: address(boldToken), - tokenOut: address(_collToken), - amount: _minCollAmount, - fee: UNIV3_FEE, - sqrtPriceLimitX96: 0 - }); - (uint256 amountIn,,,) = uniV3Quoter.quoteExactOutputSingle(params); - require(amountIn <= _maxBoldAmount, "Price too high"); - - return amountIn; - } - - function _getBoldAmountToSwapHybrid(uint256 _maxBoldAmount, uint256 _minCollAmount, IERC20 _collToken) - internal /* view */ - returns (uint256) - { - // Uniswap - uint256 wethAmount; - IQuoterV2.QuoteExactOutputSingleParams memory quoterParams; - // Coll <- WETH - if (address(WETH) != address(_collToken)) { - quoterParams = IQuoterV2.QuoteExactOutputSingleParams({ - tokenIn: address(WETH), - tokenOut: address(_collToken), - amount: _minCollAmount, - fee: UNIV3_FEE_WETH_COLL, - sqrtPriceLimitX96: 0 - }); - (wethAmount,,,) = uniV3Quoter.quoteExactOutputSingle(quoterParams); - } else { - wethAmount = _minCollAmount; - } - // WETH <- USDC - quoterParams = IQuoterV2.QuoteExactOutputSingleParams({ - tokenIn: address(USDC), - tokenOut: address(WETH), - amount: wethAmount, - fee: UNIV3_FEE_USDC_WETH, - sqrtPriceLimitX96: 0 - }); - (uint256 usdcAmount,,,) = uniV3Quoter.quoteExactOutputSingle(quoterParams); - - // Curve - // USDC <- BOLD - uint256 boldAmountToSwap = usdcCurvePool.get_dx(int128(BOLD_TOKEN_INDEX), int128(USDC_INDEX), usdcAmount); - require(boldAmountToSwap <= _maxBoldAmount, "Bold amount required too high"); - - boldAmountToSwap = Math.min(boldAmountToSwap * 101 / 100, _maxBoldAmount); // TODO - - return boldAmountToSwap; - } - - // Helpers - function testHybridExchangeHelpers() public { - for (uint256 i = 0; i < 3; i++) { - (uint256 price,) = contractsArray[i].priceFeed.fetchPrice(); - //console2.log(i, "branch"); - //console2.log(price, "price"); - //console2.log(price, "amount"); - _testHybridExchangeHelpers(price, contractsArray[i].collToken, 1 ether, 1e16); // 1% slippage - //console2.log(price * 1e3, "amount"); - _testHybridExchangeHelpers(price * 1e3, contractsArray[i].collToken, 1 ether, 1e16); // 1% slippage - //console2.log(price * 1e6, "amount"); - _testHybridExchangeHelpers(price * 1e6, contractsArray[i].collToken, 1 ether, 1e16); // 1% slippage - } - } - - function _testHybridExchangeHelpers( - uint256 _boldAmount, - IERC20 _collToken, - uint256 _desiredCollAmount, - uint256 _acceptedSlippage - ) internal { - (uint256 collAmount, uint256 slippage) = - hybridCurveUniV3ExchangeHelpers.getCollFromBold(_boldAmount, _collToken, _desiredCollAmount); - //console2.log(collAmount, "collAmount"); - //console2.log(slippage, "slippage"); - assertGe(collAmount, (DECIMAL_PRECISION - slippage) * _desiredCollAmount / DECIMAL_PRECISION); - assertLe(slippage, _acceptedSlippage); - } - - function testHybridExchangeHelpersNoDeviation() public { - (uint256 price,) = contractsArray[0].priceFeed.fetchPrice(); - (uint256 collAmount, uint256 slippage) = - hybridCurveUniV3ExchangeHelpers.getCollFromBold(price, contractsArray[0].collToken, 0); - assertGt(collAmount, 0); - assertEq(slippage, 0); - } -} diff --git a/contracts/test/zapperWETH.t.sol b/contracts/test/zapperWETH.t.sol deleted file mode 100644 index 7eebf7b2e..000000000 --- a/contracts/test/zapperWETH.t.sol +++ /dev/null @@ -1,932 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.18; - -import "./TestContracts/DevTestSetup.sol"; -import "./TestContracts/WETH.sol"; -import "src/Zappers/WETHZapper.sol"; - -contract ZapperWETHTest is DevTestSetup { - function setUp() public override { - // Start tests at a non-zero timestamp - vm.warp(block.timestamp + 600); - - accounts = new Accounts(); - createAccounts(); - - (A, B, C, D, E, F, G) = ( - accountsList[0], - accountsList[1], - accountsList[2], - accountsList[3], - accountsList[4], - accountsList[5], - accountsList[6] - ); - - WETH = new WETH9(); - - TestDeployer.TroveManagerParams[] memory troveManagerParams = new TestDeployer.TroveManagerParams[](1); - troveManagerParams[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 10e16, 110e16, 5e16, 10e16); - - TestDeployer deployer = new TestDeployer(); - TestDeployer.LiquityContractsDev[] memory contractsArray; - TestDeployer.Zappers[] memory zappersArray; - (contractsArray, collateralRegistry, boldToken,,, zappersArray) = - deployer.deployAndConnectContracts(troveManagerParams, WETH); - - // Set price feeds - contractsArray[0].priceFeed.setPrice(2000e18); - - // Give some Collateral to test accounts - uint256 initialCollateralAmount = 10_000e18; - - // A to F - for (uint256 i = 0; i < 6; i++) { - // Give some raw ETH to test accounts - deal(accountsList[i], initialCollateralAmount); - } - - // Set first branch as default - addressesRegistry = contractsArray[0].addressesRegistry; - borrowerOperations = contractsArray[0].borrowerOperations; - troveManager = contractsArray[0].troveManager; - troveNFT = contractsArray[0].troveNFT; - wethZapper = zappersArray[0].wethZapper; - } - - function testCanOpenTrove() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - uint256 ethBalanceBefore = A.balance; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - assertEq(troveNFT.ownerOf(troveId), A, "Wrong owner"); - assertGt(troveId, 0, "Trove id should be set"); - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore - (ethAmount + ETH_GAS_COMPENSATION), "ETH bal mismatch"); - } - - function testCanOpenTroveWithBatchManager() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - uint256 ethBalanceBefore = A.balance; - - registerBatchManager(B); - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 0, - batchManager: B, - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - assertEq(troveNFT.ownerOf(troveId), A, "Wrong owner"); - assertGt(troveId, 0, "Trove id should be set"); - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore - (ethAmount + ETH_GAS_COMPENSATION), "ETH bal mismatch"); - assertEq(borrowerOperations.interestBatchManagerOf(troveId), B, "Wrong batch manager"); - (,,,,,,,, address tmBatchManagerAddress,) = troveManager.Troves(troveId); - assertEq(tmBatchManagerAddress, B, "Wrong batch manager (TM)"); - } - - function testCanNotOpenTroveWithBatchManagerAndInterest() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - registerBatchManager(B); - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: B, - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - vm.expectRevert("WZ: Cannot choose interest if joining a batch"); - wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - } - - function testCanAddColl() external { - uint256 ethAmount1 = 10 ether; - uint256 boldAmount = 10000e18; - uint256 ethAmount2 = 5 ether; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 ethBalanceBefore = A.balance; - vm.startPrank(A); - wethZapper.addCollWithRawETH{value: ethAmount2}(troveId); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount1 + ethAmount2, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore - ethAmount2, "ETH bal mismatch"); - } - - function testCanWithdrawColl() external { - uint256 ethAmount1 = 10 ether; - uint256 boldAmount = 10000e18; - uint256 ethAmount2 = 1 ether; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 ethBalanceBefore = A.balance; - vm.startPrank(A); - wethZapper.withdrawCollToRawETH(troveId, ethAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount1 - ethAmount2, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore + ethAmount2, "ETH bal mismatch"); - } - - function testCannotWithdrawCollIfZapperIsNotReceiver() external { - uint256 ethAmount1 = 10 ether; - uint256 boldAmount = 10000e18; - uint256 ethAmount2 = 1 ether; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(wethZapper), B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - wethZapper.withdrawCollToRawETH(troveId, ethAmount2); - vm.stopPrank(); - } - - function testCanNotAddReceiverWithoutRemoveManager() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // Try to add a receiver for the zapper without remove manager - vm.startPrank(A); - vm.expectRevert(AddRemoveManagers.EmptyManager.selector); - wethZapper.setRemoveManagerWithReceiver(troveId, address(0), B); - vm.stopPrank(); - } - - function testCanRepayBold() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 ethBalanceBeforeA = A.balance; - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 ethBalanceBeforeB = B.balance; - - // Add a remove manager for the zapper, and send bold - vm.startPrank(A); - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - boldToken.transfer(B, boldAmount2); - vm.stopPrank(); - - // Approve and repay - vm.startPrank(B); - boldToken.approve(address(wethZapper), boldAmount2); - wethZapper.repayBold(troveId, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 - boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA - boldAmount2, "A BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBeforeA, "A ETH bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(B.balance, ethBalanceBeforeB, "B ETH bal mismatch"); - } - - function testCanWithdrawBold() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 ethBalanceBeforeA = A.balance; - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 ethBalanceBeforeB = B.balance; - - // Add a remove manager for the zapper - vm.startPrank(A); - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Withdraw bold - vm.startPrank(B); - wethZapper.withdrawBold(troveId, boldAmount2, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 + boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBeforeA, "A ETH bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(B.balance, ethBalanceBeforeB, "B ETH bal mismatch"); - } - - function testCannotWithdrawBoldIfZapperIsNotReceiver() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // Add a remove manager for the zapper - vm.startPrank(A); - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(wethZapper), C); - vm.stopPrank(); - - // Withdraw bold - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - wethZapper.withdrawBold(troveId, boldAmount2, boldAmount2); - vm.stopPrank(); - } - - // TODO: more adjustment combinations - function testCanAdjustTroveWithdrawCollAndBold() external { - uint256 ethAmount1 = 10 ether; - uint256 ethAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 ethBalanceBeforeA = A.balance; - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 ethBalanceBeforeB = B.balance; - - // Add a remove manager for the zapper - vm.startPrank(A); - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - wethZapper.adjustTroveWithRawETH(troveId, ethAmount2, false, boldAmount2, true, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount1 - ethAmount2, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 + boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBeforeA + ethAmount2, "A ETH bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(B.balance, ethBalanceBeforeB, "B ETH bal mismatch"); - } - - function testCannotAdjustTroveWithdrawCollAndBoldIfZapperIsNotReceiver() external { - uint256 ethAmount1 = 10 ether; - uint256 ethAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(wethZapper), C); - vm.stopPrank(); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - wethZapper.adjustTroveWithRawETH(troveId, ethAmount2, false, boldAmount2, true, boldAmount2); - vm.stopPrank(); - } - - function testCanAdjustTroveAddCollAndBold() external { - uint256 ethAmount1 = 10 ether; - uint256 ethAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - // A sends Bold to B - boldToken.transfer(B, boldAmount2); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 ethBalanceBeforeA = A.balance; - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 ethBalanceBeforeB = B.balance; - - // Add an add manager for the zapper - vm.startPrank(A); - wethZapper.setAddManager(troveId, B); - vm.stopPrank(); - - // Adjust (add coll and Bold) - vm.startPrank(B); - boldToken.approve(address(wethZapper), boldAmount2); - wethZapper.adjustTroveWithRawETH{value: ethAmount2}(troveId, ethAmount2, true, boldAmount2, false, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount1 + ethAmount2, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 - boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA, "A BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBeforeA, "A ETH bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB - boldAmount2, "B BOLD bal mismatch"); - assertEq(B.balance, ethBalanceBeforeB - ethAmount2, "B ETH bal mismatch"); - } - - function testCanAdjustZombieTroveWithdrawCollAndBold() external { - uint256 ethAmount1 = 10 ether; - uint256 ethAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // Add a remove manager for the zapper - vm.startPrank(A); - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Redeem to make trove zombie - vm.startPrank(A); - collateralRegistry.redeemCollateral(boldAmount1 - boldAmount2, 10, 1e18); - vm.stopPrank(); - - uint256 troveCollBefore = troveManager.getTroveEntireColl(troveId); - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 ethBalanceBeforeA = A.balance; - uint256 ethBalanceBeforeB = B.balance; - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - wethZapper.adjustZombieTroveWithRawETH(troveId, ethAmount2, false, boldAmount2, true, 0, 0, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), troveCollBefore - ethAmount2, "Trove coll mismatch"); - assertApproxEqAbs(troveManager.getTroveEntireDebt(troveId), 2 * boldAmount2, 2e18, "Trove debt mismatch"); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBeforeA + ethAmount2, "A ETH bal mismatch"); - assertEq(boldToken.balanceOf(B), 0, "B BOLD bal mismatch"); - assertEq(B.balance, ethBalanceBeforeB, "B ETH bal mismatch"); - } - - function testCannotAdjustZombieTroveWithdrawCollAndBoldIfZapperIsNotReceiver() external { - uint256 ethAmount1 = 10 ether; - uint256 ethAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(wethZapper), C); - vm.stopPrank(); - - // Redeem to make trove zombie - vm.startPrank(A); - collateralRegistry.redeemCollateral(boldAmount1 - boldAmount2, 10, 1e18); - vm.stopPrank(); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - wethZapper.adjustZombieTroveWithRawETH(troveId, ethAmount2, false, boldAmount2, true, 0, 0, boldAmount2); - vm.stopPrank(); - } - - function testCanAdjustZombieTroveAddCollAndWithdrawBold() external { - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: 10000e18, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: 10 ether + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // Add a remove manager for the zapper - vm.startPrank(A); - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - uint256 ethAmount2 = 1 ether; - uint256 boldAmount2 = 1000e18; - - // Redeem to make trove zombie - vm.startPrank(A); - collateralRegistry.redeemCollateral(10000e18 - boldAmount2, 10, 1e18); - vm.stopPrank(); - - uint256 troveCollBefore = troveManager.getTroveEntireColl(troveId); - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 ethBalanceBeforeA = A.balance; - uint256 ethBalanceBeforeB = B.balance; - - // Adjust (add coll and withdraw Bold) - vm.startPrank(B); - wethZapper.adjustZombieTroveWithRawETH{value: ethAmount2}( - troveId, ethAmount2, true, boldAmount2, true, 0, 0, boldAmount2 - ); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), troveCollBefore + ethAmount2, "Trove coll mismatch"); - assertApproxEqAbs(troveManager.getTroveEntireDebt(troveId), 2 * boldAmount2, 2e18, "Trove debt mismatch"); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBeforeA, "A ETH bal mismatch"); - assertEq(boldToken.balanceOf(B), 0, "B BOLD bal mismatch"); - assertEq(B.balance, ethBalanceBeforeB - ethAmount2, "B ETH bal mismatch"); - } - - function testCannotAdjustZombieTroveAddCollAndWithdrawBoldIfZapperIsNotReceiver() external { - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: 10000e18, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: 10 ether + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(wethZapper), C); - vm.stopPrank(); - - uint256 ethAmount2 = 1 ether; - uint256 boldAmount2 = 1000e18; - - // Redeem to make trove zombie - vm.startPrank(A); - collateralRegistry.redeemCollateral(10000e18 - boldAmount2, 10, 1e18); - vm.stopPrank(); - - // Adjust (add coll and withdraw Bold) - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - wethZapper.adjustZombieTroveWithRawETH{value: ethAmount2}( - troveId, ethAmount2, true, boldAmount2, true, 0, 0, boldAmount2 - ); - vm.stopPrank(); - } - - function testCanCloseTrove() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - uint256 ethBalanceBefore = A.balance; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // open a 2nd trove so we can close the 1st one, and send Bold to account for interest and fee - vm.startPrank(B); - deal(address(WETH), B, 100 ether + ETH_GAS_COMPENSATION); - WETH.approve(address(borrowerOperations), 100 ether + ETH_GAS_COMPENSATION); - borrowerOperations.openTrove( - B, - 0, // index, - 100 ether, // coll, - 10000e18, //boldAmount, - 0, // _upperHint - 0, // _lowerHint - MIN_ANNUAL_INTEREST_RATE, // annualInterestRate, - 10000e18, // upfrontFee - address(0), - address(0), - address(0) - ); - boldToken.transfer(A, troveManager.getTroveEntireDebt(troveId) - boldAmount); - vm.stopPrank(); - - vm.startPrank(A); - boldToken.approve(address(wethZapper), type(uint256).max); - wethZapper.closeTroveToRawETH(troveId); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), 0, "Coll mismatch"); - assertEq(troveManager.getTroveEntireDebt(troveId), 0, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), 0, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore, "ETH bal mismatch"); - } - - function testCannotCloseTroveIfZapperIsNotReceiver() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // open a 2nd trove so we can close the 1st one, and send Bold to account for interest and fee - vm.startPrank(B); - deal(address(WETH), B, 100 ether + ETH_GAS_COMPENSATION); - WETH.approve(address(borrowerOperations), 100 ether + ETH_GAS_COMPENSATION); - borrowerOperations.openTrove( - B, - 0, // index, - 100 ether, // coll, - 10000e18, //boldAmount, - 0, // _upperHint - 0, // _lowerHint - MIN_ANNUAL_INTEREST_RATE, // annualInterestRate, - 10000e18, // upfrontFee - address(0), - address(0), - address(0) - ); - boldToken.transfer(A, troveManager.getTroveEntireDebt(troveId) - boldAmount); - vm.stopPrank(); - - vm.startPrank(A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(wethZapper), C); - - boldToken.approve(address(wethZapper), type(uint256).max); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - wethZapper.closeTroveToRawETH(troveId); - vm.stopPrank(); - } - - function testExcessRepaymentByAdjustGoesBackToUser() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 ethBalanceBefore = A.balance; - uint256 collBalanceBefore = WETH.balanceOf(A); - uint256 boldDebtBefore = troveManager.getTroveEntireDebt(troveId); - - // Adjust trove: remove 1 ETH and try to repay 9k (only will repay ~8k, up to MIN_DEBT) - vm.startPrank(A); - boldToken.approve(address(wethZapper), type(uint256).max); - wethZapper.adjustTroveWithRawETH(troveId, 1 ether, false, 9000e18, false, 0); - vm.stopPrank(); - - assertEq(boldToken.balanceOf(A), boldAmount + MIN_DEBT - boldDebtBefore, "BOLD bal mismatch"); - assertEq(boldToken.balanceOf(address(wethZapper)), 0, "Zapper BOLD bal should be zero"); - assertEq(A.balance, ethBalanceBefore + 1 ether, "ETH bal mismatch"); - assertEq(address(wethZapper).balance, 0, "Zapper ETH bal should be zero"); - assertEq(WETH.balanceOf(A), collBalanceBefore, "Coll bal mismatch"); - assertEq(WETH.balanceOf(address(wethZapper)), 0, "Zapper Coll bal should be zero"); - } - - function testExcessRepaymentByRepayGoesBackToUser() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldDebtBefore = troveManager.getTroveEntireDebt(troveId); - uint256 collBalanceBefore = WETH.balanceOf(A); - - // Adjust trove: try to repay 9k (only will repay ~8k, up to MIN_DEBT) - vm.startPrank(A); - boldToken.approve(address(wethZapper), type(uint256).max); - wethZapper.repayBold(troveId, 9000e18); - vm.stopPrank(); - - assertEq(boldToken.balanceOf(A), boldAmount + MIN_DEBT - boldDebtBefore, "BOLD bal mismatch"); - assertEq(boldToken.balanceOf(address(wethZapper)), 0, "Zapper BOLD bal should be zero"); - assertEq(address(wethZapper).balance, 0, "Zapper ETH bal should be zero"); - assertEq(WETH.balanceOf(A), collBalanceBefore, "Coll bal mismatch"); - assertEq(WETH.balanceOf(address(wethZapper)), 0, "Zapper Coll bal should be zero"); - } - - // TODO: tests for add/remove managers of zapper contract -} From c5367c1a567b24143b368eede2cb4f757a9fb448 Mon Sep 17 00:00:00 2001 From: Philip Paetz Date: Fri, 22 Aug 2025 10:10:20 +0200 Subject: [PATCH 04/79] fix: macOS case-insensitive path --- contracts/script/DeployLiquity2.s.sol | 4 ++-- contracts/src/tokens/StableTokenV3.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 58e2e0d8f..c9a1b2059 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -8,7 +8,7 @@ import {IERC20 as IERC20_GOV} from "openzeppelin-contracts/contracts/token/ERC20 import {ProxyAdmin} from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; import {TransparentUpgradeableProxy} from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {IFPMMFactory} from "src/interfaces/IFPMMFactory.sol"; +import {IFPMMFactory} from "src/Interfaces/IFPMMFactory.sol"; import {ETH_GAS_COMPENSATION} from "src/Dependencies/Constants.sol"; import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; @@ -32,7 +32,7 @@ import "src/StabilityPool.sol"; import "src/CollateralRegistry.sol"; import "src/tokens/StableTokenV3.sol"; -import "src/interfaces/IStableTokenV3.sol"; +import "src/Interfaces/IStableTokenV3.sol"; import "test/TestContracts/PriceFeedTestnet.sol"; import "test/TestContracts/MetadataDeployment.sol"; import "test/Utils/Logging.sol"; diff --git a/contracts/src/tokens/StableTokenV3.sol b/contracts/src/tokens/StableTokenV3.sol index 0ead98372..26e5d5d8f 100644 --- a/contracts/src/tokens/StableTokenV3.sol +++ b/contracts/src/tokens/StableTokenV3.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.24; import {ERC20PermitUpgradeable} from "./patched/ERC20PermitUpgradeable.sol"; import {ERC20Upgradeable} from "./patched/ERC20Upgradeable.sol"; -import {IStableTokenV3} from "../interfaces/IStableTokenV3.sol"; +import {IStableTokenV3} from "../Interfaces/IStableTokenV3.sol"; /** * @title ERC20 token with minting and burning permissiones to a minter and burner roles. From 14be9e32e9aedb39d142a9f6d2a6381760ccbe21 Mon Sep 17 00:00:00 2001 From: philbow61 Date: Fri, 22 Aug 2025 13:54:44 +0200 Subject: [PATCH 05/79] fix: contract creation collision --- contracts/test/TestContracts/Deployment.t.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index 1f591e905..331c9aed1 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -562,6 +562,7 @@ contract TestDeployer is MetadataDeployment { assert(address(metadataNFT) == addresses.metadataNFT); // Pre-calc addresses + bytes32 stabilityPoolSalt = keccak256(abi.encodePacked(address(contracts.addressesRegistry))); addresses.borrowerOperations = getAddress( address(this), getBytecode(type(BorrowerOperationsTester).creationCode, address(contracts.addressesRegistry)), @@ -571,7 +572,8 @@ contract TestDeployer is MetadataDeployment { addresses.troveNFT = getAddress( address(this), getBytecode(type(TroveNFT).creationCode, address(contracts.addressesRegistry)), SALT ); - addresses.stabilityPool = getAddress(address(this), getBytecode(type(StabilityPool).creationCode, false), SALT); + addresses.stabilityPool = + getAddress(address(this), getBytecode(type(StabilityPool).creationCode, false), stabilityPoolSalt); addresses.activePool = getAddress( address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry)), SALT ); @@ -617,7 +619,7 @@ contract TestDeployer is MetadataDeployment { contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry); contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry); contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); - contracts.stabilityPool = new StabilityPool{salt: SALT}(false); + contracts.stabilityPool = new StabilityPool{salt: stabilityPoolSalt}(false); contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry); contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); @@ -634,6 +636,8 @@ contract TestDeployer is MetadataDeployment { assert(address(contracts.collSurplusPool) == addresses.collSurplusPool); assert(address(contracts.sortedTroves) == addresses.sortedTroves); + contracts.stabilityPool.initialize(contracts.addressesRegistry); + // Connect contracts _params.boldToken.setBranchAddresses( address(contracts.troveManager), From 4c19ade48075bd9e2411d0f52391abd0c2c168ed Mon Sep 17 00:00:00 2001 From: Philip Paetz Date: Mon, 25 Aug 2025 14:19:18 +0200 Subject: [PATCH 06/79] chore: trigger CI after env var change From 7c87877fd513f82b1eb66cd23e1f1947f52533d8 Mon Sep 17 00:00:00 2001 From: Philip Paetz Date: Wed, 3 Sep 2025 15:29:03 +0200 Subject: [PATCH 07/79] chore: address PR feedback --- contracts/src/Interfaces/IStableTokenV3.sol | 2 +- contracts/src/MultiTroveGetter.sol | 2 - contracts/src/TroveNFT.sol | 1 - contracts/src/tokens/StableTokenV3.sol | 541 ++++++++++---------- 4 files changed, 271 insertions(+), 275 deletions(-) diff --git a/contracts/src/Interfaces/IStableTokenV3.sol b/contracts/src/Interfaces/IStableTokenV3.sol index a0b7c1e37..5db40bbfe 100644 --- a/contracts/src/Interfaces/IStableTokenV3.sol +++ b/contracts/src/Interfaces/IStableTokenV3.sol @@ -209,4 +209,4 @@ interface IStableTokenV3 { uint256 gatewayFee, uint256 baseTxFee ) external; -} +} \ No newline at end of file diff --git a/contracts/src/MultiTroveGetter.sol b/contracts/src/MultiTroveGetter.sol index 8141eb18c..9bee71786 100644 --- a/contracts/src/MultiTroveGetter.sol +++ b/contracts/src/MultiTroveGetter.sol @@ -6,8 +6,6 @@ import "./Interfaces/ICollateralRegistry.sol"; import "./Interfaces/IMultiTroveGetter.sol"; import "./Interfaces/ISortedTroves.sol"; import "./Types/BatchId.sol"; -import "./Types/LatestTroveData.sol"; -import "./Types/LatestBatchData.sol"; /* Helper contract for grabbing Trove data for the front end. Not part of the core Liquity system. */ contract MultiTroveGetter is IMultiTroveGetter { diff --git a/contracts/src/TroveNFT.sol b/contracts/src/TroveNFT.sol index 50644e7e4..6bc2d9a2c 100644 --- a/contracts/src/TroveNFT.sol +++ b/contracts/src/TroveNFT.sol @@ -7,7 +7,6 @@ import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.s import "./Interfaces/ITroveNFT.sol"; import "./Interfaces/IAddressesRegistry.sol"; -import "./Interfaces/IBoldToken.sol"; import "./Types/LatestTroveData.sol"; import {IMetadataNFT} from "./NFTMetadata/MetadataNFT.sol"; diff --git a/contracts/src/tokens/StableTokenV3.sol b/contracts/src/tokens/StableTokenV3.sol index 26e5d5d8f..50da08e69 100644 --- a/contracts/src/tokens/StableTokenV3.sol +++ b/contracts/src/tokens/StableTokenV3.sol @@ -1,295 +1,294 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable gas-custom-errors +// // SPDX-License-Identifier: GPL-3.0-or-later +// // solhint-disable gas-custom-errors pragma solidity 0.8.24; -import {ERC20PermitUpgradeable} from "./patched/ERC20PermitUpgradeable.sol"; -import {ERC20Upgradeable} from "./patched/ERC20Upgradeable.sol"; +import { ERC20PermitUpgradeable } from "./patched/ERC20PermitUpgradeable.sol"; +import { ERC20Upgradeable } from "./patched/ERC20Upgradeable.sol"; -import {IStableTokenV3} from "../Interfaces/IStableTokenV3.sol"; +import { IStableTokenV3 } from "../Interfaces/IStableTokenV3.sol"; /** * @title ERC20 token with minting and burning permissiones to a minter and burner roles. * Direct transfers between the protocol and the user are done by the operator role. */ contract StableTokenV3 is ERC20PermitUpgradeable, IStableTokenV3 { - /* ========================================================= */ - /* ==================== State Variables ==================== */ - /* ========================================================= */ - - // Deprecated storage slots for backwards compatibility with StableTokenV2 - // solhint-disable-next-line var-name-mixedcase - address public deprecated_validators_storage_slot__; - // solhint-disable-next-line var-name-mixedcase - address public deprecated_broker_storage_slot__; - // solhint-disable-next-line var-name-mixedcase - address public deprecated_exchange_storage_slot__; - - // Mapping of allowed addresses that can mint - mapping(address => bool) public isMinter; - // Mapping of allowed addresses that can burn - mapping(address => bool) public isBurner; - // Mapping of allowed addresses that can call the operator functions - // These functions are used to do direct transfers between the protocol and the user - // This will be the StabilityPools - mapping(address => bool) public isOperator; - - /* ========================================================= */ - /* ======================== Events ========================= */ - /* ========================================================= */ - - event MinterUpdated(address indexed minter, bool isMinter); - event BurnerUpdated(address indexed burner, bool isBurner); - event OperatorUpdated(address indexed operator, bool isOperator); - - /* ========================================================= */ - /* ====================== Modifiers ======================== */ - /* ========================================================= */ - - /// @dev legacy helper to ensure only the vm can call this function - modifier onlyVm() { - require(msg.sender == address(0), "Only VM can call"); - _; + /* ========================================================= */ + /* ==================== State Variables ==================== */ + /* ========================================================= */ + + // Deprecated storage slots for backwards compatibility with StableTokenV2 + // solhint-disable-next-line var-name-mixedcase + address public deprecated_validators_storage_slot__; + // solhint-disable-next-line var-name-mixedcase + address public deprecated_broker_storage_slot__; + // solhint-disable-next-line var-name-mixedcase + address public deprecated_exchange_storage_slot__; + + // Mapping of allowed addresses that can mint + mapping(address => bool) public isMinter; + // Mapping of allowed addresses that can burn + mapping(address => bool) public isBurner; + // Mapping of allowed addresses that can call the operator functions + // These functions are used to do direct transfers between the protocol and the user + // This will be the StabilityPools + mapping(address => bool) public isOperator; + + /* ========================================================= */ + /* ======================== Events ========================= */ + /* ========================================================= */ + + event MinterUpdated(address indexed minter, bool isMinter); + event BurnerUpdated(address indexed burner, bool isBurner); + event OperatorUpdated(address indexed operator, bool isOperator); + + /* ========================================================= */ + /* ====================== Modifiers ======================== */ + /* ========================================================= */ + + /// @dev Celo contracts legacy helper to ensure only the vm can call this function + modifier onlyVm() { + require(msg.sender == address(0), "Only VM can call"); + _; + } + + /// @dev Restricts a function so it can only be executed by an address that's allowed to mint. + modifier onlyMinter() { + address sender = _msgSender(); + require(isMinter[sender], "StableTokenV3: not allowed to mint"); + _; + } + + /// @dev Restricts a function so it can only be executed by an address that's allowed to burn. + modifier onlyBurner() { + address sender = _msgSender(); + require(isBurner[sender], "StableTokenV3: not allowed to burn"); + _; + } + + /// @dev Restricts a function so it can only be executed by the operator role. + modifier onlyOperator() { + address sender = _msgSender(); + require(isOperator[sender], "StableTokenV3: not allowed to call only by operator"); + _; + } + + /* ========================================================= */ + /* ====================== Constructor ====================== */ + /* ========================================================= */ + + /** + * @notice The constructor for the StableTokenV3 contract. + * @dev Should be called with disable=true in deployments when + * it's accessed through a Proxy. + * Call this with disable=false during testing, when used + * without a proxy. + * @param disable Set to true to run `_disableInitializers()` inherited from + * openzeppelin-contracts-upgradeable/Initializable.sol + */ + constructor(bool disable) { + if (disable) { + _disableInitializers(); } - - /// @dev Restricts a function so it can only be executed by an address that's allowed to mint. - modifier onlyMinter() { - address sender = _msgSender(); - require(isMinter[sender], "StableTokenV3: not allowed to mint"); - _; - } - - /// @dev Restricts a function so it can only be executed by an address that's allowed to burn. - modifier onlyBurner() { - address sender = _msgSender(); - require(isBurner[sender], "StableTokenV3: not allowed to burn"); - _; - } - - /// @dev Restricts a function so it can only be executed by the operator role. - modifier onlyOperator() { - address sender = _msgSender(); - require(isOperator[sender], "StableTokenV3: not allowed to call only by operator"); - _; - } - - /* ========================================================= */ - /* ====================== Constructor ====================== */ - /* ========================================================= */ - - /** - * @notice The constructor for the StableTokenV3 contract. - * @dev Should be called with disable=true in deployments when - * it's accessed through a Proxy. - * Call this with disable=false during testing, when used - * without a proxy. - * @param disable Set to true to run `_disableInitializers()` inherited from - * openzeppelin-contracts-upgradeable/Initializable.sol - */ - constructor(bool disable) { - if (disable) { - _disableInitializers(); - } - } - - /// @inheritdoc IStableTokenV3 - function initialize( - // slither-disable-start shadowing-local - string memory _name, - string memory _symbol, - // slither-disable-end shadowing-local - address[] memory initialBalanceAddresses, - uint256[] memory initialBalanceValues, - address[] memory _minters, - address[] memory _burners, - address[] memory _operators - ) external reinitializer(3) { - __ERC20_init_unchained(_name, _symbol); - __ERC20Permit_init(_symbol); - _transferOwnership(_msgSender()); - - require(initialBalanceAddresses.length == initialBalanceValues.length, "Array length mismatch"); - for (uint256 i = 0; i < initialBalanceAddresses.length; i += 1) { - _mint(initialBalanceAddresses[i], initialBalanceValues[i]); - } - for (uint256 i = 0; i < _minters.length; i += 1) { - _setMinter(_minters[i], true); - } - for (uint256 i = 0; i < _burners.length; i += 1) { - _setBurner(_burners[i], true); - } - for (uint256 i = 0; i < _operators.length; i += 1) { - _setOperator(_operators[i], true); - } - } - - /// @inheritdoc IStableTokenV3 - function initializeV3(address[] memory _minters, address[] memory _burners, address[] memory _operators) - public - reinitializer(3) - onlyOwner - { - for (uint256 i = 0; i < _minters.length; i += 1) { - _setMinter(_minters[i], true); - } - for (uint256 i = 0; i < _burners.length; i += 1) { - _setBurner(_burners[i], true); - } - for (uint256 i = 0; i < _operators.length; i += 1) { - _setOperator(_operators[i], true); - } - } - - /* ============================================================ */ - /* ==================== Mutative Functions ==================== */ - /* ============================================================ */ - - /// @inheritdoc IStableTokenV3 - function setOperator(address _operator, bool _isOperator) external onlyOwner { - _setOperator(_operator, _isOperator); - } - - /// @inheritdoc IStableTokenV3 - function setMinter(address _minter, bool _isMinter) external onlyOwner { - _setMinter(_minter, _isMinter); - } - - /// @inheritdoc IStableTokenV3 - function setBurner(address _burner, bool _isBurner) external onlyOwner { - _setBurner(_burner, _isBurner); - } - - /// @inheritdoc IStableTokenV3 - function mint(address to, uint256 value) external onlyMinter returns (bool) { - _mint(to, value); - return true; + } + + /// @inheritdoc IStableTokenV3 + function initialize( + // slither-disable-start shadowing-local + string memory _name, + string memory _symbol, + // slither-disable-end shadowing-local + address[] memory initialBalanceAddresses, + uint256[] memory initialBalanceValues, + address[] memory _minters, + address[] memory _burners, + address[] memory _operators + ) external reinitializer(3) { + __ERC20_init_unchained(_name, _symbol); + __ERC20Permit_init(_symbol); + _transferOwnership(_msgSender()); + + require(initialBalanceAddresses.length == initialBalanceValues.length, "Array length mismatch"); + for (uint256 i = 0; i < initialBalanceAddresses.length; i += 1) { + _mint(initialBalanceAddresses[i], initialBalanceValues[i]); } - - /// @inheritdoc IStableTokenV3 - function burn(uint256 value) external onlyBurner returns (bool) { - _burn(msg.sender, value); - return true; + for (uint256 i = 0; i < _minters.length; i += 1) { + _setMinter(_minters[i], true); } - - /// @inheritdoc IStableTokenV3 - function sendToPool(address _sender, address _poolAddress, uint256 _amount) external onlyOperator { - _transfer(_sender, _poolAddress, _amount); - } - - /// @inheritdoc IStableTokenV3 - function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external onlyOperator { - _transfer(_poolAddress, _receiver, _amount); + for (uint256 i = 0; i < _burners.length; i += 1) { + _setBurner(_burners[i], true); } - - /// @inheritdoc IStableTokenV3 - function transferFrom(address from, address to, uint256 amount) - public - override(ERC20Upgradeable, IStableTokenV3) - returns (bool) - { - return ERC20Upgradeable.transferFrom(from, to, amount); + for (uint256 i = 0; i < _operators.length; i += 1) { + _setOperator(_operators[i], true); } - - /// @inheritdoc IStableTokenV3 - function transfer(address to, uint256 amount) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { - return ERC20Upgradeable.transfer(to, amount); + } + + /// @inheritdoc IStableTokenV3 + function initializeV3( + address[] memory _minters, + address[] memory _burners, + address[] memory _operators + ) public reinitializer(3) onlyOwner { + for (uint256 i = 0; i < _minters.length; i += 1) { + _setMinter(_minters[i], true); } - - /// @inheritdoc IStableTokenV3 - function balanceOf(address account) public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { - return ERC20Upgradeable.balanceOf(account); + for (uint256 i = 0; i < _burners.length; i += 1) { + _setBurner(_burners[i], true); } - - /// @inheritdoc IStableTokenV3 - function approve(address spender, uint256 amount) - public - override(ERC20Upgradeable, IStableTokenV3) - returns (bool) - { - return ERC20Upgradeable.approve(spender, amount); + for (uint256 i = 0; i < _operators.length; i += 1) { + _setOperator(_operators[i], true); } - - /// @inheritdoc IStableTokenV3 - function allowance(address owner, address spender) - public - view - override(ERC20Upgradeable, IStableTokenV3) - returns (uint256) - { - return ERC20Upgradeable.allowance(owner, spender); + } + + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ + + /// @inheritdoc IStableTokenV3 + function setOperator(address _operator, bool _isOperator) external onlyOwner { + _setOperator(_operator, _isOperator); + } + + /// @inheritdoc IStableTokenV3 + function setMinter(address _minter, bool _isMinter) external onlyOwner { + _setMinter(_minter, _isMinter); + } + + /// @inheritdoc IStableTokenV3 + function setBurner(address _burner, bool _isBurner) external onlyOwner { + _setBurner(_burner, _isBurner); + } + + /// @inheritdoc IStableTokenV3 + function mint(address to, uint256 value) external onlyMinter returns (bool) { + _mint(to, value); + return true; + } + + /// @inheritdoc IStableTokenV3 + function burn(uint256 value) external onlyBurner returns (bool) { + _burn(msg.sender, value); + return true; + } + + /// @inheritdoc IStableTokenV3 + function sendToPool(address _sender, address _poolAddress, uint256 _amount) external onlyOperator { + _transfer(_sender, _poolAddress, _amount); + } + + /// @inheritdoc IStableTokenV3 + function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external onlyOperator { + _transfer(_poolAddress, _receiver, _amount); + } + + /// @inheritdoc IStableTokenV3 + function transferFrom( + address from, + address to, + uint256 amount + ) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { + return ERC20Upgradeable.transferFrom(from, to, amount); + } + + /// @inheritdoc IStableTokenV3 + function transfer(address to, uint256 amount) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { + return ERC20Upgradeable.transfer(to, amount); + } + + /// @inheritdoc IStableTokenV3 + function balanceOf(address account) public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { + return ERC20Upgradeable.balanceOf(account); + } + + /// @inheritdoc IStableTokenV3 + function approve(address spender, uint256 amount) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { + return ERC20Upgradeable.approve(spender, amount); + } + + /// @inheritdoc IStableTokenV3 + function allowance( + address owner, + address spender + ) public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { + return ERC20Upgradeable.allowance(owner, spender); + } + + /// @inheritdoc IStableTokenV3 + function totalSupply() public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { + return ERC20Upgradeable.totalSupply(); + } + + /// @inheritdoc IStableTokenV3 + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public override(ERC20PermitUpgradeable, IStableTokenV3) { + ERC20PermitUpgradeable.permit(owner, spender, value, deadline, v, r, s); + } + + /// @inheritdoc IStableTokenV3 + function debitGasFees(address from, uint256 value) external onlyVm { + _burn(from, value); + } + + /// @inheritdoc IStableTokenV3 + function creditGasFees( + address from, + address feeRecipient, + address gatewayFeeRecipient, + address communityFund, + uint256 refund, + uint256 tipTxFee, + uint256 gatewayFee, + uint256 baseTxFee + ) external onlyVm { + // slither-disable-next-line uninitialized-local + uint256 amountToBurn; + _mint(from, refund + tipTxFee + gatewayFee + baseTxFee); + + if (feeRecipient != address(0)) { + _transfer(from, feeRecipient, tipTxFee); + } else if (tipTxFee > 0) { + amountToBurn += tipTxFee; } - /// @inheritdoc IStableTokenV3 - function totalSupply() public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { - return ERC20Upgradeable.totalSupply(); - } - - /// @inheritdoc IStableTokenV3 - function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - public - override(ERC20PermitUpgradeable, IStableTokenV3) - { - ERC20PermitUpgradeable.permit(owner, spender, value, deadline, v, r, s); - } - - /// @inheritdoc IStableTokenV3 - function debitGasFees(address from, uint256 value) external onlyVm { - _burn(from, value); - } - - /// @inheritdoc IStableTokenV3 - function creditGasFees( - address from, - address feeRecipient, - address gatewayFeeRecipient, - address communityFund, - uint256 refund, - uint256 tipTxFee, - uint256 gatewayFee, - uint256 baseTxFee - ) external onlyVm { - // slither-disable-next-line uninitialized-local - uint256 amountToBurn; - _mint(from, refund + tipTxFee + gatewayFee + baseTxFee); - - if (feeRecipient != address(0)) { - _transfer(from, feeRecipient, tipTxFee); - } else if (tipTxFee > 0) { - amountToBurn += tipTxFee; - } - - if (gatewayFeeRecipient != address(0)) { - _transfer(from, gatewayFeeRecipient, gatewayFee); - } else if (gatewayFee > 0) { - amountToBurn += gatewayFee; - } - - if (communityFund != address(0)) { - _transfer(from, communityFund, baseTxFee); - } else if (baseTxFee > 0) { - amountToBurn += baseTxFee; - } - - if (amountToBurn > 0) { - _burn(from, amountToBurn); - } - } - - /* =========================================================== */ - /* ==================== Private Functions ==================== */ - /* =========================================================== */ - - function _setOperator(address _operator, bool _isOperator) internal { - isOperator[_operator] = _isOperator; - emit OperatorUpdated(_operator, _isOperator); + if (gatewayFeeRecipient != address(0)) { + _transfer(from, gatewayFeeRecipient, gatewayFee); + } else if (gatewayFee > 0) { + amountToBurn += gatewayFee; } - function _setMinter(address _minter, bool _isMinter) internal { - isMinter[_minter] = _isMinter; - emit MinterUpdated(_minter, _isMinter); + if (communityFund != address(0)) { + _transfer(from, communityFund, baseTxFee); + } else if (baseTxFee > 0) { + amountToBurn += baseTxFee; } - function _setBurner(address _burner, bool _isBurner) internal { - isBurner[_burner] = _isBurner; - emit BurnerUpdated(_burner, _isBurner); + if (amountToBurn > 0) { + _burn(from, amountToBurn); } -} + } + + /* =========================================================== */ + /* ==================== Private Functions ==================== */ + /* =========================================================== */ + + function _setOperator(address _operator, bool _isOperator) internal { + isOperator[_operator] = _isOperator; + emit OperatorUpdated(_operator, _isOperator); + } + + function _setMinter(address _minter, bool _isMinter) internal { + isMinter[_minter] = _isMinter; + emit MinterUpdated(_minter, _isMinter); + } + + function _setBurner(address _burner, bool _isBurner) internal { + isBurner[_burner] = _isBurner; + emit BurnerUpdated(_burner, _isBurner); + } +} \ No newline at end of file From e7ba405971aaf35d9d68b1b46137652dd38bcc93 Mon Sep 17 00:00:00 2001 From: Philip Paetz Date: Wed, 3 Sep 2025 17:00:36 +0200 Subject: [PATCH 08/79] docs: remove patched tokens README we need to write a new one for v3 --- .../tokens/patched/ERC20PermitUpgradeable.sol | 1 - .../src/tokens/patched/ERC20Upgradeable.sol | 1 - contracts/src/tokens/patched/README.md | 75 ------------------- 3 files changed, 77 deletions(-) delete mode 100644 contracts/src/tokens/patched/README.md diff --git a/contracts/src/tokens/patched/ERC20PermitUpgradeable.sol b/contracts/src/tokens/patched/ERC20PermitUpgradeable.sol index 71246fcb0..d00d309a4 100644 --- a/contracts/src/tokens/patched/ERC20PermitUpgradeable.sol +++ b/contracts/src/tokens/patched/ERC20PermitUpgradeable.sol @@ -6,7 +6,6 @@ * and only changes the import of ERC20Upgradeable to be the local one * which is modified in order to keep storage variables consistent * with the pervious implementation of StableToken. - * See ./README.md for more details. */ pragma solidity ^0.8.0; diff --git a/contracts/src/tokens/patched/ERC20Upgradeable.sol b/contracts/src/tokens/patched/ERC20Upgradeable.sol index 895261e8c..391a3eb8d 100644 --- a/contracts/src/tokens/patched/ERC20Upgradeable.sol +++ b/contracts/src/tokens/patched/ERC20Upgradeable.sol @@ -5,7 +5,6 @@ * 🔥 MentoLabs: This is a copied file from v4.8.0 of OZ-Upgradable, which only changes * the ordering of storage variables to keep it consistent with the existing * StableToken, so this can act as a new implementation for the proxy. - * See ./README.md for more details. */ pragma solidity ^0.8.0; diff --git a/contracts/src/tokens/patched/README.md b/contracts/src/tokens/patched/README.md deleted file mode 100644 index de82c3080..000000000 --- a/contracts/src/tokens/patched/README.md +++ /dev/null @@ -1,75 +0,0 @@ -### Patched OpenZeppelin contracts - -The only way we can migrate our StableToken implementation to a modern one is if we can keep the same storage layout in the proxy. -Sadly our tokens aren't compatible with the OZ ERC20 out of the box. We can use the `bin/storage-show.sh` command to get a quick view of the storage layout: - -``` -> ./bin/storage-show.sh StableToken -0 0 _owner t_address -0 20 initialized t_bool -1 0 registry t_contract(IRegistry)4167 -2 0 name_ t_string_storage -3 0 symbol_ t_string_storage -4 0 decimals_ t_uint8 -5 0 balances t_mapping(t_address,t_uint256) -6 0 totalSupply_ t_uint256 -7 0 allowed t_mapping(t_address,t_mapping(t_address,t_uint256)) -8 0 inflationState t_struct(InflationState)10264_storage -12 0 exchangeRegistryId t_bytes32 -⏎ -``` - -To make this work I copied the contracts from version `v4.8.0` here and modified the order of storage variables in order to get to something that's compatible: - -``` -> ./bin/storage-show.sh ERC20Upgradeable -0 0 _owner t_address -0 20 _initialized t_uint8 -0 21 _initializing t_bool -1 0 __deprecated_registry_storage_slot__ t_address -2 0 _name t_string_storage -3 0 _symbol t_string_storage -4 0 __deprecated_decimals_storage_slot__ t_uint8 -5 0 _balances t_mapping(t_address,t_uint256) -6 0 _totalSupply t_uint256 -7 0 _allowances t_mapping(t_address,t_mapping(t_address,t_uint256)) -8 0 __gap t_array(t_uint256)45_storage -``` - -> Note: The columns of the table are: slot, offset, name, type - -The `initialized` bool upgrades nicely to the new `Initializable` structure in more recent OZ - it was designed this way. -We reserve some deprecated slots, and make sure the others match up 1:1. The name being different is not an issue. - -And which can then be used in `ERC20Permit` and `StableTokenV2` to finally come up with this: - -``` -> ./bin/storage-show.sh StableTokenV2 -0 0 _owner t_address -0 20 _initialized t_uint8 -0 21 _initializing t_bool -1 0 __deprecated_registry_storage_slot__ t_address -2 0 _name t_string_storage -3 0 _symbol t_string_storage -4 0 __deprecated_decimals_storage_slot__ t_uint8 -5 0 _balances t_mapping(t_address,t_uint256) -6 0 _totalSupply t_uint256 -7 0 _allowances t_mapping(t_address,t_mapping(t_address,t_uint256)) -8 0 __deeprecated_inflationState_storage_slot__ t_array(t_uint256)4_storage -12 0 __deprecated_exchangeRegistryId_storage_slot__ t_bytes32 -13 0 __gap t_array(t_uint256)40_storage -53 0 _HASHED_NAME t_bytes32 -54 0 _HASHED_VERSION t_bytes32 -55 0 __gap t_array(t_uint256)50_storage -105 0 _nonces t_mapping(t_address,t_struct(Counter)51157_storage) -106 0 _PERMIT_TYPEHASH_DEPRECATED_SLOT t_bytes32 -107 0 __gap t_array(t_uint256)49_storage -156 0 validators t_address -157 0 broker t_address -158 0 exchange t_address -``` - -In this new implementation we also remove the dependency on the `Registry` and introduce the 3 new storage variables to store addresses for the dependencies directly. -See the `test/integration/TokenUpgrade.t.sol` for a simulation of switching the implementation of an existing live token in a forked environment. - -In the future, if we want to migrate to newer versions of this we can go through the same process of copying the files and patching them to keep the storage layout consistent. From d8d77acdff700f243048260683f9bcb7870c3053 Mon Sep 17 00:00:00 2001 From: Philip Paetz Date: Wed, 3 Sep 2025 17:34:28 +0200 Subject: [PATCH 09/79] chore: remove leftover Zappers reference --- .github/workflows/contracts-tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/contracts-tests.yml b/.github/workflows/contracts-tests.yml index 7f8150760..867683647 100644 --- a/.github/workflows/contracts-tests.yml +++ b/.github/workflows/contracts-tests.yml @@ -227,7 +227,6 @@ jobs: 'test/*' 'script/*' 'src/Dependencies/Ownable.sol' - 'src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol' 'src/NFTMetadata/*' 'src/MultiTroveGetter.sol' 'src/HintHelpers.sol' From 4f29e2803e0f608e56237dcc7177b15d7821cc68 Mon Sep 17 00:00:00 2001 From: Philip Paetz Date: Thu, 4 Sep 2025 13:57:50 +0200 Subject: [PATCH 10/79] chore: skip invariant tests on CI they take too long and cost too much --- contracts/foundry.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 652e3a5f2..1e830c830 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -21,6 +21,10 @@ depth = 50 # failure_persist_dir = "/dev/null" # XXX circumvent this half-baked Foundry feature shrink_run_limit = 0 # XXX shrinking is super broken, results in completely wrong repro sequence +[profile.ci] +# Exclude invariant tests to save CI credits +no_match_test = "invariant_" + [profile.ci.invariant] shrink_run_limit = 0 # takes too damn long to shrink, don't waste Github minutes From 3514640ab4b58076409a637eb42aa5c4a3e67553 Mon Sep 17 00:00:00 2001 From: baroooo Date: Tue, 16 Sep 2025 14:26:10 +0200 Subject: [PATCH 11/79] feat: add swap function to sp --- contracts/script/DeployLiquity2.s.sol | 4 ++- contracts/src/AddressesRegistry.sol | 4 +++ .../src/Interfaces/IAddressesRegistry.sol | 2 ++ contracts/src/StabilityPool.sol | 28 +++++++++++++++++++ contracts/test/TestContracts/Deployment.t.sol | 8 ++++-- 5 files changed, 43 insertions(+), 3 deletions(-) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index c9a1b2059..e78fb77bc 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -351,7 +351,9 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { multiTroveGetter: r.multiTroveGetter, collateralRegistry: r.collateralRegistry, boldToken: IBoldToken(address(r.stableToken)), - gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS) + gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS), + // TODO: set liquidity strategy + liquidityStrategy: address(0) }); contracts.addressesRegistry.setAddresses(addressVars); } diff --git a/contracts/src/AddressesRegistry.sol b/contracts/src/AddressesRegistry.sol index b2e7e2f03..1f68098f7 100644 --- a/contracts/src/AddressesRegistry.sol +++ b/contracts/src/AddressesRegistry.sol @@ -25,6 +25,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { ICollateralRegistry public collateralRegistry; IBoldToken public boldToken; IERC20Metadata public gasToken; + address public liquidityStrategy; // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied uint256 public immutable CCR; @@ -67,6 +68,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { event CollateralRegistryAddressChanged(address _collateralRegistryAddress); event BoldTokenAddressChanged(address _boldTokenAddress); event GasTokenAddressChanged(address _gasTokenAddress); + event LiquidityStrategyAddressChanged(address _liquidityStrategyAddress); constructor( address _owner, @@ -112,6 +114,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { collateralRegistry = _vars.collateralRegistry; boldToken = _vars.boldToken; gasToken = _vars.gasToken; + liquidityStrategy = _vars.liquidityStrategy; emit CollTokenAddressChanged(address(_vars.collToken)); emit BorrowerOperationsAddressChanged(address(_vars.borrowerOperations)); @@ -131,6 +134,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { emit CollateralRegistryAddressChanged(address(_vars.collateralRegistry)); emit BoldTokenAddressChanged(address(_vars.boldToken)); emit GasTokenAddressChanged(address(_vars.gasToken)); + emit LiquidityStrategyAddressChanged(address(_vars.liquidityStrategy)); _renounceOwnership(); } diff --git a/contracts/src/Interfaces/IAddressesRegistry.sol b/contracts/src/Interfaces/IAddressesRegistry.sol index ff1172603..1c43ebd34 100644 --- a/contracts/src/Interfaces/IAddressesRegistry.sol +++ b/contracts/src/Interfaces/IAddressesRegistry.sol @@ -38,6 +38,7 @@ interface IAddressesRegistry { ICollateralRegistry collateralRegistry; IBoldToken boldToken; IERC20Metadata gasToken; + address liquidityStrategy; } function CCR() external returns (uint256); @@ -65,6 +66,7 @@ interface IAddressesRegistry { function collateralRegistry() external view returns (ICollateralRegistry); function boldToken() external view returns (IBoldToken); function gasToken() external view returns (IERC20Metadata); + function liquidityStrategy() external view returns (address); function setAddresses(AddressVars memory _vars) external; } diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index f0b590a7e..5514cd2ec 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -175,6 +175,9 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi // Each time the scale of P shifts by SCALE_FACTOR, the scale is incremented by 1 uint256 public currentScale; + // TODO: is this 1-1 between SP and liquidity strategy? + address public liquidityStrategy; + /* Coll Gain sum 'S': During its lifetime, each deposit d_t earns an Coll gain of ( d_t * [S - S_t] )/P_t, where S_t * is the depositor's snapshot of S taken at the time t when the deposit was made. * @@ -206,6 +209,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi collToken = _addressesRegistry.collToken(); troveManager = _addressesRegistry.troveManager(); boldToken = _addressesRegistry.boldToken(); + liquidityStrategy = _addressesRegistry.liquidityStrategy(); emit TroveManagerAddressChanged(address(troveManager)); emit BoldTokenAddressChanged(address(boldToken)); @@ -386,6 +390,23 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi emit B_Updated(scaleToB[currentScale], currentScale); } + // --- Liquidity strategy functions --- + + /* + * Stable token liquidity in the stability pool can be used to rebalance FPMM pools. + * Collateral will be swapped for stable tokens in the SP. + * Removed stable tokens will be factored out from LPs' positions. + * Added collateral will be added to LPs collateral gain which can be later claimed by the depositor. + */ + function swapCollateralForStable(uint256 amountStableOut, uint256 amountCollIn) external { + _requireNonZeroAmount(amountStableOut); + _requireNonZeroAmount(amountCollIn); + + _requireCallerIsLiquidityStrategy(); + + _offset(amountStableOut, amountCollIn); + } + // --- Liquidation functions --- /* @@ -395,7 +416,10 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi */ function offset(uint256 _debtToOffset, uint256 _collToAdd) external override { _requireCallerIsTroveManager(); + _offset(_debtToOffset, _collToAdd); + } + function _offset(uint256 _debtToOffset, uint256 _collToAdd) internal { scaleToS[currentScale] += P * _collToAdd / totalBoldDeposits; emit S_Updated(scaleToS[currentScale], currentScale); @@ -610,6 +634,10 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi require(initialDeposit == 0, "StabilityPool: User must have no deposit"); } + function _requireCallerIsLiquidityStrategy() internal view { + require(msg.sender == liquidityStrategy, "StabilityPool: Caller is not LiquidityStrategy"); + } + function _requireNonZeroAmount(uint256 _amount) internal pure { require(_amount > 0, "StabilityPool: Amount must be non-zero"); } diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index 331c9aed1..252320f13 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -411,7 +411,9 @@ contract TestDeployer is MetadataDeployment { collateralRegistry: _collateralRegistry, boldToken: _boldToken, collToken: _collToken, - gasToken: _gasToken + gasToken: _gasToken, + // TODO: add liquidity strategy + liquidityStrategy: address(0) }); contracts.addressesRegistry.setAddresses(addressVars); @@ -612,7 +614,9 @@ contract TestDeployer is MetadataDeployment { collateralRegistry: _params.collateralRegistry, boldToken: _params.boldToken, collToken: _params.collToken, - gasToken: _params.gasToken + gasToken: _params.gasToken, + // TODO: add liquidity strategy + liquidityStrategy: address(0) }); contracts.addressesRegistry.setAddresses(addressVars); From 404a5b1033f4381aa48f1a9f2ceff68c92c1cfee Mon Sep 17 00:00:00 2001 From: baroooo Date: Wed, 17 Sep 2025 15:21:05 +0200 Subject: [PATCH 12/79] test: swap function --- contracts/src/Interfaces/IStabilityPool.sol | 10 + contracts/src/StabilityPool.sol | 37 +- contracts/test/TestContracts/Deployment.t.sol | 4 +- contracts/test/swapCollateralForStable.t.sol | 504 ++++++++++++++++++ 4 files changed, 543 insertions(+), 12 deletions(-) create mode 100644 contracts/test/swapCollateralForStable.t.sol diff --git a/contracts/src/Interfaces/IStabilityPool.sol b/contracts/src/Interfaces/IStabilityPool.sol index 63bc0d4f4..ccb3eaaee 100644 --- a/contracts/src/Interfaces/IStabilityPool.sol +++ b/contracts/src/Interfaces/IStabilityPool.sol @@ -54,6 +54,14 @@ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { function claimAllCollGains() external; + /* + * Stable token liquidity in the stability pool can be used to rebalance FPMM pools. + * Collateral will be swapped for stable tokens in the SP. + * Removed stable tokens will be factored out from LPs' positions. + * Added collateral will be added to LPs collateral gain which can be later claimed by the depositor. + */ + function swapCollateralForStable(uint256 amountStableOut, uint256 amountCollIn) external; + /* * Initial checks: * - Caller is TroveManager @@ -64,6 +72,7 @@ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { */ function offset(uint256 _debt, uint256 _coll) external; + function deposits(address _depositor) external view returns (uint256 initialValue); function stashedColl(address _depositor) external view returns (uint256); @@ -107,6 +116,7 @@ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { function P() external view returns (uint256); function currentScale() external view returns (uint256); + function liquidityStrategy() external view returns (address); function P_PRECISION() external view returns (uint256); } diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 5514cd2ec..7e6aeddfb 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -11,6 +11,8 @@ import "./Interfaces/ITroveManager.sol"; import "./Interfaces/IBoldToken.sol"; import "./Dependencies/LiquityBaseInit.sol"; +import "forge-std/console.sol"; + /* * The Stability Pool holds Bold tokens deposited by Stability Pool depositors. * @@ -398,13 +400,16 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi * Removed stable tokens will be factored out from LPs' positions. * Added collateral will be added to LPs collateral gain which can be later claimed by the depositor. */ - function swapCollateralForStable(uint256 amountStableOut, uint256 amountCollIn) external { - _requireNonZeroAmount(amountStableOut); - _requireNonZeroAmount(amountCollIn); - + function swapCollateralForStable(uint256 amountCollIn, uint256 amountStableOut ) external { _requireCallerIsLiquidityStrategy(); - _offset(amountStableOut, amountCollIn); + _updateTrackingVariables(amountStableOut, amountCollIn); + + _swapCollateralForStable(amountCollIn, amountStableOut); + + // TODO: added this to avoid higher rate of scaling of P during rebalances for very small totalBoldDeposits + // Discuss with team if it is ok to have a larger limit for this to make scaling even less aggressive + require(totalBoldDeposits >= MIN_BOLD_IN_SP, "Total Bold deposits must be >= MIN_BOLD_IN_SP"); } // --- Liquidation functions --- @@ -416,14 +421,17 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi */ function offset(uint256 _debtToOffset, uint256 _collToAdd) external override { _requireCallerIsTroveManager(); - _offset(_debtToOffset, _collToAdd); + + _updateTrackingVariables(_debtToOffset, _collToAdd); + + _moveOffsetCollAndDebt(_collToAdd, _debtToOffset); } - function _offset(uint256 _debtToOffset, uint256 _collToAdd) internal { - scaleToS[currentScale] += P * _collToAdd / totalBoldDeposits; + function _updateTrackingVariables(uint256 _amountStableOut, uint256 _amountCollIn) internal { + scaleToS[currentScale] += P * _amountCollIn / totalBoldDeposits; emit S_Updated(scaleToS[currentScale], currentScale); - uint256 numerator = P * (totalBoldDeposits - _debtToOffset); + uint256 numerator = P * (totalBoldDeposits - _amountStableOut); uint256 newP = numerator / totalBoldDeposits; // For `P` to turn zero, `totalBoldDeposits` has to be greater than `P * (totalBoldDeposits - _debtToOffset)`. @@ -449,8 +457,17 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi emit P_Updated(newP); P = newP; + } + + function _swapCollateralForStable(uint256 _amountCollIn, uint256 _amountStableOut) internal { + _updateTotalBoldDeposits(0, _amountStableOut); + IERC20(address(boldToken)).safeTransfer(liquidityStrategy, _amountStableOut); + + collBalance += _amountCollIn; + collToken.safeTransferFrom(msg.sender, address(this), _amountCollIn); + + emit StabilityPoolCollBalanceUpdated(collBalance); - _moveOffsetCollAndDebt(_collToAdd, _debtToOffset); } function _moveOffsetCollAndDebt(uint256 _collToAdd, uint256 _debtToOffset) internal { diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index 252320f13..ebbf28b4f 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -413,7 +413,7 @@ contract TestDeployer is MetadataDeployment { collToken: _collToken, gasToken: _gasToken, // TODO: add liquidity strategy - liquidityStrategy: address(0) + liquidityStrategy: makeAddr("liquidityStrategy") }); contracts.addressesRegistry.setAddresses(addressVars); @@ -616,7 +616,7 @@ contract TestDeployer is MetadataDeployment { collToken: _params.collToken, gasToken: _params.gasToken, // TODO: add liquidity strategy - liquidityStrategy: address(0) + liquidityStrategy: makeAddr("liquidityStrategy") }); contracts.addressesRegistry.setAddresses(addressVars); diff --git a/contracts/test/swapCollateralForStable.t.sol b/contracts/test/swapCollateralForStable.t.sol new file mode 100644 index 000000000..e612ce3d9 --- /dev/null +++ b/contracts/test/swapCollateralForStable.t.sol @@ -0,0 +1,504 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.18; + +import "./TestContracts/DevTestSetup.sol"; +import "../src/StabilityPool.sol"; +import "../src/Interfaces/IStabilityPoolEvents.sol"; + +contract SwapCollateralForStableTest is DevTestSetup { + struct SwapTestVars { + uint256 spBoldBalance; + uint256 spCollBalance; + uint256 lsBoldBalance; + uint256 lsCollBalance; + uint256 depositor1CollBalance; + uint256 depositor1BoldBalance; + uint256 depositor2CollBalance; + uint256 depositor2BoldBalance; + uint256 depositor3CollBalance; + uint256 depositor3BoldBalance; + } + + address public liquidityStrategy = makeAddr("liquidityStrategy"); + + event P_Updated(uint256 _P); + event S_Updated(uint256 _S, uint256 _scale); + event ScaleUpdated(uint256 _currentScale); + event StabilityPoolCollBalanceUpdated(uint256 _newBalance); + event StabilityPoolBoldBalanceUpdated(uint256 _newBalance); + + function setUp() public override { + super.setUp(); + + deal(address(collToken), liquidityStrategy, 10_000e18); + + vm.prank(liquidityStrategy); + collToken.approve(address(stabilityPool), type(uint256).max); + + } + + function testLiquidityStrategy() public view { + assertEq(stabilityPool.liquidityStrategy(), liquidityStrategy); + } + + + function testSwapCollateralForStableRevertsWhenNotLiquidityStrategy() public { + vm.expectRevert("StabilityPool: Caller is not LiquidityStrategy"); + stabilityPool.swapCollateralForStable(1e18, 1e18); + } + + function testSwapCollateralForStableRevertsWithInsufficientStableLiquidity() public { + uint256 stableAmount = 1000e18; + + deal(address(boldToken), A, stableAmount); + + makeSPDepositAndClaim(A, stableAmount); + + uint256 collSwapAmount = 1e18; + uint256 stableSwapAmount = stableAmount; + + + vm.startPrank(liquidityStrategy); + vm.expectRevert("P must never decrease to 0"); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + + + vm.expectRevert("Total Bold deposits must be >= MIN_BOLD_IN_SP"); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount - .1e18); + vm.stopPrank(); + } + + function testSwapCollateralForStableWithSurplus() public { + uint256 stableAmount = 2000e18; + uint256 collAmount = 2e18; + SwapTestVars memory initialValues; + + priceFeed.setPrice(2000e18); + vm.startPrank(A); + borrowerOperations.openTrove( + A, + 0, + collAmount, + stableAmount, + 0, + 0, + MIN_ANNUAL_INTEREST_RATE, + 1000e18, + address(0), + address(0), + address(0) + ); + vm.stopPrank(); + + vm.startPrank(B); + borrowerOperations.openTrove( + B, + 0, + 2 * collAmount, + stableAmount + 100e18, + 0, + 0, + MIN_ANNUAL_INTEREST_RATE, + 1000e18, + address(0), + address(0), + address(0) + ); + vm.stopPrank(); + + + // B deposits to SP + makeSPDepositAndClaim(B, stableAmount + 100e18); + + initialValues.depositor1CollBalance = collToken.balanceOf(B); + initialValues.depositor1BoldBalance = boldToken.balanceOf(B); + initialValues.spBoldBalance = stabilityPool.getTotalBoldDeposits(); + initialValues.spCollBalance = stabilityPool.getCollBalance(); + initialValues.lsBoldBalance = boldToken.balanceOf(liquidityStrategy); + initialValues.lsCollBalance = collToken.balanceOf(liquidityStrategy); + + // Check SP has deposits + assertEq(initialValues.spBoldBalance, stableAmount + 100e18, "SP should have Bold deposits"); + + uint256 collSwapAmount = stableAmount / 2000; // we use the price to calculate the amount of collateral to swap + uint256 stableSwapAmount = stableAmount; + + // Simulate a rebalance by calling swapCollateralForStable as liquidity strategy + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + + // Check SP Bold has decreased by swap amount + uint256 finalSPBoldBalance = stabilityPool.getTotalBoldDeposits(); + assertEq( + finalSPBoldBalance, + initialValues.spBoldBalance - stableSwapAmount, + "SP Bold balance should decrease by swap amount" + ); + + // Check SP Coll has increased + uint256 finalSPCollBalance = stabilityPool.getCollBalance(); + assertEq( + finalSPCollBalance, + initialValues.spCollBalance + collSwapAmount, + "SP Coll balance should increase by swapped collateral" + ); + + // Check LP has received Bold + uint256 finalLSBoldBalance = boldToken.balanceOf(liquidityStrategy); + assertEq(finalLSBoldBalance, initialValues.lsBoldBalance + stableSwapAmount); + + // Check LP has sent Coll + uint256 finalLSCollBalance = collToken.balanceOf(liquidityStrategy); + assertEq(finalLSCollBalance, initialValues.lsCollBalance - collSwapAmount); + + vm.prank(B); + stabilityPool.withdrawFromSP(100e18 - 1e18, true); // subtract 1e18 to avoid MIN_BOLD_IN_SP + + // Check B has received Bold + // Received bold is less than the initial deposit because of the rebalance + assertApproxEqAbs(boldToken.balanceOf(B), initialValues.depositor1BoldBalance + 99e18, 1e18); + + // Check B has received Coll + // Even though the collateral was never deposited, depositor should have received the collateral from the rebalance + assertApproxEqAbs(collToken.balanceOf(B), initialValues.depositor1CollBalance + collSwapAmount, 1e18); + } + + function testSwapCollateralForStableWithLargerAmounts() public { + uint256 stableAmount = 1e32; // Large amount + + deal(address(collToken), address(liquidityStrategy), 1e32); + deal(address(boldToken), A, stableAmount); + + makeSPDepositAndClaim(A, stableAmount); + + uint256 collSwapAmount = 1e30; + uint256 stableSwapAmount = stableAmount - 1e18; // avoid MIN_BOLD_IN_SP + + SwapTestVars memory initialValues; + initialValues.spBoldBalance = stabilityPool.getTotalBoldDeposits(); + initialValues.spCollBalance = stabilityPool.getCollBalance(); + initialValues.lsBoldBalance = boldToken.balanceOf(liquidityStrategy); + initialValues.lsCollBalance = collToken.balanceOf(liquidityStrategy); + + + + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + + // Verify balances updated correctly with larger amounts + assertEq(stabilityPool.getTotalBoldDeposits(), initialValues.spBoldBalance - stableSwapAmount); + assertEq(stabilityPool.getCollBalance(), initialValues.spCollBalance + collSwapAmount); + assertEq(boldToken.balanceOf(liquidityStrategy), initialValues.lsBoldBalance + stableSwapAmount); + assertEq(collToken.balanceOf(liquidityStrategy), initialValues.lsCollBalance - collSwapAmount); + } + + // ========== MULTIPLE DEPOSITORS TESTS ========== + + function testSwapCollateralForStableWithMultipleDepositors() public { + uint256 depositA = 1000e18; // 1/6 + uint256 depositB = 2000e18; // 1/3 + uint256 depositC = 3000e18; // 1/2 + + deal(address(boldToken), A, depositA); + deal(address(boldToken), B, depositB); + deal(address(boldToken), C, depositC); + + // Multiple depositors + makeSPDepositAndClaim(A, depositA); + makeSPDepositAndClaim(B, depositB); + makeSPDepositAndClaim(C, depositC); + + + SwapTestVars memory initialValues; + initialValues.spBoldBalance = stabilityPool.getTotalBoldDeposits(); + initialValues.spCollBalance = stabilityPool.getCollBalance(); + initialValues.lsBoldBalance = boldToken.balanceOf(liquidityStrategy); + initialValues.lsCollBalance = collToken.balanceOf(liquidityStrategy); + initialValues.depositor1CollBalance = collToken.balanceOf(A); + initialValues.depositor1BoldBalance = boldToken.balanceOf(A); + initialValues.depositor2CollBalance = collToken.balanceOf(B); + initialValues.depositor2BoldBalance = boldToken.balanceOf(B); + initialValues.depositor3CollBalance = collToken.balanceOf(C); + initialValues.depositor3BoldBalance = boldToken.balanceOf(C); + + + uint256 collSwapAmount = 1e18; + uint256 stableSwapAmount = 1000e18; + + + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + + // Verify SP balances + assertEq(stabilityPool.getTotalBoldDeposits(), initialValues.spBoldBalance - stableSwapAmount); + assertEq(stabilityPool.getCollBalance(), initialValues.spCollBalance + collSwapAmount); + + // Verify liquidity strategy balances + assertEq(boldToken.balanceOf(liquidityStrategy), initialValues.lsBoldBalance + stableSwapAmount); + assertEq(collToken.balanceOf(liquidityStrategy), initialValues.lsCollBalance - collSwapAmount); + + // Withdraw from all depositors to check they receive proportional collateral gains + vm.startPrank(A); + stabilityPool.withdrawFromSP(depositA, true); + vm.stopPrank(); + + vm.startPrank(B); + stabilityPool.withdrawFromSP(depositB, true); + vm.stopPrank(); + + vm.startPrank(C); + // since this is the last depositor, we need to take depriciation and MIN_BOLD_IN_SP into account + stabilityPool.withdrawFromSP(depositC - (stableSwapAmount / 2 + 1e18), true); + vm.stopPrank(); + + // A should gain 1/6 of the collateral because they deposited 1/6 of the total deposits + assertApproxEqAbs(collToken.balanceOf(A), initialValues.depositor1CollBalance + collSwapAmount / 6, 1e18); + + // A should loose 1/6 of the Bold because they deposited 1/6 of the total deposits + assertApproxEqAbs(boldToken.balanceOf(A), depositA - (initialValues.depositor1BoldBalance + stableSwapAmount / 6), 1e18); + + // B should receive 1/3 of the collateral because they deposited 1/3 of the total deposits + assertApproxEqAbs(collToken.balanceOf(B), initialValues.depositor2CollBalance + collSwapAmount / 3, 1e18); + + // B should loose 1/3 of the Bold because they deposited 1/3 of the total deposits + assertApproxEqAbs(boldToken.balanceOf(B), depositB - (initialValues.depositor2BoldBalance + stableSwapAmount / 3), 1e18); + + // C should receive 1/2 of the collateral because they deposited 1/2 of the total deposits + assertApproxEqAbs(collToken.balanceOf(C), initialValues.depositor3CollBalance + collSwapAmount / 2, 1e18); + + // C should loose 1/2 of the Bold because they deposited 1/2 of the total deposits + assertApproxEqAbs(boldToken.balanceOf(C), depositC - (initialValues.depositor3BoldBalance + stableSwapAmount / 2 + 1e18), 1e18); + } + + // ========== INTEGRATION WITH LIQUIDATIONS ========== + + function testSwapCollateralForStableAfterLiquidation() public { + uint256 stableAmount = 2000e18; + uint256 collAmount = 2e18; + + priceFeed.setPrice(2000e18); + + // Create troves + vm.startPrank(A); + uint256 ATroveId = borrowerOperations.openTrove( + A, + 0, + collAmount, + stableAmount, + 0, + 0, + MIN_ANNUAL_INTEREST_RATE, + 1000e18, + address(0), + address(0), + address(0) + ); + vm.stopPrank(); + + vm.startPrank(B); + borrowerOperations.openTrove( + B, + 0, + 2 * collAmount, + stableAmount + 100e18, + 0, + 0, + MIN_ANNUAL_INTEREST_RATE, + 1000e18, + address(0), + address(0), + address(0) + ); + vm.stopPrank(); + + uint256 cBoldDeposit = 20_000e18; + deal(address(boldToken), C, cBoldDeposit); + // C deposits to SP + makeSPDepositAndClaim(C, cBoldDeposit); + + uint256 initialSPBold = stabilityPool.getTotalBoldDeposits(); + uint256 initialSPColl = stabilityPool.getCollBalance(); + + // Perform a swap first + uint256 collSwapAmount = 0.5e18; + uint256 stableSwapAmount = 1000e18; + + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + + // Now perform a liquidation + priceFeed.setPrice(900e18); // Drop price to trigger liquidation + + vm.startPrank(A); + troveManager.liquidate(ATroveId); + vm.stopPrank(); + + // Check that the liquidation worked correctly after the swap + uint256 finalSPBold = stabilityPool.getTotalBoldDeposits(); + uint256 finalSPColl = stabilityPool.getCollBalance(); + + // SP should have less Bold (due to liquidation offset + swap out) and more Coll (from liquidation + swap in) + assertApproxEqAbs(finalSPBold, initialSPBold - stableAmount - stableSwapAmount, 1e18); + assertApproxEqAbs(finalSPColl, initialSPColl + collAmount + collSwapAmount, 1e18); + + uint256 initialCColl = collToken.balanceOf(C); + + uint256 compoundedBoldDeposit = stabilityPool.getCompoundedBoldDeposit(C); + // C should be able to withdraw and receive both swap and liquidation gains + vm.startPrank(C); + stabilityPool.withdrawFromSP(compoundedBoldDeposit - 1e18, true); + vm.stopPrank(); + + // C should have received collateral from both the swap and the liquidation + assertApproxEqAbs(collToken.balanceOf(C), initialCColl + collSwapAmount + collAmount, 1e18); + + // C should have less Bold than deposited + assertApproxEqAbs(boldToken.balanceOf(C), cBoldDeposit - (stableSwapAmount + stableAmount), 1e18); + } + + // ========== EVENT TESTING ========== + + function testSwapCollateralForStableEmitsCorrectEvents() public { + uint256 stableAmount = 1000e18; + + uint256 collSwapAmount = 1e18; + uint256 stableSwapAmount = stableAmount / 2; + + + deal(address(boldToken), A, stableAmount); + deal(address(collToken), liquidityStrategy, collSwapAmount); + + makeSPDepositAndClaim(A, stableAmount); + + + // Record initial balances for event verification + uint256 initialSPBold = stabilityPool.getTotalBoldDeposits(); + uint256 initialSPColl = stabilityPool.getCollBalance(); + + vm.startPrank(liquidityStrategy); + + // Expect events to be emitted in the correct order + // First: S_Updated (from _updateTrackingVariables) + // S_Updated value = P * _amountCollIn / totalBoldDeposits + // = 1e36 * 1e18 / 1000e18 = 1e33 + vm.expectEmit(true, true, true, true); + emit S_Updated(1e33, 0); + + // Second: P_Updated (from _updateTrackingVariables) + // P_Updated value = P * (totalBoldDeposits - _amountStableOut) / totalBoldDeposits + // = 1e36 * (1000e18 - 1000e18 / 2) / 1000e18 = 5e35 + vm.expectEmit(true, true, true, true); + emit P_Updated(5e35); + + // Third: StabilityPoolBoldBalanceUpdated (from _swapCollateralForStable) + vm.expectEmit(true, true, true, true); + emit StabilityPoolBoldBalanceUpdated(initialSPBold - stableSwapAmount); + + // Fourth: StabilityPoolCollBalanceUpdated (from _swapCollateralForStable) + vm.expectEmit(true, true, true, true); + emit StabilityPoolCollBalanceUpdated(initialSPColl + collSwapAmount); + + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + } + + // // ========== SCALE CHANGES TESTS ========== + + // function testSwapCollateralForStableWithScaleChanges() public { + // // Create a scenario that will trigger scale changes + // uint256 largeDeposit = 1000000e18; + // makeSPDepositAndClaim(A, largeDeposit); + + // // Perform multiple large swaps to potentially trigger scale changes + // uint256 collSwapAmount = 100e18; + // uint256 stableSwapAmount = 200000e18; // Large amount relative to deposits + + // uint256 initialScale = stabilityPool.currentScale(); + + // vm.startPrank(liquidityStrategy); + // stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + // vm.stopPrank(); + + // // Check if scale changed (it might not in this case, but the mechanism should work) + // uint256 finalScale = stabilityPool.currentScale(); + // assertTrue(finalScale >= initialScale); + + // // Verify the swap still worked correctly regardless of scale changes + // assertEq(stabilityPool.getTotalBoldDeposits(), largeDeposit - stableSwapAmount); + // assertEq(stabilityPool.getCollBalance(), collSwapAmount); + // } + + // // ========== BOUNDARY CONDITIONS ========== + + // function testSwapCollateralForStableAtMinimumDeposit() public { + // // Test with minimum possible deposit + // uint256 minDeposit = 1e18; // MIN_BOLD_IN_SP + // makeSPDepositAndClaim(A, minDeposit); + + // uint256 collSwapAmount = 0.001e18; // Very small + // uint256 stableSwapAmount = 0.5e18; // Half of minimum deposit + + // vm.startPrank(liquidityStrategy); + // stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + // vm.stopPrank(); + + // // Should still work and leave at least MIN_BOLD_IN_SP + // assertTrue(stabilityPool.getTotalBoldDeposits() >= 1e18); + // assertEq(stabilityPool.getCollBalance(), collSwapAmount); + // } + + // function testSwapCollateralForStableWithMaxUint256() public { + // // Test with very large numbers (but not actually max uint256 to avoid overflow) + // uint256 largeDeposit = 1000000000e18; // 1 billion tokens + // makeSPDepositAndClaim(A, largeDeposit); + + // uint256 collSwapAmount = 1000000e18; // 1 million collateral + // uint256 stableSwapAmount = 100000000e18; // 100 million stable + + // vm.startPrank(liquidityStrategy); + // stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + // vm.stopPrank(); + + // // Should handle large numbers correctly + // assertEq(stabilityPool.getTotalBoldDeposits(), largeDeposit - stableSwapAmount); + // assertEq(stabilityPool.getCollBalance(), collSwapAmount); + // assertEq(boldToken.balanceOf(liquidityStrategy), stableSwapAmount); + // assertEq(collToken.balanceOf(liquidityStrategy), 10000e18 - collSwapAmount); // Initial 10000e18 - swapped + // } + + // // ========== REENTRANCY AND SECURITY TESTS ========== + + // function testSwapCollateralForStableCannotBeCalledByNonLiquidityStrategy() public { + // makeSPDepositAndClaim(A, 1000e18); + + // // Try calling from different addresses + // address[] memory testAddresses = new address[](4); + // testAddresses[0] = A; + // testAddresses[1] = B; + // testAddresses[2] = C; + // testAddresses[3] = address(this); + + // for (uint256 i = 0; i < testAddresses.length; i++) { + // vm.startPrank(testAddresses[i]); + // vm.expectRevert("StabilityPool: Caller is not LiquidityStrategy"); + // stabilityPool.swapCollateralForStable(1e18, 1000e18); + // vm.stopPrank(); + // } + // } + + // function testSwapCollateralForStableWithZeroSPDeposits() public { + // // Try to swap when SP has no deposits + // uint256 collSwapAmount = 1e18; + // uint256 stableSwapAmount = 1000e18; + + // vm.startPrank(liquidityStrategy); + // vm.expectRevert(); // Should revert due to division by zero or insufficient balance + // stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + // vm.stopPrank(); + // } +} From 4b30ffaabd4907e7825855603f1beeb9515bdd12 Mon Sep 17 00:00:00 2001 From: baroooo Date: Thu, 18 Sep 2025 14:13:00 +0200 Subject: [PATCH 13/79] test: scale change and min deposit amount --- contracts/src/StabilityPool.sol | 3 +- contracts/test/swapCollateralForStable.t.sol | 160 +++++++------------ 2 files changed, 61 insertions(+), 102 deletions(-) diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 7e6aeddfb..ebfc5c1f5 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -177,7 +177,6 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi // Each time the scale of P shifts by SCALE_FACTOR, the scale is incremented by 1 uint256 public currentScale; - // TODO: is this 1-1 between SP and liquidity strategy? address public liquidityStrategy; /* Coll Gain sum 'S': During its lifetime, each deposit d_t earns an Coll gain of ( d_t * [S - S_t] )/P_t, where S_t @@ -204,7 +203,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi _disableInitializers(); } } - + function initialize(IAddressesRegistry _addressesRegistry) external initializer { __LiquityBase_init(_addressesRegistry); diff --git a/contracts/test/swapCollateralForStable.t.sol b/contracts/test/swapCollateralForStable.t.sol index e612ce3d9..9c510ddb4 100644 --- a/contracts/test/swapCollateralForStable.t.sol +++ b/contracts/test/swapCollateralForStable.t.sol @@ -195,8 +195,6 @@ contract SwapCollateralForStableTest is DevTestSetup { assertEq(collToken.balanceOf(liquidityStrategy), initialValues.lsCollBalance - collSwapAmount); } - // ========== MULTIPLE DEPOSITORS TESTS ========== - function testSwapCollateralForStableWithMultipleDepositors() public { uint256 depositA = 1000e18; // 1/6 uint256 depositB = 2000e18; // 1/3 @@ -274,8 +272,6 @@ contract SwapCollateralForStableTest is DevTestSetup { assertApproxEqAbs(boldToken.balanceOf(C), depositC - (initialValues.depositor3BoldBalance + stableSwapAmount / 2 + 1e18), 1e18); } - // ========== INTEGRATION WITH LIQUIDATIONS ========== - function testSwapCollateralForStableAfterLiquidation() public { uint256 stableAmount = 2000e18; uint256 collAmount = 2e18; @@ -361,8 +357,6 @@ contract SwapCollateralForStableTest is DevTestSetup { assertApproxEqAbs(boldToken.balanceOf(C), cBoldDeposit - (stableSwapAmount + stableAmount), 1e18); } - // ========== EVENT TESTING ========== - function testSwapCollateralForStableEmitsCorrectEvents() public { uint256 stableAmount = 1000e18; @@ -407,98 +401,64 @@ contract SwapCollateralForStableTest is DevTestSetup { vm.stopPrank(); } - // // ========== SCALE CHANGES TESTS ========== - - // function testSwapCollateralForStableWithScaleChanges() public { - // // Create a scenario that will trigger scale changes - // uint256 largeDeposit = 1000000e18; - // makeSPDepositAndClaim(A, largeDeposit); - - // // Perform multiple large swaps to potentially trigger scale changes - // uint256 collSwapAmount = 100e18; - // uint256 stableSwapAmount = 200000e18; // Large amount relative to deposits - - // uint256 initialScale = stabilityPool.currentScale(); - - // vm.startPrank(liquidityStrategy); - // stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); - // vm.stopPrank(); - - // // Check if scale changed (it might not in this case, but the mechanism should work) - // uint256 finalScale = stabilityPool.currentScale(); - // assertTrue(finalScale >= initialScale); - - // // Verify the swap still worked correctly regardless of scale changes - // assertEq(stabilityPool.getTotalBoldDeposits(), largeDeposit - stableSwapAmount); - // assertEq(stabilityPool.getCollBalance(), collSwapAmount); - // } - - // // ========== BOUNDARY CONDITIONS ========== - - // function testSwapCollateralForStableAtMinimumDeposit() public { - // // Test with minimum possible deposit - // uint256 minDeposit = 1e18; // MIN_BOLD_IN_SP - // makeSPDepositAndClaim(A, minDeposit); - - // uint256 collSwapAmount = 0.001e18; // Very small - // uint256 stableSwapAmount = 0.5e18; // Half of minimum deposit - - // vm.startPrank(liquidityStrategy); - // stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); - // vm.stopPrank(); - - // // Should still work and leave at least MIN_BOLD_IN_SP - // assertTrue(stabilityPool.getTotalBoldDeposits() >= 1e18); - // assertEq(stabilityPool.getCollBalance(), collSwapAmount); - // } - - // function testSwapCollateralForStableWithMaxUint256() public { - // // Test with very large numbers (but not actually max uint256 to avoid overflow) - // uint256 largeDeposit = 1000000000e18; // 1 billion tokens - // makeSPDepositAndClaim(A, largeDeposit); - - // uint256 collSwapAmount = 1000000e18; // 1 million collateral - // uint256 stableSwapAmount = 100000000e18; // 100 million stable - - // vm.startPrank(liquidityStrategy); - // stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); - // vm.stopPrank(); - - // // Should handle large numbers correctly - // assertEq(stabilityPool.getTotalBoldDeposits(), largeDeposit - stableSwapAmount); - // assertEq(stabilityPool.getCollBalance(), collSwapAmount); - // assertEq(boldToken.balanceOf(liquidityStrategy), stableSwapAmount); - // assertEq(collToken.balanceOf(liquidityStrategy), 10000e18 - collSwapAmount); // Initial 10000e18 - swapped - // } - - // // ========== REENTRANCY AND SECURITY TESTS ========== - - // function testSwapCollateralForStableCannotBeCalledByNonLiquidityStrategy() public { - // makeSPDepositAndClaim(A, 1000e18); - - // // Try calling from different addresses - // address[] memory testAddresses = new address[](4); - // testAddresses[0] = A; - // testAddresses[1] = B; - // testAddresses[2] = C; - // testAddresses[3] = address(this); - - // for (uint256 i = 0; i < testAddresses.length; i++) { - // vm.startPrank(testAddresses[i]); - // vm.expectRevert("StabilityPool: Caller is not LiquidityStrategy"); - // stabilityPool.swapCollateralForStable(1e18, 1000e18); - // vm.stopPrank(); - // } - // } - - // function testSwapCollateralForStableWithZeroSPDeposits() public { - // // Try to swap when SP has no deposits - // uint256 collSwapAmount = 1e18; - // uint256 stableSwapAmount = 1000e18; - - // vm.startPrank(liquidityStrategy); - // vm.expectRevert(); // Should revert due to division by zero or insufficient balance - // stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); - // vm.stopPrank(); - // } + function testSwapCollateralForStableWithScaleChanges() public { + // Create a scenario that will trigger scale change + uint256 stableAmount = 10_000_000_000e18; + uint256 collSwapAmount = 1_000_000e18; + uint256 stableSwapAmount = stableAmount - 1e18; // Large amount relative to deposits + + deal(address(boldToken), A, stableAmount/2); + deal(address(boldToken), B, stableAmount/2); + deal(address(collToken), liquidityStrategy, collSwapAmount); + + makeSPDepositAndClaim(A, stableAmount/2); + makeSPDepositAndClaim(B, stableAmount/2); + + + uint256 initialScale = stabilityPool.currentScale(); + + + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + + // 1e36 * (1e18 / 1e28) = 1e26 + // 1e26 < 1e27, so the scale should increase + + uint256 finalScale = stabilityPool.currentScale(); + assertGt(finalScale, initialScale); + assertEq(finalScale, 1); + + // P should be scaled back up 1e26 * 1e9 = 1e35 + assertEq(stabilityPool.P(), 1e35); + + // // Verify the swap still worked correctly regardless of scale changes + assertEq(stabilityPool.getTotalBoldDeposits(), stableAmount - stableSwapAmount); + assertEq(stabilityPool.getCollBalance(), collSwapAmount); + + uint256 compundedBoldDeposit = stabilityPool.getCompoundedBoldDeposit(A); + assertEq(compundedBoldDeposit, (stableAmount - stableSwapAmount) / 2); + + uint256 depositorCollGain = stabilityPool.getDepositorCollGain(A); + assertEq(depositorCollGain, collSwapAmount / 2 ); + } + + + function testSwapCollateralForStableAtMinimumDeposit() public { + uint256 stableAmount = 2e18; + + deal(address(boldToken), A, stableAmount); + makeSPDepositAndClaim(A, stableAmount); + + uint256 collSwapAmount = 0.001e18; + uint256 stableSwapAmount = 1e18; + + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + + // Should still work and leave at least MIN_BOLD_IN_SP + assertEq(stabilityPool.getTotalBoldDeposits(), 1e18); + assertEq(stabilityPool.getCollBalance(), collSwapAmount); + } } From c9118aae5d01a7d67639cf8d1f52414da7bc5a1b Mon Sep 17 00:00:00 2001 From: baroooo Date: Fri, 19 Sep 2025 09:46:57 +0200 Subject: [PATCH 14/79] feat: MIN_BOLD_AFTER_REBALANCE --- contracts/src/Interfaces/IStabilityPool.sol | 2 +- contracts/src/StabilityPool.sol | 12 ++--- contracts/test/swapCollateralForStable.t.sol | 56 ++++++++++---------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/contracts/src/Interfaces/IStabilityPool.sol b/contracts/src/Interfaces/IStabilityPool.sol index ccb3eaaee..8e18f6451 100644 --- a/contracts/src/Interfaces/IStabilityPool.sol +++ b/contracts/src/Interfaces/IStabilityPool.sol @@ -72,7 +72,6 @@ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { */ function offset(uint256 _debt, uint256 _coll) external; - function deposits(address _depositor) external view returns (uint256 initialValue); function stashedColl(address _depositor) external view returns (uint256); @@ -119,4 +118,5 @@ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { function liquidityStrategy() external view returns (address); function P_PRECISION() external view returns (uint256); + function MIN_BOLD_AFTER_REBALANCE() external view returns (uint256); } diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index ebfc5c1f5..07c7536a3 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -11,8 +11,6 @@ import "./Interfaces/ITroveManager.sol"; import "./Interfaces/IBoldToken.sol"; import "./Dependencies/LiquityBaseInit.sol"; -import "forge-std/console.sol"; - /* * The Stability Pool holds Bold tokens deposited by Stability Pool depositors. * @@ -174,6 +172,10 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi // The number of scale changes after which an untouched deposit stops receiving yield / coll gains uint256 public constant SCALE_SPAN = 2; + // The minimum amount of Bold in the SP after a rebalance + // Introduced to avoid higher rate of scale changes + uint256 public constant MIN_BOLD_AFTER_REBALANCE = 1_000e18; + // Each time the scale of P shifts by SCALE_FACTOR, the scale is incremented by 1 uint256 public currentScale; @@ -203,7 +205,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi _disableInitializers(); } } - + function initialize(IAddressesRegistry _addressesRegistry) external initializer { __LiquityBase_init(_addressesRegistry); @@ -406,9 +408,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi _swapCollateralForStable(amountCollIn, amountStableOut); - // TODO: added this to avoid higher rate of scaling of P during rebalances for very small totalBoldDeposits - // Discuss with team if it is ok to have a larger limit for this to make scaling even less aggressive - require(totalBoldDeposits >= MIN_BOLD_IN_SP, "Total Bold deposits must be >= MIN_BOLD_IN_SP"); + require(totalBoldDeposits >= MIN_BOLD_AFTER_REBALANCE, "Total Bold deposits must be >= MIN_BOLD_AFTER_REBALANCE"); } // --- Liquidation functions --- diff --git a/contracts/test/swapCollateralForStable.t.sol b/contracts/test/swapCollateralForStable.t.sol index 9c510ddb4..2abf9ea8a 100644 --- a/contracts/test/swapCollateralForStable.t.sol +++ b/contracts/test/swapCollateralForStable.t.sol @@ -64,23 +64,22 @@ contract SwapCollateralForStableTest is DevTestSetup { stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); - vm.expectRevert("Total Bold deposits must be >= MIN_BOLD_IN_SP"); + vm.expectRevert("Total Bold deposits must be >= MIN_BOLD_AFTER_REBALANCE"); stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount - .1e18); vm.stopPrank(); } function testSwapCollateralForStableWithSurplus() public { - uint256 stableAmount = 2000e18; - uint256 collAmount = 2e18; SwapTestVars memory initialValues; priceFeed.setPrice(2000e18); vm.startPrank(A); - borrowerOperations.openTrove( + + borrowerOperations.openTrove( A, 0, - collAmount, - stableAmount, + 2e18, + 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, @@ -95,8 +94,8 @@ contract SwapCollateralForStableTest is DevTestSetup { borrowerOperations.openTrove( B, 0, - 2 * collAmount, - stableAmount + 100e18, + 4e18, + 4000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, @@ -109,7 +108,7 @@ contract SwapCollateralForStableTest is DevTestSetup { // B deposits to SP - makeSPDepositAndClaim(B, stableAmount + 100e18); + makeSPDepositAndClaim(B, 4000e18); initialValues.depositor1CollBalance = collToken.balanceOf(B); initialValues.depositor1BoldBalance = boldToken.balanceOf(B); @@ -119,10 +118,10 @@ contract SwapCollateralForStableTest is DevTestSetup { initialValues.lsCollBalance = collToken.balanceOf(liquidityStrategy); // Check SP has deposits - assertEq(initialValues.spBoldBalance, stableAmount + 100e18, "SP should have Bold deposits"); + assertEq(initialValues.spBoldBalance, 4000e18, "SP should have Bold deposits"); - uint256 collSwapAmount = stableAmount / 2000; // we use the price to calculate the amount of collateral to swap - uint256 stableSwapAmount = stableAmount; + uint256 collSwapAmount = 1e18; + uint256 stableSwapAmount = 2000e18; // Simulate a rebalance by calling swapCollateralForStable as liquidity strategy vm.startPrank(liquidityStrategy); @@ -154,19 +153,20 @@ contract SwapCollateralForStableTest is DevTestSetup { assertEq(finalLSCollBalance, initialValues.lsCollBalance - collSwapAmount); vm.prank(B); - stabilityPool.withdrawFromSP(100e18 - 1e18, true); // subtract 1e18 to avoid MIN_BOLD_IN_SP + stabilityPool.withdrawFromSP(finalSPBoldBalance - 1000e18, true); // subtract 1000e18 to avoid MIN_BOLD_AFTER_REBALANCE // Check B has received Bold // Received bold is less than the initial deposit because of the rebalance - assertApproxEqAbs(boldToken.balanceOf(B), initialValues.depositor1BoldBalance + 99e18, 1e18); + assertApproxEqAbs(boldToken.balanceOf(B), initialValues.depositor1BoldBalance + 1000e18, 1e18); // Check B has received Coll // Even though the collateral was never deposited, depositor should have received the collateral from the rebalance - assertApproxEqAbs(collToken.balanceOf(B), initialValues.depositor1CollBalance + collSwapAmount, 1e18); + assertEq(collToken.balanceOf(B), initialValues.depositor1CollBalance + collSwapAmount); + } function testSwapCollateralForStableWithLargerAmounts() public { - uint256 stableAmount = 1e32; // Large amount + uint256 stableAmount = 1e32; deal(address(collToken), address(liquidityStrategy), 1e32); deal(address(boldToken), A, stableAmount); @@ -174,7 +174,7 @@ contract SwapCollateralForStableTest is DevTestSetup { makeSPDepositAndClaim(A, stableAmount); uint256 collSwapAmount = 1e30; - uint256 stableSwapAmount = stableAmount - 1e18; // avoid MIN_BOLD_IN_SP + uint256 stableSwapAmount = stableAmount - 1000e18; // avoid MIN_BOLD_AFTER_REBALANCE SwapTestVars memory initialValues; initialValues.spBoldBalance = stabilityPool.getTotalBoldDeposits(); @@ -358,9 +358,9 @@ contract SwapCollateralForStableTest is DevTestSetup { } function testSwapCollateralForStableEmitsCorrectEvents() public { - uint256 stableAmount = 1000e18; + uint256 stableAmount = 10_000e18; - uint256 collSwapAmount = 1e18; + uint256 collSwapAmount = 1000e18; uint256 stableSwapAmount = stableAmount / 2; @@ -379,13 +379,13 @@ contract SwapCollateralForStableTest is DevTestSetup { // Expect events to be emitted in the correct order // First: S_Updated (from _updateTrackingVariables) // S_Updated value = P * _amountCollIn / totalBoldDeposits - // = 1e36 * 1e18 / 1000e18 = 1e33 + // = 1e36 * 1000e18 / 10_000e18 = 1e35 vm.expectEmit(true, true, true, true); - emit S_Updated(1e33, 0); + emit S_Updated(1e35, 0); // Second: P_Updated (from _updateTrackingVariables) // P_Updated value = P * (totalBoldDeposits - _amountStableOut) / totalBoldDeposits - // = 1e36 * (1000e18 - 1000e18 / 2) / 1000e18 = 5e35 + // = 1e36 * (10000e18 - 10000e18 / 2) / 10000e18 = 5e35 vm.expectEmit(true, true, true, true); emit P_Updated(5e35); @@ -403,9 +403,9 @@ contract SwapCollateralForStableTest is DevTestSetup { function testSwapCollateralForStableWithScaleChanges() public { // Create a scenario that will trigger scale change - uint256 stableAmount = 10_000_000_000e18; + uint256 stableAmount = 10_000_000_000_000e18; uint256 collSwapAmount = 1_000_000e18; - uint256 stableSwapAmount = stableAmount - 1e18; // Large amount relative to deposits + uint256 stableSwapAmount = stableAmount - 1000e18; // Swap out all liquidity - MIN_BOLD_AFTER_REBALANCE deal(address(boldToken), A, stableAmount/2); deal(address(boldToken), B, stableAmount/2); @@ -445,20 +445,20 @@ contract SwapCollateralForStableTest is DevTestSetup { function testSwapCollateralForStableAtMinimumDeposit() public { - uint256 stableAmount = 2e18; + uint256 stableAmount = 2000e18; deal(address(boldToken), A, stableAmount); makeSPDepositAndClaim(A, stableAmount); - uint256 collSwapAmount = 0.001e18; - uint256 stableSwapAmount = 1e18; + uint256 collSwapAmount = 1e18; + uint256 stableSwapAmount = 1000e18; vm.startPrank(liquidityStrategy); stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); vm.stopPrank(); // Should still work and leave at least MIN_BOLD_IN_SP - assertEq(stabilityPool.getTotalBoldDeposits(), 1e18); + assertEq(stabilityPool.getTotalBoldDeposits(), 1_000e18); assertEq(stabilityPool.getCollBalance(), collSwapAmount); } } From b380a591194e015d071324a3ef3cb41db464710b Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:44:03 -0500 Subject: [PATCH 15/79] feat: add system params interface --- contracts/src/Interfaces/ISystemParams.sol | 175 +++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 contracts/src/Interfaces/ISystemParams.sol diff --git a/contracts/src/Interfaces/ISystemParams.sol b/contracts/src/Interfaces/ISystemParams.sol new file mode 100644 index 000000000..d7d3d806e --- /dev/null +++ b/contracts/src/Interfaces/ISystemParams.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.24; + +interface ISystemParams { + /* ========== ERRORS ========== */ + + error InvalidMinDebt(); + error InvalidInterestRateBounds(); + error InvalidFeeValue(); + error InvalidTimeValue(); + error InvalidGasCompensation(); + error MinInterestRateGtMax(); + error InvalidCCR(); + error InvalidMCR(); + error InvalidBCR(); + error InvalidSCR(); + error SPPenaltyTooLow(); + error SPPenaltyGtRedist(); + error RedistPenaltyTooHigh(); + + /* ========== EVENTS ========== */ + + /// @notice Emitted when debt parameters are updated. + event DebtParamsUpdated(uint256 minDebt, uint256 ethGasCompensation); + + /// @notice Emitted when collateral parameters are updated. + event CollateralParamsUpdated( + uint256 ccr, + uint256 scr, + uint256 mcr, + uint256 bcr, + uint256 liquidationPenaltySP, + uint256 liquidationPenaltyRedistribution + ); + + /// @notice Emitted when Interest rate parameters are updated. + event InterestParamsUpdated( + uint256 minAnnualInterestRate, + uint256 maxAnnualInterestRate, + uint128 maxAnnualBatchManagementFee, + uint256 upfrontInterestPeriod, + uint256 interestRateAdjCooldown, + uint128 minInterestRateChangePeriod + ); + + /// @notice Emitted when redemption parameters are updated. + event RedemptionParamsUpdated( + uint256 redemptionFeeFloor, + uint256 initialBaseRate, + uint256 redemptionMinuteDecayFactor, + uint256 redemptionBeta, + uint256 urgentRedemptionBonus + ); + + /// @notice Emitted when stability pool parameters are updated. + event PoolParamsUpdated(uint256 spYieldSplit, uint256 minBoldInSP); + + /// @notice Emitted when gas compensation parameters are updated. + event GasCompParamsUpdated( + uint256 collGasCompensationDivisor, + uint256 collGasCompensationCap + ); + + /// @notice Emitted when the version is updated. + event VersionUpdated(uint256 oldVersion, uint256 newVersion); + + /* ========== DEBT PARAMETERS ========== */ + + /// @notice Minimum amount of net debt a trove must have. + function MIN_DEBT() external view returns (uint256); + + // TODO(@bayo): Consider rename to native if this makes sense + // and update comment. + /// @notice Amount of ETH to be locked in gas pool on opening troves. + function ETH_GAS_COMPENSATION() external view returns (uint256); + + /* ========== COLLATERAL PARAMETERS ========== */ + + /** + * @notice Critical system collateral ratio. + * @dev If the system's total collateral ratio (TCR) falls below the CCR, some borrowing + * operation restrictions are applied. + */ + function CCR() external view returns (uint256); + + /** + * @notice Shutdown system collateral ratio. + * @dev If the system's total collateral ratio (TCR) for a given collateral falls below the SCR, + * the protocol triggers the shutdown of the borrow market and permanently disables all + * borrowing operations except for closing Troves. + */ + function SCR() external view returns (uint256); + + /// @notice Minimum collateral ratio for individual troves. + function MCR() external view returns (uint256); + + /** + * @notice Extra buffer of collateral ratio to join a batch or adjust a trove inside + * a batch (on top of MCR). + */ + function BCR() external view returns (uint256); + + /// @notice Liquidation penalty for troves offset to the SP + function LIQUIDATION_PENALTY_SP() external view returns (uint256); + + /// @notice Liquidation penalty for troves redistributed. + function LIQUIDATION_PENALTY_REDISTRIBUTION() + external + view + returns (uint256); + + /* ========== INTEREST PARAMETERS ========== */ + + /// @notice Min annual interest rate for a trove. + function MIN_ANNUAL_INTEREST_RATE() external view returns (uint256); + + /// @notice Max annual interest rate for a trove. + function MAX_ANNUAL_INTEREST_RATE() external view returns (uint256); + + /// @notice Max fee that batch managers can charge. + function MAX_ANNUAL_BATCH_MANAGEMENT_FEE() external view returns (uint128); + + /// @notice Time period for which interest is charged upfront to prevent rate gaming. + function UPFRONT_INTEREST_PERIOD() external view returns (uint256); + + /// @notice Wait time in between interest rate adjustments. + function INTEREST_RATE_ADJ_COOLDOWN() external view returns (uint256); + + /// @notice Minimum time in between interest rate changes triggered by a batch Manager. + function MIN_INTEREST_RATE_CHANGE_PERIOD() external view returns (uint128); + + /* ========== REDEMPTION PARAMETERS ========== */ + + /// @notice Minimum redemption fee percentage. + function REDEMPTION_FEE_FLOOR() external view returns (uint256); + + /// @notice The initial redemption fee value. + function INITIAL_BASE_RATE() external view returns (uint256); + + /** + * @notice Factor to reduce the redemption fee per minute. + * @dev The more minutes that pass without redemptions the more the base rate decays. + */ + function REDEMPTION_MINUTE_DECAY_FACTOR() external view returns (uint256); + + /// @notice Divisor controlling base rate sensitivity to redemption volume (higher = less sensitive). + function REDEMPTION_BETA() external view returns (uint256); + + /// @notice Extra collateral bonus given to redeemers during urgent redemptions after shutdown. + function URGENT_REDEMPTION_BONUS() external view returns (uint256); + + /* ========== STABILITY POOL PARAMETERS ========== */ + + /// @notice Percentage of minted interest yield allocated to Stability Pool depositors. + function SP_YIELD_SPLIT() external view returns (uint256); + + /// @notice Minimum BOLD that must remain in Stability Pool to prevent complete drainage. + function MIN_BOLD_IN_SP() external view returns (uint256); + + /* ========== GAS COMPENSATION PARAMETERS ========== */ + + /// @notice Divisor for calculating collateral gas compensation for liquidators. + function COLL_GAS_COMPENSATION_DIVISOR() external view returns (uint256); + + /// @notice Maximum collateral gas compensation cap for liquidators. + function COLL_GAS_COMPENSATION_CAP() external view returns (uint256); + + /* ========== VERSION ========== */ + + /** + * @notice Returns the version of the system params contract. + */ + function version() external view returns (uint256); +} From 7e1ae631495af88a71f56cdc8d0c4b1c99a15b7b Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 9 Sep 2025 18:30:22 -0500 Subject: [PATCH 16/79] feat: add sys params interface --- contracts/src/Interfaces/ISystemParams.sol | 186 +++++++++++++++++---- 1 file changed, 153 insertions(+), 33 deletions(-) diff --git a/contracts/src/Interfaces/ISystemParams.sol b/contracts/src/Interfaces/ISystemParams.sol index d7d3d806e..5376fadfc 100644 --- a/contracts/src/Interfaces/ISystemParams.sol +++ b/contracts/src/Interfaces/ISystemParams.sol @@ -21,8 +21,23 @@ interface ISystemParams { /* ========== EVENTS ========== */ - /// @notice Emitted when debt parameters are updated. - event DebtParamsUpdated(uint256 minDebt, uint256 ethGasCompensation); + /// @notice Emitted when min debt is updated. + event MinDebtUpdated(uint256 oldMinDebt, uint256 newMinDebt); + + /// @notice Emitted when liquidation parameters are updated. + event LiquidationParamsUpdated( + uint256 minLiquidationPenaltySP, + uint256 maxLiquidationPenaltyRedistribution, + uint256 liquidationPenaltySP, + uint256 liquidationPenaltyRedistribution + ); + + /// @notice Emitted when gas compensation parameters are updated. + event GasCompParamsUpdated( + uint256 collGasCompensationDivisor, + uint256 collGasCompensationCap, + uint256 ethGasCompensation + ); /// @notice Emitted when collateral parameters are updated. event CollateralParamsUpdated( @@ -30,8 +45,6 @@ interface ISystemParams { uint256 scr, uint256 mcr, uint256 bcr, - uint256 liquidationPenaltySP, - uint256 liquidationPenaltyRedistribution ); /// @notice Emitted when Interest rate parameters are updated. @@ -41,7 +54,8 @@ interface ISystemParams { uint128 maxAnnualBatchManagementFee, uint256 upfrontInterestPeriod, uint256 interestRateAdjCooldown, - uint128 minInterestRateChangePeriod + uint128 minInterestRateChangePeriod, + uint256 maxBatchSharesRatio ); /// @notice Emitted when redemption parameters are updated. @@ -54,13 +68,8 @@ interface ISystemParams { ); /// @notice Emitted when stability pool parameters are updated. - event PoolParamsUpdated(uint256 spYieldSplit, uint256 minBoldInSP); + event StabilityPoolParamsUpdated(uint256 spYieldSplit, uint256 minBoldInSP); - /// @notice Emitted when gas compensation parameters are updated. - event GasCompParamsUpdated( - uint256 collGasCompensationDivisor, - uint256 collGasCompensationCap - ); /// @notice Emitted when the version is updated. event VersionUpdated(uint256 oldVersion, uint256 newVersion); @@ -70,7 +79,35 @@ interface ISystemParams { /// @notice Minimum amount of net debt a trove must have. function MIN_DEBT() external view returns (uint256); - // TODO(@bayo): Consider rename to native if this makes sense + /* ========== LIQUIDATION PARAMETERS ========== */ + + // TODO(@bayological): These min and max params are used to validate + // the penalaties are within expected ranges when + // being set. Do we want these to be configurable? + /// @notice Minimum liquidation penalty for troves offset to the SP + function MIN_LIQUIDATION_PENALTY_SP() external view returns (uint256); + + /// @notice Maximum liquidation penalty for troves redistributed. + function MAX_LIQUIDATION_PENALTY_REDISTRIBUTION() external view returns (uint256); + + /// @notice Liquidation penalty for troves offset to the SP + function LIQUIDATION_PENALTY_SP() external view returns (uint256); + + /// @notice Liquidation penalty for troves redistributed. + function LIQUIDATION_PENALTY_REDISTRIBUTION() + external + view + returns (uint256); + + /* ========== GAS COMPENSATION PARAMETERS ========== */ + + /// @notice Divisor for calculating collateral gas compensation for liquidators. + function COLL_GAS_COMPENSATION_DIVISOR() external view returns (uint256); + + /// @notice Maximum collateral gas compensation cap for liquidators. + function COLL_GAS_COMPENSATION_CAP() external view returns (uint256); + + // TODO(@bayological): Consider rename to native(or just drop eth) if this makes sense // and update comment. /// @notice Amount of ETH to be locked in gas pool on opening troves. function ETH_GAS_COMPENSATION() external view returns (uint256); @@ -101,15 +138,6 @@ interface ISystemParams { */ function BCR() external view returns (uint256); - /// @notice Liquidation penalty for troves offset to the SP - function LIQUIDATION_PENALTY_SP() external view returns (uint256); - - /// @notice Liquidation penalty for troves redistributed. - function LIQUIDATION_PENALTY_REDISTRIBUTION() - external - view - returns (uint256); - /* ========== INTEREST PARAMETERS ========== */ /// @notice Min annual interest rate for a trove. @@ -130,6 +158,9 @@ interface ISystemParams { /// @notice Minimum time in between interest rate changes triggered by a batch Manager. function MIN_INTEREST_RATE_CHANGE_PERIOD() external view returns (uint128); + /// @notice Maximum ratio between batch debt and shares to prevent inflation attacks. + function MAX_BATCH_SHARES_RATIO() external view returns (uint256); + /* ========== REDEMPTION PARAMETERS ========== */ /// @notice Minimum redemption fee percentage. @@ -138,10 +169,7 @@ interface ISystemParams { /// @notice The initial redemption fee value. function INITIAL_BASE_RATE() external view returns (uint256); - /** - * @notice Factor to reduce the redemption fee per minute. - * @dev The more minutes that pass without redemptions the more the base rate decays. - */ + /// @notice Factor to reduce the redemption fee per minute. function REDEMPTION_MINUTE_DECAY_FACTOR() external view returns (uint256); /// @notice Divisor controlling base rate sensitivity to redemption volume (higher = less sensitive). @@ -158,18 +186,110 @@ interface ISystemParams { /// @notice Minimum BOLD that must remain in Stability Pool to prevent complete drainage. function MIN_BOLD_IN_SP() external view returns (uint256); - /* ========== GAS COMPENSATION PARAMETERS ========== */ + /* ========== VERSION ========== */ - /// @notice Divisor for calculating collateral gas compensation for liquidators. - function COLL_GAS_COMPENSATION_DIVISOR() external view returns (uint256); + /// @notice Returns the version of the system params contract. + function version() external view returns (uint256); - /// @notice Maximum collateral gas compensation cap for liquidators. - function COLL_GAS_COMPENSATION_CAP() external view returns (uint256); + /* ========== FUNCTIONS ========== */ - /* ========== VERSION ========== */ + /** + * @notice Update the minimum debt + * @param _minDebt The new minimum debt amount + */ + function updateMinDebt( + uint256 _minDebt + ) external; /** - * @notice Returns the version of the system params contract. + * @notice Update the liquidation params. + * @param _minLiquidationPenaltySP The minimum liquidation penalty for stability pool + * @param _maxLiquidationPenaltyRedistribution The maximum liquidation penalty for redistribution + * @param _liquidationPenaltySP The liquidation penalty for stability pool + * @param _liquidationPenaltyRedistribution The liquidation penalty for redistribution */ - function version() external view returns (uint256); + function updateLiquidationParams( + uint256 _minLiquidationPenaltySP, + uint256 _maxLiquidationPenaltyRedistribution, + uint256 _liquidationPenaltySP, + uint256 _liquidationPenaltyRedistribution + ) external; + + /** + * @notice Update gas compensation parameters. + * @param _collGasCompensationDivisor Collateral gas compensation divisor. + * @param _collGasCompensationCap Collateral gas compensation cap. + * @param _ethGasCompensation Amount of ETH to be locked in gas pool on opening troves. + */ + function updateGasCompParams( + uint256 _collGasCompensationDivisor, + uint256 _collGasCompensationCap, + uint256 _ethGasCompensation + ) external; + + /** + * @notice Update the collateral related parameters. + * @param _ccr The critical collateral ratio. + * @param _scr The shutdown collateral ratio. + * @param _mcr The minimum collateral ratio. + * @param _bcr The base collateral ratio. + */ + function updateCollateralParams( + uint256 _ccr, + uint256 _scr, + uint256 _mcr, + uint256 _bcr, + ) external; + + /** + * @notice Update interest related parameters. + * @param _minAnnualInterestRate The minimum annual interest rate + * @param _maxAnnualInterestRate The maximum annual interest rate + * @param _maxAnnualBatchManagementFee The maximum annual batch management fee + * @param _upfrontInterestPeriod The upfront interest period + * @param _interestRateAdjCooldown The interest rate adjustment cooldown + * @param _minInterestRateChangePeriod The minimum interest rate change period + * @param _maxBatchSharesRatio The maximum ratio between batch debt and shares to prevent inflation attacks + */ + function updateInterestParams( + uint256 _minAnnualInterestRate, + uint256 _maxAnnualInterestRate, + uint128 _maxAnnualBatchManagementFee, + uint256 _upfrontInterestPeriod, + uint256 _interestRateAdjCooldown, + uint128 _minInterestRateChangePeriod, + uint256 _maxBatchSharesRatio + ) external; + + /** + * @notice Update redemption related parameters. + * @param _redemptionFeeFloor The min redemption fee percentage + * @param _initialBaseRate The initial base rate + * @param _redemptionMinuteDecayFactor Factor to reduce the redemption fee per minute. + * @param _redemptionBeta The redemption beta + * @param _urgentRedemptionBonus The urgent redemption bonus given to redeemers after shutdownn + */ + function updateRedemptionParams( + uint256 _redemptionFeeFloor, + uint256 _initialBaseRate, + uint256 _redemptionMinuteDecayFactor, + uint256 _redemptionBeta, + uint256 _urgentRedemptionBonus + ) external; + + /** + * @notice Update stability pool related parameters. + * @param _spYieldSplit Percentage of minted interest yield allocated to Stability Pool depositors. + * @param _minBoldInSP Minimum BOLD that must remain in Stability Pool to prevent complete drainage. + */ + function updatePoolParams( + uint256 _spYieldSplit, + uint256 _minBoldInSP + ) external; + + /** + * @notice Update the version of the system params contract. + * @param _newVersion The new version number. + */ + function updateVersion(uint256 _newVersion) external; } From 33502523da71afedbff92de28b7f045d5dc8b9e9 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:11:00 -0500 Subject: [PATCH 17/79] chore: remove comma --- contracts/src/Interfaces/ISystemParams.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/Interfaces/ISystemParams.sol b/contracts/src/Interfaces/ISystemParams.sol index 5376fadfc..3313f8c43 100644 --- a/contracts/src/Interfaces/ISystemParams.sol +++ b/contracts/src/Interfaces/ISystemParams.sol @@ -44,7 +44,7 @@ interface ISystemParams { uint256 ccr, uint256 scr, uint256 mcr, - uint256 bcr, + uint256 bcr ); /// @notice Emitted when Interest rate parameters are updated. @@ -238,7 +238,7 @@ interface ISystemParams { uint256 _ccr, uint256 _scr, uint256 _mcr, - uint256 _bcr, + uint256 _bcr ) external; /** From b5f62a0d366a74c0e99b60fd152a0f9671641361 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:13:59 -0500 Subject: [PATCH 18/79] feat: implement system params --- contracts/src/SystemParams.sol | 311 +++++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 contracts/src/SystemParams.sol diff --git a/contracts/src/SystemParams.sol b/contracts/src/SystemParams.sol new file mode 100644 index 000000000..a8eef7718 --- /dev/null +++ b/contracts/src/SystemParams.sol @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.24; + +import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import {Initializable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; + +import {ISystemParams} from "./Interfaces/ISystemParams.sol"; +import {_100pct, _1pct} from "./Dependencies/Constants.sol"; + +/** + * @title System Parameters + * @author Mento Labs + * @notice This contract manages the system-wide parameters for the protocol. + */ +contract SystemParams is Initializable, OwnableUpgradeable, ISystemParams { + uint256 public version; + + /* ========== DEBT PARAMETERS ========== */ + + uint256 public MIN_DEBT; + + /* ========== LIQUIDATION PARAMETERS ========== */ + + uint256 public MIN_LIQUIDATION_PENALTY_SP; + uint256 public MAX_LIQUIDATION_PENALTY_REDISTRIBUTION; + uint256 public LIQUIDATION_PENALTY_SP; + uint256 public LIQUIDATION_PENALTY_REDISTRIBUTION; + + /* ========== GAS COMPENSATION PARAMETERS ========== */ + + uint256 public COLL_GAS_COMPENSATION_DIVISOR; + uint256 public COLL_GAS_COMPENSATION_CAP; + uint256 public ETH_GAS_COMPENSATION; + + /* ========== COLLATERAL PARAMETERS ========== */ + + uint256 public CCR; + uint256 public SCR; + uint256 public MCR; + uint256 public BCR; + + /* ========== INTEREST PARAMETERS ========== */ + + uint256 public MIN_ANNUAL_INTEREST_RATE; + uint256 public MAX_ANNUAL_INTEREST_RATE; + uint128 public MAX_ANNUAL_BATCH_MANAGEMENT_FEE; + uint256 public UPFRONT_INTEREST_PERIOD; + uint256 public INTEREST_RATE_ADJ_COOLDOWN; + uint128 public MIN_INTEREST_RATE_CHANGE_PERIOD; + uint256 public MAX_BATCH_SHARES_RATIO; + + /* ========== REDEMPTION PARAMETERS ========== */ + + uint256 public REDEMPTION_FEE_FLOOR; + uint256 public INITIAL_BASE_RATE; + uint256 public REDEMPTION_MINUTE_DECAY_FACTOR; + uint256 public REDEMPTION_BETA; + uint256 public URGENT_REDEMPTION_BONUS; + + /* ========== STABILITY POOL PARAMETERS ========== */ + + uint256 public SP_YIELD_SPLIT; + uint256 public MIN_BOLD_IN_SP; + + /* ========== CONSTRUCTOR ========== */ + + /** + * @notice Contract constructor + * @param disable Boolean to disable initializers for implementation contract + */ + constructor(bool disable) { + if (disable) { + _disableInitializers(); + } + } + + /* ========== INITIALIZATION ========== */ + + function initialize(address owner_) public initializer { + __Ownable_init(); + transferOwnership(owner_); + + // Debt parameters + MIN_DEBT = 2000e18; + + // Liquidation parameters + MIN_LIQUIDATION_PENALTY_SP = 5 * _1pct; // 5% + MAX_LIQUIDATION_PENALTY_REDISTRIBUTION = 20 * _1pct; // 20% + LIQUIDATION_PENALTY_SP = 5e16; // 5% + LIQUIDATION_PENALTY_REDISTRIBUTION = 20e16; // 20% + + // Gas compensation parameters + COLL_GAS_COMPENSATION_DIVISOR = 200; // dividing by 200 yields 0.5% + COLL_GAS_COMPENSATION_CAP = 2 ether; // Max coll gas compensation capped at 2 ETH + ETH_GAS_COMPENSATION = 0.0375 ether; + + // Collateral parameters + CCR = 150 * _1pct; // 150% + SCR = 110 * _1pct; // 110% + MCR = 110 * _1pct; // 110% + BCR = 10 * _1pct; // 10% + + // Interest parameters + MIN_ANNUAL_INTEREST_RATE = _1pct / 2; // 0.5% + MAX_ANNUAL_INTEREST_RATE = 250 * _1pct; // 250% + MAX_ANNUAL_BATCH_MANAGEMENT_FEE = uint128(_100pct / 10); // 10% + UPFRONT_INTEREST_PERIOD = 7 days; + INTEREST_RATE_ADJ_COOLDOWN = 7 days; + MIN_INTEREST_RATE_CHANGE_PERIOD = 1 hours; // only applies to batch managers / batched Troves + MAX_BATCH_SHARES_RATIO = 1e9; + + // Redemption parameters + REDEMPTION_FEE_FLOOR = _1pct / 2; // 0.5% + INITIAL_BASE_RATE = _100pct; // 100% initial redemption rate + + // Half-life of 6h. 6h = 360 min + // (1/2) = d^360 => d = (1/2)^(1/360) + REDEMPTION_MINUTE_DECAY_FACTOR = 998076443575628800; + REDEMPTION_BETA = 1; + URGENT_REDEMPTION_BONUS = 2 * _1pct; // 2% + + // Stability pool parameters + SP_YIELD_SPLIT = 75 * _1pct; // 75% + MIN_BOLD_IN_SP = 1e18; + + version = 1; + emit VersionUpdated(0, 1); + } + + /* ========== ADMIN FUNCTIONS ========== */ + + /// @inheritdoc ISystemParams + function updateMinDebt(uint256 _minDebt) external onlyOwner { + if (_minDebt == 0 || _minDebt > 10000e18) revert InvalidMinDebt(); + + uint256 oldMinDebt = MIN_DEBT; + MIN_DEBT = _minDebt; + + emit MinDebtUpdated(oldMinDebt, _minDebt); + } + + /// @inheritdoc ISystemParams + function updateLiquidationParams( + uint256 _minLiquidationPenaltySP, + uint256 _maxLiquidationPenaltyRedistribution, + uint256 _liquidationPenaltySP, + uint256 _liquidationPenaltyRedistribution + ) external onlyOwner { + // TODO: Review checks. + if (_liquidationPenaltySP < _minLiquidationPenaltySP) + revert SPPenaltyTooLow(); + if (_liquidationPenaltySP > _liquidationPenaltyRedistribution) + revert SPPenaltyGtRedist(); + if ( + _liquidationPenaltyRedistribution > + _maxLiquidationPenaltyRedistribution + ) revert RedistPenaltyTooHigh(); + + MIN_LIQUIDATION_PENALTY_SP = _minLiquidationPenaltySP; + MAX_LIQUIDATION_PENALTY_REDISTRIBUTION = _maxLiquidationPenaltyRedistribution; + LIQUIDATION_PENALTY_SP = _liquidationPenaltySP; + LIQUIDATION_PENALTY_REDISTRIBUTION = _liquidationPenaltyRedistribution; + + emit LiquidationParamsUpdated( + _minLiquidationPenaltySP, + _maxLiquidationPenaltyRedistribution, + _liquidationPenaltySP, + _liquidationPenaltyRedistribution + ); + } + + /// @inheritdoc ISystemParams + function updateGasCompParams( + uint256 _collGasCompensationDivisor, + uint256 _collGasCompensationCap, + uint256 _ethGasCompensation + ) external onlyOwner { + if ( + _collGasCompensationDivisor == 0 || + _collGasCompensationDivisor > 1000 + ) revert InvalidGasCompensation(); + if (_collGasCompensationCap == 0 || _collGasCompensationCap > 10 ether) + revert InvalidGasCompensation(); + if (_ethGasCompensation == 0 || _ethGasCompensation > 1 ether) + revert InvalidGasCompensation(); + + COLL_GAS_COMPENSATION_DIVISOR = _collGasCompensationDivisor; + COLL_GAS_COMPENSATION_CAP = _collGasCompensationCap; + ETH_GAS_COMPENSATION = _ethGasCompensation; + + emit GasCompParamsUpdated( + _collGasCompensationDivisor, + _collGasCompensationCap, + _ethGasCompensation + ); + } + + /// @inheritdoc ISystemParams + function updateCollateralParams( + uint256 _ccr, + uint256 _scr, + uint256 _mcr, + uint256 _bcr + ) external onlyOwner { + if (_ccr <= _100pct || _ccr >= 2 * _100pct) revert InvalidCCR(); + if (_mcr <= _100pct || _mcr >= 2 * _100pct) revert InvalidMCR(); + if (_bcr < 5 * _1pct || _bcr >= 50 * _1pct) revert InvalidBCR(); + if (_scr <= _100pct || _scr >= 2 * _100pct) revert InvalidSCR(); + + CCR = _ccr; + SCR = _scr; + MCR = _mcr; + BCR = _bcr; + + emit CollateralParamsUpdated(_ccr, _scr, _mcr, _bcr); + } + + /// @inheritdoc ISystemParams + function updateInterestParams( + uint256 _minAnnualInterestRate, + uint256 _maxAnnualInterestRate, + uint128 _maxAnnualBatchManagementFee, + uint256 _upfrontInterestPeriod, + uint256 _interestRateAdjCooldown, + uint128 _minInterestRateChangePeriod, + uint256 _maxBatchSharesRatio + ) external onlyOwner { + if (_minAnnualInterestRate > _maxAnnualInterestRate) + revert MinInterestRateGtMax(); + if (_maxAnnualInterestRate > 10 * _100pct) + revert InvalidInterestRateBounds(); // Max 1000% + if (_maxAnnualBatchManagementFee > _100pct) revert InvalidFeeValue(); + + if (_upfrontInterestPeriod == 0 || _upfrontInterestPeriod > 365 days) + revert InvalidTimeValue(); + if ( + _interestRateAdjCooldown == 0 || _interestRateAdjCooldown > 365 days + ) revert InvalidTimeValue(); + if ( + _minInterestRateChangePeriod == 0 || + _minInterestRateChangePeriod > 30 days + ) revert InvalidTimeValue(); + + MIN_ANNUAL_INTEREST_RATE = _minAnnualInterestRate; + MAX_ANNUAL_INTEREST_RATE = _maxAnnualInterestRate; + MAX_ANNUAL_BATCH_MANAGEMENT_FEE = _maxAnnualBatchManagementFee; + UPFRONT_INTEREST_PERIOD = _upfrontInterestPeriod; + INTEREST_RATE_ADJ_COOLDOWN = _interestRateAdjCooldown; + MIN_INTEREST_RATE_CHANGE_PERIOD = _minInterestRateChangePeriod; + MAX_BATCH_SHARES_RATIO = _maxBatchSharesRatio; + + emit InterestParamsUpdated( + _minAnnualInterestRate, + _maxAnnualInterestRate, + _maxAnnualBatchManagementFee, + _upfrontInterestPeriod, + _interestRateAdjCooldown, + _minInterestRateChangePeriod, + _maxBatchSharesRatio + ); + } + + /// @inheritdoc ISystemParams + function updateRedemptionParams( + uint256 _redemptionFeeFloor, + uint256 _initialBaseRate, + uint256 _redemptionMinuteDecayFactor, + uint256 _redemptionBeta, + uint256 _urgentRedemptionBonus + ) external onlyOwner { + if (_redemptionFeeFloor > _100pct) revert InvalidFeeValue(); + if (_initialBaseRate > 10 * _100pct) revert InvalidFeeValue(); + if (_urgentRedemptionBonus > _100pct) revert InvalidFeeValue(); + + REDEMPTION_FEE_FLOOR = _redemptionFeeFloor; + INITIAL_BASE_RATE = _initialBaseRate; + REDEMPTION_MINUTE_DECAY_FACTOR = _redemptionMinuteDecayFactor; + REDEMPTION_BETA = _redemptionBeta; + URGENT_REDEMPTION_BONUS = _urgentRedemptionBonus; + + emit RedemptionParamsUpdated( + _redemptionFeeFloor, + _initialBaseRate, + _redemptionMinuteDecayFactor, + _redemptionBeta, + _urgentRedemptionBonus + ); + } + + /// @inheritdoc ISystemParams + function updatePoolParams( + uint256 _spYieldSplit, + uint256 _minBoldInSP + ) external onlyOwner { + if (_spYieldSplit > _100pct) revert InvalidFeeValue(); + if (_minBoldInSP == 0) revert InvalidMinDebt(); + + SP_YIELD_SPLIT = _spYieldSplit; + MIN_BOLD_IN_SP = _minBoldInSP; + + emit StabilityPoolParamsUpdated(_spYieldSplit, _minBoldInSP); + } + + /// @inheritdoc ISystemParams + function updateVersion(uint256 newVersion) external onlyOwner { + uint256 oldVersion = version; + version = newVersion; + emit VersionUpdated(oldVersion, newVersion); + } +} From bbcf228689f8960fa109d3e778e4aacf9a3ddb39 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:16:47 -0500 Subject: [PATCH 19/79] refactor: remove params from address registry --- contracts/src/AddressesRegistry.sol | 57 +++---------------- .../src/Interfaces/IAddressesRegistry.sol | 7 --- 2 files changed, 7 insertions(+), 57 deletions(-) diff --git a/contracts/src/AddressesRegistry.sol b/contracts/src/AddressesRegistry.sol index 1f68098f7..ab6003511 100644 --- a/contracts/src/AddressesRegistry.sol +++ b/contracts/src/AddressesRegistry.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.24; import "./Dependencies/Ownable.sol"; -import {MIN_LIQUIDATION_PENALTY_SP, MAX_LIQUIDATION_PENALTY_REDISTRIBUTION} from "./Dependencies/Constants.sol"; import "./Interfaces/IAddressesRegistry.sol"; contract AddressesRegistry is Ownable, IAddressesRegistry { @@ -27,29 +26,6 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { IERC20Metadata public gasToken; address public liquidityStrategy; - // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied - uint256 public immutable CCR; - // Shutdown system collateral ratio. If the system's total collateral ratio (TCR) for a given collateral falls below the SCR, - // the protocol triggers the shutdown of the borrow market and permanently disables all borrowing operations except for closing Troves. - uint256 public immutable SCR; - - // Minimum collateral ratio for individual troves - uint256 public immutable MCR; - // Extra buffer of collateral ratio to join a batch or adjust a trove inside a batch (on top of MCR) - uint256 public immutable BCR; - // Liquidation penalty for troves offset to the SP - uint256 public immutable LIQUIDATION_PENALTY_SP; - // Liquidation penalty for troves redistributed - uint256 public immutable LIQUIDATION_PENALTY_REDISTRIBUTION; - - error InvalidCCR(); - error InvalidMCR(); - error InvalidBCR(); - error InvalidSCR(); - error SPPenaltyTooLow(); - error SPPenaltyGtRedist(); - error RedistPenaltyTooHigh(); - event CollTokenAddressChanged(address _collTokenAddress); event BorrowerOperationsAddressChanged(address _borrowerOperationsAddress); event TroveManagerAddressChanged(address _troveManagerAddress); @@ -70,30 +46,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { event GasTokenAddressChanged(address _gasTokenAddress); event LiquidityStrategyAddressChanged(address _liquidityStrategyAddress); - constructor( - address _owner, - uint256 _ccr, - uint256 _mcr, - uint256 _bcr, - uint256 _scr, - uint256 _liquidationPenaltySP, - uint256 _liquidationPenaltyRedistribution - ) Ownable(_owner) { - if (_ccr <= 1e18 || _ccr >= 2e18) revert InvalidCCR(); - if (_mcr <= 1e18 || _mcr >= 2e18) revert InvalidMCR(); - if (_bcr < 5e16 || _bcr >= 50e16) revert InvalidBCR(); - if (_scr <= 1e18 || _scr >= 2e18) revert InvalidSCR(); - if (_liquidationPenaltySP < MIN_LIQUIDATION_PENALTY_SP) revert SPPenaltyTooLow(); - if (_liquidationPenaltySP > _liquidationPenaltyRedistribution) revert SPPenaltyGtRedist(); - if (_liquidationPenaltyRedistribution > MAX_LIQUIDATION_PENALTY_REDISTRIBUTION) revert RedistPenaltyTooHigh(); - - CCR = _ccr; - SCR = _scr; - MCR = _mcr; - BCR = _bcr; - LIQUIDATION_PENALTY_SP = _liquidationPenaltySP; - LIQUIDATION_PENALTY_REDISTRIBUTION = _liquidationPenaltyRedistribution; - } + constructor(address _owner) Ownable(_owner) {} function setAddresses(AddressVars memory _vars) external onlyOwner { collToken = _vars.collToken; @@ -117,7 +70,9 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { liquidityStrategy = _vars.liquidityStrategy; emit CollTokenAddressChanged(address(_vars.collToken)); - emit BorrowerOperationsAddressChanged(address(_vars.borrowerOperations)); + emit BorrowerOperationsAddressChanged( + address(_vars.borrowerOperations) + ); emit TroveManagerAddressChanged(address(_vars.troveManager)); emit TroveNFTAddressChanged(address(_vars.troveNFT)); emit MetadataNFTAddressChanged(address(_vars.metadataNFT)); @@ -131,7 +86,9 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { emit InterestRouterAddressChanged(address(_vars.interestRouter)); emit HintHelpersAddressChanged(address(_vars.hintHelpers)); emit MultiTroveGetterAddressChanged(address(_vars.multiTroveGetter)); - emit CollateralRegistryAddressChanged(address(_vars.collateralRegistry)); + emit CollateralRegistryAddressChanged( + address(_vars.collateralRegistry) + ); emit BoldTokenAddressChanged(address(_vars.boldToken)); emit GasTokenAddressChanged(address(_vars.gasToken)); emit LiquidityStrategyAddressChanged(address(_vars.liquidityStrategy)); diff --git a/contracts/src/Interfaces/IAddressesRegistry.sol b/contracts/src/Interfaces/IAddressesRegistry.sol index 1c43ebd34..971f52f48 100644 --- a/contracts/src/Interfaces/IAddressesRegistry.sol +++ b/contracts/src/Interfaces/IAddressesRegistry.sol @@ -41,13 +41,6 @@ interface IAddressesRegistry { address liquidityStrategy; } - function CCR() external returns (uint256); - function SCR() external returns (uint256); - function MCR() external returns (uint256); - function BCR() external returns (uint256); - function LIQUIDATION_PENALTY_SP() external returns (uint256); - function LIQUIDATION_PENALTY_REDISTRIBUTION() external returns (uint256); - function collToken() external view returns (IERC20Metadata); function borrowerOperations() external view returns (IBorrowerOperations); function troveManager() external view returns (ITroveManager); From d6b381173ce3e99afc2f70432ab8d3b60f997184 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:27:14 -0500 Subject: [PATCH 20/79] =?UTF-8?q?refactor:=20watch=20it=20burn=20?= =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=9A=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/Dependencies/Constants.sol | 70 +----------------------- 1 file changed, 1 insertion(+), 69 deletions(-) diff --git a/contracts/src/Dependencies/Constants.sol b/contracts/src/Dependencies/Constants.sol index 9e0231988..26df4ab68 100644 --- a/contracts/src/Dependencies/Constants.sol +++ b/contracts/src/Dependencies/Constants.sol @@ -9,79 +9,11 @@ uint256 constant DECIMAL_PRECISION = 1e18; uint256 constant _100pct = DECIMAL_PRECISION; uint256 constant _1pct = DECIMAL_PRECISION / 100; -// Amount of ETH to be locked in gas pool on opening troves -uint256 constant ETH_GAS_COMPENSATION = 0.0375 ether; - -// Liquidation -uint256 constant MIN_LIQUIDATION_PENALTY_SP = 5e16; // 5% -uint256 constant MAX_LIQUIDATION_PENALTY_REDISTRIBUTION = 20e16; // 20% - -// Collateral branch parameters (SETH = staked ETH, i.e. wstETH / rETH) -uint256 constant CCR_WETH = 150 * _1pct; -uint256 constant CCR_SETH = 160 * _1pct; - -uint256 constant MCR_WETH = 110 * _1pct; -uint256 constant MCR_SETH = 120 * _1pct; - -uint256 constant SCR_WETH = 110 * _1pct; -uint256 constant SCR_SETH = 120 * _1pct; - -// Batch CR buffer (same for all branches for now) -// On top of MCR to join a batch, or adjust inside a batch -uint256 constant BCR_ALL = 10 * _1pct; - -uint256 constant LIQUIDATION_PENALTY_SP_WETH = 5 * _1pct; -uint256 constant LIQUIDATION_PENALTY_SP_SETH = 5 * _1pct; - -uint256 constant LIQUIDATION_PENALTY_REDISTRIBUTION_WETH = 10 * _1pct; -uint256 constant LIQUIDATION_PENALTY_REDISTRIBUTION_SETH = 20 * _1pct; - -// Fraction of collateral awarded to liquidator -uint256 constant COLL_GAS_COMPENSATION_DIVISOR = 200; // dividing by 200 yields 0.5% -uint256 constant COLL_GAS_COMPENSATION_CAP = 2 ether; // Max coll gas compensation capped at 2 ETH - -// Minimum amount of net Bold debt a trove must have -uint256 constant MIN_DEBT = 2000e18; - -uint256 constant MIN_ANNUAL_INTEREST_RATE = _1pct / 2; // 0.5% -uint256 constant MAX_ANNUAL_INTEREST_RATE = 250 * _1pct; - -// Batch management params -uint128 constant MAX_ANNUAL_BATCH_MANAGEMENT_FEE = uint128(_100pct / 10); // 10% -uint128 constant MIN_INTEREST_RATE_CHANGE_PERIOD = 1 hours; // only applies to batch managers / batched Troves - -uint256 constant REDEMPTION_FEE_FLOOR = _1pct / 2; // 0.5% - -// For the debt / shares ratio to increase by a factor 1e9 -// at a average annual debt increase (compounded interest + fees) of 10%, it would take more than 217 years (log(1e9)/log(1.1)) -// at a average annual debt increase (compounded interest + fees) of 50%, it would take more than 51 years (log(1e9)/log(1.5)) -// The increase pace could be forced to be higher through an inflation attack, -// but precisely the fact that we have this max value now prevents the attack -uint256 constant MAX_BATCH_SHARES_RATIO = 1e9; - -// Half-life of 6h. 6h = 360 min -// (1/2) = d^360 => d = (1/2)^(1/360) -uint256 constant REDEMPTION_MINUTE_DECAY_FACTOR = 998076443575628800; - -// BETA: 18 digit decimal. Parameter by which to divide the redeemed fraction, in order to calc the new base rate from a redemption. -// Corresponds to (1 / ALPHA) in the white paper. -uint256 constant REDEMPTION_BETA = 1; - -// To prevent redemptions unless Bold depegs below 0.95 and allow the system to take off -uint256 constant INITIAL_BASE_RATE = _100pct; // 100% initial redemption rate - -// Discount to be used once the shutdown thas been triggered -uint256 constant URGENT_REDEMPTION_BONUS = 2e16; // 2% - uint256 constant ONE_MINUTE = 1 minutes; uint256 constant ONE_YEAR = 365 days; -uint256 constant UPFRONT_INTEREST_PERIOD = 7 days; -uint256 constant INTEREST_RATE_ADJ_COOLDOWN = 7 days; - -uint256 constant SP_YIELD_SPLIT = 75 * _1pct; // 75% -uint256 constant MIN_BOLD_IN_SP = 1e18; +// TODO(@bayological): Remve this and refactor the tests that use it // Dummy contract that lets legacy Hardhat tests query some of the constants contract Constants { uint256 public constant _ETH_GAS_COMPENSATION = ETH_GAS_COMPENSATION; From 13fa82963813ab38f0a66c69fcbb77d4944d8f4d Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:27:29 -0500 Subject: [PATCH 21/79] test: add test specific constants --- contracts/test/multicollateral.t.sol | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/contracts/test/multicollateral.t.sol b/contracts/test/multicollateral.t.sol index d07a428fd..85eccad72 100644 --- a/contracts/test/multicollateral.t.sol +++ b/contracts/test/multicollateral.t.sol @@ -9,6 +9,27 @@ contract MulticollateralTest is DevTestSetup { uint256 NUM_COLLATERALS = 4; TestDeployer.LiquityContractsDev[] public contractsArray; + // Test constants + // Collateral branch parameters (SETH = staked ETH, i.e. wstETH / rETH) + uint256 constant CCR_WETH = 150 * _1pct; + uint256 constant CCR_SETH = 160 * _1pct; + + uint256 constant MCR_WETH = 110 * _1pct; + uint256 constant MCR_SETH = 120 * _1pct; + + uint256 constant SCR_WETH = 110 * _1pct; + uint256 constant SCR_SETH = 120 * _1pct; + + // Batch CR buffer (same for all branches for now) + // On top of MCR to join a batch, or adjust inside a batch + uint256 constant BCR_ALL = 10 * _1pct; + + uint256 constant LIQUIDATION_PENALTY_SP_WETH = 5 * _1pct; + uint256 constant LIQUIDATION_PENALTY_SP_SETH = 5 * _1pct; + + uint256 constant LIQUIDATION_PENALTY_REDISTRIBUTION_WETH = 10 * _1pct; + uint256 constant LIQUIDATION_PENALTY_REDISTRIBUTION_SETH = 20 * _1pct; + function openMulticollateralTroveNoHints100pctWithIndex( uint256 _collIndex, address _account, From 3882ccb194c91c860d5f9e2eaf54504457830805 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:43:36 -0500 Subject: [PATCH 22/79] refactor: replace constants use with sys params --- contracts/src/ActivePool.sol | 7 +++++- contracts/src/BorrowerOperations.sol | 32 ++++++++++++++++++++++------ contracts/src/CollateralRegistry.sol | 15 ++++++++++--- contracts/src/HintHelpers.sol | 9 +++++++- contracts/src/StabilityPool.sol | 15 ++++++++----- contracts/src/TroveManager.sol | 28 ++++++++++++++++++------ 6 files changed, 84 insertions(+), 22 deletions(-) diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index eff5042b2..8ac5e63fb 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -11,6 +11,7 @@ import "./Interfaces/IAddressesRegistry.sol"; import "./Interfaces/IBoldToken.sol"; import "./Interfaces/IInterestRouter.sol"; import "./Interfaces/IDefaultPool.sol"; +import "./Interfaces/ISystemParams.sol"; /* * The Active Pool holds the collateral and Bold debt (but not Bold tokens) for all active troves. @@ -34,6 +35,8 @@ contract ActivePool is IActivePool { IInterestRouter public immutable interestRouter; IBoldRewardsReceiver public immutable stabilityPool; + uint256 public immutable SP_YIELD_SPLIT; + uint256 internal collBalance; // deposited coll tracker // Aggregate recorded debt tracker. Updated whenever a Trove's debt is touched AND whenever the aggregate pending interest is minted. @@ -71,7 +74,7 @@ contract ActivePool is IActivePool { event ActivePoolBoldDebtUpdated(uint256 _recordedDebtSum); event ActivePoolCollBalanceUpdated(uint256 _collBalance); - constructor(IAddressesRegistry _addressesRegistry) { + constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) { collToken = _addressesRegistry.collToken(); borrowerOperationsAddress = address(_addressesRegistry.borrowerOperations()); troveManagerAddress = address(_addressesRegistry.troveManager()); @@ -80,6 +83,8 @@ contract ActivePool is IActivePool { interestRouter = _addressesRegistry.interestRouter(); boldToken = _addressesRegistry.boldToken(); + SP_YIELD_SPLIT = _systemParams.SP_YIELD_SPLIT(); + emit CollTokenAddressChanged(address(collToken)); emit BorrowerOperationsAddressChanged(borrowerOperationsAddress); emit TroveManagerAddressChanged(troveManagerAddress); diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index fa2d0e48d..33c8540c8 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -10,6 +10,7 @@ import "./Interfaces/ITroveManager.sol"; import "./Interfaces/IBoldToken.sol"; import "./Interfaces/ICollSurplusPool.sol"; import "./Interfaces/ISortedTroves.sol"; +import "./Interfaces/ISystemParams.sol"; import "./Dependencies/LiquityBase.sol"; import "./Dependencies/AddRemoveManagers.sol"; import "./Types/LatestTroveData.sol"; @@ -44,6 +45,16 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // Extra buffer of collateral ratio to join a batch or adjust a trove inside a batch (on top of MCR) uint256 public immutable BCR; + uint256 public immutable ETH_GAS_COMPENSATION; + uint256 public immutable MIN_DEBT; + uint128 public immutable MAX_ANNUAL_BATCH_MANAGEMENT_FEE; + uint128 public immutable MIN_INTEREST_RATE_CHANGE_PERIOD; + uint256 public immutable INTEREST_RATE_ADJ_COOLDOWN; + uint256 public immutable MAX_BATCH_SHARES_RATIO; + uint256 public immutable UPFRONT_INTEREST_PERIOD; + uint256 public immutable MIN_ANNUAL_INTEREST_RATE; + uint256 public immutable MAX_ANNUAL_INTEREST_RATE; + /* * Mapping from TroveId to individual delegate for interest rate setting. * @@ -164,21 +175,30 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio event ShutDown(uint256 _tcr); - constructor(IAddressesRegistry _addressesRegistry) + constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) AddRemoveManagers(_addressesRegistry) LiquityBase(_addressesRegistry) { // This makes impossible to open a trove with zero withdrawn Bold - assert(MIN_DEBT > 0); + assert(_systemParams.MIN_DEBT() > 0); collToken = _addressesRegistry.collToken(); gasToken = _addressesRegistry.gasToken(); - CCR = _addressesRegistry.CCR(); - SCR = _addressesRegistry.SCR(); - MCR = _addressesRegistry.MCR(); - BCR = _addressesRegistry.BCR(); + CCR = _systemParams.CCR(); + SCR = _systemParams.SCR(); + MCR = _systemParams.MCR(); + BCR = _systemParams.BCR(); + + ETH_GAS_COMPENSATION = _systemParams.ETH_GAS_COMPENSATION(); + MIN_DEBT = _systemParams.MIN_DEBT(); + MAX_ANNUAL_BATCH_MANAGEMENT_FEE = _systemParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE(); + MIN_INTEREST_RATE_CHANGE_PERIOD = _systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD(); + MAX_BATCH_SHARES_RATIO = _systemParams.MAX_BATCH_SHARES_RATIO(); + UPFRONT_INTEREST_PERIOD = _systemParams.UPFRONT_INTEREST_PERIOD(); + MIN_ANNUAL_INTEREST_RATE = _systemParams.MIN_ANNUAL_INTEREST_RATE(); + MAX_ANNUAL_INTEREST_RATE = _systemParams.MAX_ANNUAL_INTEREST_RATE(); troveManager = _addressesRegistry.troveManager(); gasPoolAddress = _addressesRegistry.gasPoolAddress(); diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index 84dc90f56..38ac7ae3c 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -6,6 +6,7 @@ import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.s import "./Interfaces/ITroveManager.sol"; import "./Interfaces/IBoldToken.sol"; +import "./Interfaces/ISystemParams.sol"; import "./Dependencies/Constants.sol"; import "./Dependencies/LiquityMath.sol"; @@ -39,6 +40,10 @@ contract CollateralRegistry is ICollateralRegistry { IBoldToken public immutable boldToken; + uint256 public immutable REDEMPTION_BETA; + uint256 public immutable REDEMPTION_MINUTE_DECAY_FACTOR; + uint256 public immutable REDEMPTION_FEE_FLOOR; + uint256 public baseRate; // The timestamp of the latest fee operation (redemption or new Bold issuance) @@ -47,7 +52,7 @@ contract CollateralRegistry is ICollateralRegistry { event BaseRateUpdated(uint256 _baseRate); event LastFeeOpTimeUpdated(uint256 _lastFeeOpTime); - constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers) { + constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers, ISystemParams _systemParams) { uint256 numTokens = _tokens.length; require(numTokens > 0, "Collateral list cannot be empty"); require(numTokens <= 10, "Collateral list too long"); @@ -77,9 +82,13 @@ contract CollateralRegistry is ICollateralRegistry { troveManager8 = numTokens > 8 ? _troveManagers[8] : ITroveManager(address(0)); troveManager9 = numTokens > 9 ? _troveManagers[9] : ITroveManager(address(0)); + REDEMPTION_BETA = _systemParams.REDEMPTION_BETA(); + REDEMPTION_MINUTE_DECAY_FACTOR = _systemParams.REDEMPTION_MINUTE_DECAY_FACTOR(); + REDEMPTION_FEE_FLOOR = _systemParams.REDEMPTION_FEE_FLOOR(); + // Initialize the baseRate state variable - baseRate = INITIAL_BASE_RATE; - emit BaseRateUpdated(INITIAL_BASE_RATE); + baseRate = _systemParams.INITIAL_BASE_RATE(); + emit BaseRateUpdated(_systemParams.INITIAL_BASE_RATE()); } struct RedemptionTotals { diff --git a/contracts/src/HintHelpers.sol b/contracts/src/HintHelpers.sol index 6d2b722d5..bf3c34e27 100644 --- a/contracts/src/HintHelpers.sol +++ b/contracts/src/HintHelpers.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.24; import "./Interfaces/ICollateralRegistry.sol"; import "./Interfaces/IActivePool.sol"; import "./Interfaces/ISortedTroves.sol"; +import "./Interfaces/ISystemParams.sol"; import "./Dependencies/LiquityMath.sol"; import "./Dependencies/Constants.sol"; import "./Interfaces/IHintHelpers.sol"; @@ -17,8 +18,14 @@ contract HintHelpers is IHintHelpers { ICollateralRegistry public immutable collateralRegistry; - constructor(ICollateralRegistry _collateralRegistry) { + uint256 public immutable INTEREST_RATE_ADJ_COOLDOWN; + uint256 public immutable UPFRONT_INTEREST_PERIOD; + + constructor(ICollateralRegistry _collateralRegistry, ISystemParams _systemParams) { collateralRegistry = _collateralRegistry; + + INTEREST_RATE_ADJ_COOLDOWN = _systemParams.INTEREST_RATE_ADJ_COOLDOWN(); + UPFRONT_INTEREST_PERIOD = _systemParams.UPFRONT_INTEREST_PERIOD(); } /* getApproxHint() - return id of a Trove that is, on average, (length / numTrials) positions away in the diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 07c7536a3..0bc772a18 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -9,6 +9,7 @@ import "./Interfaces/IAddressesRegistry.sol"; import "./Interfaces/IStabilityPoolEvents.sol"; import "./Interfaces/ITroveManager.sol"; import "./Interfaces/IBoldToken.sol"; +import "./Interfaces/ISystemParams.sol"; import "./Dependencies/LiquityBaseInit.sol"; /* @@ -60,7 +61,7 @@ import "./Dependencies/LiquityBaseInit.sol"; * * A series of liquidations that nearly empty the Pool (and thus each multiply P by a very small number in range ]0,1[ ) may push P * to its 36 digit decimal limit, and round it to 0, when in fact the Pool hasn't been emptied: this would break deposit tracking. - * + * * P is stored at 36-digit precision as a uint. That is, a value of "1" is represented by a value of 1e36 in the code. * * So, to track P accurately, we use a scale factor: if a liquidation would cause P to decrease below 1e27, @@ -173,12 +174,14 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi uint256 public constant SCALE_SPAN = 2; // The minimum amount of Bold in the SP after a rebalance - // Introduced to avoid higher rate of scale changes + // Introduced to avoid higher rate of scale changes uint256 public constant MIN_BOLD_AFTER_REBALANCE = 1_000e18; // Each time the scale of P shifts by SCALE_FACTOR, the scale is incremented by 1 uint256 public currentScale; + uint256 public immutable MIN_BOLD_IN_SP; + address public liquidityStrategy; /* Coll Gain sum 'S': During its lifetime, each deposit d_t earns an Coll gain of ( d_t * [S - S_t] )/P_t, where S_t @@ -206,7 +209,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi } } - function initialize(IAddressesRegistry _addressesRegistry) external initializer { + function initialize(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) external initializer { __LiquityBase_init(_addressesRegistry); collToken = _addressesRegistry.collToken(); @@ -214,6 +217,8 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi boldToken = _addressesRegistry.boldToken(); liquidityStrategy = _addressesRegistry.liquidityStrategy(); + MIN_BOLD_IN_SP = _systemParams.MIN_BOLD_IN_SP(); + emit TroveManagerAddressChanged(address(troveManager)); emit BoldTokenAddressChanged(address(boldToken)); } @@ -407,7 +412,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi _updateTrackingVariables(amountStableOut, amountCollIn); _swapCollateralForStable(amountCollIn, amountStableOut); - + require(totalBoldDeposits >= MIN_BOLD_AFTER_REBALANCE, "Total Bold deposits must be >= MIN_BOLD_AFTER_REBALANCE"); } @@ -464,7 +469,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi collBalance += _amountCollIn; collToken.safeTransferFrom(msg.sender, address(this), _amountCollIn); - + emit StabilityPoolCollBalanceUpdated(collBalance); } diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 9bd74cbb3..4d3b0f29b 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -12,6 +12,7 @@ import "./Interfaces/ITroveEvents.sol"; import "./Interfaces/ITroveNFT.sol"; import "./Interfaces/ICollateralRegistry.sol"; import "./Interfaces/IWETH.sol"; +import "./Interfaces/ISystemParams.sol"; import "./Dependencies/LiquityBase.sol"; contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { @@ -43,6 +44,14 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // Liquidation penalty for troves redistributed uint256 internal immutable LIQUIDATION_PENALTY_REDISTRIBUTION; + uint256 internal immutable COLL_GAS_COMPENSATION_DIVISOR; + uint256 internal immutable COLL_GAS_COMPENSATION_CAP; + uint256 internal immutable MIN_BOLD_IN_SP; + uint256 internal immutable ETH_GAS_COMPENSATION; + uint256 internal immutable MIN_DEBT; + uint256 internal immutable URGENT_REDEMPTION_BONUS; + uint256 internal immutable MAX_BATCH_SHARES_RATIO; + // --- Data structures --- // Store the necessary data for a trove @@ -186,12 +195,19 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { event SortedTrovesAddressChanged(address _sortedTrovesAddress); event CollateralRegistryAddressChanged(address _collateralRegistryAddress); - constructor(IAddressesRegistry _addressesRegistry) LiquityBase(_addressesRegistry) { - CCR = _addressesRegistry.CCR(); - MCR = _addressesRegistry.MCR(); - SCR = _addressesRegistry.SCR(); - LIQUIDATION_PENALTY_SP = _addressesRegistry.LIQUIDATION_PENALTY_SP(); - LIQUIDATION_PENALTY_REDISTRIBUTION = _addressesRegistry.LIQUIDATION_PENALTY_REDISTRIBUTION(); + constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) LiquityBase(_addressesRegistry) { + CCR = _systemParams.CCR(); + MCR = _systemParams.MCR(); + SCR = _systemParams.SCR(); + LIQUIDATION_PENALTY_SP = _systemParams.LIQUIDATION_PENALTY_SP(); + LIQUIDATION_PENALTY_REDISTRIBUTION = _systemParams.LIQUIDATION_PENALTY_REDISTRIBUTION(); + COLL_GAS_COMPENSATION_DIVISOR = _systemParams.COLL_GAS_COMPENSATION_DIVISOR(); + COLL_GAS_COMPENSATION_CAP = _systemParams.COLL_GAS_COMPENSATION_CAP(); + MIN_BOLD_IN_SP = _systemParams.MIN_BOLD_IN_SP(); + ETH_GAS_COMPENSATION = _systemParams.ETH_GAS_COMPENSATION(); + MIN_DEBT = _systemParams.MIN_DEBT(); + URGENT_REDEMPTION_BONUS = _systemParams.URGENT_REDEMPTION_BONUS(); + MAX_BATCH_SHARES_RATIO = _systemParams.MAX_BATCH_SHARES_RATIO(); troveNFT = _addressesRegistry.troveNFT(); borrowerOperations = _addressesRegistry.borrowerOperations(); From 95581365c275add8dcff82d935c90099e930a3aa Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:06:18 -0500 Subject: [PATCH 23/79] refactor: update tests --- contracts/script/DeployLiquity2.s.sol | 912 +++++++++--------- contracts/src/Dependencies/Constants.sol | 4 +- contracts/test/SPInvariants.t.sol | 120 +-- .../TestContracts/BaseMultiCollateralTest.sol | 4 + contracts/test/TestContracts/BaseTest.sol | 430 +++++++-- .../BorrowerOperationsTester.t.sol | 3 +- contracts/test/TestContracts/Deployment.t.sol | 38 +- contracts/test/TestContracts/DevTestSetup.sol | 20 + .../TestContracts/InvariantsTestHandler.t.sol | 88 +- .../SPInvariantsTestHandler.t.sol | 438 ++++----- .../TestContracts/TroveManagerTester.t.sol | 17 +- contracts/test/multicollateral.t.sol | 46 +- 12 files changed, 1204 insertions(+), 916 deletions(-) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index e78fb77bc..525f333af 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -1,455 +1,457 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.24; - -import {StdCheats} from "forge-std/StdCheats.sol"; -import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; -import {IERC20 as IERC20_GOV} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {ProxyAdmin} from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; -import {TransparentUpgradeableProxy} from - "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {IFPMMFactory} from "src/Interfaces/IFPMMFactory.sol"; - -import {ETH_GAS_COMPENSATION} from "src/Dependencies/Constants.sol"; -import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; -import {StringFormatting} from "test/Utils/StringFormatting.sol"; -import {Accounts} from "test/TestContracts/Accounts.sol"; -import {ERC20Faucet} from "test/TestContracts/ERC20Faucet.sol"; -import {WETHTester} from "test/TestContracts/WETHTester.sol"; -import "src/AddressesRegistry.sol"; -import "src/ActivePool.sol"; -import "src/BoldToken.sol"; -import "src/BorrowerOperations.sol"; -import "src/TroveManager.sol"; -import "src/TroveNFT.sol"; -import "src/CollSurplusPool.sol"; -import "src/DefaultPool.sol"; -import "src/GasPool.sol"; -import "src/HintHelpers.sol"; -import "src/MultiTroveGetter.sol"; -import "src/SortedTroves.sol"; -import "src/StabilityPool.sol"; - -import "src/CollateralRegistry.sol"; -import "src/tokens/StableTokenV3.sol"; -import "src/Interfaces/IStableTokenV3.sol"; -import "test/TestContracts/PriceFeedTestnet.sol"; -import "test/TestContracts/MetadataDeployment.sol"; -import "test/Utils/Logging.sol"; -import "test/Utils/StringEquality.sol"; -import "forge-std/console2.sol"; - -contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { - using Strings for *; - using StringFormatting for *; - using StringEquality for string; - - bytes32 SALT; - address deployer; - - struct LiquityContracts { - IAddressesRegistry addressesRegistry; - IActivePool activePool; - IBorrowerOperations borrowerOperations; - ICollSurplusPool collSurplusPool; - IDefaultPool defaultPool; - ISortedTroves sortedTroves; - IStabilityPool stabilityPool; - ITroveManager troveManager; - ITroveNFT troveNFT; - MetadataNFT metadataNFT; - IPriceFeed priceFeed; - GasPool gasPool; - IInterestRouter interestRouter; - IERC20Metadata collToken; - } - - struct LiquityContractAddresses { - address activePool; - address borrowerOperations; - address collSurplusPool; - address defaultPool; - address sortedTroves; - address stabilityPool; - address troveManager; - address troveNFT; - address metadataNFT; - address priceFeed; - address gasPool; - address interestRouter; - } - - struct TroveManagerParams { - uint256 CCR; - uint256 MCR; - uint256 SCR; - uint256 BCR; - uint256 LIQUIDATION_PENALTY_SP; - uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; - } - - struct DeploymentVars { - uint256 numCollaterals; - IERC20Metadata[] collaterals; - IAddressesRegistry[] addressesRegistries; - ITroveManager[] troveManagers; - LiquityContracts contracts; - bytes bytecode; - address boldTokenAddress; - uint256 i; - } - - struct DemoTroveParams { - uint256 collIndex; - uint256 owner; - uint256 ownerIndex; - uint256 coll; - uint256 debt; - uint256 annualInterestRate; - } - - struct DeploymentResult { - LiquityContracts contracts; - ICollateralRegistry collateralRegistry; - HintHelpers hintHelpers; - MultiTroveGetter multiTroveGetter; - ProxyAdmin proxyAdmin; - IStableTokenV3 stableToken; - address stabilityPoolImpl; - address stableTokenV3Impl; - address fpmm; - } - - struct DeploymentConfig { - address USDm_ALFAJORES_ADDRESS; - address proxyAdmin; - address fpmmFactory; - address fpmmImplementation; - address referenceRateFeedID; - string stableTokenName; - string stableTokenSymbol; - // Parameters for the TroveManager - uint256 CCR; - uint256 MCR; - uint256 SCR; - uint256 BCR; - uint256 LIQUIDATION_PENALTY_SP; - uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; - } - - DeploymentConfig internal CONFIG = DeploymentConfig({ - USDm_ALFAJORES_ADDRESS: 0x9E2d4412d0f434cC85500b79447d9323a7416f09, - proxyAdmin: 0xe4DdacCAdb64114215FCe8251B57B2AEB5C2C0E2, - fpmmFactory: 0xd8098494a749a3fDAD2D2e7Fa5272D8f274D8FF6, - fpmmImplementation: 0x0292efcB331C6603eaa29D570d12eB336D6c01d6, - referenceRateFeedID: 0x206B25Ea01E188Ee243131aFdE526bA6E131a016, - stableTokenName: "EUR.m Test", - stableTokenSymbol: "EUR.m", - // TODO: reconsider these values - CCR: 150e16, - MCR: 110e16, - SCR: 110e16, - BCR: 40e16, - LIQUIDATION_PENALTY_SP: 5e16, - LIQUIDATION_PENALTY_REDISTRIBUTION: 10e16 - }); - - function run() external { - string memory saltStr = vm.envOr("SALT", block.timestamp.toString()); - SALT = keccak256(bytes(saltStr)); - - uint256 privateKey = vm.envUint("DEPLOYER"); - deployer = vm.addr(privateKey); - vm.startBroadcast(privateKey); - - _log("Deployer: ", deployer.toHexString()); - _log("Deployer balance: ", deployer.balance.decimal()); - _log("CREATE2 salt: ", 'keccak256(bytes("', saltStr, '")) = ', uint256(SALT).toHexString()); - _log("Chain ID: ", block.chainid.toString()); - - DeploymentResult memory deployed = _deployAndConnectContracts(); - - vm.stopBroadcast(); - - vm.writeFile("script/deployment-manifest.json", _getManifestJson(deployed)); - } - - // See: https://solidity-by-example.org/app/create2/ - function getBytecode(bytes memory _creationCode, address _addressesRegistry) public pure returns (bytes memory) { - return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry)); - } - - function _deployAndConnectContracts() internal returns (DeploymentResult memory r) { - _deployProxyInfrastructure(r); - _deployStableToken(r); - _deployFPMM(r); - - TroveManagerParams memory troveManagerParams = TroveManagerParams({ - CCR: CONFIG.CCR, - MCR: CONFIG.MCR, - SCR: CONFIG.SCR, - BCR: CONFIG.BCR, - LIQUIDATION_PENALTY_SP: CONFIG.LIQUIDATION_PENALTY_SP, - LIQUIDATION_PENALTY_REDISTRIBUTION: CONFIG.LIQUIDATION_PENALTY_REDISTRIBUTION - }); - - IAddressesRegistry addressesRegistry = new AddressesRegistry( - deployer, - troveManagerParams.CCR, - troveManagerParams.MCR, - troveManagerParams.BCR, - troveManagerParams.SCR, - troveManagerParams.LIQUIDATION_PENALTY_SP, - troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION - ); - - address troveManagerAddress = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(TroveManager).creationCode, address(addressesRegistry))) - ); - - IERC20Metadata collToken = IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS); - - IERC20Metadata[] memory collaterals = new IERC20Metadata[](1); - collaterals[0] = collToken; - - ITroveManager[] memory troveManagers = new ITroveManager[](1); - troveManagers[0] = ITroveManager(troveManagerAddress); - - r.collateralRegistry = new CollateralRegistry(IBoldToken(address(r.stableToken)), collaterals, troveManagers); - r.hintHelpers = new HintHelpers(r.collateralRegistry); - r.multiTroveGetter = new MultiTroveGetter(r.collateralRegistry); - - IPriceFeed priceFeed = new PriceFeedTestnet(); - - r.contracts = - _deployAndConnectCollateralContracts(collToken, priceFeed, addressesRegistry, troveManagerAddress, r); - } - - function _deployProxyInfrastructure(DeploymentResult memory r) internal { - r.proxyAdmin = ProxyAdmin(CONFIG.proxyAdmin); - r.stableTokenV3Impl = address(new StableTokenV3{salt: SALT}(true)); - r.stabilityPoolImpl = address(new StabilityPool{salt: SALT}(true)); - - assert( - address(r.stableTokenV3Impl) - == vm.computeCreate2Address( - SALT, keccak256(bytes.concat(type(StableTokenV3).creationCode, abi.encode(true))) - ) - ); - assert( - address(r.stabilityPoolImpl) - == vm.computeCreate2Address( - SALT, keccak256(bytes.concat(type(StabilityPool).creationCode, abi.encode(true))) - ) - ); - } - - function _deployStableToken(DeploymentResult memory r) internal { - r.stableToken = IStableTokenV3( - address(new TransparentUpgradeableProxy(address(r.stableTokenV3Impl), address(r.proxyAdmin), "")) - ); - } - - function _deployFPMM(DeploymentResult memory r) internal { - r.fpmm = IFPMMFactory(CONFIG.fpmmFactory).deployFPMM( - CONFIG.fpmmImplementation, address(r.stableToken), CONFIG.USDm_ALFAJORES_ADDRESS, CONFIG.referenceRateFeedID - ); - } - - function _deployAndConnectCollateralContracts( - IERC20Metadata _collToken, - IPriceFeed _priceFeed, - IAddressesRegistry _addressesRegistry, - address _troveManagerAddress, - DeploymentResult memory r - ) internal returns (LiquityContracts memory contracts) { - LiquityContractAddresses memory addresses; - contracts.collToken = _collToken; - contracts.addressesRegistry = _addressesRegistry; - contracts.priceFeed = _priceFeed; - // TODO: replace with governance timelock on mainnet - contracts.interestRouter = IInterestRouter(0x56fD3F2bEE130e9867942D0F463a16fBE49B8d81); - - addresses.troveManager = _troveManagerAddress; - - contracts.metadataNFT = deployMetadata(SALT); - addresses.metadataNFT = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(MetadataNFT).creationCode, address(initializedFixedAssetReader))) - ); - assert(address(contracts.metadataNFT) == addresses.metadataNFT); - - addresses.borrowerOperations = - _computeCreate2Address(type(BorrowerOperations).creationCode, address(contracts.addressesRegistry)); - addresses.troveNFT = _computeCreate2Address(type(TroveNFT).creationCode, address(contracts.addressesRegistry)); - addresses.activePool = - _computeCreate2Address(type(ActivePool).creationCode, address(contracts.addressesRegistry)); - addresses.defaultPool = - _computeCreate2Address(type(DefaultPool).creationCode, address(contracts.addressesRegistry)); - addresses.gasPool = _computeCreate2Address(type(GasPool).creationCode, address(contracts.addressesRegistry)); - addresses.collSurplusPool = - _computeCreate2Address(type(CollSurplusPool).creationCode, address(contracts.addressesRegistry)); - addresses.sortedTroves = - _computeCreate2Address(type(SortedTroves).creationCode, address(contracts.addressesRegistry)); - - // Deploy StabilityPool proxy - address stabilityPool = - address(new TransparentUpgradeableProxy(address(r.stabilityPoolImpl), address(r.proxyAdmin), "")); - - contracts.stabilityPool = IStabilityPool(stabilityPool); - // Set up addresses in registry - _setupAddressesRegistry(contracts, addresses, r); - - // Deploy core protocol contracts - _deployProtocolContracts(contracts, addresses); - - IStabilityPool(stabilityPool).initialize(contracts.addressesRegistry); - - address[] memory minters = new address[](2); - minters[0] = address(contracts.borrowerOperations); - minters[1] = address(contracts.activePool); - - address[] memory burners = new address[](4); - burners[0] = address(contracts.troveManager); - burners[1] = address(r.collateralRegistry); - burners[2] = address(contracts.borrowerOperations); - burners[3] = address(contracts.stabilityPool); - - address[] memory operators = new address[](1); - operators[0] = address(contracts.stabilityPool); - - r.stableToken.initialize( - CONFIG.stableTokenName, - CONFIG.stableTokenSymbol, - new address[](0), - new uint256[](0), - minters, - burners, - operators - ); - } - - function _setupAddressesRegistry( - LiquityContracts memory contracts, - LiquityContractAddresses memory addresses, - DeploymentResult memory r - ) internal { - IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ - collToken: contracts.collToken, - borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), - troveManager: ITroveManager(addresses.troveManager), - troveNFT: ITroveNFT(addresses.troveNFT), - metadataNFT: IMetadataNFT(addresses.metadataNFT), - stabilityPool: contracts.stabilityPool, - priceFeed: contracts.priceFeed, - activePool: IActivePool(addresses.activePool), - defaultPool: IDefaultPool(addresses.defaultPool), - gasPoolAddress: addresses.gasPool, - collSurplusPool: ICollSurplusPool(addresses.collSurplusPool), - sortedTroves: ISortedTroves(addresses.sortedTroves), - interestRouter: contracts.interestRouter, - hintHelpers: r.hintHelpers, - multiTroveGetter: r.multiTroveGetter, - collateralRegistry: r.collateralRegistry, - boldToken: IBoldToken(address(r.stableToken)), - gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS), - // TODO: set liquidity strategy - liquidityStrategy: address(0) - }); - contracts.addressesRegistry.setAddresses(addressVars); - } - - function _deployProtocolContracts(LiquityContracts memory contracts, LiquityContractAddresses memory addresses) - internal - { - contracts.borrowerOperations = new BorrowerOperations{salt: SALT}(contracts.addressesRegistry); - contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry); - contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); - contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry); - contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); - contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); - contracts.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); - contracts.sortedTroves = new SortedTroves{salt: SALT}(contracts.addressesRegistry); - - assert(address(contracts.borrowerOperations) == addresses.borrowerOperations); - assert(address(contracts.troveManager) == addresses.troveManager); - assert(address(contracts.troveNFT) == addresses.troveNFT); - assert(address(contracts.activePool) == addresses.activePool); - assert(address(contracts.defaultPool) == addresses.defaultPool); - assert(address(contracts.gasPool) == addresses.gasPool); - assert(address(contracts.collSurplusPool) == addresses.collSurplusPool); - assert(address(contracts.sortedTroves) == addresses.sortedTroves); - } - - function _computeCreate2Address(bytes memory creationCode, address _addressesRegistry) - internal - view - returns (address) - { - return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry))); - } - - function _getBranchContractsJson(LiquityContracts memory c) internal view returns (string memory) { - return string.concat( - "{", - string.concat( - // Avoid stack too deep by chunking concats - string.concat( - string.concat('"collSymbol":"', c.collToken.symbol(), '",'), // purely for human-readability - string.concat('"collToken":"', address(c.collToken).toHexString(), '",'), - string.concat('"addressesRegistry":"', address(c.addressesRegistry).toHexString(), '",'), - string.concat('"activePool":"', address(c.activePool).toHexString(), '",'), - string.concat('"borrowerOperations":"', address(c.borrowerOperations).toHexString(), '",'), - string.concat('"collSurplusPool":"', address(c.collSurplusPool).toHexString(), '",'), - string.concat('"defaultPool":"', address(c.defaultPool).toHexString(), '",'), - string.concat('"sortedTroves":"', address(c.sortedTroves).toHexString(), '",') - ), - string.concat( - string.concat('"stabilityPool":"', address(c.stabilityPool).toHexString(), '",'), - string.concat('"troveManager":"', address(c.troveManager).toHexString(), '",'), - string.concat('"troveNFT":"', address(c.troveNFT).toHexString(), '",'), - string.concat('"metadataNFT":"', address(c.metadataNFT).toHexString(), '",'), - string.concat('"priceFeed":"', address(c.priceFeed).toHexString(), '",'), - string.concat('"gasPool":"', address(c.gasPool).toHexString(), '",'), - string.concat('"interestRouter":"', address(c.interestRouter).toHexString(), '",') - ) - ), - "}" - ); - } - - function _getDeploymentConstants() internal pure returns (string memory) { - return string.concat( - "{", - string.concat( - string.concat('"ETH_GAS_COMPENSATION":"', ETH_GAS_COMPENSATION.toString(), '",'), - string.concat('"INTEREST_RATE_ADJ_COOLDOWN":"', INTEREST_RATE_ADJ_COOLDOWN.toString(), '",'), - string.concat('"MAX_ANNUAL_INTEREST_RATE":"', MAX_ANNUAL_INTEREST_RATE.toString(), '",'), - string.concat('"MIN_ANNUAL_INTEREST_RATE":"', MIN_ANNUAL_INTEREST_RATE.toString(), '",'), - string.concat('"MIN_DEBT":"', MIN_DEBT.toString(), '",'), - string.concat('"SP_YIELD_SPLIT":"', SP_YIELD_SPLIT.toString(), '",'), - string.concat('"UPFRONT_INTEREST_PERIOD":"', UPFRONT_INTEREST_PERIOD.toString(), '"') // no comma - ), - "}" - ); - } - - function _getManifestJson(DeploymentResult memory deployed) internal view returns (string memory) { - string[] memory branches = new string[](1); - - branches[0] = _getBranchContractsJson(deployed.contracts); - - return string.concat( - "{", - string.concat('"constants":', _getDeploymentConstants(), ","), - string.concat('"collateralRegistry":"', address(deployed.collateralRegistry).toHexString(), '",'), - string.concat('"boldToken":"', address(deployed.stableToken).toHexString(), '",'), - string.concat('"hintHelpers":"', address(deployed.hintHelpers).toHexString(), '",'), - string.concat('"stableTokenV3Impl":"', address(deployed.stableTokenV3Impl).toHexString(), '",'), - string.concat('"stabilityPoolImpl":"', address(deployed.stabilityPoolImpl).toHexString(), '",'), - string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",'), - string.concat('"fpmm":"', address(deployed.fpmm).toHexString(), '",'), - string.concat('"branches":[', branches.join(","), "]"), - "}" - ); - } -} +// TODO(@bayological): Fix compilation errors with core contracts & replace address registry param usage with sys params + +// // SPDX-License-Identifier: UNLICENSED +// pragma solidity 0.8.24; + +// import {StdCheats} from "forge-std/StdCheats.sol"; +// import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +// import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; +// import {IERC20 as IERC20_GOV} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +// import {ProxyAdmin} from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; +// import {TransparentUpgradeableProxy} from +// "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +// import {IFPMMFactory} from "src/Interfaces/IFPMMFactory.sol"; + +// import {ETH_GAS_COMPENSATION} from "src/Dependencies/Constants.sol"; +// import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; +// import {StringFormatting} from "test/Utils/StringFormatting.sol"; +// import {Accounts} from "test/TestContracts/Accounts.sol"; +// import {ERC20Faucet} from "test/TestContracts/ERC20Faucet.sol"; +// import {WETHTester} from "test/TestContracts/WETHTester.sol"; +// import "src/AddressesRegistry.sol"; +// import "src/ActivePool.sol"; +// import "src/BoldToken.sol"; +// import "src/BorrowerOperations.sol"; +// import "src/TroveManager.sol"; +// import "src/TroveNFT.sol"; +// import "src/CollSurplusPool.sol"; +// import "src/DefaultPool.sol"; +// import "src/GasPool.sol"; +// import "src/HintHelpers.sol"; +// import "src/MultiTroveGetter.sol"; +// import "src/SortedTroves.sol"; +// import "src/StabilityPool.sol"; + +// import "src/CollateralRegistry.sol"; +// import "src/tokens/StableTokenV3.sol"; +// import "src/Interfaces/IStableTokenV3.sol"; +// import "test/TestContracts/PriceFeedTestnet.sol"; +// import "test/TestContracts/MetadataDeployment.sol"; +// import "test/Utils/Logging.sol"; +// import "test/Utils/StringEquality.sol"; +// import "forge-std/console2.sol"; + +// contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { +// using Strings for *; +// using StringFormatting for *; +// using StringEquality for string; + +// bytes32 SALT; +// address deployer; + +// struct LiquityContracts { +// IAddressesRegistry addressesRegistry; +// IActivePool activePool; +// IBorrowerOperations borrowerOperations; +// ICollSurplusPool collSurplusPool; +// IDefaultPool defaultPool; +// ISortedTroves sortedTroves; +// IStabilityPool stabilityPool; +// ITroveManager troveManager; +// ITroveNFT troveNFT; +// MetadataNFT metadataNFT; +// IPriceFeed priceFeed; +// GasPool gasPool; +// IInterestRouter interestRouter; +// IERC20Metadata collToken; +// } + +// struct LiquityContractAddresses { +// address activePool; +// address borrowerOperations; +// address collSurplusPool; +// address defaultPool; +// address sortedTroves; +// address stabilityPool; +// address troveManager; +// address troveNFT; +// address metadataNFT; +// address priceFeed; +// address gasPool; +// address interestRouter; +// } + +// struct TroveManagerParams { +// uint256 CCR; +// uint256 MCR; +// uint256 SCR; +// uint256 BCR; +// uint256 LIQUIDATION_PENALTY_SP; +// uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; +// } + +// struct DeploymentVars { +// uint256 numCollaterals; +// IERC20Metadata[] collaterals; +// IAddressesRegistry[] addressesRegistries; +// ITroveManager[] troveManagers; +// LiquityContracts contracts; +// bytes bytecode; +// address boldTokenAddress; +// uint256 i; +// } + +// struct DemoTroveParams { +// uint256 collIndex; +// uint256 owner; +// uint256 ownerIndex; +// uint256 coll; +// uint256 debt; +// uint256 annualInterestRate; +// } + +// struct DeploymentResult { +// LiquityContracts contracts; +// ICollateralRegistry collateralRegistry; +// HintHelpers hintHelpers; +// MultiTroveGetter multiTroveGetter; +// ProxyAdmin proxyAdmin; +// IStableTokenV3 stableToken; +// address stabilityPoolImpl; +// address stableTokenV3Impl; +// address fpmm; +// } + +// struct DeploymentConfig { +// address USDm_ALFAJORES_ADDRESS; +// address proxyAdmin; +// address fpmmFactory; +// address fpmmImplementation; +// address referenceRateFeedID; +// string stableTokenName; +// string stableTokenSymbol; +// // Parameters for the TroveManager +// uint256 CCR; +// uint256 MCR; +// uint256 SCR; +// uint256 BCR; +// uint256 LIQUIDATION_PENALTY_SP; +// uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; +// } + +// DeploymentConfig internal CONFIG = DeploymentConfig({ +// USDm_ALFAJORES_ADDRESS: 0x9E2d4412d0f434cC85500b79447d9323a7416f09, +// proxyAdmin: 0xe4DdacCAdb64114215FCe8251B57B2AEB5C2C0E2, +// fpmmFactory: 0xd8098494a749a3fDAD2D2e7Fa5272D8f274D8FF6, +// fpmmImplementation: 0x0292efcB331C6603eaa29D570d12eB336D6c01d6, +// referenceRateFeedID: 0x206B25Ea01E188Ee243131aFdE526bA6E131a016, +// stableTokenName: "EUR.m Test", +// stableTokenSymbol: "EUR.m", +// // TODO: reconsider these values +// CCR: 150e16, +// MCR: 110e16, +// SCR: 110e16, +// BCR: 40e16, +// LIQUIDATION_PENALTY_SP: 5e16, +// LIQUIDATION_PENALTY_REDISTRIBUTION: 10e16 +// }); + +// function run() external { +// string memory saltStr = vm.envOr("SALT", block.timestamp.toString()); +// SALT = keccak256(bytes(saltStr)); + +// uint256 privateKey = vm.envUint("DEPLOYER"); +// deployer = vm.addr(privateKey); +// vm.startBroadcast(privateKey); + +// _log("Deployer: ", deployer.toHexString()); +// _log("Deployer balance: ", deployer.balance.decimal()); +// _log("CREATE2 salt: ", 'keccak256(bytes("', saltStr, '")) = ', uint256(SALT).toHexString()); +// _log("Chain ID: ", block.chainid.toString()); + +// DeploymentResult memory deployed = _deployAndConnectContracts(); + +// vm.stopBroadcast(); + +// vm.writeFile("script/deployment-manifest.json", _getManifestJson(deployed)); +// } + +// // See: https://solidity-by-example.org/app/create2/ +// function getBytecode(bytes memory _creationCode, address _addressesRegistry) public pure returns (bytes memory) { +// return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry)); +// } + +// function _deployAndConnectContracts() internal returns (DeploymentResult memory r) { +// _deployProxyInfrastructure(r); +// _deployStableToken(r); +// _deployFPMM(r); + +// TroveManagerParams memory troveManagerParams = TroveManagerParams({ +// CCR: CONFIG.CCR, +// MCR: CONFIG.MCR, +// SCR: CONFIG.SCR, +// BCR: CONFIG.BCR, +// LIQUIDATION_PENALTY_SP: CONFIG.LIQUIDATION_PENALTY_SP, +// LIQUIDATION_PENALTY_REDISTRIBUTION: CONFIG.LIQUIDATION_PENALTY_REDISTRIBUTION +// }); + +// //TODO(@bayological): Initialize system params and remove address reg here + +// IAddressesRegistry addressesRegistry = new AddressesRegistry( +// deployer, +// troveManagerParams.CCR, +// troveManagerParams.MCR, +// troveManagerParams.BCR, +// troveManagerParams.SCR, +// troveManagerParams.LIQUIDATION_PENALTY_SP, +// troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION +// ); + +// address troveManagerAddress = vm.computeCreate2Address( +// SALT, keccak256(getBytecode(type(TroveManager).creationCode, address(addressesRegistry))) +// ); + +// IERC20Metadata collToken = IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS); + +// IERC20Metadata[] memory collaterals = new IERC20Metadata[](1); +// collaterals[0] = collToken; + +// ITroveManager[] memory troveManagers = new ITroveManager[](1); +// troveManagers[0] = ITroveManager(troveManagerAddress); + +// r.collateralRegistry = new CollateralRegistry(IBoldToken(address(r.stableToken)), collaterals, troveManagers); +// r.hintHelpers = new HintHelpers(r.collateralRegistry); +// r.multiTroveGetter = new MultiTroveGetter(r.collateralRegistry); + +// IPriceFeed priceFeed = new PriceFeedTestnet(); + +// r.contracts = +// _deployAndConnectCollateralContracts(collToken, priceFeed, addressesRegistry, troveManagerAddress, r); +// } + +// function _deployProxyInfrastructure(DeploymentResult memory r) internal { +// r.proxyAdmin = ProxyAdmin(CONFIG.proxyAdmin); +// r.stableTokenV3Impl = address(new StableTokenV3{salt: SALT}(true)); +// r.stabilityPoolImpl = address(new StabilityPool{salt: SALT}(true)); + +// assert( +// address(r.stableTokenV3Impl) +// == vm.computeCreate2Address( +// SALT, keccak256(bytes.concat(type(StableTokenV3).creationCode, abi.encode(true))) +// ) +// ); +// assert( +// address(r.stabilityPoolImpl) +// == vm.computeCreate2Address( +// SALT, keccak256(bytes.concat(type(StabilityPool).creationCode, abi.encode(true))) +// ) +// ); +// } + +// function _deployStableToken(DeploymentResult memory r) internal { +// r.stableToken = IStableTokenV3( +// address(new TransparentUpgradeableProxy(address(r.stableTokenV3Impl), address(r.proxyAdmin), "")) +// ); +// } + +// function _deployFPMM(DeploymentResult memory r) internal { +// r.fpmm = IFPMMFactory(CONFIG.fpmmFactory).deployFPMM( +// CONFIG.fpmmImplementation, address(r.stableToken), CONFIG.USDm_ALFAJORES_ADDRESS, CONFIG.referenceRateFeedID +// ); +// } + +// function _deployAndConnectCollateralContracts( +// IERC20Metadata _collToken, +// IPriceFeed _priceFeed, +// IAddressesRegistry _addressesRegistry, +// address _troveManagerAddress, +// DeploymentResult memory r +// ) internal returns (LiquityContracts memory contracts) { +// LiquityContractAddresses memory addresses; +// contracts.collToken = _collToken; +// contracts.addressesRegistry = _addressesRegistry; +// contracts.priceFeed = _priceFeed; +// // TODO: replace with governance timelock on mainnet +// contracts.interestRouter = IInterestRouter(0x56fD3F2bEE130e9867942D0F463a16fBE49B8d81); + +// addresses.troveManager = _troveManagerAddress; + +// contracts.metadataNFT = deployMetadata(SALT); +// addresses.metadataNFT = vm.computeCreate2Address( +// SALT, keccak256(getBytecode(type(MetadataNFT).creationCode, address(initializedFixedAssetReader))) +// ); +// assert(address(contracts.metadataNFT) == addresses.metadataNFT); + +// addresses.borrowerOperations = +// _computeCreate2Address(type(BorrowerOperations).creationCode, address(contracts.addressesRegistry)); +// addresses.troveNFT = _computeCreate2Address(type(TroveNFT).creationCode, address(contracts.addressesRegistry)); +// addresses.activePool = +// _computeCreate2Address(type(ActivePool).creationCode, address(contracts.addressesRegistry)); +// addresses.defaultPool = +// _computeCreate2Address(type(DefaultPool).creationCode, address(contracts.addressesRegistry)); +// addresses.gasPool = _computeCreate2Address(type(GasPool).creationCode, address(contracts.addressesRegistry)); +// addresses.collSurplusPool = +// _computeCreate2Address(type(CollSurplusPool).creationCode, address(contracts.addressesRegistry)); +// addresses.sortedTroves = +// _computeCreate2Address(type(SortedTroves).creationCode, address(contracts.addressesRegistry)); + +// // Deploy StabilityPool proxy +// address stabilityPool = +// address(new TransparentUpgradeableProxy(address(r.stabilityPoolImpl), address(r.proxyAdmin), "")); + +// contracts.stabilityPool = IStabilityPool(stabilityPool); +// // Set up addresses in registry +// _setupAddressesRegistry(contracts, addresses, r); + +// // Deploy core protocol contracts +// _deployProtocolContracts(contracts, addresses); + +// IStabilityPool(stabilityPool).initialize(contracts.addressesRegistry); + +// address[] memory minters = new address[](2); +// minters[0] = address(contracts.borrowerOperations); +// minters[1] = address(contracts.activePool); + +// address[] memory burners = new address[](4); +// burners[0] = address(contracts.troveManager); +// burners[1] = address(r.collateralRegistry); +// burners[2] = address(contracts.borrowerOperations); +// burners[3] = address(contracts.stabilityPool); + +// address[] memory operators = new address[](1); +// operators[0] = address(contracts.stabilityPool); + +// r.stableToken.initialize( +// CONFIG.stableTokenName, +// CONFIG.stableTokenSymbol, +// new address[](0), +// new uint256[](0), +// minters, +// burners, +// operators +// ); +// } + +// function _setupAddressesRegistry( +// LiquityContracts memory contracts, +// LiquityContractAddresses memory addresses, +// DeploymentResult memory r +// ) internal { +// IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ +// collToken: contracts.collToken, +// borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), +// troveManager: ITroveManager(addresses.troveManager), +// troveNFT: ITroveNFT(addresses.troveNFT), +// metadataNFT: IMetadataNFT(addresses.metadataNFT), +// stabilityPool: contracts.stabilityPool, +// priceFeed: contracts.priceFeed, +// activePool: IActivePool(addresses.activePool), +// defaultPool: IDefaultPool(addresses.defaultPool), +// gasPoolAddress: addresses.gasPool, +// collSurplusPool: ICollSurplusPool(addresses.collSurplusPool), +// sortedTroves: ISortedTroves(addresses.sortedTroves), +// interestRouter: contracts.interestRouter, +// hintHelpers: r.hintHelpers, +// multiTroveGetter: r.multiTroveGetter, +// collateralRegistry: r.collateralRegistry, +// boldToken: IBoldToken(address(r.stableToken)), +// gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS) +// }); +// contracts.addressesRegistry.setAddresses(addressVars); +// } + +// function _deployProtocolContracts(LiquityContracts memory contracts, LiquityContractAddresses memory addresses) +// internal +// { +// contracts.borrowerOperations = new BorrowerOperations{salt: SALT}(contracts.addressesRegistry); +// contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry); +// contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); +// contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry); +// contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); +// contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); +// contracts.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); +// contracts.sortedTroves = new SortedTroves{salt: SALT}(contracts.addressesRegistry); + +// assert(address(contracts.borrowerOperations) == addresses.borrowerOperations); +// assert(address(contracts.troveManager) == addresses.troveManager); +// assert(address(contracts.troveNFT) == addresses.troveNFT); +// assert(address(contracts.activePool) == addresses.activePool); +// assert(address(contracts.defaultPool) == addresses.defaultPool); +// assert(address(contracts.gasPool) == addresses.gasPool); +// assert(address(contracts.collSurplusPool) == addresses.collSurplusPool); +// assert(address(contracts.sortedTroves) == addresses.sortedTroves); +// } + +// function _computeCreate2Address(bytes memory creationCode, address _addressesRegistry) +// internal +// view +// returns (address) +// { +// return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry))); +// } + +// function _getBranchContractsJson(LiquityContracts memory c) internal view returns (string memory) { +// return string.concat( +// "{", +// string.concat( +// // Avoid stack too deep by chunking concats +// string.concat( +// string.concat('"collSymbol":"', c.collToken.symbol(), '",'), // purely for human-readability +// string.concat('"collToken":"', address(c.collToken).toHexString(), '",'), +// string.concat('"addressesRegistry":"', address(c.addressesRegistry).toHexString(), '",'), +// string.concat('"activePool":"', address(c.activePool).toHexString(), '",'), +// string.concat('"borrowerOperations":"', address(c.borrowerOperations).toHexString(), '",'), +// string.concat('"collSurplusPool":"', address(c.collSurplusPool).toHexString(), '",'), +// string.concat('"defaultPool":"', address(c.defaultPool).toHexString(), '",'), +// string.concat('"sortedTroves":"', address(c.sortedTroves).toHexString(), '",') +// ), +// string.concat( +// string.concat('"stabilityPool":"', address(c.stabilityPool).toHexString(), '",'), +// string.concat('"troveManager":"', address(c.troveManager).toHexString(), '",'), +// string.concat('"troveNFT":"', address(c.troveNFT).toHexString(), '",'), +// string.concat('"metadataNFT":"', address(c.metadataNFT).toHexString(), '",'), +// string.concat('"priceFeed":"', address(c.priceFeed).toHexString(), '",'), +// string.concat('"gasPool":"', address(c.gasPool).toHexString(), '",'), +// string.concat('"interestRouter":"', address(c.interestRouter).toHexString(), '",') +// ) +// ), +// "}" +// ); +// } + +// function _getDeploymentConstants() internal pure returns (string memory) { +// return string.concat( +// "{", +// string.concat( +// string.concat('"ETH_GAS_COMPENSATION":"', ETH_GAS_COMPENSATION.toString(), '",'), +// string.concat('"INTEREST_RATE_ADJ_COOLDOWN":"', INTEREST_RATE_ADJ_COOLDOWN.toString(), '",'), +// string.concat('"MAX_ANNUAL_INTEREST_RATE":"', MAX_ANNUAL_INTEREST_RATE.toString(), '",'), +// string.concat('"MIN_ANNUAL_INTEREST_RATE":"', MIN_ANNUAL_INTEREST_RATE.toString(), '",'), +// string.concat('"MIN_DEBT":"', MIN_DEBT.toString(), '",'), +// string.concat('"SP_YIELD_SPLIT":"', SP_YIELD_SPLIT.toString(), '",'), +// string.concat('"UPFRONT_INTEREST_PERIOD":"', UPFRONT_INTEREST_PERIOD.toString(), '"') // no comma +// ), +// "}" +// ); +// } + +// function _getManifestJson(DeploymentResult memory deployed) internal view returns (string memory) { +// string[] memory branches = new string[](1); + +// branches[0] = _getBranchContractsJson(deployed.contracts); + +// return string.concat( +// "{", +// string.concat('"constants":', _getDeploymentConstants(), ","), +// string.concat('"collateralRegistry":"', address(deployed.collateralRegistry).toHexString(), '",'), +// string.concat('"boldToken":"', address(deployed.stableToken).toHexString(), '",'), +// string.concat('"hintHelpers":"', address(deployed.hintHelpers).toHexString(), '",'), +// string.concat('"stableTokenV3Impl":"', address(deployed.stableTokenV3Impl).toHexString(), '",'), +// string.concat('"stabilityPoolImpl":"', address(deployed.stabilityPoolImpl).toHexString(), '",'), +// string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",'), +// string.concat('"fpmm":"', address(deployed.fpmm).toHexString(), '",'), +// string.concat('"branches":[', branches.join(","), "]"), +// "}" +// ); +// } +// } diff --git a/contracts/src/Dependencies/Constants.sol b/contracts/src/Dependencies/Constants.sol index 26df4ab68..0c0d6a418 100644 --- a/contracts/src/Dependencies/Constants.sol +++ b/contracts/src/Dependencies/Constants.sol @@ -16,6 +16,6 @@ uint256 constant ONE_YEAR = 365 days; // TODO(@bayological): Remve this and refactor the tests that use it // Dummy contract that lets legacy Hardhat tests query some of the constants contract Constants { - uint256 public constant _ETH_GAS_COMPENSATION = ETH_GAS_COMPENSATION; - uint256 public constant _MIN_DEBT = MIN_DEBT; + uint256 public constant _ETH_GAS_COMPENSATION = 123; + uint256 public constant _MIN_DEBT = 123; } diff --git a/contracts/test/SPInvariants.t.sol b/contracts/test/SPInvariants.t.sol index 250ef549e..181267abe 100644 --- a/contracts/test/SPInvariants.t.sol +++ b/contracts/test/SPInvariants.t.sol @@ -1,73 +1,73 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +// // SPDX-License-Identifier: MIT +// pragma solidity 0.8.24; -import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; -import {IStabilityPool} from "src/Interfaces/IStabilityPool.sol"; -import {HintHelpers} from "src/HintHelpers.sol"; -import {Assertions} from "./TestContracts/Assertions.sol"; -import {BaseInvariantTest} from "./TestContracts/BaseInvariantTest.sol"; -import {TestDeployer} from "./TestContracts/Deployment.t.sol"; -import {SPInvariantsTestHandler} from "./TestContracts/SPInvariantsTestHandler.t.sol"; +// import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; +// import {IStabilityPool} from "src/Interfaces/IStabilityPool.sol"; +// import {HintHelpers} from "src/HintHelpers.sol"; +// import {Assertions} from "./TestContracts/Assertions.sol"; +// import {BaseInvariantTest} from "./TestContracts/BaseInvariantTest.sol"; +// import {TestDeployer} from "./TestContracts/Deployment.t.sol"; +// import {SPInvariantsTestHandler} from "./TestContracts/SPInvariantsTestHandler.t.sol"; -abstract contract SPInvariantsBase is Assertions, BaseInvariantTest { - IStabilityPool stabilityPool; - SPInvariantsTestHandler handler; +// abstract contract SPInvariantsBase is Assertions, BaseInvariantTest { +// IStabilityPool stabilityPool; +// SPInvariantsTestHandler handler; - function setUp() public override { - super.setUp(); +// function setUp() public override { +// super.setUp(); - TestDeployer deployer = new TestDeployer(); - (TestDeployer.LiquityContractsDev memory contracts,, IBoldToken boldToken, HintHelpers hintHelpers,,) = - deployer.deployAndConnectContracts(); - stabilityPool = contracts.stabilityPool; +// TestDeployer deployer = new TestDeployer(); +// (TestDeployer.LiquityContractsDev memory contracts,, IBoldToken boldToken, HintHelpers hintHelpers,,) = +// deployer.deployAndConnectContracts(); +// stabilityPool = contracts.stabilityPool; - handler = new SPInvariantsTestHandler( - SPInvariantsTestHandler.Contracts({ - boldToken: boldToken, - borrowerOperations: contracts.borrowerOperations, - collateralToken: contracts.collToken, - priceFeed: contracts.priceFeed, - stabilityPool: contracts.stabilityPool, - troveManager: contracts.troveManager, - collSurplusPool: contracts.pools.collSurplusPool - }), - hintHelpers - ); +// handler = new SPInvariantsTestHandler( +// SPInvariantsTestHandler.Contracts({ +// boldToken: boldToken, +// borrowerOperations: contracts.borrowerOperations, +// collateralToken: contracts.collToken, +// priceFeed: contracts.priceFeed, +// stabilityPool: contracts.stabilityPool, +// troveManager: contracts.troveManager, +// collSurplusPool: contracts.pools.collSurplusPool +// }), +// hintHelpers +// ); - vm.label(address(handler), "handler"); - targetContract(address(handler)); - } +// vm.label(address(handler), "handler"); +// targetContract(address(handler)); +// } - function assert_AllFundsClaimable() internal view { - uint256 stabilityPoolColl = stabilityPool.getCollBalance(); - uint256 stabilityPoolBold = stabilityPool.getTotalBoldDeposits(); - uint256 yieldGainsOwed = stabilityPool.getYieldGainsOwed(); +// function assert_AllFundsClaimable() internal view { +// uint256 stabilityPoolColl = stabilityPool.getCollBalance(); +// uint256 stabilityPoolBold = stabilityPool.getTotalBoldDeposits(); +// uint256 yieldGainsOwed = stabilityPool.getYieldGainsOwed(); - uint256 claimableColl = 0; - uint256 claimableBold = 0; - uint256 sumYieldGains = 0; +// uint256 claimableColl = 0; +// uint256 claimableBold = 0; +// uint256 sumYieldGains = 0; - for (uint256 i = 0; i < actors.length; ++i) { - claimableColl += stabilityPool.getDepositorCollGain(actors[i].account); - claimableBold += stabilityPool.getCompoundedBoldDeposit(actors[i].account); - sumYieldGains += stabilityPool.getDepositorYieldGain(actors[i].account); - } +// for (uint256 i = 0; i < actors.length; ++i) { +// claimableColl += stabilityPool.getDepositorCollGain(actors[i].account); +// claimableBold += stabilityPool.getCompoundedBoldDeposit(actors[i].account); +// sumYieldGains += stabilityPool.getDepositorYieldGain(actors[i].account); +// } - // These tolerances might seem quite loose, but we have to consider - // that we're dealing with quintillions of BOLD in this test - assertGeDecimal(stabilityPoolColl, claimableColl, 18, "SP coll insolvency"); - assertApproxEqAbsRelDecimal(stabilityPoolColl, claimableColl, 1e-5 ether, 1, 18, "SP coll loss"); +// // These tolerances might seem quite loose, but we have to consider +// // that we're dealing with quintillions of BOLD in this test +// assertGeDecimal(stabilityPoolColl, claimableColl, 18, "SP coll insolvency"); +// assertApproxEqAbsRelDecimal(stabilityPoolColl, claimableColl, 1e-5 ether, 1, 18, "SP coll loss"); - assertGeDecimal(stabilityPoolBold, claimableBold, 18, "SP BOLD insolvency"); - assertApproxEqAbsRelDecimal(stabilityPoolBold, claimableBold, 1e-7 ether, 1, 18, "SP BOLD loss"); +// assertGeDecimal(stabilityPoolBold, claimableBold, 18, "SP BOLD insolvency"); +// assertApproxEqAbsRelDecimal(stabilityPoolBold, claimableBold, 1e-7 ether, 1, 18, "SP BOLD loss"); - assertGeDecimal(yieldGainsOwed, sumYieldGains, 18, "SP yield insolvency"); - assertApproxEqAbsRelDecimal(yieldGainsOwed, sumYieldGains, 1 ether, 1, 18, "SP yield loss"); - } -} +// assertGeDecimal(yieldGainsOwed, sumYieldGains, 18, "SP yield insolvency"); +// assertApproxEqAbsRelDecimal(yieldGainsOwed, sumYieldGains, 1 ether, 1, 18, "SP yield loss"); +// } +// } -contract SPInvariantsTest is SPInvariantsBase { - function invariant_AllFundsClaimable() external view { - assert_AllFundsClaimable(); - } -} +// contract SPInvariantsTest is SPInvariantsBase { +// function invariant_AllFundsClaimable() external view { +// assert_AllFundsClaimable(); +// } +// } diff --git a/contracts/test/TestContracts/BaseMultiCollateralTest.sol b/contracts/test/TestContracts/BaseMultiCollateralTest.sol index 6824306aa..36f4eafb7 100644 --- a/contracts/test/TestContracts/BaseMultiCollateralTest.sol +++ b/contracts/test/TestContracts/BaseMultiCollateralTest.sol @@ -8,6 +8,7 @@ import {IWETH} from "src/Interfaces/IWETH.sol"; import {HintHelpers} from "src/HintHelpers.sol"; import {MultiTroveGetter} from "src/MultiTroveGetter.sol"; import {TestDeployer} from "./Deployment.t.sol"; +import {ISystemParams} from "src/Interfaces/ISystemParams.sol"; contract BaseMultiCollateralTest { struct Contracts { @@ -17,6 +18,7 @@ contract BaseMultiCollateralTest { HintHelpers hintHelpers; MultiTroveGetter multiTroveGetter; TestDeployer.LiquityContractsDev[] branches; + ISystemParams systemParams; } IERC20 weth; @@ -24,12 +26,14 @@ contract BaseMultiCollateralTest { IBoldToken boldToken; HintHelpers hintHelpers; TestDeployer.LiquityContractsDev[] branches; + ISystemParams systemParams; function setupContracts(Contracts memory contracts) internal { weth = contracts.weth; collateralRegistry = contracts.collateralRegistry; boldToken = contracts.boldToken; hintHelpers = contracts.hintHelpers; + systemParams = contracts.systemParams; for (uint256 i = 0; i < contracts.branches.length; ++i) { branches.push(contracts.branches[i]); diff --git a/contracts/test/TestContracts/BaseTest.sol b/contracts/test/TestContracts/BaseTest.sol index 507878b04..35eb3d422 100644 --- a/contracts/test/TestContracts/BaseTest.sol +++ b/contracts/test/TestContracts/BaseTest.sol @@ -32,6 +32,22 @@ contract BaseTest is TestAccounts, Logging, TroveId { uint256 SCR; uint256 LIQUIDATION_PENALTY_SP; uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; + uint256 UPFRONT_INTEREST_PERIOD; + uint128 MIN_INTEREST_RATE_CHANGE_PERIOD; + uint256 MIN_DEBT; + uint256 SP_YIELD_SPLIT; + uint256 MIN_ANNUAL_INTEREST_RATE; + uint256 INTEREST_RATE_ADJ_COOLDOWN; + uint256 ETH_GAS_COMPENSATION; + uint256 COLL_GAS_COMPENSATION_DIVISOR; + uint256 MAX_ANNUAL_INTEREST_RATE; + uint256 MIN_BOLD_IN_SP; + uint256 INITIAL_BASE_RATE; + uint256 REDEMPTION_FEE_FLOOR; + uint256 MAX_BATCH_SHARES_RATIO; + uint128 MAX_ANNUAL_BATCH_MANAGEMENT_FEE; + uint256 REDEMPTION_MINUTE_DECAY_FACTOR; + uint256 URGENT_REDEMPTION_BONUS; // Core contracts IAddressesRegistry addressesRegistry; @@ -52,6 +68,7 @@ contract BaseTest is TestAccounts, Logging, TroveId { IERC20 collToken; HintHelpers hintHelpers; IWETH WETH; // used for gas compensation + ISystemParams systemParams; // Structs for use in test where we need to bi-pass "stack-too-deep" errors struct ABCDEF { @@ -65,72 +82,119 @@ contract BaseTest is TestAccounts, Logging, TroveId { // --- functions --- - function getTroveEntireColl(uint256 _troveId) internal view returns (uint256) { - LatestTroveData memory trove = troveManager.getLatestTroveData(_troveId); + function getTroveEntireColl( + uint256 _troveId + ) internal view returns (uint256) { + LatestTroveData memory trove = troveManager.getLatestTroveData( + _troveId + ); return trove.entireColl; } - function getTroveEntireDebt(uint256 _troveId) internal view returns (uint256) { - LatestTroveData memory trove = troveManager.getLatestTroveData(_troveId); + function getTroveEntireDebt( + uint256 _troveId + ) internal view returns (uint256) { + LatestTroveData memory trove = troveManager.getLatestTroveData( + _troveId + ); return trove.entireDebt; } - function getTroveEntireColl(ITroveManager _troveManager, uint256 _troveId) internal view returns (uint256) { - LatestTroveData memory trove = _troveManager.getLatestTroveData(_troveId); + function getTroveEntireColl( + ITroveManager _troveManager, + uint256 _troveId + ) internal view returns (uint256) { + LatestTroveData memory trove = _troveManager.getLatestTroveData( + _troveId + ); return trove.entireColl; } - function getTroveEntireDebt(ITroveManager _troveManager, uint256 _troveId) internal view returns (uint256) { - LatestTroveData memory trove = _troveManager.getLatestTroveData(_troveId); + function getTroveEntireDebt( + ITroveManager _troveManager, + uint256 _troveId + ) internal view returns (uint256) { + LatestTroveData memory trove = _troveManager.getLatestTroveData( + _troveId + ); return trove.entireDebt; } - function calcInterest(uint256 weightedRecordedDebt, uint256 period) internal pure returns (uint256) { - return weightedRecordedDebt * period / 365 days / DECIMAL_PRECISION; + function calcInterest( + uint256 weightedRecordedDebt, + uint256 period + ) internal pure returns (uint256) { + return (weightedRecordedDebt * period) / 365 days / DECIMAL_PRECISION; } - function calcUpfrontFee(uint256 debt, uint256 avgInterestRate) internal pure returns (uint256) { + function calcUpfrontFee( + uint256 debt, + uint256 avgInterestRate + ) internal view returns (uint256) { return calcInterest(debt * avgInterestRate, UPFRONT_INTEREST_PERIOD); } - function predictOpenTroveUpfrontFee(uint256 borrowedAmount, uint256 interestRate) internal view returns (uint256) { - return hintHelpers.predictOpenTroveUpfrontFee(0, borrowedAmount, interestRate); + function predictOpenTroveUpfrontFee( + uint256 borrowedAmount, + uint256 interestRate + ) internal view returns (uint256) { + return + hintHelpers.predictOpenTroveUpfrontFee( + 0, + borrowedAmount, + interestRate + ); } - function predictAdjustInterestRateUpfrontFee(uint256 troveId, uint256 newInterestRate) - internal - view - returns (uint256) - { - return hintHelpers.predictAdjustInterestRateUpfrontFee(0, troveId, newInterestRate); + function predictAdjustInterestRateUpfrontFee( + uint256 troveId, + uint256 newInterestRate + ) internal view returns (uint256) { + return + hintHelpers.predictAdjustInterestRateUpfrontFee( + 0, + troveId, + newInterestRate + ); } - function forcePredictAdjustInterestRateUpfrontFee(uint256 troveId, uint256 newInterestRate) - internal - view - returns (uint256) - { - return hintHelpers.forcePredictAdjustInterestRateUpfrontFee(0, troveId, newInterestRate); + function forcePredictAdjustInterestRateUpfrontFee( + uint256 troveId, + uint256 newInterestRate + ) internal view returns (uint256) { + return + hintHelpers.forcePredictAdjustInterestRateUpfrontFee( + 0, + troveId, + newInterestRate + ); } - function predictAdjustTroveUpfrontFee(uint256 troveId, uint256 debtIncrease) internal view returns (uint256) { - return hintHelpers.predictAdjustTroveUpfrontFee(0, troveId, debtIncrease); + function predictAdjustTroveUpfrontFee( + uint256 troveId, + uint256 debtIncrease + ) internal view returns (uint256) { + return + hintHelpers.predictAdjustTroveUpfrontFee(0, troveId, debtIncrease); } - function predictJoinBatchInterestRateUpfrontFee(uint256 _troveId, address _batchAddress) - internal - view - returns (uint256) - { - return hintHelpers.predictJoinBatchInterestRateUpfrontFee(0, _troveId, _batchAddress); + function predictJoinBatchInterestRateUpfrontFee( + uint256 _troveId, + address _batchAddress + ) internal view returns (uint256) { + return + hintHelpers.predictJoinBatchInterestRateUpfrontFee( + 0, + _troveId, + _batchAddress + ); } // Quick and dirty binary search instead of Newton's, because it's easier - function findAmountToBorrowWithOpenTrove(uint256 targetDebt, uint256 interestRate) - internal - view - returns (uint256 borrow, uint256 upfrontFee) - { + function findAmountToBorrowWithOpenTrove( + uint256 targetDebt, + uint256 interestRate + ) internal view returns (uint256 borrow, uint256 upfrontFee) { uint256 borrowRight = targetDebt; upfrontFee = predictOpenTroveUpfrontFee(borrowRight, interestRate); uint256 borrowLeft = borrowRight - upfrontFee; @@ -150,11 +214,10 @@ contract BaseTest is TestAccounts, Logging, TroveId { } } - function findAmountToBorrowWithAdjustTrove(uint256 troveId, uint256 targetDebt) - internal - view - returns (uint256 borrow, uint256 upfrontFee) - { + function findAmountToBorrowWithAdjustTrove( + uint256 troveId, + uint256 targetDebt + ) internal view returns (uint256 borrow, uint256 upfrontFee) { uint256 entireDebt = troveManager.getTroveEntireDebt(troveId); assert(targetDebt >= entireDebt); @@ -177,15 +240,25 @@ contract BaseTest is TestAccounts, Logging, TroveId { } } - function getRedeemableDebt(uint256 troveId) internal view returns (uint256) { + function getRedeemableDebt( + uint256 troveId + ) internal view returns (uint256) { return troveManager.getTroveEntireDebt(troveId); } - function openTroveNoHints100pct(address _account, uint256 _coll, uint256 _boldAmount, uint256 _annualInterestRate) - public - returns (uint256 troveId) - { - (troveId,) = openTroveHelper(_account, 0, _coll, _boldAmount, _annualInterestRate); + function openTroveNoHints100pct( + address _account, + uint256 _coll, + uint256 _boldAmount, + uint256 _annualInterestRate + ) public returns (uint256 troveId) { + (troveId, ) = openTroveHelper( + _account, + 0, + _coll, + _boldAmount, + _annualInterestRate + ); } function openTroveNoHints100pctWithIndex( @@ -195,7 +268,13 @@ contract BaseTest is TestAccounts, Logging, TroveId { uint256 _boldAmount, uint256 _annualInterestRate ) public returns (uint256 troveId) { - (troveId,) = openTroveHelper(_account, _index, _coll, _boldAmount, _annualInterestRate); + (troveId, ) = openTroveHelper( + _account, + _index, + _coll, + _boldAmount, + _annualInterestRate + ); } function openTroveHelper( @@ -205,7 +284,10 @@ contract BaseTest is TestAccounts, Logging, TroveId { uint256 _boldAmount, uint256 _annualInterestRate ) public returns (uint256 troveId, uint256 upfrontFee) { - upfrontFee = predictOpenTroveUpfrontFee(_boldAmount, _annualInterestRate); + upfrontFee = predictOpenTroveUpfrontFee( + _boldAmount, + _annualInterestRate + ); vm.startPrank(_account); @@ -233,11 +315,24 @@ contract BaseTest is TestAccounts, Logging, TroveId { uint256 _debt, uint256 _interestRate ) public returns (uint256 troveId) { - (uint256 borrow, uint256 upfrontFee) = findAmountToBorrowWithOpenTrove(_debt, _interestRate); + (uint256 borrow, uint256 upfrontFee) = findAmountToBorrowWithOpenTrove( + _debt, + _interestRate + ); vm.prank(_account); troveId = borrowerOperations.openTrove( - _account, _index, _coll, borrow, 0, 0, _interestRate, upfrontFee, address(0), address(0), address(0) + _account, + _index, + _coll, + borrow, + 0, + 0, + _interestRate, + upfrontFee, + address(0), + address(0), + address(0) ); } @@ -248,13 +343,26 @@ contract BaseTest is TestAccounts, Logging, TroveId { uint256 _debt, uint256 _interestRate ) public returns (uint256 troveId, uint256 coll) { - (uint256 borrow, uint256 upfrontFee) = findAmountToBorrowWithOpenTrove(_debt, _interestRate); + (uint256 borrow, uint256 upfrontFee) = findAmountToBorrowWithOpenTrove( + _debt, + _interestRate + ); uint256 price = priceFeed.getPrice(); coll = mulDivCeil(_debt, _ICR, price); vm.prank(_account); troveId = borrowerOperations.openTrove( - _account, _index, coll, borrow, 0, 0, _interestRate, upfrontFee, address(0), address(0), address(0) + _account, + _index, + coll, + borrow, + 0, + 0, + _interestRate, + upfrontFee, + address(0), + address(0), + address(0) ); } @@ -310,20 +418,32 @@ contract BaseTest is TestAccounts, Logging, TroveId { vm.stopPrank(); } - function changeInterestRateNoHints(address _account, uint256 _troveId, uint256 _newAnnualInterestRate) - public - returns (uint256 upfrontFee) - { - upfrontFee = predictAdjustInterestRateUpfrontFee(_troveId, _newAnnualInterestRate); + function changeInterestRateNoHints( + address _account, + uint256 _troveId, + uint256 _newAnnualInterestRate + ) public returns (uint256 upfrontFee) { + upfrontFee = predictAdjustInterestRateUpfrontFee( + _troveId, + _newAnnualInterestRate + ); vm.startPrank(_account); - borrowerOperations.adjustTroveInterestRate(_troveId, _newAnnualInterestRate, 0, 0, upfrontFee); + borrowerOperations.adjustTroveInterestRate( + _troveId, + _newAnnualInterestRate, + 0, + 0, + upfrontFee + ); vm.stopPrank(); } function checkBelowCriticalThreshold(bool _true) public view { uint256 price = priceFeed.getPrice(); - bool belowCriticalThreshold = troveManager.checkBelowCriticalThreshold(price); + bool belowCriticalThreshold = troveManager.checkBelowCriticalThreshold( + price + ); assertEq(belowCriticalThreshold, _true); } @@ -339,7 +459,10 @@ contract BaseTest is TestAccounts, Logging, TroveId { vm.stopPrank(); } - function makeSPWithdrawalAndClaim(address _account, uint256 _amount) public { + function makeSPWithdrawalAndClaim( + address _account, + uint256 _amount + ) public { vm.startPrank(_account); stabilityPool.withdrawFromSP(_amount, true); vm.stopPrank(); @@ -363,25 +486,45 @@ contract BaseTest is TestAccounts, Logging, TroveId { vm.stopPrank(); } - function withdrawBold100pct(address _account, uint256 _troveId, uint256 _debtIncrease) public { + function withdrawBold100pct( + address _account, + uint256 _troveId, + uint256 _debtIncrease + ) public { vm.startPrank(_account); - borrowerOperations.withdrawBold(_troveId, _debtIncrease, predictAdjustTroveUpfrontFee(_troveId, _debtIncrease)); + borrowerOperations.withdrawBold( + _troveId, + _debtIncrease, + predictAdjustTroveUpfrontFee(_troveId, _debtIncrease) + ); vm.stopPrank(); } - function repayBold(address _account, uint256 _troveId, uint256 _debtDecrease) public { + function repayBold( + address _account, + uint256 _troveId, + uint256 _debtDecrease + ) public { vm.startPrank(_account); borrowerOperations.repayBold(_troveId, _debtDecrease); vm.stopPrank(); } - function addColl(address _account, uint256 _troveId, uint256 _collIncrease) public { + function addColl( + address _account, + uint256 _troveId, + uint256 _collIncrease + ) public { vm.startPrank(_account); borrowerOperations.addColl(_troveId, _collIncrease); vm.stopPrank(); } - function withdrawColl(address _account, uint256 _troveId, uint256 _collDecrease) public { + function withdrawColl( + address _account, + uint256 _troveId, + uint256 _collDecrease + ) public { vm.startPrank(_account); borrowerOperations.withdrawColl(_troveId, _collDecrease); vm.stopPrank(); @@ -405,7 +548,10 @@ contract BaseTest is TestAccounts, Logging, TroveId { vm.stopPrank(); } - function batchLiquidateTroves(address _from, uint256[] memory _trovesList) public { + function batchLiquidateTroves( + address _from, + uint256[] memory _trovesList + ) public { vm.startPrank(_from); troveManager.batchLiquidateTroves(_trovesList); vm.stopPrank(); @@ -417,13 +563,23 @@ contract BaseTest is TestAccounts, Logging, TroveId { vm.stopPrank(); } - function getShareofSPReward(address _depositor, uint256 _reward) public view returns (uint256) { - return _reward * stabilityPool.getCompoundedBoldDeposit(_depositor) / stabilityPool.getTotalBoldDeposits(); + function getShareofSPReward( + address _depositor, + uint256 _reward + ) public view returns (uint256) { + return + (_reward * stabilityPool.getCompoundedBoldDeposit(_depositor)) / + stabilityPool.getTotalBoldDeposits(); } function registerBatchManager(address _account) internal { registerBatchManager( - _account, uint128(1e16), uint128(20e16), uint128(5e16), uint128(25e14), MIN_INTEREST_RATE_CHANGE_PERIOD + _account, + uint128(1e16), + uint128(20e16), + uint128(5e16), + uint128(25e14), + MIN_INTEREST_RATE_CHANGE_PERIOD ); } @@ -437,7 +593,11 @@ contract BaseTest is TestAccounts, Logging, TroveId { ) internal { vm.startPrank(_account); borrowerOperations.registerBatchManager( - _minInterestRate, _maxInterestRate, _currentInterestRate, _fee, _minInterestRateChangePeriod + _minInterestRate, + _maxInterestRate, + _currentInterestRate, + _fee, + _minInterestRateChangePeriod ); vm.stopPrank(); } @@ -453,7 +613,15 @@ contract BaseTest is TestAccounts, Logging, TroveId { address _batchAddress, uint256 _annualInterestRate ) internal returns (uint256) { - return openTroveAndJoinBatchManagerWithIndex(_troveOwner, 0, _coll, _debt, _batchAddress, _annualInterestRate); + return + openTroveAndJoinBatchManagerWithIndex( + _troveOwner, + 0, + _coll, + _debt, + _batchAddress, + _annualInterestRate + ); } function openTroveAndJoinBatchManagerWithIndex( @@ -475,30 +643,40 @@ contract BaseTest is TestAccounts, Logging, TroveId { ); } - IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams memory params = IBorrowerOperations - .OpenTroveAndJoinInterestBatchManagerParams({ - owner: _troveOwner, - ownerIndex: _index, - collAmount: _coll, - boldAmount: _debt, - upperHint: 0, - lowerHint: 0, - interestBatchManager: _batchAddress, - maxUpfrontFee: 1e24, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); + IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams + memory params = IBorrowerOperations + .OpenTroveAndJoinInterestBatchManagerParams({ + owner: _troveOwner, + ownerIndex: _index, + collAmount: _coll, + boldAmount: _debt, + upperHint: 0, + lowerHint: 0, + interestBatchManager: _batchAddress, + maxUpfrontFee: 1e24, + addManager: address(0), + removeManager: address(0), + receiver: address(0) + }); vm.startPrank(_troveOwner); - uint256 troveId = borrowerOperations.openTroveAndJoinInterestBatchManager(params); + uint256 troveId = borrowerOperations + .openTroveAndJoinInterestBatchManager(params); vm.stopPrank(); return troveId; } - function setBatchInterestRate(address _batchAddress, uint256 _newAnnualInterestRate) internal { + function setBatchInterestRate( + address _batchAddress, + uint256 _newAnnualInterestRate + ) internal { vm.startPrank(_batchAddress); - borrowerOperations.setBatchManagerAnnualInterestRate(uint128(_newAnnualInterestRate), 0, 0, type(uint256).max); + borrowerOperations.setBatchManagerAnnualInterestRate( + uint128(_newAnnualInterestRate), + 0, + 0, + type(uint256).max + ); vm.stopPrank(); } @@ -521,20 +699,53 @@ contract BaseTest is TestAccounts, Logging, TroveId { setInterestBatchManager(_troveOwner, _troveId, _newBatchManager); } - function setInterestBatchManager(address _troveOwner, uint256 _troveId, address _newBatchManager) internal { + function setInterestBatchManager( + address _troveOwner, + uint256 _troveId, + address _newBatchManager + ) internal { vm.startPrank(_troveOwner); - borrowerOperations.setInterestBatchManager(_troveId, _newBatchManager, 0, 0, type(uint256).max); + borrowerOperations.setInterestBatchManager( + _troveId, + _newBatchManager, + 0, + 0, + type(uint256).max + ); vm.stopPrank(); } - function removeFromBatch(address _troveOwner, uint256 _troveId, uint256 _newAnnualInterestRate) internal { + function removeFromBatch( + address _troveOwner, + uint256 _troveId, + uint256 _newAnnualInterestRate + ) internal { vm.startPrank(_troveOwner); - borrowerOperations.removeFromBatch(_troveId, _newAnnualInterestRate, 0, 0, type(uint256).max); + borrowerOperations.removeFromBatch( + _troveId, + _newAnnualInterestRate, + 0, + 0, + type(uint256).max + ); vm.stopPrank(); } - function switchBatchManager(address _troveOwner, uint256 _troveId, address _newBatchManager) internal { - switchBatchManager(_troveOwner, _troveId, 0, 0, _newBatchManager, 0, 0, type(uint256).max); + function switchBatchManager( + address _troveOwner, + uint256 _troveId, + address _newBatchManager + ) internal { + switchBatchManager( + _troveOwner, + _troveId, + 0, + 0, + _newBatchManager, + 0, + 0, + type(uint256).max + ); } function switchBatchManager( @@ -549,7 +760,13 @@ contract BaseTest is TestAccounts, Logging, TroveId { ) internal { vm.startPrank(_troveOwner); borrowerOperations.switchBatchManager( - _troveId, _removeUpperHint, _removeLowerHint, _newBatchManager, _addUpperHint, _addLowerHint, _maxUpfrontFee + _troveId, + _removeUpperHint, + _removeLowerHint, + _newBatchManager, + _addUpperHint, + _addLowerHint, + _maxUpfrontFee ); vm.stopPrank(); } @@ -570,15 +787,26 @@ contract BaseTest is TestAccounts, Logging, TroveId { return x > y ? x - y : y - x; } - function assertApproximatelyEqual(uint256 _x, uint256 _y, uint256 _margin) public pure { + function assertApproximatelyEqual( + uint256 _x, + uint256 _y, + uint256 _margin + ) public pure { assertApproxEqAbs(_x, _y, _margin, ""); } - function assertApproximatelyEqual(uint256 _x, uint256 _y, uint256 _margin, string memory _reason) public pure { + function assertApproximatelyEqual( + uint256 _x, + uint256 _y, + uint256 _margin, + string memory _reason + ) public pure { assertApproxEqAbs(_x, _y, _margin, _reason); } - function uintToArray(uint256 _value) public pure returns (uint256[] memory result) { + function uintToArray( + uint256 _value + ) public pure returns (uint256[] memory result) { result = new uint256[](1); result[0] = _value; } diff --git a/contracts/test/TestContracts/BorrowerOperationsTester.t.sol b/contracts/test/TestContracts/BorrowerOperationsTester.t.sol index ab24961d2..2886879fe 100644 --- a/contracts/test/TestContracts/BorrowerOperationsTester.t.sol +++ b/contracts/test/TestContracts/BorrowerOperationsTester.t.sol @@ -3,13 +3,14 @@ pragma solidity 0.8.24; import "src/Interfaces/IAddressesRegistry.sol"; +import "src/Interfaces/ISystemParams.sol"; import "src/BorrowerOperations.sol"; import "./Interfaces/IBorrowerOperationsTester.sol"; /* Tester contract inherits from BorrowerOperations, and provides external functions for testing the parent's internal functions. */ contract BorrowerOperationsTester is BorrowerOperations, IBorrowerOperationsTester { - constructor(IAddressesRegistry _addressesRegistry) BorrowerOperations(_addressesRegistry) {} + constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) BorrowerOperations(_addressesRegistry, _systemParams) {} function get_CCR() external view returns (uint256) { return CCR; diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index ebbf28b4f..8310c3457 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -60,6 +60,7 @@ contract TestDeployer is MetadataDeployment { IInterestRouter interestRouter; IERC20Metadata collToken; LiquityContractsDevPools pools; + ISystemParams systemParams; } struct LiquityContracts { @@ -76,6 +77,7 @@ contract TestDeployer is MetadataDeployment { GasPool gasPool; IInterestRouter interestRouter; IERC20Metadata collToken; + ISystemParams systemParams; } struct LiquityContractAddresses { @@ -119,6 +121,7 @@ contract TestDeployer is MetadataDeployment { IBoldToken boldToken; HintHelpers hintHelpers; MultiTroveGetter multiTroveGetter; + ISystemParams systemParams; } struct DeploymentVarsMainnet { @@ -322,15 +325,7 @@ contract TestDeployer is MetadataDeployment { internal returns (IAddressesRegistry, address) { - IAddressesRegistry addressesRegistry = new AddressesRegistry( - address(this), - _troveManagerParams.CCR, - _troveManagerParams.MCR, - _troveManagerParams.BCR, - _troveManagerParams.SCR, - _troveManagerParams.LIQUIDATION_PENALTY_SP, - _troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION - ); + IAddressesRegistry addressesRegistry = new AddressesRegistry(address(this)); address troveManagerAddress = getAddress( address(this), getBytecode(type(TroveManagerTester).creationCode, address(addressesRegistry)), SALT ); @@ -338,6 +333,11 @@ contract TestDeployer is MetadataDeployment { return (addressesRegistry, troveManagerAddress); } + // TODO(@bayological): Implement + // function deploySystemParamsDev() public returns (ISystemParams) { + // return new SystemParams(); + // } + function _deployAndConnectCollateralContractsDev( IERC20Metadata _collToken, IBoldToken _boldToken, @@ -417,11 +417,11 @@ contract TestDeployer is MetadataDeployment { }); contracts.addressesRegistry.setAddresses(addressVars); - contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry); - contracts.troveManager = new TroveManagerTester{salt: SALT}(contracts.addressesRegistry); + contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); + contracts.troveManager = new TroveManagerTester{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); contracts.stabilityPool = new StabilityPool{salt: stabilityPoolSalt}(false); - contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry); + contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); contracts.pools.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); contracts.pools.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); contracts.pools.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); @@ -468,6 +468,10 @@ contract TestDeployer is MetadataDeployment { vars.oracleParams.stEthUsdStalenessThreshold = _24_HOURS; vars.oracleParams.rEthEthStalenessThreshold = _48_HOURS; + // Deploy System Params + // TODO(@bayological): Implement + result.systemParams = _deploySystemParamsMainnet(); + // Colls: WETH, WSTETH, RETH vars.numCollaterals = 3; result.contractsArray = new LiquityContracts[](vars.numCollaterals); @@ -526,6 +530,8 @@ contract TestDeployer is MetadataDeployment { result.boldToken.setCollateralRegistry(address(result.collateralRegistry)); } + // TODO(@bayological): Update below to remove params that are now in SystemParams from addressesRegistry constructor + function _deployAddressesRegistryMainnet(TroveManagerParams memory _troveManagerParams) internal returns (IAddressesRegistry, address) @@ -545,6 +551,8 @@ contract TestDeployer is MetadataDeployment { return (addressesRegistry, troveManagerAddress); } + // TODO(@bayological): The below function needs to be updated to deploy SystemParams + function _deployAndConnectCollateralContractsMainnet( DeploymentParamsMainnet memory _params, ExternalAddresses memory _externalAddresses, @@ -620,11 +628,11 @@ contract TestDeployer is MetadataDeployment { }); contracts.addressesRegistry.setAddresses(addressVars); - contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry); - contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry); + contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); + contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); contracts.stabilityPool = new StabilityPool{salt: stabilityPoolSalt}(false); - contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry); + contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); contracts.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); diff --git a/contracts/test/TestContracts/DevTestSetup.sol b/contracts/test/TestContracts/DevTestSetup.sol index 83cc2bd9c..72b260260 100644 --- a/contracts/test/TestContracts/DevTestSetup.sol +++ b/contracts/test/TestContracts/DevTestSetup.sol @@ -64,6 +64,7 @@ contract DevTestSetup is BaseTest { troveNFT = contracts.troveNFT; metadataNFT = addressesRegistry.metadataNFT(); mockInterestRouter = contracts.interestRouter; + systemParams = contracts.systemParams; // Give some Coll to test accounts, and approve it to BorrowerOperations uint256 initialCollAmount = 10_000_000_000e18; @@ -77,6 +78,25 @@ contract DevTestSetup is BaseTest { BCR = troveManager.get_BCR(); LIQUIDATION_PENALTY_SP = troveManager.get_LIQUIDATION_PENALTY_SP(); LIQUIDATION_PENALTY_REDISTRIBUTION = troveManager.get_LIQUIDATION_PENALTY_REDISTRIBUTION(); + + MIN_DEBT = systemParams.MIN_DEBT(); + SP_YIELD_SPLIT = systemParams.SP_YIELD_SPLIT(); + MIN_ANNUAL_INTEREST_RATE = systemParams.MIN_ANNUAL_INTEREST_RATE(); + INTEREST_RATE_ADJ_COOLDOWN = systemParams.INTEREST_RATE_ADJ_COOLDOWN(); + ETH_GAS_COMPENSATION = systemParams.ETH_GAS_COMPENSATION(); + COLL_GAS_COMPENSATION_DIVISOR = systemParams.COLL_GAS_COMPENSATION_DIVISOR(); + MAX_ANNUAL_INTEREST_RATE = systemParams.MAX_ANNUAL_INTEREST_RATE(); + MIN_BOLD_IN_SP = systemParams.MIN_BOLD_IN_SP(); + REDEMPTION_FEE_FLOOR = systemParams.REDEMPTION_FEE_FLOOR(); + INITIAL_BASE_RATE = systemParams.INITIAL_BASE_RATE(); + MAX_BATCH_SHARES_RATIO = systemParams.MAX_BATCH_SHARES_RATIO(); + MAX_ANNUAL_BATCH_MANAGEMENT_FEE = systemParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE(); + REDEMPTION_MINUTE_DECAY_FACTOR = systemParams.REDEMPTION_MINUTE_DECAY_FACTOR(); + URGENT_REDEMPTION_BONUS = systemParams.URGENT_REDEMPTION_BONUS(); + + // TODO(@bayological): These may need initializing. They come from borrower ops but can be fetched from sys params + // UPFRONT_INTEREST_PERIOD; + // MIN_INTEREST_RATE_CHANGE_PERIOD; } function _setupForWithdrawCollGainToTrove() internal returns (uint256, uint256, uint256) { diff --git a/contracts/test/TestContracts/InvariantsTestHandler.t.sol b/contracts/test/TestContracts/InvariantsTestHandler.t.sol index 36e86535b..ba068f9cc 100644 --- a/contracts/test/TestContracts/InvariantsTestHandler.t.sol +++ b/contracts/test/TestContracts/InvariantsTestHandler.t.sol @@ -24,32 +24,9 @@ import {Assertions} from "./Assertions.sol"; import {BaseHandler} from "./BaseHandler.sol"; import {BaseMultiCollateralTest} from "./BaseMultiCollateralTest.sol"; import {TestDeployer} from "./Deployment.t.sol"; +import {ISystemParams} from "src/Interfaces/ISystemParams.sol"; -import { - _100pct, - _1pct, - COLL_GAS_COMPENSATION_CAP, - COLL_GAS_COMPENSATION_DIVISOR, - DECIMAL_PRECISION, - ETH_GAS_COMPENSATION, - INITIAL_BASE_RATE, - INTEREST_RATE_ADJ_COOLDOWN, - MAX_ANNUAL_BATCH_MANAGEMENT_FEE, - MAX_ANNUAL_INTEREST_RATE, - MIN_ANNUAL_INTEREST_RATE, - MIN_ANNUAL_INTEREST_RATE, - MIN_BOLD_IN_SP, - MIN_DEBT, - MIN_INTEREST_RATE_CHANGE_PERIOD, - ONE_MINUTE, - ONE_YEAR, - REDEMPTION_BETA, - REDEMPTION_FEE_FLOOR, - REDEMPTION_MINUTE_DECAY_FACTOR, - SP_YIELD_SPLIT, - UPFRONT_INTEREST_PERIOD, - URGENT_REDEMPTION_BONUS -} from "src/Dependencies/Constants.sol"; +import { _100pct, _1pct, DECIMAL_PRECISION, ONE_MINUTE, ONE_YEAR } from "src/Dependencies/Constants.sol"; uint256 constant TIME_DELTA_MIN = 0; uint256 constant TIME_DELTA_MAX = ONE_YEAR; @@ -57,21 +34,12 @@ uint256 constant TIME_DELTA_MAX = ONE_YEAR; uint256 constant BORROWED_MIN = 0 ether; // Sometimes try borrowing too little uint256 constant BORROWED_MAX = 100_000 ether; -uint256 constant INTEREST_RATE_MIN = MIN_ANNUAL_INTEREST_RATE - 1; // Sometimes try rates lower than the min -uint256 constant INTEREST_RATE_MAX = MAX_ANNUAL_INTEREST_RATE + 1; // Sometimes try rates exceeding the max - uint256 constant ICR_MIN = 1.1 ether - 1; uint256 constant ICR_MAX = 3 ether; uint256 constant TCR_MIN = 0.9 ether; uint256 constant TCR_MAX = 3 ether; -uint256 constant BATCH_MANAGEMENT_FEE_MIN = 0; -uint256 constant BATCH_MANAGEMENT_FEE_MAX = MAX_ANNUAL_BATCH_MANAGEMENT_FEE + 1; // Sometimes try too high - -uint256 constant RATE_CHANGE_PERIOD_MIN = MIN_INTEREST_RATE_CHANGE_PERIOD - 1; // Sometimes try too low -uint256 constant RATE_CHANGE_PERIOD_MAX = TIME_DELTA_MAX; - enum AdjustedTroveProperties { onlyColl, onlyDebt, @@ -361,7 +329,7 @@ contract InvariantsTestHandler is Assertions, BaseHandler, BaseMultiCollateralTe uint256 _handlerBold; // Used to keep track of base rate - uint256 _baseRate = INITIAL_BASE_RATE; + uint256 _baseRate; uint256 _timeSinceLastRedemption = 0; // Used to keep track of mintable interest @@ -389,6 +357,29 @@ contract InvariantsTestHandler is Assertions, BaseHandler, BaseMultiCollateralTe // Urgent redemption transient state UrgentRedemptionTransientState _urgentRedemption; + // System params-based variables + uint256 immutable INTEREST_RATE_MIN; + uint256 immutable INTEREST_RATE_MAX; + uint256 immutable BATCH_MANAGEMENT_FEE_MIN; + uint256 immutable BATCH_MANAGEMENT_FEE_MAX; + uint256 immutable RATE_CHANGE_PERIOD_MIN; + uint256 immutable RATE_CHANGE_PERIOD_MAX = TIME_DELTA_MAX; + uint256 immutable ETH_GAS_COMPENSATION; + uint256 immutable MIN_ANNUAL_INTEREST_RATE; + uint256 immutable MIN_DEBT; + uint256 immutable INTEREST_RATE_ADJ_COOLDOWN; + uint256 immutable MIN_BOLD_IN_SP; + uint256 immutable MAX_ANNUAL_INTEREST_RATE; + uint128 immutable MAX_ANNUAL_BATCH_MANAGEMENT_FEE; + uint128 immutable MIN_INTEREST_RATE_CHANGE_PERIOD; + uint256 immutable REDEMPTION_MINUTE_DECAY_FACTOR; + uint256 immutable REDEMPTION_BETA; + uint256 immutable REDEMPTION_FEE_FLOOR; + uint256 immutable SP_YIELD_SPLIT; + uint256 immutable COLL_GAS_COMPENSATION_DIVISOR; + uint256 immutable COLL_GAS_COMPENSATION_CAP; + uint256 immutable URGENT_REDEMPTION_BONUS; + constructor(Contracts memory contracts, bool assumeNoExpectedFailures) { _functionCaller = new FunctionCaller(); _assumeNoExpectedFailures = assumeNoExpectedFailures; @@ -404,6 +395,29 @@ contract InvariantsTestHandler is Assertions, BaseHandler, BaseMultiCollateralTe LIQ_PENALTY_REDIST[i] = c.troveManager.get_LIQUIDATION_PENALTY_REDISTRIBUTION(); _price[i] = c.priceFeed.getPrice(); } + + // Set system params-based variables + INTEREST_RATE_MIN = systemParams.MIN_ANNUAL_INTEREST_RATE() - 1; // Sometimes try rates lower than the min + INTEREST_RATE_MAX = systemParams.MAX_ANNUAL_INTEREST_RATE() + 1; // Sometimes try rates exceeding the max + BATCH_MANAGEMENT_FEE_MIN = 0; + BATCH_MANAGEMENT_FEE_MAX = systemParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE() + 1; // Sometimes try too high + RATE_CHANGE_PERIOD_MIN = systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD() - 1; // Sometimes try too low + _baseRate = systemParams.INITIAL_BASE_RATE(); + ETH_GAS_COMPENSATION = systemParams.ETH_GAS_COMPENSATION(); + MIN_ANNUAL_INTEREST_RATE = systemParams.MIN_ANNUAL_INTEREST_RATE(); + MIN_DEBT = systemParams.MIN_DEBT(); + INTEREST_RATE_ADJ_COOLDOWN = systemParams.INTEREST_RATE_ADJ_COOLDOWN(); + MIN_BOLD_IN_SP = systemParams.MIN_BOLD_IN_SP(); + MAX_ANNUAL_INTEREST_RATE = systemParams.MAX_ANNUAL_INTEREST_RATE(); + MAX_ANNUAL_BATCH_MANAGEMENT_FEE = systemParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE(); + MIN_INTEREST_RATE_CHANGE_PERIOD = systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD(); + REDEMPTION_MINUTE_DECAY_FACTOR = systemParams.REDEMPTION_MINUTE_DECAY_FACTOR(); + REDEMPTION_BETA = systemParams.REDEMPTION_BETA(); + REDEMPTION_FEE_FLOOR = systemParams.REDEMPTION_FEE_FLOOR(); + SP_YIELD_SPLIT = systemParams.SP_YIELD_SPLIT(); + COLL_GAS_COMPENSATION_DIVISOR = systemParams.COLL_GAS_COMPENSATION_DIVISOR(); + COLL_GAS_COMPENSATION_CAP = systemParams.COLL_GAS_COMPENSATION_CAP(); + URGENT_REDEMPTION_BONUS = systemParams.URGENT_REDEMPTION_BONUS(); } ////////////////////////////////////////////// @@ -3006,11 +3020,11 @@ contract InvariantsTestHandler is Assertions, BaseHandler, BaseMultiCollateralTe return (selector, "AddRemoveManagers.NotOwnerNorRemoveManager()"); } - if (selector == AddressesRegistry.InvalidMCR.selector) { + if (selector == ISystemParams.InvalidMCR.selector) { return (selector, "BorrowerOperations.InvalidMCR()"); } - if (selector == AddressesRegistry.InvalidSCR.selector) { + if (selector == ISystemParams.InvalidSCR.selector) { return (selector, "BorrowerOperations.InvalidSCR()"); } diff --git a/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol b/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol index f8d821ec0..70a06228f 100644 --- a/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol +++ b/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol @@ -1,219 +1,219 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; -import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; -import {IStabilityPool} from "src/Interfaces/IStabilityPool.sol"; -import {ITroveManager} from "src/Interfaces/ITroveManager.sol"; -import {ICollSurplusPool} from "src/Interfaces/ICollSurplusPool.sol"; -import {HintHelpers} from "src/HintHelpers.sol"; -import {IPriceFeedTestnet} from "./Interfaces/IPriceFeedTestnet.sol"; -import {ITroveManagerTester} from "./Interfaces/ITroveManagerTester.sol"; -import {LiquityMath} from "src/Dependencies/LiquityMath.sol"; -import {mulDivCeil} from "../Utils/Math.sol"; -import {StringFormatting} from "../Utils/StringFormatting.sol"; -import {TroveId} from "../Utils/TroveId.sol"; -import {BaseHandler} from "./BaseHandler.sol"; - -import { - DECIMAL_PRECISION, - _1pct, - _100pct, - ETH_GAS_COMPENSATION, - COLL_GAS_COMPENSATION_DIVISOR, - MIN_ANNUAL_INTEREST_RATE, - MIN_BOLD_IN_SP -} from "src/Dependencies/Constants.sol"; - -using {mulDivCeil} for uint256; - -// Test parameters -uint256 constant OPEN_TROVE_BORROWED_MIN = 2_000 ether; -uint256 constant OPEN_TROVE_BORROWED_MAX = 100e18 ether; -uint256 constant OPEN_TROVE_ICR = 1.5 ether; // CCR -uint256 constant LIQUIDATION_ICR = MCR - _1pct; - -// Universal constants -uint256 constant MCR = 1.1 ether; - -contract SPInvariantsTestHandler is BaseHandler, TroveId { - using StringFormatting for uint256; - - struct Contracts { - IBoldToken boldToken; - IBorrowerOperations borrowerOperations; - IERC20 collateralToken; - IPriceFeedTestnet priceFeed; - IStabilityPool stabilityPool; - ITroveManagerTester troveManager; - ICollSurplusPool collSurplusPool; - } - - IBoldToken immutable boldToken; - IBorrowerOperations immutable borrowerOperations; - IERC20 collateralToken; - IPriceFeedTestnet immutable priceFeed; - IStabilityPool immutable stabilityPool; - ITroveManagerTester immutable troveManager; - ICollSurplusPool immutable collSurplusPool; - HintHelpers immutable hintHelpers; - - uint256 immutable initialPrice; - mapping(address owner => uint256) troveIndexOf; - - // Ghost variables - uint256 myBold = 0; - uint256 spBold = 0; - uint256 spColl = 0; - - // Fixtures - uint256[] fixtureDeposited; - - constructor(Contracts memory contracts, HintHelpers hintHelpers_) { - boldToken = contracts.boldToken; - borrowerOperations = contracts.borrowerOperations; - collateralToken = contracts.collateralToken; - priceFeed = contracts.priceFeed; - stabilityPool = contracts.stabilityPool; - troveManager = contracts.troveManager; - collSurplusPool = contracts.collSurplusPool; - hintHelpers = hintHelpers_; - - initialPrice = priceFeed.getPrice(); - } - - function openTrove(uint256 borrowed) external returns (uint256 debt) { - uint256 i = troveIndexOf[msg.sender]; - vm.assume(troveManager.getTroveStatus(addressToTroveId(msg.sender, i)) != ITroveManager.Status.active); - - borrowed = _bound(borrowed, OPEN_TROVE_BORROWED_MIN, OPEN_TROVE_BORROWED_MAX); - uint256 price = priceFeed.getPrice(); - debt = borrowed + hintHelpers.predictOpenTroveUpfrontFee(0, borrowed, MIN_ANNUAL_INTEREST_RATE); - uint256 coll = debt.mulDivCeil(OPEN_TROVE_ICR, price); - assertEqDecimal(coll * price / debt, OPEN_TROVE_ICR, 18, "Wrong ICR"); - - info("coll = ", coll.decimal(), ", debt = ", debt.decimal()); - logCall("openTrove", borrowed.decimal()); - - deal(address(collateralToken), msg.sender, coll + ETH_GAS_COMPENSATION); - vm.prank(msg.sender); - collateralToken.approve(address(borrowerOperations), coll + ETH_GAS_COMPENSATION); - vm.prank(msg.sender); - uint256 troveId = borrowerOperations.openTrove( - msg.sender, - i, - coll, - borrowed, - 0, - 0, - MIN_ANNUAL_INTEREST_RATE, - type(uint256).max, - address(0), - address(0), - address(0) - ); - (uint256 actualDebt,,,,) = troveManager.getEntireDebtAndColl(troveId); - assertEqDecimal(debt, actualDebt, 18, "Wrong debt"); - - // Sweep funds - vm.prank(msg.sender); - boldToken.transfer(address(this), borrowed); - assertEqDecimal(boldToken.balanceOf(msg.sender), 0, 18, "Incomplete BOLD sweep"); - myBold += borrowed; - - // Use these interesting values as SP deposit amounts later - fixtureDeposited.push(debt); - fixtureDeposited.push(debt + debt / DECIMAL_PRECISION + 1); // See https://github.com/liquity/dev/security/advisories/GHSA-m9f3-hrx8-x2g3 - } - - function provideToSp(uint256 deposited, bool useFixture) external { - vm.assume(myBold > 0); - - uint256 collBefore = collateralToken.balanceOf(msg.sender); - uint256 collGain = stabilityPool.getDepositorCollGain(msg.sender); - uint256 boldGain = stabilityPool.getDepositorYieldGainWithPending(msg.sender); - - // Poor man's fixturing, because Foundry's fixtures don't seem to work under invariant testing - if (useFixture && fixtureDeposited.length > 0) { - info("pulling `deposited` from fixture"); - deposited = fixtureDeposited[_bound(deposited, 0, fixtureDeposited.length - 1)]; - } - - deposited = _bound(deposited, 1, myBold); - - logCall("provideToSp", deposited.decimal(), "false"); - - boldToken.transfer(msg.sender, deposited); - vm.prank(msg.sender); - // Provide to SP and claim Coll and BOLD gains - stabilityPool.provideToSP(deposited, true); - - info("totalBoldDeposits = ", stabilityPool.getTotalBoldDeposits().decimal()); - _log(); - - uint256 collAfter = collateralToken.balanceOf(msg.sender); - assertEqDecimal(collAfter, collBefore + collGain, 18, "Wrong Coll gain"); - - // Sweep BOLD gain - vm.prank(msg.sender); - boldToken.transfer(address(this), boldGain); - assertEqDecimal(boldToken.balanceOf(msg.sender), 0, 18, "Incomplete BOLD sweep"); - myBold += boldGain; - - myBold -= deposited; - spBold += deposited; - spColl -= collGain; - - assertEqDecimal(spBold, stabilityPool.getTotalBoldDeposits(), 18, "Wrong SP BOLD balance"); - assertEqDecimal(spColl, stabilityPool.getCollBalance(), 18, "Wrong SP Coll balance"); - } - - function liquidateMe() external { - vm.assume(troveManager.getTroveIdsCount() > 1); - uint256 troveId = addressToTroveId(msg.sender, troveIndexOf[msg.sender]); - vm.assume(troveManager.getTroveStatus(troveId) == ITroveManager.Status.active); - - (uint256 debt, uint256 coll,,,) = troveManager.getEntireDebtAndColl(troveId); - vm.assume(debt <= (spBold > MIN_BOLD_IN_SP ? spBold - MIN_BOLD_IN_SP : 0)); // only interested in SP offset, no redistribution - - logCall("liquidateMe"); - - priceFeed.setPrice(initialPrice * LIQUIDATION_ICR / OPEN_TROVE_ICR); - - uint256 collBefore = collateralToken.balanceOf(address(this)); - uint256 accountSurplusBefore = collSurplusPool.getCollateral(msg.sender); - uint256 totalBoldDeposits = stabilityPool.getTotalBoldDeposits(); - uint256 boldInSPForOffsets = totalBoldDeposits - LiquityMath._min(MIN_BOLD_IN_SP, totalBoldDeposits); - uint256 collCompensation = troveManager.getCollGasCompensation(coll, debt, boldInSPForOffsets); - // Calc claimable coll based on the remaining coll to liquidate, less the liq. penalty that goes to the SP depositors - uint256 seizedColl = debt * (_100pct + troveManager.get_LIQUIDATION_PENALTY_SP()) / priceFeed.getPrice(); - // The Trove owner bears the gas compensation costs - uint256 claimableColl = coll - seizedColl - collCompensation; - - troveManager.liquidate(troveId); - - priceFeed.setPrice(initialPrice); - - info("totalBoldDeposits = ", stabilityPool.getTotalBoldDeposits().decimal()); - info("P = ", stabilityPool.P().decimal()); - _log(); - - uint256 collAfter = collateralToken.balanceOf(address(this)); - uint256 accountSurplusAfter = collSurplusPool.getCollateral(msg.sender); - // Check liquidator got the compensation - // This is first branch, so coll token is WETH (used for ETH liquidation reserve) - assertEqDecimal(collAfter, collBefore + collCompensation + ETH_GAS_COMPENSATION, 18, "Wrong Coll compensation"); - // Check claimable coll surplus is correct - uint256 accountSurplusDelta = accountSurplusAfter - accountSurplusBefore; - assertEqDecimal(accountSurplusDelta, claimableColl, 18, "Wrong account surplus"); - - ++troveIndexOf[msg.sender]; - - spBold -= debt; - spColl += coll - claimableColl - collCompensation; - - assertEqDecimal(spBold, stabilityPool.getTotalBoldDeposits(), 18, "Wrong SP BOLD balance"); - assertEqDecimal(spColl, stabilityPool.getCollBalance(), 18, "Wrong SP Coll balance"); - } -} +// // SPDX-License-Identifier: MIT +// pragma solidity 0.8.24; + +// import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +// import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; +// import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; +// import {IStabilityPool} from "src/Interfaces/IStabilityPool.sol"; +// import {ITroveManager} from "src/Interfaces/ITroveManager.sol"; +// import {ICollSurplusPool} from "src/Interfaces/ICollSurplusPool.sol"; +// import {HintHelpers} from "src/HintHelpers.sol"; +// import {IPriceFeedTestnet} from "./Interfaces/IPriceFeedTestnet.sol"; +// import {ITroveManagerTester} from "./Interfaces/ITroveManagerTester.sol"; +// import {LiquityMath} from "src/Dependencies/LiquityMath.sol"; +// import {mulDivCeil} from "../Utils/Math.sol"; +// import {StringFormatting} from "../Utils/StringFormatting.sol"; +// import {TroveId} from "../Utils/TroveId.sol"; +// import {BaseHandler} from "./BaseHandler.sol"; + +// import { +// DECIMAL_PRECISION, +// _1pct, +// _100pct, +// ETH_GAS_COMPENSATION, +// COLL_GAS_COMPENSATION_DIVISOR, +// MIN_ANNUAL_INTEREST_RATE, +// MIN_BOLD_IN_SP +// } from "src/Dependencies/Constants.sol"; + +// using {mulDivCeil} for uint256; + +// // Test parameters +// uint256 constant OPEN_TROVE_BORROWED_MIN = 2_000 ether; +// uint256 constant OPEN_TROVE_BORROWED_MAX = 100e18 ether; +// uint256 constant OPEN_TROVE_ICR = 1.5 ether; // CCR +// uint256 constant LIQUIDATION_ICR = MCR - _1pct; + +// // Universal constants +// uint256 constant MCR = 1.1 ether; + +// contract SPInvariantsTestHandler is BaseHandler, TroveId { +// using StringFormatting for uint256; + +// struct Contracts { +// IBoldToken boldToken; +// IBorrowerOperations borrowerOperations; +// IERC20 collateralToken; +// IPriceFeedTestnet priceFeed; +// IStabilityPool stabilityPool; +// ITroveManagerTester troveManager; +// ICollSurplusPool collSurplusPool; +// } + +// IBoldToken immutable boldToken; +// IBorrowerOperations immutable borrowerOperations; +// IERC20 collateralToken; +// IPriceFeedTestnet immutable priceFeed; +// IStabilityPool immutable stabilityPool; +// ITroveManagerTester immutable troveManager; +// ICollSurplusPool immutable collSurplusPool; +// HintHelpers immutable hintHelpers; + +// uint256 immutable initialPrice; +// mapping(address owner => uint256) troveIndexOf; + +// // Ghost variables +// uint256 myBold = 0; +// uint256 spBold = 0; +// uint256 spColl = 0; + +// // Fixtures +// uint256[] fixtureDeposited; + +// constructor(Contracts memory contracts, HintHelpers hintHelpers_) { +// boldToken = contracts.boldToken; +// borrowerOperations = contracts.borrowerOperations; +// collateralToken = contracts.collateralToken; +// priceFeed = contracts.priceFeed; +// stabilityPool = contracts.stabilityPool; +// troveManager = contracts.troveManager; +// collSurplusPool = contracts.collSurplusPool; +// hintHelpers = hintHelpers_; + +// initialPrice = priceFeed.getPrice(); +// } + +// function openTrove(uint256 borrowed) external returns (uint256 debt) { +// uint256 i = troveIndexOf[msg.sender]; +// vm.assume(troveManager.getTroveStatus(addressToTroveId(msg.sender, i)) != ITroveManager.Status.active); + +// borrowed = _bound(borrowed, OPEN_TROVE_BORROWED_MIN, OPEN_TROVE_BORROWED_MAX); +// uint256 price = priceFeed.getPrice(); +// debt = borrowed + hintHelpers.predictOpenTroveUpfrontFee(0, borrowed, MIN_ANNUAL_INTEREST_RATE); +// uint256 coll = debt.mulDivCeil(OPEN_TROVE_ICR, price); +// assertEqDecimal(coll * price / debt, OPEN_TROVE_ICR, 18, "Wrong ICR"); + +// info("coll = ", coll.decimal(), ", debt = ", debt.decimal()); +// logCall("openTrove", borrowed.decimal()); + +// deal(address(collateralToken), msg.sender, coll + ETH_GAS_COMPENSATION); +// vm.prank(msg.sender); +// collateralToken.approve(address(borrowerOperations), coll + ETH_GAS_COMPENSATION); +// vm.prank(msg.sender); +// uint256 troveId = borrowerOperations.openTrove( +// msg.sender, +// i, +// coll, +// borrowed, +// 0, +// 0, +// MIN_ANNUAL_INTEREST_RATE, +// type(uint256).max, +// address(0), +// address(0), +// address(0) +// ); +// (uint256 actualDebt,,,,) = troveManager.getEntireDebtAndColl(troveId); +// assertEqDecimal(debt, actualDebt, 18, "Wrong debt"); + +// // Sweep funds +// vm.prank(msg.sender); +// boldToken.transfer(address(this), borrowed); +// assertEqDecimal(boldToken.balanceOf(msg.sender), 0, 18, "Incomplete BOLD sweep"); +// myBold += borrowed; + +// // Use these interesting values as SP deposit amounts later +// fixtureDeposited.push(debt); +// fixtureDeposited.push(debt + debt / DECIMAL_PRECISION + 1); // See https://github.com/liquity/dev/security/advisories/GHSA-m9f3-hrx8-x2g3 +// } + +// function provideToSp(uint256 deposited, bool useFixture) external { +// vm.assume(myBold > 0); + +// uint256 collBefore = collateralToken.balanceOf(msg.sender); +// uint256 collGain = stabilityPool.getDepositorCollGain(msg.sender); +// uint256 boldGain = stabilityPool.getDepositorYieldGainWithPending(msg.sender); + +// // Poor man's fixturing, because Foundry's fixtures don't seem to work under invariant testing +// if (useFixture && fixtureDeposited.length > 0) { +// info("pulling `deposited` from fixture"); +// deposited = fixtureDeposited[_bound(deposited, 0, fixtureDeposited.length - 1)]; +// } + +// deposited = _bound(deposited, 1, myBold); + +// logCall("provideToSp", deposited.decimal(), "false"); + +// boldToken.transfer(msg.sender, deposited); +// vm.prank(msg.sender); +// // Provide to SP and claim Coll and BOLD gains +// stabilityPool.provideToSP(deposited, true); + +// info("totalBoldDeposits = ", stabilityPool.getTotalBoldDeposits().decimal()); +// _log(); + +// uint256 collAfter = collateralToken.balanceOf(msg.sender); +// assertEqDecimal(collAfter, collBefore + collGain, 18, "Wrong Coll gain"); + +// // Sweep BOLD gain +// vm.prank(msg.sender); +// boldToken.transfer(address(this), boldGain); +// assertEqDecimal(boldToken.balanceOf(msg.sender), 0, 18, "Incomplete BOLD sweep"); +// myBold += boldGain; + +// myBold -= deposited; +// spBold += deposited; +// spColl -= collGain; + +// assertEqDecimal(spBold, stabilityPool.getTotalBoldDeposits(), 18, "Wrong SP BOLD balance"); +// assertEqDecimal(spColl, stabilityPool.getCollBalance(), 18, "Wrong SP Coll balance"); +// } + +// function liquidateMe() external { +// vm.assume(troveManager.getTroveIdsCount() > 1); +// uint256 troveId = addressToTroveId(msg.sender, troveIndexOf[msg.sender]); +// vm.assume(troveManager.getTroveStatus(troveId) == ITroveManager.Status.active); + +// (uint256 debt, uint256 coll,,,) = troveManager.getEntireDebtAndColl(troveId); +// vm.assume(debt <= (spBold > MIN_BOLD_IN_SP ? spBold - MIN_BOLD_IN_SP : 0)); // only interested in SP offset, no redistribution + +// logCall("liquidateMe"); + +// priceFeed.setPrice(initialPrice * LIQUIDATION_ICR / OPEN_TROVE_ICR); + +// uint256 collBefore = collateralToken.balanceOf(address(this)); +// uint256 accountSurplusBefore = collSurplusPool.getCollateral(msg.sender); +// uint256 totalBoldDeposits = stabilityPool.getTotalBoldDeposits(); +// uint256 boldInSPForOffsets = totalBoldDeposits - LiquityMath._min(MIN_BOLD_IN_SP, totalBoldDeposits); +// uint256 collCompensation = troveManager.getCollGasCompensation(coll, debt, boldInSPForOffsets); +// // Calc claimable coll based on the remaining coll to liquidate, less the liq. penalty that goes to the SP depositors +// uint256 seizedColl = debt * (_100pct + troveManager.get_LIQUIDATION_PENALTY_SP()) / priceFeed.getPrice(); +// // The Trove owner bears the gas compensation costs +// uint256 claimableColl = coll - seizedColl - collCompensation; + +// troveManager.liquidate(troveId); + +// priceFeed.setPrice(initialPrice); + +// info("totalBoldDeposits = ", stabilityPool.getTotalBoldDeposits().decimal()); +// info("P = ", stabilityPool.P().decimal()); +// _log(); + +// uint256 collAfter = collateralToken.balanceOf(address(this)); +// uint256 accountSurplusAfter = collSurplusPool.getCollateral(msg.sender); +// // Check liquidator got the compensation +// // This is first branch, so coll token is WETH (used for ETH liquidation reserve) +// assertEqDecimal(collAfter, collBefore + collCompensation + ETH_GAS_COMPENSATION, 18, "Wrong Coll compensation"); +// // Check claimable coll surplus is correct +// uint256 accountSurplusDelta = accountSurplusAfter - accountSurplusBefore; +// assertEqDecimal(accountSurplusDelta, claimableColl, 18, "Wrong account surplus"); + +// ++troveIndexOf[msg.sender]; + +// spBold -= debt; +// spColl += coll - claimableColl - collCompensation; + +// assertEqDecimal(spBold, stabilityPool.getTotalBoldDeposits(), 18, "Wrong SP BOLD balance"); +// assertEqDecimal(spColl, stabilityPool.getCollBalance(), 18, "Wrong SP Coll balance"); +// } +// } diff --git a/contracts/test/TestContracts/TroveManagerTester.t.sol b/contracts/test/TestContracts/TroveManagerTester.t.sol index 87242aff6..d1a85f375 100644 --- a/contracts/test/TestContracts/TroveManagerTester.t.sol +++ b/contracts/test/TestContracts/TroveManagerTester.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.24; import "src/Interfaces/IAddressesRegistry.sol"; import "src/Interfaces/ICollateralRegistry.sol"; +import "src/Interfaces/ISystemParams.sol"; import "src/TroveManager.sol"; import "./Interfaces/ITroveManagerTester.sol"; @@ -16,8 +17,12 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { // Extra buffer of collateral ratio to join a batch or adjust a trove inside a batch (on top of MCR) uint256 public immutable BCR; - constructor(IAddressesRegistry _addressesRegistry) TroveManager(_addressesRegistry) { - BCR = _addressesRegistry.BCR(); + uint256 public immutable UPFRONT_INTEREST_PERIOD; + + + constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) TroveManager(_addressesRegistry, _systemParams) { + BCR = _systemParams.BCR(); + UPFRONT_INTEREST_PERIOD = _systemParams.UPFRONT_INTEREST_PERIOD(); } // Single liquidation function. Closes the trove if its ICR is lower than the minimum collateral ratio. @@ -119,10 +124,14 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { return _getCollGasCompensation(_coll); } - function getETHGasCompensation() external pure returns (uint256) { + function getETHGasCompensation() external view returns (uint256) { return ETH_GAS_COMPENSATION; } + function get_MIN_DEBT() external view returns (uint256) { + return MIN_DEBT; + } + /* function unprotectedDecayBaseRateFromBorrowing() external returns (uint256) { baseRate = _calcDecayedBaseRate(); @@ -158,7 +167,7 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { return _calcUpfrontFee(openTrove.debtIncrease, avgInterestRate); } - function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal pure returns (uint256) { + function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal view returns (uint256) { return _calcInterest(_debt * _avgInterestRate, UPFRONT_INTEREST_PERIOD); } diff --git a/contracts/test/multicollateral.t.sol b/contracts/test/multicollateral.t.sol index 85eccad72..95e888724 100644 --- a/contracts/test/multicollateral.t.sol +++ b/contracts/test/multicollateral.t.sol @@ -7,28 +7,7 @@ import "./TestContracts/DevTestSetup.sol"; contract MulticollateralTest is DevTestSetup { uint256 NUM_COLLATERALS = 4; - TestDeployer.LiquityContractsDev[] public contractsArray; - - // Test constants - // Collateral branch parameters (SETH = staked ETH, i.e. wstETH / rETH) - uint256 constant CCR_WETH = 150 * _1pct; - uint256 constant CCR_SETH = 160 * _1pct; - - uint256 constant MCR_WETH = 110 * _1pct; - uint256 constant MCR_SETH = 120 * _1pct; - - uint256 constant SCR_WETH = 110 * _1pct; - uint256 constant SCR_SETH = 120 * _1pct; - - // Batch CR buffer (same for all branches for now) - // On top of MCR to join a batch, or adjust inside a batch - uint256 constant BCR_ALL = 10 * _1pct; - - uint256 constant LIQUIDATION_PENALTY_SP_WETH = 5 * _1pct; - uint256 constant LIQUIDATION_PENALTY_SP_SETH = 5 * _1pct; - - uint256 constant LIQUIDATION_PENALTY_REDISTRIBUTION_WETH = 10 * _1pct; - uint256 constant LIQUIDATION_PENALTY_REDISTRIBUTION_SETH = 20 * _1pct; + TestDeployer.LiquityContractsDev[] public contractsArray; function openMulticollateralTroveNoHints100pctWithIndex( uint256 _collIndex, @@ -765,6 +744,29 @@ contract MulticollateralTest is DevTestSetup { contract CsBold013 is TestAccounts { uint256 constant INITIAL_PRICE = 2_000 ether; + // TODO: Determine appropriate values for test(WETH, SETH) or remove test + // Collateral branch parameters (SETH = staked ETH, i.e. wstETH / rETH) + uint256 constant CCR_WETH = 150 * _1pct; + uint256 constant CCR_SETH = 160 * _1pct; + + uint256 constant MCR_WETH = 110 * _1pct; + uint256 constant MCR_SETH = 120 * _1pct; + + uint256 constant SCR_WETH = 110 * _1pct; + uint256 constant SCR_SETH = 120 * _1pct; + + // Batch CR buffer (same for all branches for now) + // On top of MCR to join a batch, or adjust inside a batch + uint256 constant BCR_ALL = 10 * _1pct; + + uint256 constant LIQUIDATION_PENALTY_SP_WETH = 5 * _1pct; + uint256 constant LIQUIDATION_PENALTY_SP_SETH = 5 * _1pct; + + uint256 constant LIQUIDATION_PENALTY_REDISTRIBUTION_WETH = 10 * _1pct; + uint256 constant LIQUIDATION_PENALTY_REDISTRIBUTION_SETH = 20 * _1pct; + + uint256 constant MIN_ANNUAL_INTEREST_RATE = _1pct / 2; + IBoldToken boldToken; ICollateralRegistry collateralRegistry; IHintHelpers hintHelpers; From 51b449d426aa34d83c9e74e050f7db5385a6da53 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:11:53 -0500 Subject: [PATCH 24/79] refactor: update deployment script --- contracts/src/Interfaces/IStabilityPool.sol | 3 +- .../CollateralRegistryTester.sol | 5 +- contracts/test/TestContracts/Deployment.t.sol | 104 ++++++++++-------- 3 files changed, 62 insertions(+), 50 deletions(-) diff --git a/contracts/src/Interfaces/IStabilityPool.sol b/contracts/src/Interfaces/IStabilityPool.sol index 8e18f6451..f695a59f7 100644 --- a/contracts/src/Interfaces/IStabilityPool.sol +++ b/contracts/src/Interfaces/IStabilityPool.sol @@ -8,6 +8,7 @@ import "./IBoldToken.sol"; import "./ITroveManager.sol"; import "./IBoldRewardsReceiver.sol"; import "./IAddressesRegistry.sol"; +import "./ISystemParams.sol"; /* * The Stability Pool holds Bold tokens deposited by Stability Pool depositors. @@ -30,7 +31,7 @@ import "./IAddressesRegistry.sol"; * */ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { - function initialize(IAddressesRegistry _addressesRegistry) external; + function initialize(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) external; function boldToken() external view returns (IBoldToken); function troveManager() external view returns (ITroveManager); diff --git a/contracts/test/TestContracts/CollateralRegistryTester.sol b/contracts/test/TestContracts/CollateralRegistryTester.sol index e6254944b..3f4a92918 100644 --- a/contracts/test/TestContracts/CollateralRegistryTester.sol +++ b/contracts/test/TestContracts/CollateralRegistryTester.sol @@ -3,13 +3,14 @@ pragma solidity 0.8.24; import "src/CollateralRegistry.sol"; +import "src/Interfaces/ISystemParams.sol"; /* Tester contract inherits from CollateralRegistry, and provides external functions for testing the parent's internal functions. */ contract CollateralRegistryTester is CollateralRegistry { - constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers) - CollateralRegistry(_boldToken, _tokens, _troveManagers) + constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers, ISystemParams _systemParams) + CollateralRegistry(_boldToken, _tokens, _troveManagers, _systemParams) {} function unprotectedDecayBaseRateFromBorrowing() external returns (uint256) { diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index 8310c3457..064ccf830 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -13,6 +13,7 @@ import "src/HintHelpers.sol"; import "src/MultiTroveGetter.sol"; import "src/SortedTroves.sol"; import "src/StabilityPool.sol"; +import {SystemParams, ISystemParams} from "src/SystemParams.sol"; import "./BorrowerOperationsTester.t.sol"; import "./TroveManagerTester.t.sol"; import "./CollateralRegistryTester.sol"; @@ -168,6 +169,10 @@ contract TestDeployer is MetadataDeployment { return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry)); } + function getBytecode(bytes memory _creationCode, address _addressesRegistry, address _systemParams) public pure returns (bytes memory) { + return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry, _systemParams)); + } + function getBytecode(bytes memory _creationCode, bool _disable) public pure returns (bytes memory) { return abi.encodePacked(_creationCode, abi.encode(_disable)); } @@ -258,6 +263,10 @@ contract TestDeployer is MetadataDeployment { { DeploymentVarsDev memory vars; vars.numCollaterals = troveManagerParamsArray.length; + + // Deploy SystemParams + ISystemParams systemParams = deploySystemParamsDev(); + // Deploy Bold vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(address(this))); vars.boldTokenAddress = getAddress(address(this), vars.bytecode, SALT); @@ -272,7 +281,7 @@ contract TestDeployer is MetadataDeployment { // Deploy the first branch with WETH collateral vars.collaterals[0] = _WETH; (IAddressesRegistry addressesRegistry, address troveManagerAddress) = - _deployAddressesRegistryDev(troveManagerParamsArray[0]); + _deployAddressesRegistryDev(systemParams, troveManagerParamsArray[0]); vars.addressesRegistries[0] = addressesRegistry; vars.troveManagers[0] = ITroveManager(troveManagerAddress); for (vars.i = 1; vars.i < vars.numCollaterals; vars.i++) { @@ -284,13 +293,13 @@ contract TestDeployer is MetadataDeployment { ); vars.collaterals[vars.i] = collToken; // Addresses registry and TM address - (addressesRegistry, troveManagerAddress) = _deployAddressesRegistryDev(troveManagerParamsArray[vars.i]); + (addressesRegistry, troveManagerAddress) = _deployAddressesRegistryDev(systemParams, troveManagerParamsArray[vars.i]); vars.addressesRegistries[vars.i] = addressesRegistry; vars.troveManagers[vars.i] = ITroveManager(troveManagerAddress); } - collateralRegistry = new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers); - hintHelpers = new HintHelpers(collateralRegistry); + collateralRegistry = new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers, systemParams); + hintHelpers = new HintHelpers(collateralRegistry, systemParams); multiTroveGetter = new MultiTroveGetter(collateralRegistry); contractsArray[0] = _deployAndConnectCollateralContractsDev( @@ -301,7 +310,8 @@ contract TestDeployer is MetadataDeployment { vars.addressesRegistries[0], address(vars.troveManagers[0]), hintHelpers, - multiTroveGetter + multiTroveGetter, + systemParams ); // Deploy the remaining branches with LST collateral @@ -314,29 +324,31 @@ contract TestDeployer is MetadataDeployment { vars.addressesRegistries[vars.i], address(vars.troveManagers[vars.i]), hintHelpers, - multiTroveGetter + multiTroveGetter, + systemParams ); } boldToken.setCollateralRegistry(address(collateralRegistry)); } - function _deployAddressesRegistryDev(TroveManagerParams memory _troveManagerParams) + function _deployAddressesRegistryDev(ISystemParams _systemParams, TroveManagerParams memory _troveManagerParams) internal returns (IAddressesRegistry, address) { IAddressesRegistry addressesRegistry = new AddressesRegistry(address(this)); address troveManagerAddress = getAddress( - address(this), getBytecode(type(TroveManagerTester).creationCode, address(addressesRegistry)), SALT + address(this), getBytecode(type(TroveManagerTester).creationCode, address(addressesRegistry), address(_systemParams)), SALT ); return (addressesRegistry, troveManagerAddress); } - // TODO(@bayological): Implement - // function deploySystemParamsDev() public returns (ISystemParams) { - // return new SystemParams(); - // } + function deploySystemParamsDev() public returns (ISystemParams) { + SystemParams systemParams = new SystemParams{salt: SALT}(false); + systemParams.initialize(address(this)); + return ISystemParams(systemParams); + } function _deployAndConnectCollateralContractsDev( IERC20Metadata _collToken, @@ -346,10 +358,12 @@ contract TestDeployer is MetadataDeployment { IAddressesRegistry _addressesRegistry, address _troveManagerAddress, IHintHelpers _hintHelpers, - IMultiTroveGetter _multiTroveGetter + IMultiTroveGetter _multiTroveGetter, + ISystemParams _systemParams ) internal returns (LiquityContractsDev memory contracts) { LiquityContractAddresses memory addresses; contracts.collToken = _collToken; + contracts.systemParams = _systemParams; // Deploy all contracts, using testers for TM and PriceFeed contracts.addressesRegistry = _addressesRegistry; @@ -366,7 +380,7 @@ contract TestDeployer is MetadataDeployment { // Pre-calc addresses addresses.borrowerOperations = getAddress( address(this), - getBytecode(type(BorrowerOperationsTester).creationCode, address(contracts.addressesRegistry)), + getBytecode(type(BorrowerOperationsTester).creationCode, address(contracts.addressesRegistry), address(_systemParams)), SALT ); addresses.troveManager = _troveManagerAddress; @@ -377,7 +391,7 @@ contract TestDeployer is MetadataDeployment { addresses.stabilityPool = getAddress(address(this), getBytecode(type(StabilityPool).creationCode, bool(false)), stabilityPoolSalt); addresses.activePool = getAddress( - address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry)), SALT + address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry), address(_systemParams)), SALT ); addresses.defaultPool = getAddress( address(this), getBytecode(type(DefaultPool).creationCode, address(contracts.addressesRegistry)), SALT @@ -417,11 +431,11 @@ contract TestDeployer is MetadataDeployment { }); contracts.addressesRegistry.setAddresses(addressVars); - contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); - contracts.troveManager = new TroveManagerTester{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); + contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry, _systemParams); + contracts.troveManager = new TroveManagerTester{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); contracts.stabilityPool = new StabilityPool{salt: stabilityPoolSalt}(false); - contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); + contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.pools.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); contracts.pools.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); contracts.pools.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); @@ -437,7 +451,7 @@ contract TestDeployer is MetadataDeployment { assert(address(contracts.pools.collSurplusPool) == addresses.collSurplusPool); assert(address(contracts.sortedTroves) == addresses.sortedTroves); - contracts.stabilityPool.initialize(contracts.addressesRegistry); + contracts.stabilityPool.initialize(contracts.addressesRegistry, _systemParams); // Connect contracts _boldToken.setBranchAddresses( @@ -490,25 +504,25 @@ contract TestDeployer is MetadataDeployment { IWETH WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); vars.collaterals[0] = WETH; (vars.addressesRegistries[0], troveManagerAddress) = - _deployAddressesRegistryMainnet(_troveManagerParamsArray[0]); + _deployAddressesRegistryMainnet(result.systemParams, _troveManagerParamsArray[0]); vars.troveManagers[0] = ITroveManager(troveManagerAddress); // RETH vars.collaterals[1] = IERC20Metadata(0xae78736Cd615f374D3085123A210448E74Fc6393); (vars.addressesRegistries[1], troveManagerAddress) = - _deployAddressesRegistryMainnet(_troveManagerParamsArray[1]); + _deployAddressesRegistryMainnet(result.systemParams, _troveManagerParamsArray[1]); vars.troveManagers[1] = ITroveManager(troveManagerAddress); // WSTETH vars.collaterals[2] = IERC20Metadata(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); (vars.addressesRegistries[2], troveManagerAddress) = - _deployAddressesRegistryMainnet(_troveManagerParamsArray[2]); + _deployAddressesRegistryMainnet(result.systemParams, _troveManagerParamsArray[2]); vars.troveManagers[2] = ITroveManager(troveManagerAddress); // Deploy registry and register the TMs - result.collateralRegistry = new CollateralRegistryTester(result.boldToken, vars.collaterals, vars.troveManagers); + result.collateralRegistry = new CollateralRegistryTester(result.boldToken, vars.collaterals, vars.troveManagers, result.systemParams); - result.hintHelpers = new HintHelpers(result.collateralRegistry); + result.hintHelpers = new HintHelpers(result.collateralRegistry, result.systemParams); result.multiTroveGetter = new MultiTroveGetter(result.collateralRegistry); // Deploy each set of core contracts @@ -524,42 +538,32 @@ contract TestDeployer is MetadataDeployment { params.hintHelpers = result.hintHelpers; params.multiTroveGetter = result.multiTroveGetter; result.contractsArray[vars.i] = - _deployAndConnectCollateralContractsMainnet(params, result.externalAddresses, vars.oracleParams); + _deployAndConnectCollateralContractsMainnet(params, result.externalAddresses, vars.oracleParams, result.systemParams); } result.boldToken.setCollateralRegistry(address(result.collateralRegistry)); } - // TODO(@bayological): Update below to remove params that are now in SystemParams from addressesRegistry constructor - - function _deployAddressesRegistryMainnet(TroveManagerParams memory _troveManagerParams) + function _deployAddressesRegistryMainnet(ISystemParams _systemParams, TroveManagerParams memory _troveManagerParams) internal returns (IAddressesRegistry, address) { - IAddressesRegistry addressesRegistry = new AddressesRegistry( - address(this), - _troveManagerParams.CCR, - _troveManagerParams.MCR, - _troveManagerParams.BCR, - _troveManagerParams.SCR, - _troveManagerParams.LIQUIDATION_PENALTY_SP, - _troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION - ); + IAddressesRegistry addressesRegistry = new AddressesRegistry(address(this)); address troveManagerAddress = - getAddress(address(this), getBytecode(type(TroveManager).creationCode, address(addressesRegistry)), SALT); + getAddress(address(this), getBytecode(type(TroveManager).creationCode, address(addressesRegistry), address(_systemParams)), SALT); return (addressesRegistry, troveManagerAddress); } - // TODO(@bayological): The below function needs to be updated to deploy SystemParams - function _deployAndConnectCollateralContractsMainnet( DeploymentParamsMainnet memory _params, ExternalAddresses memory _externalAddresses, - OracleParams memory _oracleParams + OracleParams memory _oracleParams, + ISystemParams _systemParams ) internal returns (LiquityContracts memory contracts) { LiquityContractAddresses memory addresses; contracts.collToken = _params.collToken; + contracts.systemParams = _systemParams; contracts.interestRouter = new MockInterestRouter(); contracts.addressesRegistry = _params.addressesRegistry; @@ -575,7 +579,7 @@ contract TestDeployer is MetadataDeployment { bytes32 stabilityPoolSalt = keccak256(abi.encodePacked(address(contracts.addressesRegistry))); addresses.borrowerOperations = getAddress( address(this), - getBytecode(type(BorrowerOperationsTester).creationCode, address(contracts.addressesRegistry)), + getBytecode(type(BorrowerOperationsTester).creationCode, address(contracts.addressesRegistry), address(_systemParams)), SALT ); addresses.troveManager = _params.troveManagerAddress; @@ -585,7 +589,7 @@ contract TestDeployer is MetadataDeployment { addresses.stabilityPool = getAddress(address(this), getBytecode(type(StabilityPool).creationCode, false), stabilityPoolSalt); addresses.activePool = getAddress( - address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry)), SALT + address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry), address(_systemParams)), SALT ); addresses.defaultPool = getAddress( address(this), getBytecode(type(DefaultPool).creationCode, address(contracts.addressesRegistry)), SALT @@ -628,11 +632,11 @@ contract TestDeployer is MetadataDeployment { }); contracts.addressesRegistry.setAddresses(addressVars); - contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); - contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); + contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry, _systemParams); + contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); contracts.stabilityPool = new StabilityPool{salt: stabilityPoolSalt}(false); - contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); + contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); contracts.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); @@ -648,7 +652,7 @@ contract TestDeployer is MetadataDeployment { assert(address(contracts.collSurplusPool) == addresses.collSurplusPool); assert(address(contracts.sortedTroves) == addresses.sortedTroves); - contracts.stabilityPool.initialize(contracts.addressesRegistry); + contracts.stabilityPool.initialize(contracts.addressesRegistry, _systemParams); // Connect contracts _params.boldToken.setBranchAddresses( @@ -694,4 +698,10 @@ contract TestDeployer is MetadataDeployment { _borrowerOperationsAddress ); } + + function _deploySystemParamsMainnet() internal returns (ISystemParams) { + SystemParams systemParams = new SystemParams{salt: SALT}(false); + systemParams.initialize(address(this)); + return ISystemParams(systemParams); + } } From 0868e7217c064121bdc24784132b9a8dd6ae0ba5 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:13:31 -0500 Subject: [PATCH 25/79] refactor: remove unneeded params --- contracts/test/SortedTroves.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/SortedTroves.t.sol b/contracts/test/SortedTroves.t.sol index d5bd58caa..a961c7c17 100644 --- a/contracts/test/SortedTroves.t.sol +++ b/contracts/test/SortedTroves.t.sol @@ -470,7 +470,7 @@ contract SortedTrovesTest is Test { function setUp() public { bytes32 SALT = keccak256("LiquityV2"); AddressesRegistry addressesRegistry = - new AddressesRegistry(address(this), 150e16, 110e16, 10e16, 110e16, 5e16, 10e16); + new AddressesRegistry(address(this)); bytes32 hash = keccak256( abi.encodePacked( bytes1(0xff), From 959bba10e6247f0955bb070ceee0d9f0b4108c9b Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 17 Sep 2025 13:45:29 -0500 Subject: [PATCH 26/79] refactor: update function visibility --- contracts/src/BorrowerOperations.sol | 6 +++--- contracts/src/CollateralRegistry.sol | 4 ++-- contracts/src/HintHelpers.sol | 2 +- contracts/src/StabilityPool.sol | 2 +- contracts/src/TroveManager.sol | 6 +++--- contracts/test/TestContracts/DevTestSetup.sol | 2 +- .../test/TestContracts/Interfaces/ITroveManagerTester.sol | 4 ++-- contracts/test/TestContracts/InvariantsTestHandler.t.sol | 4 ++-- contracts/test/TestContracts/TroveManagerTester.t.sol | 4 ++-- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 33c8540c8..2092b932e 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -1211,7 +1211,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio return _troveEntireDebt; } - function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal pure returns (uint256) { + function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal view returns (uint256) { return _calcInterest(_debt * _avgInterestRate, UPFRONT_INTEREST_PERIOD); } @@ -1491,7 +1491,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - function _requireAtLeastMinDebt(uint256 _debt) internal pure { + function _requireAtLeastMinDebt(uint256 _debt) internal view { if (_debt < MIN_DEBT) { revert DebtBelowMin(); } @@ -1512,7 +1512,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - function _requireValidAnnualInterestRate(uint256 _annualInterestRate) internal pure { + function _requireValidAnnualInterestRate(uint256 _annualInterestRate) internal view { if (_annualInterestRate < MIN_ANNUAL_INTEREST_RATE) { revert InterestRateTooLow(); } diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index 38ac7ae3c..53b88a450 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -242,7 +242,7 @@ contract CollateralRegistry is ICollateralRegistry { return baseRate * decayFactor / DECIMAL_PRECISION; } - function _calcRedemptionRate(uint256 _baseRate) internal pure returns (uint256) { + function _calcRedemptionRate(uint256 _baseRate) internal view returns (uint256) { return LiquityMath._min( REDEMPTION_FEE_FLOOR + _baseRate, DECIMAL_PRECISION // cap at a maximum of 100% @@ -312,7 +312,7 @@ contract CollateralRegistry is ICollateralRegistry { // require functions - function _requireValidMaxFeePercentage(uint256 _maxFeePercentage) internal pure { + function _requireValidMaxFeePercentage(uint256 _maxFeePercentage) internal view { require( _maxFeePercentage >= REDEMPTION_FEE_FLOOR && _maxFeePercentage <= DECIMAL_PRECISION, "Max fee percentage must be between 0.5% and 100%" diff --git a/contracts/src/HintHelpers.sol b/contracts/src/HintHelpers.sol index bf3c34e27..302549a0f 100644 --- a/contracts/src/HintHelpers.sol +++ b/contracts/src/HintHelpers.sol @@ -76,7 +76,7 @@ contract HintHelpers is IHintHelpers { } } - function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal pure returns (uint256) { + function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal view returns (uint256) { return _debt * _avgInterestRate * UPFRONT_INTEREST_PERIOD / ONE_YEAR / DECIMAL_PRECISION; } diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 0bc772a18..6ea19b52a 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -180,7 +180,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi // Each time the scale of P shifts by SCALE_FACTOR, the scale is incremented by 1 uint256 public currentScale; - uint256 public immutable MIN_BOLD_IN_SP; + uint256 public MIN_BOLD_IN_SP; address public liquidityStrategy; diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 4d3b0f29b..b2505d0b0 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -348,7 +348,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } // Return the amount of Coll to be drawn from a trove's collateral and sent as gas compensation. - function _getCollGasCompensation(uint256 _coll) internal pure returns (uint256) { + function _getCollGasCompensation(uint256 _coll) internal view returns (uint256) { // _entireDebt should never be zero, but we add the condition defensively to avoid an unexpected revert return LiquityMath._min(_coll / COLL_GAS_COMPENSATION_DIVISOR, COLL_GAS_COMPENSATION_CAP); } @@ -534,7 +534,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { LiquidationValues memory _singleLiquidation, LiquidationValues memory totals, TroveChange memory troveChange - ) internal pure { + ) internal view { // Tally all the values with their respective running totals totals.collGasCompensation += _singleLiquidation.collGasCompensation; totals.ETHGasCompensation += ETH_GAS_COMPENSATION; @@ -1920,7 +1920,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 _currentBatchDebtShares, uint256 _batchDebt, bool _checkBatchSharesRatio - ) internal pure { + ) internal view { // debt / shares should be below MAX_BATCH_SHARES_RATIO if (_currentBatchDebtShares * MAX_BATCH_SHARES_RATIO < _batchDebt && _checkBatchSharesRatio) { revert BatchSharesRatioTooHigh(); diff --git a/contracts/test/TestContracts/DevTestSetup.sol b/contracts/test/TestContracts/DevTestSetup.sol index 72b260260..a74446f95 100644 --- a/contracts/test/TestContracts/DevTestSetup.sol +++ b/contracts/test/TestContracts/DevTestSetup.sol @@ -357,7 +357,7 @@ contract DevTestSetup is BaseTest { assertEq(uint8(troveManager.getTroveStatus(_troveIDs.B)), uint8(ITroveManager.Status.active)); } - function _getSPYield(uint256 _aggInterest) internal pure returns (uint256) { + function _getSPYield(uint256 _aggInterest) internal view returns (uint256) { uint256 spYield = SP_YIELD_SPLIT * _aggInterest / 1e18; assertGt(spYield, 0); assertLe(spYield, _aggInterest); diff --git a/contracts/test/TestContracts/Interfaces/ITroveManagerTester.sol b/contracts/test/TestContracts/Interfaces/ITroveManagerTester.sol index 31d5e1bb7..fbdb4a162 100644 --- a/contracts/test/TestContracts/Interfaces/ITroveManagerTester.sol +++ b/contracts/test/TestContracts/Interfaces/ITroveManagerTester.sol @@ -31,10 +31,10 @@ interface ITroveManagerTester is ITroveManager { function checkBelowCriticalThreshold(uint256 _price) external view returns (bool); function computeICR(uint256 _coll, uint256 _debt, uint256 _price) external pure returns (uint256); - function getCollGasCompensation(uint256 _coll) external pure returns (uint256); + function getCollGasCompensation(uint256 _coll) external view returns (uint256); function getCollGasCompensation(uint256 _coll, uint256 _debt, uint256 _boldInSPForOffsets) external - pure + view returns (uint256); function getEffectiveRedemptionFeeInColl(uint256 _redeemAmount, uint256 _price) external view returns (uint256); diff --git a/contracts/test/TestContracts/InvariantsTestHandler.t.sol b/contracts/test/TestContracts/InvariantsTestHandler.t.sol index ba068f9cc..00bc582b2 100644 --- a/contracts/test/TestContracts/InvariantsTestHandler.t.sol +++ b/contracts/test/TestContracts/InvariantsTestHandler.t.sol @@ -2413,11 +2413,11 @@ contract InvariantsTestHandler is Assertions, BaseHandler, BaseMultiCollateralTe return _baseRate * decaySinceLastRedemption / DECIMAL_PRECISION; } - function _getBaseRateIncrease(uint256 boldSupply, uint256 redeemed) internal pure returns (uint256) { + function _getBaseRateIncrease(uint256 boldSupply, uint256 redeemed) internal view returns (uint256) { return boldSupply > 0 ? redeemed * DECIMAL_PRECISION / boldSupply / REDEMPTION_BETA : 0; } - function _getRedemptionRate(uint256 baseRate) internal pure returns (uint256) { + function _getRedemptionRate(uint256 baseRate) internal view returns (uint256) { return Math.min(REDEMPTION_FEE_FLOOR + baseRate, _100pct); } diff --git a/contracts/test/TestContracts/TroveManagerTester.t.sol b/contracts/test/TestContracts/TroveManagerTester.t.sol index d1a85f375..626f194c2 100644 --- a/contracts/test/TestContracts/TroveManagerTester.t.sol +++ b/contracts/test/TestContracts/TroveManagerTester.t.sol @@ -110,7 +110,7 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { function getCollGasCompensation(uint256 _entireColl, uint256 _entireDebt, uint256 _boldInSPForOffsets) external - pure + view returns (uint256) { uint256 collSubjectToGasCompensation = _entireColl; @@ -120,7 +120,7 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { return _getCollGasCompensation(collSubjectToGasCompensation); } - function getCollGasCompensation(uint256 _coll) external pure returns (uint256) { + function getCollGasCompensation(uint256 _coll) external view returns (uint256) { return _getCollGasCompensation(_coll); } From 935305e91f711a79a941b19d9e07acd299ac3cb7 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 17 Sep 2025 13:45:42 -0500 Subject: [PATCH 27/79] refactor: remove unused param --- contracts/test/TestContracts/Deployment.t.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index 064ccf830..9643f5cd8 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -278,10 +278,13 @@ contract TestDeployer is MetadataDeployment { vars.addressesRegistries = new IAddressesRegistry[](vars.numCollaterals); vars.troveManagers = new ITroveManager[](vars.numCollaterals); + // TODO: Before sys params we deployed address registry per branch + // We should do the same for sys params + // Deploy the first branch with WETH collateral vars.collaterals[0] = _WETH; (IAddressesRegistry addressesRegistry, address troveManagerAddress) = - _deployAddressesRegistryDev(systemParams, troveManagerParamsArray[0]); + _deployAddressesRegistryDev(systemParams); vars.addressesRegistries[0] = addressesRegistry; vars.troveManagers[0] = ITroveManager(troveManagerAddress); for (vars.i = 1; vars.i < vars.numCollaterals; vars.i++) { @@ -293,7 +296,7 @@ contract TestDeployer is MetadataDeployment { ); vars.collaterals[vars.i] = collToken; // Addresses registry and TM address - (addressesRegistry, troveManagerAddress) = _deployAddressesRegistryDev(systemParams, troveManagerParamsArray[vars.i]); + (addressesRegistry, troveManagerAddress) = _deployAddressesRegistryDev(systemParams); vars.addressesRegistries[vars.i] = addressesRegistry; vars.troveManagers[vars.i] = ITroveManager(troveManagerAddress); } @@ -332,7 +335,7 @@ contract TestDeployer is MetadataDeployment { boldToken.setCollateralRegistry(address(collateralRegistry)); } - function _deployAddressesRegistryDev(ISystemParams _systemParams, TroveManagerParams memory _troveManagerParams) + function _deployAddressesRegistryDev(ISystemParams _systemParams) internal returns (IAddressesRegistry, address) { From 80c83435005aba445b00d2d39b0f6d471446cafe Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 23 Sep 2025 13:11:29 -0500 Subject: [PATCH 28/79] refactor: update deployment script --- contracts/script/DeployLiquity2.s.sol | 901 +++++++++++++------------- contracts/src/SystemParams.sol | 4 +- 2 files changed, 448 insertions(+), 457 deletions(-) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 525f333af..8b42221c2 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -1,457 +1,448 @@ // TODO(@bayological): Fix compilation errors with core contracts & replace address registry param usage with sys params -// // SPDX-License-Identifier: UNLICENSED -// pragma solidity 0.8.24; - -// import {StdCheats} from "forge-std/StdCheats.sol"; -// import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -// import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; -// import {IERC20 as IERC20_GOV} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -// import {ProxyAdmin} from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; -// import {TransparentUpgradeableProxy} from -// "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -// import {IFPMMFactory} from "src/Interfaces/IFPMMFactory.sol"; - -// import {ETH_GAS_COMPENSATION} from "src/Dependencies/Constants.sol"; -// import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; -// import {StringFormatting} from "test/Utils/StringFormatting.sol"; -// import {Accounts} from "test/TestContracts/Accounts.sol"; -// import {ERC20Faucet} from "test/TestContracts/ERC20Faucet.sol"; -// import {WETHTester} from "test/TestContracts/WETHTester.sol"; -// import "src/AddressesRegistry.sol"; -// import "src/ActivePool.sol"; -// import "src/BoldToken.sol"; -// import "src/BorrowerOperations.sol"; -// import "src/TroveManager.sol"; -// import "src/TroveNFT.sol"; -// import "src/CollSurplusPool.sol"; -// import "src/DefaultPool.sol"; -// import "src/GasPool.sol"; -// import "src/HintHelpers.sol"; -// import "src/MultiTroveGetter.sol"; -// import "src/SortedTroves.sol"; -// import "src/StabilityPool.sol"; - -// import "src/CollateralRegistry.sol"; -// import "src/tokens/StableTokenV3.sol"; -// import "src/Interfaces/IStableTokenV3.sol"; -// import "test/TestContracts/PriceFeedTestnet.sol"; -// import "test/TestContracts/MetadataDeployment.sol"; -// import "test/Utils/Logging.sol"; -// import "test/Utils/StringEquality.sol"; -// import "forge-std/console2.sol"; - -// contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { -// using Strings for *; -// using StringFormatting for *; -// using StringEquality for string; - -// bytes32 SALT; -// address deployer; - -// struct LiquityContracts { -// IAddressesRegistry addressesRegistry; -// IActivePool activePool; -// IBorrowerOperations borrowerOperations; -// ICollSurplusPool collSurplusPool; -// IDefaultPool defaultPool; -// ISortedTroves sortedTroves; -// IStabilityPool stabilityPool; -// ITroveManager troveManager; -// ITroveNFT troveNFT; -// MetadataNFT metadataNFT; -// IPriceFeed priceFeed; -// GasPool gasPool; -// IInterestRouter interestRouter; -// IERC20Metadata collToken; -// } - -// struct LiquityContractAddresses { -// address activePool; -// address borrowerOperations; -// address collSurplusPool; -// address defaultPool; -// address sortedTroves; -// address stabilityPool; -// address troveManager; -// address troveNFT; -// address metadataNFT; -// address priceFeed; -// address gasPool; -// address interestRouter; -// } - -// struct TroveManagerParams { -// uint256 CCR; -// uint256 MCR; -// uint256 SCR; -// uint256 BCR; -// uint256 LIQUIDATION_PENALTY_SP; -// uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; -// } - -// struct DeploymentVars { -// uint256 numCollaterals; -// IERC20Metadata[] collaterals; -// IAddressesRegistry[] addressesRegistries; -// ITroveManager[] troveManagers; -// LiquityContracts contracts; -// bytes bytecode; -// address boldTokenAddress; -// uint256 i; -// } - -// struct DemoTroveParams { -// uint256 collIndex; -// uint256 owner; -// uint256 ownerIndex; -// uint256 coll; -// uint256 debt; -// uint256 annualInterestRate; -// } - -// struct DeploymentResult { -// LiquityContracts contracts; -// ICollateralRegistry collateralRegistry; -// HintHelpers hintHelpers; -// MultiTroveGetter multiTroveGetter; -// ProxyAdmin proxyAdmin; -// IStableTokenV3 stableToken; -// address stabilityPoolImpl; -// address stableTokenV3Impl; -// address fpmm; -// } - -// struct DeploymentConfig { -// address USDm_ALFAJORES_ADDRESS; -// address proxyAdmin; -// address fpmmFactory; -// address fpmmImplementation; -// address referenceRateFeedID; -// string stableTokenName; -// string stableTokenSymbol; -// // Parameters for the TroveManager -// uint256 CCR; -// uint256 MCR; -// uint256 SCR; -// uint256 BCR; -// uint256 LIQUIDATION_PENALTY_SP; -// uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; -// } - -// DeploymentConfig internal CONFIG = DeploymentConfig({ -// USDm_ALFAJORES_ADDRESS: 0x9E2d4412d0f434cC85500b79447d9323a7416f09, -// proxyAdmin: 0xe4DdacCAdb64114215FCe8251B57B2AEB5C2C0E2, -// fpmmFactory: 0xd8098494a749a3fDAD2D2e7Fa5272D8f274D8FF6, -// fpmmImplementation: 0x0292efcB331C6603eaa29D570d12eB336D6c01d6, -// referenceRateFeedID: 0x206B25Ea01E188Ee243131aFdE526bA6E131a016, -// stableTokenName: "EUR.m Test", -// stableTokenSymbol: "EUR.m", -// // TODO: reconsider these values -// CCR: 150e16, -// MCR: 110e16, -// SCR: 110e16, -// BCR: 40e16, -// LIQUIDATION_PENALTY_SP: 5e16, -// LIQUIDATION_PENALTY_REDISTRIBUTION: 10e16 -// }); - -// function run() external { -// string memory saltStr = vm.envOr("SALT", block.timestamp.toString()); -// SALT = keccak256(bytes(saltStr)); - -// uint256 privateKey = vm.envUint("DEPLOYER"); -// deployer = vm.addr(privateKey); -// vm.startBroadcast(privateKey); - -// _log("Deployer: ", deployer.toHexString()); -// _log("Deployer balance: ", deployer.balance.decimal()); -// _log("CREATE2 salt: ", 'keccak256(bytes("', saltStr, '")) = ', uint256(SALT).toHexString()); -// _log("Chain ID: ", block.chainid.toString()); - -// DeploymentResult memory deployed = _deployAndConnectContracts(); - -// vm.stopBroadcast(); - -// vm.writeFile("script/deployment-manifest.json", _getManifestJson(deployed)); -// } - -// // See: https://solidity-by-example.org/app/create2/ -// function getBytecode(bytes memory _creationCode, address _addressesRegistry) public pure returns (bytes memory) { -// return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry)); -// } - -// function _deployAndConnectContracts() internal returns (DeploymentResult memory r) { -// _deployProxyInfrastructure(r); -// _deployStableToken(r); -// _deployFPMM(r); - -// TroveManagerParams memory troveManagerParams = TroveManagerParams({ -// CCR: CONFIG.CCR, -// MCR: CONFIG.MCR, -// SCR: CONFIG.SCR, -// BCR: CONFIG.BCR, -// LIQUIDATION_PENALTY_SP: CONFIG.LIQUIDATION_PENALTY_SP, -// LIQUIDATION_PENALTY_REDISTRIBUTION: CONFIG.LIQUIDATION_PENALTY_REDISTRIBUTION -// }); - -// //TODO(@bayological): Initialize system params and remove address reg here - -// IAddressesRegistry addressesRegistry = new AddressesRegistry( -// deployer, -// troveManagerParams.CCR, -// troveManagerParams.MCR, -// troveManagerParams.BCR, -// troveManagerParams.SCR, -// troveManagerParams.LIQUIDATION_PENALTY_SP, -// troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION -// ); - -// address troveManagerAddress = vm.computeCreate2Address( -// SALT, keccak256(getBytecode(type(TroveManager).creationCode, address(addressesRegistry))) -// ); - -// IERC20Metadata collToken = IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS); - -// IERC20Metadata[] memory collaterals = new IERC20Metadata[](1); -// collaterals[0] = collToken; - -// ITroveManager[] memory troveManagers = new ITroveManager[](1); -// troveManagers[0] = ITroveManager(troveManagerAddress); - -// r.collateralRegistry = new CollateralRegistry(IBoldToken(address(r.stableToken)), collaterals, troveManagers); -// r.hintHelpers = new HintHelpers(r.collateralRegistry); -// r.multiTroveGetter = new MultiTroveGetter(r.collateralRegistry); - -// IPriceFeed priceFeed = new PriceFeedTestnet(); - -// r.contracts = -// _deployAndConnectCollateralContracts(collToken, priceFeed, addressesRegistry, troveManagerAddress, r); -// } - -// function _deployProxyInfrastructure(DeploymentResult memory r) internal { -// r.proxyAdmin = ProxyAdmin(CONFIG.proxyAdmin); -// r.stableTokenV3Impl = address(new StableTokenV3{salt: SALT}(true)); -// r.stabilityPoolImpl = address(new StabilityPool{salt: SALT}(true)); - -// assert( -// address(r.stableTokenV3Impl) -// == vm.computeCreate2Address( -// SALT, keccak256(bytes.concat(type(StableTokenV3).creationCode, abi.encode(true))) -// ) -// ); -// assert( -// address(r.stabilityPoolImpl) -// == vm.computeCreate2Address( -// SALT, keccak256(bytes.concat(type(StabilityPool).creationCode, abi.encode(true))) -// ) -// ); -// } - -// function _deployStableToken(DeploymentResult memory r) internal { -// r.stableToken = IStableTokenV3( -// address(new TransparentUpgradeableProxy(address(r.stableTokenV3Impl), address(r.proxyAdmin), "")) -// ); -// } - -// function _deployFPMM(DeploymentResult memory r) internal { -// r.fpmm = IFPMMFactory(CONFIG.fpmmFactory).deployFPMM( -// CONFIG.fpmmImplementation, address(r.stableToken), CONFIG.USDm_ALFAJORES_ADDRESS, CONFIG.referenceRateFeedID -// ); -// } - -// function _deployAndConnectCollateralContracts( -// IERC20Metadata _collToken, -// IPriceFeed _priceFeed, -// IAddressesRegistry _addressesRegistry, -// address _troveManagerAddress, -// DeploymentResult memory r -// ) internal returns (LiquityContracts memory contracts) { -// LiquityContractAddresses memory addresses; -// contracts.collToken = _collToken; -// contracts.addressesRegistry = _addressesRegistry; -// contracts.priceFeed = _priceFeed; -// // TODO: replace with governance timelock on mainnet -// contracts.interestRouter = IInterestRouter(0x56fD3F2bEE130e9867942D0F463a16fBE49B8d81); - -// addresses.troveManager = _troveManagerAddress; - -// contracts.metadataNFT = deployMetadata(SALT); -// addresses.metadataNFT = vm.computeCreate2Address( -// SALT, keccak256(getBytecode(type(MetadataNFT).creationCode, address(initializedFixedAssetReader))) -// ); -// assert(address(contracts.metadataNFT) == addresses.metadataNFT); - -// addresses.borrowerOperations = -// _computeCreate2Address(type(BorrowerOperations).creationCode, address(contracts.addressesRegistry)); -// addresses.troveNFT = _computeCreate2Address(type(TroveNFT).creationCode, address(contracts.addressesRegistry)); -// addresses.activePool = -// _computeCreate2Address(type(ActivePool).creationCode, address(contracts.addressesRegistry)); -// addresses.defaultPool = -// _computeCreate2Address(type(DefaultPool).creationCode, address(contracts.addressesRegistry)); -// addresses.gasPool = _computeCreate2Address(type(GasPool).creationCode, address(contracts.addressesRegistry)); -// addresses.collSurplusPool = -// _computeCreate2Address(type(CollSurplusPool).creationCode, address(contracts.addressesRegistry)); -// addresses.sortedTroves = -// _computeCreate2Address(type(SortedTroves).creationCode, address(contracts.addressesRegistry)); - -// // Deploy StabilityPool proxy -// address stabilityPool = -// address(new TransparentUpgradeableProxy(address(r.stabilityPoolImpl), address(r.proxyAdmin), "")); - -// contracts.stabilityPool = IStabilityPool(stabilityPool); -// // Set up addresses in registry -// _setupAddressesRegistry(contracts, addresses, r); - -// // Deploy core protocol contracts -// _deployProtocolContracts(contracts, addresses); - -// IStabilityPool(stabilityPool).initialize(contracts.addressesRegistry); - -// address[] memory minters = new address[](2); -// minters[0] = address(contracts.borrowerOperations); -// minters[1] = address(contracts.activePool); - -// address[] memory burners = new address[](4); -// burners[0] = address(contracts.troveManager); -// burners[1] = address(r.collateralRegistry); -// burners[2] = address(contracts.borrowerOperations); -// burners[3] = address(contracts.stabilityPool); - -// address[] memory operators = new address[](1); -// operators[0] = address(contracts.stabilityPool); - -// r.stableToken.initialize( -// CONFIG.stableTokenName, -// CONFIG.stableTokenSymbol, -// new address[](0), -// new uint256[](0), -// minters, -// burners, -// operators -// ); -// } - -// function _setupAddressesRegistry( -// LiquityContracts memory contracts, -// LiquityContractAddresses memory addresses, -// DeploymentResult memory r -// ) internal { -// IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ -// collToken: contracts.collToken, -// borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), -// troveManager: ITroveManager(addresses.troveManager), -// troveNFT: ITroveNFT(addresses.troveNFT), -// metadataNFT: IMetadataNFT(addresses.metadataNFT), -// stabilityPool: contracts.stabilityPool, -// priceFeed: contracts.priceFeed, -// activePool: IActivePool(addresses.activePool), -// defaultPool: IDefaultPool(addresses.defaultPool), -// gasPoolAddress: addresses.gasPool, -// collSurplusPool: ICollSurplusPool(addresses.collSurplusPool), -// sortedTroves: ISortedTroves(addresses.sortedTroves), -// interestRouter: contracts.interestRouter, -// hintHelpers: r.hintHelpers, -// multiTroveGetter: r.multiTroveGetter, -// collateralRegistry: r.collateralRegistry, -// boldToken: IBoldToken(address(r.stableToken)), -// gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS) -// }); -// contracts.addressesRegistry.setAddresses(addressVars); -// } - -// function _deployProtocolContracts(LiquityContracts memory contracts, LiquityContractAddresses memory addresses) -// internal -// { -// contracts.borrowerOperations = new BorrowerOperations{salt: SALT}(contracts.addressesRegistry); -// contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry); -// contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); -// contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry); -// contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); -// contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); -// contracts.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); -// contracts.sortedTroves = new SortedTroves{salt: SALT}(contracts.addressesRegistry); - -// assert(address(contracts.borrowerOperations) == addresses.borrowerOperations); -// assert(address(contracts.troveManager) == addresses.troveManager); -// assert(address(contracts.troveNFT) == addresses.troveNFT); -// assert(address(contracts.activePool) == addresses.activePool); -// assert(address(contracts.defaultPool) == addresses.defaultPool); -// assert(address(contracts.gasPool) == addresses.gasPool); -// assert(address(contracts.collSurplusPool) == addresses.collSurplusPool); -// assert(address(contracts.sortedTroves) == addresses.sortedTroves); -// } - -// function _computeCreate2Address(bytes memory creationCode, address _addressesRegistry) -// internal -// view -// returns (address) -// { -// return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry))); -// } - -// function _getBranchContractsJson(LiquityContracts memory c) internal view returns (string memory) { -// return string.concat( -// "{", -// string.concat( -// // Avoid stack too deep by chunking concats -// string.concat( -// string.concat('"collSymbol":"', c.collToken.symbol(), '",'), // purely for human-readability -// string.concat('"collToken":"', address(c.collToken).toHexString(), '",'), -// string.concat('"addressesRegistry":"', address(c.addressesRegistry).toHexString(), '",'), -// string.concat('"activePool":"', address(c.activePool).toHexString(), '",'), -// string.concat('"borrowerOperations":"', address(c.borrowerOperations).toHexString(), '",'), -// string.concat('"collSurplusPool":"', address(c.collSurplusPool).toHexString(), '",'), -// string.concat('"defaultPool":"', address(c.defaultPool).toHexString(), '",'), -// string.concat('"sortedTroves":"', address(c.sortedTroves).toHexString(), '",') -// ), -// string.concat( -// string.concat('"stabilityPool":"', address(c.stabilityPool).toHexString(), '",'), -// string.concat('"troveManager":"', address(c.troveManager).toHexString(), '",'), -// string.concat('"troveNFT":"', address(c.troveNFT).toHexString(), '",'), -// string.concat('"metadataNFT":"', address(c.metadataNFT).toHexString(), '",'), -// string.concat('"priceFeed":"', address(c.priceFeed).toHexString(), '",'), -// string.concat('"gasPool":"', address(c.gasPool).toHexString(), '",'), -// string.concat('"interestRouter":"', address(c.interestRouter).toHexString(), '",') -// ) -// ), -// "}" -// ); -// } - -// function _getDeploymentConstants() internal pure returns (string memory) { -// return string.concat( -// "{", -// string.concat( -// string.concat('"ETH_GAS_COMPENSATION":"', ETH_GAS_COMPENSATION.toString(), '",'), -// string.concat('"INTEREST_RATE_ADJ_COOLDOWN":"', INTEREST_RATE_ADJ_COOLDOWN.toString(), '",'), -// string.concat('"MAX_ANNUAL_INTEREST_RATE":"', MAX_ANNUAL_INTEREST_RATE.toString(), '",'), -// string.concat('"MIN_ANNUAL_INTEREST_RATE":"', MIN_ANNUAL_INTEREST_RATE.toString(), '",'), -// string.concat('"MIN_DEBT":"', MIN_DEBT.toString(), '",'), -// string.concat('"SP_YIELD_SPLIT":"', SP_YIELD_SPLIT.toString(), '",'), -// string.concat('"UPFRONT_INTEREST_PERIOD":"', UPFRONT_INTEREST_PERIOD.toString(), '"') // no comma -// ), -// "}" -// ); -// } - -// function _getManifestJson(DeploymentResult memory deployed) internal view returns (string memory) { -// string[] memory branches = new string[](1); - -// branches[0] = _getBranchContractsJson(deployed.contracts); - -// return string.concat( -// "{", -// string.concat('"constants":', _getDeploymentConstants(), ","), -// string.concat('"collateralRegistry":"', address(deployed.collateralRegistry).toHexString(), '",'), -// string.concat('"boldToken":"', address(deployed.stableToken).toHexString(), '",'), -// string.concat('"hintHelpers":"', address(deployed.hintHelpers).toHexString(), '",'), -// string.concat('"stableTokenV3Impl":"', address(deployed.stableTokenV3Impl).toHexString(), '",'), -// string.concat('"stabilityPoolImpl":"', address(deployed.stabilityPoolImpl).toHexString(), '",'), -// string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",'), -// string.concat('"fpmm":"', address(deployed.fpmm).toHexString(), '",'), -// string.concat('"branches":[', branches.join(","), "]"), -// "}" -// ); -// } -// } +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.24; + +import {StdCheats} from "forge-std/StdCheats.sol"; +import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; +import {IERC20 as IERC20_GOV} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {ProxyAdmin} from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from + "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IFPMMFactory} from "src/Interfaces/IFPMMFactory.sol"; +import {SystemParams} from "src/SystemParams.sol"; + +import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; +import {StringFormatting} from "test/Utils/StringFormatting.sol"; +import {Accounts} from "test/TestContracts/Accounts.sol"; +import {ERC20Faucet} from "test/TestContracts/ERC20Faucet.sol"; +import {WETHTester} from "test/TestContracts/WETHTester.sol"; +import "src/AddressesRegistry.sol"; +import "src/ActivePool.sol"; +import "src/BoldToken.sol"; +import "src/BorrowerOperations.sol"; +import "src/TroveManager.sol"; +import "src/TroveNFT.sol"; +import "src/CollSurplusPool.sol"; +import "src/DefaultPool.sol"; +import "src/GasPool.sol"; +import "src/HintHelpers.sol"; +import "src/MultiTroveGetter.sol"; +import "src/SortedTroves.sol"; +import "src/StabilityPool.sol"; + +import "src/CollateralRegistry.sol"; +import "src/tokens/StableTokenV3.sol"; +import "src/Interfaces/IStableTokenV3.sol"; +import "test/TestContracts/PriceFeedTestnet.sol"; +import "test/TestContracts/MetadataDeployment.sol"; +import "test/Utils/Logging.sol"; +import "test/Utils/StringEquality.sol"; +import "forge-std/console2.sol"; + +contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { + using Strings for *; + using StringFormatting for *; + using StringEquality for string; + + bytes32 SALT; + address deployer; + + struct LiquityContracts { + IAddressesRegistry addressesRegistry; + IActivePool activePool; + IBorrowerOperations borrowerOperations; + ICollSurplusPool collSurplusPool; + IDefaultPool defaultPool; + ISortedTroves sortedTroves; + IStabilityPool stabilityPool; + ITroveManager troveManager; + ITroveNFT troveNFT; + MetadataNFT metadataNFT; + IPriceFeed priceFeed; + GasPool gasPool; + IInterestRouter interestRouter; + IERC20Metadata collToken; + ISystemParams systemParams; + } + + struct LiquityContractAddresses { + address activePool; + address borrowerOperations; + address collSurplusPool; + address defaultPool; + address sortedTroves; + address stabilityPool; + address troveManager; + address troveNFT; + address metadataNFT; + address priceFeed; + address gasPool; + address interestRouter; + } + + struct DeploymentVars { + uint256 numCollaterals; + IERC20Metadata[] collaterals; + IAddressesRegistry[] addressesRegistries; + ITroveManager[] troveManagers; + LiquityContracts contracts; + bytes bytecode; + address boldTokenAddress; + uint256 i; + } + + struct DemoTroveParams { + uint256 collIndex; + uint256 owner; + uint256 ownerIndex; + uint256 coll; + uint256 debt; + uint256 annualInterestRate; + } + + struct DeploymentResult { + LiquityContracts contracts; + ICollateralRegistry collateralRegistry; + HintHelpers hintHelpers; + MultiTroveGetter multiTroveGetter; + ProxyAdmin proxyAdmin; + IStableTokenV3 stableToken; + ISystemParams systemParams; + address stabilityPoolImpl; + address stableTokenV3Impl; + address fpmm; + address systemParamsImpl; + } + + struct DeploymentConfig { + address USDm_ALFAJORES_ADDRESS; + address proxyAdmin; + address fpmmFactory; + address fpmmImplementation; + address referenceRateFeedID; + string stableTokenName; + string stableTokenSymbol; + } + + DeploymentConfig internal CONFIG = DeploymentConfig({ + USDm_ALFAJORES_ADDRESS: 0x9E2d4412d0f434cC85500b79447d9323a7416f09, + proxyAdmin: 0xe4DdacCAdb64114215FCe8251B57B2AEB5C2C0E2, + fpmmFactory: 0xd8098494a749a3fDAD2D2e7Fa5272D8f274D8FF6, + fpmmImplementation: 0x0292efcB331C6603eaa29D570d12eB336D6c01d6, + referenceRateFeedID: 0x206B25Ea01E188Ee243131aFdE526bA6E131a016, + stableTokenName: "EUR.m Test", + stableTokenSymbol: "EUR.m" + }); + + function run() external { + string memory saltStr = vm.envOr("SALT", block.timestamp.toString()); + SALT = keccak256(bytes(saltStr)); + + uint256 privateKey = vm.envUint("DEPLOYER"); + deployer = vm.addr(privateKey); + vm.startBroadcast(privateKey); + + _log("Deployer: ", deployer.toHexString()); + _log("Deployer balance: ", deployer.balance.decimal()); + _log("CREATE2 salt: ", 'keccak256(bytes("', saltStr, '")) = ', uint256(SALT).toHexString()); + _log("Chain ID: ", block.chainid.toString()); + + DeploymentResult memory deployed = _deployAndConnectContracts(); + + vm.stopBroadcast(); + + vm.writeFile("script/deployment-manifest.json", _getManifestJson(deployed)); + } + + // See: https://solidity-by-example.org/app/create2/ + function getBytecode(bytes memory _creationCode, address _addressesRegistry) public pure returns (bytes memory) { + return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry)); + } + + function getBytecode(bytes memory _creationCode, address _addressesRegistry, address _systemParams) public pure returns (bytes memory) { + return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry, _systemParams)); + } + + function _deployAndConnectContracts() internal returns (DeploymentResult memory r) { + _deployProxyInfrastructure(r); + _deployStableToken(r); + _deployFPMM(r); + _deploySystemParams(r); + + IAddressesRegistry addressesRegistry = new AddressesRegistry(deployer); + + address troveManagerAddress = vm.computeCreate2Address( + SALT, keccak256(getBytecode(type(TroveManager).creationCode, address(addressesRegistry), address(r.systemParams))) + ); + + IERC20Metadata collToken = IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS); + + IERC20Metadata[] memory collaterals = new IERC20Metadata[](1); + collaterals[0] = collToken; + + ITroveManager[] memory troveManagers = new ITroveManager[](1); + troveManagers[0] = ITroveManager(troveManagerAddress); + + r.collateralRegistry = new CollateralRegistry(IBoldToken(address(r.stableToken)), collaterals, troveManagers, r.systemParams); + r.hintHelpers = new HintHelpers(r.collateralRegistry, r.systemParams); + r.multiTroveGetter = new MultiTroveGetter(r.collateralRegistry); + + IPriceFeed priceFeed = new PriceFeedTestnet(); + + r.contracts = + _deployAndConnectCollateralContracts(collToken, priceFeed, addressesRegistry, troveManagerAddress, r); + } + + function _deployProxyInfrastructure(DeploymentResult memory r) internal { + r.proxyAdmin = ProxyAdmin(CONFIG.proxyAdmin); + r.stableTokenV3Impl = address(new StableTokenV3{salt: SALT}(true)); + r.stabilityPoolImpl = address(new StabilityPool{salt: SALT}(true)); + r.systemParamsImpl = address(new SystemParams{salt: SALT}(true)); + + assert( + address(r.stableTokenV3Impl) + == vm.computeCreate2Address( + SALT, keccak256(bytes.concat(type(StableTokenV3).creationCode, abi.encode(true))) + ) + ); + assert( + address(r.stabilityPoolImpl) + == vm.computeCreate2Address( + SALT, keccak256(bytes.concat(type(StabilityPool).creationCode, abi.encode(true))) + ) + ); + + assert( + address(r.systemParamsImpl) + == vm.computeCreate2Address( + SALT, keccak256(bytes.concat(type(SystemParams).creationCode, abi.encode(true))) + ) + ); + } + + function _deployStableToken(DeploymentResult memory r) internal { + r.stableToken = IStableTokenV3( + address(new TransparentUpgradeableProxy(address(r.stableTokenV3Impl), address(r.proxyAdmin), "")) + ); + } + + function _deployFPMM(DeploymentResult memory r) internal { + r.fpmm = IFPMMFactory(CONFIG.fpmmFactory).deployFPMM( + CONFIG.fpmmImplementation, address(r.stableToken), CONFIG.USDm_ALFAJORES_ADDRESS, CONFIG.referenceRateFeedID + ); + } + + function _deploySystemParams(DeploymentResult memory r) internal { + r.systemParams = ISystemParams( + address(new TransparentUpgradeableProxy(address(r.systemParamsImpl), address(r.proxyAdmin), "")) + ); + } + + function _deployAndConnectCollateralContracts( + IERC20Metadata _collToken, + IPriceFeed _priceFeed, + IAddressesRegistry _addressesRegistry, + address _troveManagerAddress, + DeploymentResult memory r + ) internal returns (LiquityContracts memory contracts) { + LiquityContractAddresses memory addresses; + contracts.collToken = _collToken; + contracts.addressesRegistry = _addressesRegistry; + contracts.priceFeed = _priceFeed; + // TODO: replace with governance timelock on mainnet + contracts.interestRouter = IInterestRouter(0x56fD3F2bEE130e9867942D0F463a16fBE49B8d81); + + addresses.troveManager = _troveManagerAddress; + + contracts.metadataNFT = deployMetadata(SALT); + addresses.metadataNFT = vm.computeCreate2Address( + SALT, keccak256(getBytecode(type(MetadataNFT).creationCode, address(initializedFixedAssetReader))) + ); + assert(address(contracts.metadataNFT) == addresses.metadataNFT); + + addresses.borrowerOperations = + _computeCreate2Address(type(BorrowerOperations).creationCode, address(contracts.addressesRegistry)); + addresses.troveNFT = _computeCreate2Address(type(TroveNFT).creationCode, address(contracts.addressesRegistry)); + addresses.activePool = + _computeCreate2Address(type(ActivePool).creationCode, address(contracts.addressesRegistry)); + addresses.defaultPool = + _computeCreate2Address(type(DefaultPool).creationCode, address(contracts.addressesRegistry)); + addresses.gasPool = _computeCreate2Address(type(GasPool).creationCode, address(contracts.addressesRegistry)); + addresses.collSurplusPool = + _computeCreate2Address(type(CollSurplusPool).creationCode, address(contracts.addressesRegistry)); + addresses.sortedTroves = + _computeCreate2Address(type(SortedTroves).creationCode, address(contracts.addressesRegistry)); + + // Deploy StabilityPool proxy + address stabilityPool = + address(new TransparentUpgradeableProxy(address(r.stabilityPoolImpl), address(r.proxyAdmin), "")); + + contracts.stabilityPool = IStabilityPool(stabilityPool); + // Set up addresses in registry + _setupAddressesRegistry(contracts, addresses, r); + + // Deploy core protocol contracts + _deployProtocolContracts(contracts, addresses); + + IStabilityPool(stabilityPool).initialize(contracts.addressesRegistry, contracts.systemParams); + + address[] memory minters = new address[](2); + minters[0] = address(contracts.borrowerOperations); + minters[1] = address(contracts.activePool); + + address[] memory burners = new address[](4); + burners[0] = address(contracts.troveManager); + burners[1] = address(r.collateralRegistry); + burners[2] = address(contracts.borrowerOperations); + burners[3] = address(contracts.stabilityPool); + + address[] memory operators = new address[](1); + operators[0] = address(contracts.stabilityPool); + + r.stableToken.initialize( + CONFIG.stableTokenName, + CONFIG.stableTokenSymbol, + new address[](0), + new uint256[](0), + minters, + burners, + operators + ); + } + + function _setupAddressesRegistry( + LiquityContracts memory contracts, + LiquityContractAddresses memory addresses, + DeploymentResult memory r + ) internal { + IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ + collToken: contracts.collToken, + borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), + troveManager: ITroveManager(addresses.troveManager), + troveNFT: ITroveNFT(addresses.troveNFT), + metadataNFT: IMetadataNFT(addresses.metadataNFT), + stabilityPool: contracts.stabilityPool, + priceFeed: contracts.priceFeed, + activePool: IActivePool(addresses.activePool), + defaultPool: IDefaultPool(addresses.defaultPool), + gasPoolAddress: addresses.gasPool, + collSurplusPool: ICollSurplusPool(addresses.collSurplusPool), + sortedTroves: ISortedTroves(addresses.sortedTroves), + interestRouter: contracts.interestRouter, + hintHelpers: r.hintHelpers, + multiTroveGetter: r.multiTroveGetter, + collateralRegistry: r.collateralRegistry, + boldToken: IBoldToken(address(r.stableToken)), + gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS) + }); + contracts.addressesRegistry.setAddresses(addressVars); + } + + function _deployProtocolContracts(LiquityContracts memory contracts, LiquityContractAddresses memory addresses) + internal + { + contracts.borrowerOperations = new BorrowerOperations{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); + contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); + contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); + contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); + contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); + contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); + contracts.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); + contracts.sortedTroves = new SortedTroves{salt: SALT}(contracts.addressesRegistry); + + assert(address(contracts.borrowerOperations) == addresses.borrowerOperations); + assert(address(contracts.troveManager) == addresses.troveManager); + assert(address(contracts.troveNFT) == addresses.troveNFT); + assert(address(contracts.activePool) == addresses.activePool); + assert(address(contracts.defaultPool) == addresses.defaultPool); + assert(address(contracts.gasPool) == addresses.gasPool); + assert(address(contracts.collSurplusPool) == addresses.collSurplusPool); + assert(address(contracts.sortedTroves) == addresses.sortedTroves); + } + + function _computeCreate2Address(bytes memory creationCode, address _addressesRegistry) + internal + view + returns (address) + { + return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry))); + } + + function _computeCreate2AddressWithPar(bytes memory creationCode, address _addressesRegistry) + internal + view + returns (address) + { + return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry))); + } + + + + function _getBranchContractsJson(LiquityContracts memory c) internal view returns (string memory) { + return string.concat( + "{", + string.concat( + // Avoid stack too deep by chunking concats + string.concat( + string.concat('"collSymbol":"', c.collToken.symbol(), '",'), // purely for human-readability + string.concat('"collToken":"', address(c.collToken).toHexString(), '",'), + string.concat('"addressesRegistry":"', address(c.addressesRegistry).toHexString(), '",'), + string.concat('"activePool":"', address(c.activePool).toHexString(), '",'), + string.concat('"borrowerOperations":"', address(c.borrowerOperations).toHexString(), '",'), + string.concat('"collSurplusPool":"', address(c.collSurplusPool).toHexString(), '",'), + string.concat('"defaultPool":"', address(c.defaultPool).toHexString(), '",'), + string.concat('"sortedTroves":"', address(c.sortedTroves).toHexString(), '",'), + string.concat('"systemParams":"', address(c.systemParams).toHexString(), '",') + ), + string.concat( + string.concat('"stabilityPool":"', address(c.stabilityPool).toHexString(), '",'), + string.concat('"troveManager":"', address(c.troveManager).toHexString(), '",'), + string.concat('"troveNFT":"', address(c.troveNFT).toHexString(), '",'), + string.concat('"metadataNFT":"', address(c.metadataNFT).toHexString(), '",'), + string.concat('"priceFeed":"', address(c.priceFeed).toHexString(), '",'), + string.concat('"gasPool":"', address(c.gasPool).toHexString(), '",'), + string.concat('"interestRouter":"', address(c.interestRouter).toHexString(), '",') + ) + ), + "}" + ); + } + + function _getDeploymentConstants(ISystemParams params) internal view returns (string memory) { + return string.concat( + "{", + string.concat( + string.concat('"ETH_GAS_COMPENSATION":"', params.ETH_GAS_COMPENSATION().toString(), '",'), + string.concat('"INTEREST_RATE_ADJ_COOLDOWN":"', params.INTEREST_RATE_ADJ_COOLDOWN().toString(), '",'), + string.concat('"MAX_ANNUAL_INTEREST_RATE":"', params.MAX_ANNUAL_INTEREST_RATE().toString(), '",'), + string.concat('"MIN_ANNUAL_INTEREST_RATE":"', params.MIN_ANNUAL_INTEREST_RATE().toString(), '",'), + string.concat('"MIN_DEBT":"', params.MIN_DEBT().toString(), '",'), + string.concat('"SP_YIELD_SPLIT":"', params.SP_YIELD_SPLIT().toString(), '",'), + string.concat('"UPFRONT_INTEREST_PERIOD":"', params.UPFRONT_INTEREST_PERIOD().toString(), '"') // no comma + ), + "}" + ); + } + + function _getManifestJson(DeploymentResult memory deployed) internal view returns (string memory) { + string[] memory branches = new string[](1); + + branches[0] = _getBranchContractsJson(deployed.contracts); + + return string.concat( + "{", + string.concat('"constants":', _getDeploymentConstants(deployed.contracts.systemParams), ","), + string.concat('"collateralRegistry":"', address(deployed.collateralRegistry).toHexString(), '",'), + string.concat('"boldToken":"', address(deployed.stableToken).toHexString(), '",'), + string.concat('"hintHelpers":"', address(deployed.hintHelpers).toHexString(), '",'), + string.concat('"stableTokenV3Impl":"', address(deployed.stableTokenV3Impl).toHexString(), '",'), + string.concat('"stabilityPoolImpl":"', address(deployed.stabilityPoolImpl).toHexString(), '",'), + string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",'), + string.concat('"fpmm":"', address(deployed.fpmm).toHexString(), '",'), + string.concat('"branches":[', branches.join(","), "]"), + "}" + ); + } +} diff --git a/contracts/src/SystemParams.sol b/contracts/src/SystemParams.sol index a8eef7718..8b5f05b06 100644 --- a/contracts/src/SystemParams.sol +++ b/contracts/src/SystemParams.sol @@ -88,7 +88,7 @@ contract SystemParams is Initializable, OwnableUpgradeable, ISystemParams { MIN_LIQUIDATION_PENALTY_SP = 5 * _1pct; // 5% MAX_LIQUIDATION_PENALTY_REDISTRIBUTION = 20 * _1pct; // 20% LIQUIDATION_PENALTY_SP = 5e16; // 5% - LIQUIDATION_PENALTY_REDISTRIBUTION = 20e16; // 20% + LIQUIDATION_PENALTY_REDISTRIBUTION = 10e16; // 10% // Gas compensation parameters COLL_GAS_COMPENSATION_DIVISOR = 200; // dividing by 200 yields 0.5% @@ -99,7 +99,7 @@ contract SystemParams is Initializable, OwnableUpgradeable, ISystemParams { CCR = 150 * _1pct; // 150% SCR = 110 * _1pct; // 110% MCR = 110 * _1pct; // 110% - BCR = 10 * _1pct; // 10% + BCR = 40 * _1pct; // 40% // Interest parameters MIN_ANNUAL_INTEREST_RATE = _1pct / 2; // 0.5% From becb8d3ec46376a15ee0d886338697e85cc13116 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:40:17 -0500 Subject: [PATCH 29/79] test: init params --- contracts/src/BorrowerOperations.sol | 1 + contracts/test/TestContracts/DevTestSetup.sol | 6 ++---- contracts/test/liquidationsLST.t.sol | 2 ++ contracts/test/multicollateral.t.sol | 3 +++ contracts/test/shutdown.t.sol | 3 +++ contracts/test/troveNFT.t.sol | 3 +++ 6 files changed, 14 insertions(+), 4 deletions(-) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 2092b932e..e65202398 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -199,6 +199,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio UPFRONT_INTEREST_PERIOD = _systemParams.UPFRONT_INTEREST_PERIOD(); MIN_ANNUAL_INTEREST_RATE = _systemParams.MIN_ANNUAL_INTEREST_RATE(); MAX_ANNUAL_INTEREST_RATE = _systemParams.MAX_ANNUAL_INTEREST_RATE(); + INTEREST_RATE_ADJ_COOLDOWN = _systemParams.INTEREST_RATE_ADJ_COOLDOWN(); troveManager = _addressesRegistry.troveManager(); gasPoolAddress = _addressesRegistry.gasPoolAddress(); diff --git a/contracts/test/TestContracts/DevTestSetup.sol b/contracts/test/TestContracts/DevTestSetup.sol index a74446f95..e0c719afb 100644 --- a/contracts/test/TestContracts/DevTestSetup.sol +++ b/contracts/test/TestContracts/DevTestSetup.sol @@ -93,10 +93,8 @@ contract DevTestSetup is BaseTest { MAX_ANNUAL_BATCH_MANAGEMENT_FEE = systemParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE(); REDEMPTION_MINUTE_DECAY_FACTOR = systemParams.REDEMPTION_MINUTE_DECAY_FACTOR(); URGENT_REDEMPTION_BONUS = systemParams.URGENT_REDEMPTION_BONUS(); - - // TODO(@bayological): These may need initializing. They come from borrower ops but can be fetched from sys params - // UPFRONT_INTEREST_PERIOD; - // MIN_INTEREST_RATE_CHANGE_PERIOD; + MIN_INTEREST_RATE_CHANGE_PERIOD = systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD(); + UPFRONT_INTEREST_PERIOD = systemParams.UPFRONT_INTEREST_PERIOD(); } function _setupForWithdrawCollGainToTrove() internal returns (uint256, uint256, uint256) { diff --git a/contracts/test/liquidationsLST.t.sol b/contracts/test/liquidationsLST.t.sol index c5cad93da..14bedfd36 100644 --- a/contracts/test/liquidationsLST.t.sol +++ b/contracts/test/liquidationsLST.t.sol @@ -38,8 +38,10 @@ contract LiquidationsLSTTest is DevTestSetup { stabilityPool = contracts.stabilityPool; troveManager = contracts.troveManager; mockInterestRouter = contracts.interestRouter; + systemParams = contracts.systemParams; MCR = troveManager.get_MCR(); + MIN_ANNUAL_INTEREST_RATE = systemParams.MIN_ANNUAL_INTEREST_RATE(); // Give some Coll to test accounts, and approve it to BorrowerOperations uint256 initialCollAmount = 10_000e18; diff --git a/contracts/test/multicollateral.t.sol b/contracts/test/multicollateral.t.sol index 95e888724..4dfe4b16c 100644 --- a/contracts/test/multicollateral.t.sol +++ b/contracts/test/multicollateral.t.sol @@ -109,6 +109,9 @@ contract MulticollateralTest is DevTestSetup { vm.stopPrank(); } } + + systemParams = contractsArray[0].systemParams; + UPFRONT_INTEREST_PERIOD = systemParams.UPFRONT_INTEREST_PERIOD(); } function testMultiCollateralDeployment() public { diff --git a/contracts/test/shutdown.t.sol b/contracts/test/shutdown.t.sol index 06c97a4a8..ffea08af0 100644 --- a/contracts/test/shutdown.t.sol +++ b/contracts/test/shutdown.t.sol @@ -74,8 +74,11 @@ contract ShutdownTest is DevTestSetup { borrowerOperations = contractsArray[0].borrowerOperations; troveManager = contractsArray[0].troveManager; priceFeed = contractsArray[0].priceFeed; + systemParams = contractsArray[0].systemParams; MCR = troveManager.get_MCR(); SCR = troveManager.get_SCR(); + UPFRONT_INTEREST_PERIOD = systemParams.UPFRONT_INTEREST_PERIOD(); + MIN_INTEREST_RATE_CHANGE_PERIOD = systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD(); } function openMulticollateralTroveNoHints100pctWithIndex( diff --git a/contracts/test/troveNFT.t.sol b/contracts/test/troveNFT.t.sol index 80eaf5f29..c7a113511 100644 --- a/contracts/test/troveNFT.t.sol +++ b/contracts/test/troveNFT.t.sol @@ -109,6 +109,9 @@ contract troveNFTTest is DevTestSetup { } } + systemParams = contractsArray[0].systemParams; + UPFRONT_INTEREST_PERIOD = systemParams.UPFRONT_INTEREST_PERIOD(); + troveIds = new uint256[](NUM_VARIANTS); // 0 = WETH From 4e173ad55ef28d354c0d58719ac9bab0729d08a5 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:48:38 -0500 Subject: [PATCH 30/79] chore: set correct BCR --- contracts/src/SystemParams.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/SystemParams.sol b/contracts/src/SystemParams.sol index 8b5f05b06..8d421b0b9 100644 --- a/contracts/src/SystemParams.sol +++ b/contracts/src/SystemParams.sol @@ -99,7 +99,7 @@ contract SystemParams is Initializable, OwnableUpgradeable, ISystemParams { CCR = 150 * _1pct; // 150% SCR = 110 * _1pct; // 110% MCR = 110 * _1pct; // 110% - BCR = 40 * _1pct; // 40% + BCR = 10 * _1pct; // 10% // Interest parameters MIN_ANNUAL_INTEREST_RATE = _1pct / 2; // 0.5% From ba6d180b9e4f46478697c797d5aacf93cbfefe9f Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:49:08 -0500 Subject: [PATCH 31/79] test: init params --- contracts/test/multicollateral.t.sol | 1 + contracts/test/shutdown.t.sol | 1 + 2 files changed, 2 insertions(+) diff --git a/contracts/test/multicollateral.t.sol b/contracts/test/multicollateral.t.sol index 4dfe4b16c..1b8bc513d 100644 --- a/contracts/test/multicollateral.t.sol +++ b/contracts/test/multicollateral.t.sol @@ -112,6 +112,7 @@ contract MulticollateralTest is DevTestSetup { systemParams = contractsArray[0].systemParams; UPFRONT_INTEREST_PERIOD = systemParams.UPFRONT_INTEREST_PERIOD(); + URGENT_REDEMPTION_BONUS = systemParams.URGENT_REDEMPTION_BONUS(); } function testMultiCollateralDeployment() public { diff --git a/contracts/test/shutdown.t.sol b/contracts/test/shutdown.t.sol index ffea08af0..68646cd45 100644 --- a/contracts/test/shutdown.t.sol +++ b/contracts/test/shutdown.t.sol @@ -79,6 +79,7 @@ contract ShutdownTest is DevTestSetup { SCR = troveManager.get_SCR(); UPFRONT_INTEREST_PERIOD = systemParams.UPFRONT_INTEREST_PERIOD(); MIN_INTEREST_RATE_CHANGE_PERIOD = systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD(); + URGENT_REDEMPTION_BONUS = systemParams.URGENT_REDEMPTION_BONUS(); } function openMulticollateralTroveNoHints100pctWithIndex( From 10a64a2e309037040f3c841b1a4b0bc8dda19cef Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:28:03 -0500 Subject: [PATCH 32/79] test: update invariant tests --- contracts/test/AnchoredInvariantsTest.t.sol | 1 + contracts/test/AnchoredSPInvariantsTest.t.sol | 1 + contracts/test/Invariants.t.sol | 1 + contracts/test/TestContracts/Deployment.t.sol | 37 +++++++++++++------ contracts/test/shutdown.t.sol | 7 ++++ 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/contracts/test/AnchoredInvariantsTest.t.sol b/contracts/test/AnchoredInvariantsTest.t.sol index 31d3157cb..7ca2a57ea 100644 --- a/contracts/test/AnchoredInvariantsTest.t.sol +++ b/contracts/test/AnchoredInvariantsTest.t.sol @@ -28,6 +28,7 @@ contract AnchoredInvariantsTest is Logging, BaseInvariantTest, BaseMultiCollater Contracts memory contracts; (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth) = deployer.deployAndConnectContractsMultiColl(p); + contracts.systemParams = contracts.branches[0].systemParams; setupContracts(contracts); handler = new InvariantsTestHandler({contracts: contracts, assumeNoExpectedFailures: true}); diff --git a/contracts/test/AnchoredSPInvariantsTest.t.sol b/contracts/test/AnchoredSPInvariantsTest.t.sol index c99b30124..cb74e23cb 100644 --- a/contracts/test/AnchoredSPInvariantsTest.t.sol +++ b/contracts/test/AnchoredSPInvariantsTest.t.sol @@ -28,6 +28,7 @@ contract AnchoredInvariantsTest is Logging, BaseInvariantTest, BaseMultiCollater Contracts memory contracts; (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth) = deployer.deployAndConnectContractsMultiColl(p); + contracts.systemParams = contracts.branches[0].systemParams; setupContracts(contracts); handler = new InvariantsTestHandler({contracts: contracts, assumeNoExpectedFailures: true}); diff --git a/contracts/test/Invariants.t.sol b/contracts/test/Invariants.t.sol index 6269f2718..bf4ed54db 100644 --- a/contracts/test/Invariants.t.sol +++ b/contracts/test/Invariants.t.sol @@ -88,6 +88,7 @@ contract InvariantsTest is Assertions, Logging, BaseInvariantTest, BaseMultiColl Contracts memory contracts; (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth) = deployer.deployAndConnectContractsMultiColl(p); + contracts.systemParams = contracts.branches[0].systemParams; setupContracts(contracts); handler = new InvariantsTestHandler({contracts: contracts, assumeNoExpectedFailures: true}); diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index 9643f5cd8..af47c915a 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -264,8 +264,7 @@ contract TestDeployer is MetadataDeployment { DeploymentVarsDev memory vars; vars.numCollaterals = troveManagerParamsArray.length; - // Deploy SystemParams - ISystemParams systemParams = deploySystemParamsDev(); + ISystemParams[] memory systemParamsArray = new ISystemParams[](vars.numCollaterals); // Deploy Bold vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(address(this))); @@ -278,13 +277,14 @@ contract TestDeployer is MetadataDeployment { vars.addressesRegistries = new IAddressesRegistry[](vars.numCollaterals); vars.troveManagers = new ITroveManager[](vars.numCollaterals); - // TODO: Before sys params we deployed address registry per branch - // We should do the same for sys params + for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { + systemParamsArray[vars.i] = deploySystemParamsDev(troveManagerParamsArray[vars.i], vars.i); + } // Deploy the first branch with WETH collateral vars.collaterals[0] = _WETH; (IAddressesRegistry addressesRegistry, address troveManagerAddress) = - _deployAddressesRegistryDev(systemParams); + _deployAddressesRegistryDev(systemParamsArray[0]); vars.addressesRegistries[0] = addressesRegistry; vars.troveManagers[0] = ITroveManager(troveManagerAddress); for (vars.i = 1; vars.i < vars.numCollaterals; vars.i++) { @@ -296,13 +296,13 @@ contract TestDeployer is MetadataDeployment { ); vars.collaterals[vars.i] = collToken; // Addresses registry and TM address - (addressesRegistry, troveManagerAddress) = _deployAddressesRegistryDev(systemParams); + (addressesRegistry, troveManagerAddress) = _deployAddressesRegistryDev(systemParamsArray[vars.i]); vars.addressesRegistries[vars.i] = addressesRegistry; vars.troveManagers[vars.i] = ITroveManager(troveManagerAddress); } - collateralRegistry = new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers, systemParams); - hintHelpers = new HintHelpers(collateralRegistry, systemParams); + collateralRegistry = new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers, systemParamsArray[0]); + hintHelpers = new HintHelpers(collateralRegistry, systemParamsArray[0]); multiTroveGetter = new MultiTroveGetter(collateralRegistry); contractsArray[0] = _deployAndConnectCollateralContractsDev( @@ -314,7 +314,7 @@ contract TestDeployer is MetadataDeployment { address(vars.troveManagers[0]), hintHelpers, multiTroveGetter, - systemParams + systemParamsArray[0] ); // Deploy the remaining branches with LST collateral @@ -328,7 +328,7 @@ contract TestDeployer is MetadataDeployment { address(vars.troveManagers[vars.i]), hintHelpers, multiTroveGetter, - systemParams + systemParamsArray[vars.i] ); } @@ -347,12 +347,25 @@ contract TestDeployer is MetadataDeployment { return (addressesRegistry, troveManagerAddress); } - function deploySystemParamsDev() public returns (ISystemParams) { - SystemParams systemParams = new SystemParams{salt: SALT}(false); + function deploySystemParamsDev(TroveManagerParams memory params, uint256 index) public returns (ISystemParams) { + bytes32 uniqueSalt = keccak256(abi.encodePacked(SALT, index)); + SystemParams systemParams = new SystemParams{salt: uniqueSalt}(false); systemParams.initialize(address(this)); + + systemParams.updateCollateralParams(params.CCR, params.SCR, params.MCR, params.BCR); + systemParams.updateLiquidationParams(5e16, 20e16, params.LIQUIDATION_PENALTY_SP, params.LIQUIDATION_PENALTY_REDISTRIBUTION); + return ISystemParams(systemParams); } + function updateSystemParamsCollateral(ISystemParams systemParams, uint256 ccr, uint256 scr, uint256 mcr, uint256 bcr) public { + systemParams.updateCollateralParams(ccr, scr, mcr, bcr); + } + + function updateSystemParamsLiquidation(ISystemParams systemParams, uint256 minSP, uint256 maxRedist, uint256 sp, uint256 redist) public { + systemParams.updateLiquidationParams(minSP, maxRedist, sp, redist); + } + function _deployAndConnectCollateralContractsDev( IERC20Metadata _collToken, IBoldToken _boldToken, diff --git a/contracts/test/shutdown.t.sol b/contracts/test/shutdown.t.sol index 68646cd45..ab0656464 100644 --- a/contracts/test/shutdown.t.sol +++ b/contracts/test/shutdown.t.sol @@ -40,6 +40,13 @@ contract ShutdownTest is DevTestSetup { for (uint256 c = 0; c < NUM_COLLATERALS; c++) { contractsArray.push(_contractsArray[c]); } + + // Initialize SystemParams-based variables + systemParams = contractsArray[0].systemParams; + UPFRONT_INTEREST_PERIOD = systemParams.UPFRONT_INTEREST_PERIOD(); + URGENT_REDEMPTION_BONUS = systemParams.URGENT_REDEMPTION_BONUS(); + MIN_INTEREST_RATE_CHANGE_PERIOD = systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD(); + SP_YIELD_SPLIT = systemParams.SP_YIELD_SPLIT(); // Set price feeds contractsArray[0].priceFeed.setPrice(2000e18); contractsArray[1].priceFeed.setPrice(200e18); From 8099781aae2163de379ccbc580478ba2e11aef51 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:08:31 -0500 Subject: [PATCH 33/79] test: init params --- contracts/test/TestContracts/Deployment.t.sol | 4 ++-- contracts/test/liquidationsLST.t.sol | 1 + contracts/test/multicollateral.t.sol | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index af47c915a..b290c1923 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -264,8 +264,6 @@ contract TestDeployer is MetadataDeployment { DeploymentVarsDev memory vars; vars.numCollaterals = troveManagerParamsArray.length; - ISystemParams[] memory systemParamsArray = new ISystemParams[](vars.numCollaterals); - // Deploy Bold vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(address(this))); vars.boldTokenAddress = getAddress(address(this), vars.bytecode, SALT); @@ -277,6 +275,8 @@ contract TestDeployer is MetadataDeployment { vars.addressesRegistries = new IAddressesRegistry[](vars.numCollaterals); vars.troveManagers = new ITroveManager[](vars.numCollaterals); + ISystemParams[] memory systemParamsArray = new ISystemParams[](vars.numCollaterals); + for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { systemParamsArray[vars.i] = deploySystemParamsDev(troveManagerParamsArray[vars.i], vars.i); } diff --git a/contracts/test/liquidationsLST.t.sol b/contracts/test/liquidationsLST.t.sol index 14bedfd36..c9651f52a 100644 --- a/contracts/test/liquidationsLST.t.sol +++ b/contracts/test/liquidationsLST.t.sol @@ -42,6 +42,7 @@ contract LiquidationsLSTTest is DevTestSetup { MCR = troveManager.get_MCR(); MIN_ANNUAL_INTEREST_RATE = systemParams.MIN_ANNUAL_INTEREST_RATE(); + MIN_BOLD_IN_SP = systemParams.MIN_BOLD_IN_SP(); // Give some Coll to test accounts, and approve it to BorrowerOperations uint256 initialCollAmount = 10_000e18; diff --git a/contracts/test/multicollateral.t.sol b/contracts/test/multicollateral.t.sol index 1b8bc513d..b95be7829 100644 --- a/contracts/test/multicollateral.t.sol +++ b/contracts/test/multicollateral.t.sol @@ -113,6 +113,8 @@ contract MulticollateralTest is DevTestSetup { systemParams = contractsArray[0].systemParams; UPFRONT_INTEREST_PERIOD = systemParams.UPFRONT_INTEREST_PERIOD(); URGENT_REDEMPTION_BONUS = systemParams.URGENT_REDEMPTION_BONUS(); + REDEMPTION_FEE_FLOOR = systemParams.REDEMPTION_FEE_FLOOR(); + INITIAL_BASE_RATE = systemParams.INITIAL_BASE_RATE(); } function testMultiCollateralDeployment() public { From 1d7a5d7964255e8632e9a16ff7c072d15521af16 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:19:19 -0500 Subject: [PATCH 34/79] chore: remove comment --- contracts/script/DeployLiquity2.s.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 8b42221c2..6714516bd 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -1,5 +1,3 @@ -// TODO(@bayological): Fix compilation errors with core contracts & replace address registry param usage with sys params - // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.24; From 26db118be909b59cd83cf06651d436b15350125a Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:46:52 -0500 Subject: [PATCH 35/79] chore: remove unused constants --- contracts/src/Dependencies/Constants.sol | 8 -------- 1 file changed, 8 deletions(-) diff --git a/contracts/src/Dependencies/Constants.sol b/contracts/src/Dependencies/Constants.sol index 0c0d6a418..d9f9bef55 100644 --- a/contracts/src/Dependencies/Constants.sol +++ b/contracts/src/Dependencies/Constants.sol @@ -11,11 +11,3 @@ uint256 constant _1pct = DECIMAL_PRECISION / 100; uint256 constant ONE_MINUTE = 1 minutes; uint256 constant ONE_YEAR = 365 days; - - -// TODO(@bayological): Remve this and refactor the tests that use it -// Dummy contract that lets legacy Hardhat tests query some of the constants -contract Constants { - uint256 public constant _ETH_GAS_COMPENSATION = 123; - uint256 public constant _MIN_DEBT = 123; -} From a08b4982b232ad68bcb347aa986c4e60a279a34d Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:28:15 -0500 Subject: [PATCH 36/79] test: update sp invariant tests --- contracts/test/SPInvariants.t.sol | 121 ++--- .../SPInvariantsTestHandler.t.sol | 450 +++++++++--------- 2 files changed, 292 insertions(+), 279 deletions(-) diff --git a/contracts/test/SPInvariants.t.sol b/contracts/test/SPInvariants.t.sol index 181267abe..d84346864 100644 --- a/contracts/test/SPInvariants.t.sol +++ b/contracts/test/SPInvariants.t.sol @@ -1,73 +1,74 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity 0.8.24; +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; -// import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; -// import {IStabilityPool} from "src/Interfaces/IStabilityPool.sol"; -// import {HintHelpers} from "src/HintHelpers.sol"; -// import {Assertions} from "./TestContracts/Assertions.sol"; -// import {BaseInvariantTest} from "./TestContracts/BaseInvariantTest.sol"; -// import {TestDeployer} from "./TestContracts/Deployment.t.sol"; -// import {SPInvariantsTestHandler} from "./TestContracts/SPInvariantsTestHandler.t.sol"; +import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; +import {IStabilityPool} from "src/Interfaces/IStabilityPool.sol"; +import {HintHelpers} from "src/HintHelpers.sol"; +import {Assertions} from "./TestContracts/Assertions.sol"; +import {BaseInvariantTest} from "./TestContracts/BaseInvariantTest.sol"; +import {TestDeployer} from "./TestContracts/Deployment.t.sol"; +import {SPInvariantsTestHandler} from "./TestContracts/SPInvariantsTestHandler.t.sol"; -// abstract contract SPInvariantsBase is Assertions, BaseInvariantTest { -// IStabilityPool stabilityPool; -// SPInvariantsTestHandler handler; +abstract contract SPInvariantsBase is Assertions, BaseInvariantTest { + IStabilityPool stabilityPool; + SPInvariantsTestHandler handler; -// function setUp() public override { -// super.setUp(); + function setUp() public override { + super.setUp(); -// TestDeployer deployer = new TestDeployer(); -// (TestDeployer.LiquityContractsDev memory contracts,, IBoldToken boldToken, HintHelpers hintHelpers,,) = -// deployer.deployAndConnectContracts(); -// stabilityPool = contracts.stabilityPool; + TestDeployer deployer = new TestDeployer(); + (TestDeployer.LiquityContractsDev memory contracts,, IBoldToken boldToken, HintHelpers hintHelpers,,) = + deployer.deployAndConnectContracts(); + stabilityPool = contracts.stabilityPool; -// handler = new SPInvariantsTestHandler( -// SPInvariantsTestHandler.Contracts({ -// boldToken: boldToken, -// borrowerOperations: contracts.borrowerOperations, -// collateralToken: contracts.collToken, -// priceFeed: contracts.priceFeed, -// stabilityPool: contracts.stabilityPool, -// troveManager: contracts.troveManager, -// collSurplusPool: contracts.pools.collSurplusPool -// }), -// hintHelpers -// ); + handler = new SPInvariantsTestHandler( + SPInvariantsTestHandler.Contracts({ + boldToken: boldToken, + borrowerOperations: contracts.borrowerOperations, + collateralToken: contracts.collToken, + priceFeed: contracts.priceFeed, + stabilityPool: contracts.stabilityPool, + troveManager: contracts.troveManager, + collSurplusPool: contracts.pools.collSurplusPool, + systemParams: contracts.systemParams + }), + hintHelpers + ); -// vm.label(address(handler), "handler"); -// targetContract(address(handler)); -// } + vm.label(address(handler), "handler"); + targetContract(address(handler)); + } -// function assert_AllFundsClaimable() internal view { -// uint256 stabilityPoolColl = stabilityPool.getCollBalance(); -// uint256 stabilityPoolBold = stabilityPool.getTotalBoldDeposits(); -// uint256 yieldGainsOwed = stabilityPool.getYieldGainsOwed(); + function assert_AllFundsClaimable() internal view { + uint256 stabilityPoolColl = stabilityPool.getCollBalance(); + uint256 stabilityPoolBold = stabilityPool.getTotalBoldDeposits(); + uint256 yieldGainsOwed = stabilityPool.getYieldGainsOwed(); -// uint256 claimableColl = 0; -// uint256 claimableBold = 0; -// uint256 sumYieldGains = 0; + uint256 claimableColl = 0; + uint256 claimableBold = 0; + uint256 sumYieldGains = 0; -// for (uint256 i = 0; i < actors.length; ++i) { -// claimableColl += stabilityPool.getDepositorCollGain(actors[i].account); -// claimableBold += stabilityPool.getCompoundedBoldDeposit(actors[i].account); -// sumYieldGains += stabilityPool.getDepositorYieldGain(actors[i].account); -// } + for (uint256 i = 0; i < actors.length; ++i) { + claimableColl += stabilityPool.getDepositorCollGain(actors[i].account); + claimableBold += stabilityPool.getCompoundedBoldDeposit(actors[i].account); + sumYieldGains += stabilityPool.getDepositorYieldGain(actors[i].account); + } -// // These tolerances might seem quite loose, but we have to consider -// // that we're dealing with quintillions of BOLD in this test -// assertGeDecimal(stabilityPoolColl, claimableColl, 18, "SP coll insolvency"); -// assertApproxEqAbsRelDecimal(stabilityPoolColl, claimableColl, 1e-5 ether, 1, 18, "SP coll loss"); + // These tolerances might seem quite loose, but we have to consider + // that we're dealing with quintillions of BOLD in this test + assertGeDecimal(stabilityPoolColl, claimableColl, 18, "SP coll insolvency"); + assertApproxEqAbsRelDecimal(stabilityPoolColl, claimableColl, 1e-5 ether, 1, 18, "SP coll loss"); -// assertGeDecimal(stabilityPoolBold, claimableBold, 18, "SP BOLD insolvency"); -// assertApproxEqAbsRelDecimal(stabilityPoolBold, claimableBold, 1e-7 ether, 1, 18, "SP BOLD loss"); + assertGeDecimal(stabilityPoolBold, claimableBold, 18, "SP BOLD insolvency"); + assertApproxEqAbsRelDecimal(stabilityPoolBold, claimableBold, 1e-7 ether, 1, 18, "SP BOLD loss"); -// assertGeDecimal(yieldGainsOwed, sumYieldGains, 18, "SP yield insolvency"); -// assertApproxEqAbsRelDecimal(yieldGainsOwed, sumYieldGains, 1 ether, 1, 18, "SP yield loss"); -// } -// } + assertGeDecimal(yieldGainsOwed, sumYieldGains, 18, "SP yield insolvency"); + assertApproxEqAbsRelDecimal(yieldGainsOwed, sumYieldGains, 1 ether, 1, 18, "SP yield loss"); + } +} -// contract SPInvariantsTest is SPInvariantsBase { -// function invariant_AllFundsClaimable() external view { -// assert_AllFundsClaimable(); -// } -// } +contract SPInvariantsTest is SPInvariantsBase { + function invariant_AllFundsClaimable() external view { + assert_AllFundsClaimable(); + } +} diff --git a/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol b/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol index 70a06228f..6c5dd2d26 100644 --- a/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol +++ b/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol @@ -1,219 +1,231 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity 0.8.24; - -// import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -// import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; -// import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; -// import {IStabilityPool} from "src/Interfaces/IStabilityPool.sol"; -// import {ITroveManager} from "src/Interfaces/ITroveManager.sol"; -// import {ICollSurplusPool} from "src/Interfaces/ICollSurplusPool.sol"; -// import {HintHelpers} from "src/HintHelpers.sol"; -// import {IPriceFeedTestnet} from "./Interfaces/IPriceFeedTestnet.sol"; -// import {ITroveManagerTester} from "./Interfaces/ITroveManagerTester.sol"; -// import {LiquityMath} from "src/Dependencies/LiquityMath.sol"; -// import {mulDivCeil} from "../Utils/Math.sol"; -// import {StringFormatting} from "../Utils/StringFormatting.sol"; -// import {TroveId} from "../Utils/TroveId.sol"; -// import {BaseHandler} from "./BaseHandler.sol"; - -// import { -// DECIMAL_PRECISION, -// _1pct, -// _100pct, -// ETH_GAS_COMPENSATION, -// COLL_GAS_COMPENSATION_DIVISOR, -// MIN_ANNUAL_INTEREST_RATE, -// MIN_BOLD_IN_SP -// } from "src/Dependencies/Constants.sol"; - -// using {mulDivCeil} for uint256; - -// // Test parameters -// uint256 constant OPEN_TROVE_BORROWED_MIN = 2_000 ether; -// uint256 constant OPEN_TROVE_BORROWED_MAX = 100e18 ether; -// uint256 constant OPEN_TROVE_ICR = 1.5 ether; // CCR -// uint256 constant LIQUIDATION_ICR = MCR - _1pct; - -// // Universal constants -// uint256 constant MCR = 1.1 ether; - -// contract SPInvariantsTestHandler is BaseHandler, TroveId { -// using StringFormatting for uint256; - -// struct Contracts { -// IBoldToken boldToken; -// IBorrowerOperations borrowerOperations; -// IERC20 collateralToken; -// IPriceFeedTestnet priceFeed; -// IStabilityPool stabilityPool; -// ITroveManagerTester troveManager; -// ICollSurplusPool collSurplusPool; -// } - -// IBoldToken immutable boldToken; -// IBorrowerOperations immutable borrowerOperations; -// IERC20 collateralToken; -// IPriceFeedTestnet immutable priceFeed; -// IStabilityPool immutable stabilityPool; -// ITroveManagerTester immutable troveManager; -// ICollSurplusPool immutable collSurplusPool; -// HintHelpers immutable hintHelpers; - -// uint256 immutable initialPrice; -// mapping(address owner => uint256) troveIndexOf; - -// // Ghost variables -// uint256 myBold = 0; -// uint256 spBold = 0; -// uint256 spColl = 0; - -// // Fixtures -// uint256[] fixtureDeposited; - -// constructor(Contracts memory contracts, HintHelpers hintHelpers_) { -// boldToken = contracts.boldToken; -// borrowerOperations = contracts.borrowerOperations; -// collateralToken = contracts.collateralToken; -// priceFeed = contracts.priceFeed; -// stabilityPool = contracts.stabilityPool; -// troveManager = contracts.troveManager; -// collSurplusPool = contracts.collSurplusPool; -// hintHelpers = hintHelpers_; - -// initialPrice = priceFeed.getPrice(); -// } - -// function openTrove(uint256 borrowed) external returns (uint256 debt) { -// uint256 i = troveIndexOf[msg.sender]; -// vm.assume(troveManager.getTroveStatus(addressToTroveId(msg.sender, i)) != ITroveManager.Status.active); - -// borrowed = _bound(borrowed, OPEN_TROVE_BORROWED_MIN, OPEN_TROVE_BORROWED_MAX); -// uint256 price = priceFeed.getPrice(); -// debt = borrowed + hintHelpers.predictOpenTroveUpfrontFee(0, borrowed, MIN_ANNUAL_INTEREST_RATE); -// uint256 coll = debt.mulDivCeil(OPEN_TROVE_ICR, price); -// assertEqDecimal(coll * price / debt, OPEN_TROVE_ICR, 18, "Wrong ICR"); - -// info("coll = ", coll.decimal(), ", debt = ", debt.decimal()); -// logCall("openTrove", borrowed.decimal()); - -// deal(address(collateralToken), msg.sender, coll + ETH_GAS_COMPENSATION); -// vm.prank(msg.sender); -// collateralToken.approve(address(borrowerOperations), coll + ETH_GAS_COMPENSATION); -// vm.prank(msg.sender); -// uint256 troveId = borrowerOperations.openTrove( -// msg.sender, -// i, -// coll, -// borrowed, -// 0, -// 0, -// MIN_ANNUAL_INTEREST_RATE, -// type(uint256).max, -// address(0), -// address(0), -// address(0) -// ); -// (uint256 actualDebt,,,,) = troveManager.getEntireDebtAndColl(troveId); -// assertEqDecimal(debt, actualDebt, 18, "Wrong debt"); - -// // Sweep funds -// vm.prank(msg.sender); -// boldToken.transfer(address(this), borrowed); -// assertEqDecimal(boldToken.balanceOf(msg.sender), 0, 18, "Incomplete BOLD sweep"); -// myBold += borrowed; - -// // Use these interesting values as SP deposit amounts later -// fixtureDeposited.push(debt); -// fixtureDeposited.push(debt + debt / DECIMAL_PRECISION + 1); // See https://github.com/liquity/dev/security/advisories/GHSA-m9f3-hrx8-x2g3 -// } - -// function provideToSp(uint256 deposited, bool useFixture) external { -// vm.assume(myBold > 0); - -// uint256 collBefore = collateralToken.balanceOf(msg.sender); -// uint256 collGain = stabilityPool.getDepositorCollGain(msg.sender); -// uint256 boldGain = stabilityPool.getDepositorYieldGainWithPending(msg.sender); - -// // Poor man's fixturing, because Foundry's fixtures don't seem to work under invariant testing -// if (useFixture && fixtureDeposited.length > 0) { -// info("pulling `deposited` from fixture"); -// deposited = fixtureDeposited[_bound(deposited, 0, fixtureDeposited.length - 1)]; -// } - -// deposited = _bound(deposited, 1, myBold); - -// logCall("provideToSp", deposited.decimal(), "false"); - -// boldToken.transfer(msg.sender, deposited); -// vm.prank(msg.sender); -// // Provide to SP and claim Coll and BOLD gains -// stabilityPool.provideToSP(deposited, true); - -// info("totalBoldDeposits = ", stabilityPool.getTotalBoldDeposits().decimal()); -// _log(); - -// uint256 collAfter = collateralToken.balanceOf(msg.sender); -// assertEqDecimal(collAfter, collBefore + collGain, 18, "Wrong Coll gain"); - -// // Sweep BOLD gain -// vm.prank(msg.sender); -// boldToken.transfer(address(this), boldGain); -// assertEqDecimal(boldToken.balanceOf(msg.sender), 0, 18, "Incomplete BOLD sweep"); -// myBold += boldGain; - -// myBold -= deposited; -// spBold += deposited; -// spColl -= collGain; - -// assertEqDecimal(spBold, stabilityPool.getTotalBoldDeposits(), 18, "Wrong SP BOLD balance"); -// assertEqDecimal(spColl, stabilityPool.getCollBalance(), 18, "Wrong SP Coll balance"); -// } - -// function liquidateMe() external { -// vm.assume(troveManager.getTroveIdsCount() > 1); -// uint256 troveId = addressToTroveId(msg.sender, troveIndexOf[msg.sender]); -// vm.assume(troveManager.getTroveStatus(troveId) == ITroveManager.Status.active); - -// (uint256 debt, uint256 coll,,,) = troveManager.getEntireDebtAndColl(troveId); -// vm.assume(debt <= (spBold > MIN_BOLD_IN_SP ? spBold - MIN_BOLD_IN_SP : 0)); // only interested in SP offset, no redistribution - -// logCall("liquidateMe"); - -// priceFeed.setPrice(initialPrice * LIQUIDATION_ICR / OPEN_TROVE_ICR); - -// uint256 collBefore = collateralToken.balanceOf(address(this)); -// uint256 accountSurplusBefore = collSurplusPool.getCollateral(msg.sender); -// uint256 totalBoldDeposits = stabilityPool.getTotalBoldDeposits(); -// uint256 boldInSPForOffsets = totalBoldDeposits - LiquityMath._min(MIN_BOLD_IN_SP, totalBoldDeposits); -// uint256 collCompensation = troveManager.getCollGasCompensation(coll, debt, boldInSPForOffsets); -// // Calc claimable coll based on the remaining coll to liquidate, less the liq. penalty that goes to the SP depositors -// uint256 seizedColl = debt * (_100pct + troveManager.get_LIQUIDATION_PENALTY_SP()) / priceFeed.getPrice(); -// // The Trove owner bears the gas compensation costs -// uint256 claimableColl = coll - seizedColl - collCompensation; - -// troveManager.liquidate(troveId); - -// priceFeed.setPrice(initialPrice); - -// info("totalBoldDeposits = ", stabilityPool.getTotalBoldDeposits().decimal()); -// info("P = ", stabilityPool.P().decimal()); -// _log(); - -// uint256 collAfter = collateralToken.balanceOf(address(this)); -// uint256 accountSurplusAfter = collSurplusPool.getCollateral(msg.sender); -// // Check liquidator got the compensation -// // This is first branch, so coll token is WETH (used for ETH liquidation reserve) -// assertEqDecimal(collAfter, collBefore + collCompensation + ETH_GAS_COMPENSATION, 18, "Wrong Coll compensation"); -// // Check claimable coll surplus is correct -// uint256 accountSurplusDelta = accountSurplusAfter - accountSurplusBefore; -// assertEqDecimal(accountSurplusDelta, claimableColl, 18, "Wrong account surplus"); - -// ++troveIndexOf[msg.sender]; - -// spBold -= debt; -// spColl += coll - claimableColl - collCompensation; - -// assertEqDecimal(spBold, stabilityPool.getTotalBoldDeposits(), 18, "Wrong SP BOLD balance"); -// assertEqDecimal(spColl, stabilityPool.getCollBalance(), 18, "Wrong SP Coll balance"); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; +import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; +import {IStabilityPool} from "src/Interfaces/IStabilityPool.sol"; +import {ITroveManager} from "src/Interfaces/ITroveManager.sol"; +import {ICollSurplusPool} from "src/Interfaces/ICollSurplusPool.sol"; +import {HintHelpers} from "src/HintHelpers.sol"; +import {IPriceFeedTestnet} from "./Interfaces/IPriceFeedTestnet.sol"; +import {ITroveManagerTester} from "./Interfaces/ITroveManagerTester.sol"; +import {LiquityMath} from "src/Dependencies/LiquityMath.sol"; +import {ISystemParams} from "src/Interfaces/ISystemParams.sol"; +import {mulDivCeil} from "../Utils/Math.sol"; +import {StringFormatting} from "../Utils/StringFormatting.sol"; +import {TroveId} from "../Utils/TroveId.sol"; +import {BaseHandler} from "./BaseHandler.sol"; + +import { + DECIMAL_PRECISION, + _1pct, + _100pct +} from "src/Dependencies/Constants.sol"; + +using {mulDivCeil} for uint256; + +// Test parameters +uint256 constant OPEN_TROVE_BORROWED_MIN = 2_000 ether; +uint256 constant OPEN_TROVE_BORROWED_MAX = 100e18 ether; +uint256 constant OPEN_TROVE_ICR = 1.5 ether; // CCR +uint256 constant LIQUIDATION_ICR = MCR - _1pct; + +// Universal constants +uint256 constant MCR = 1.1 ether; + +contract SPInvariantsTestHandler is BaseHandler, TroveId { + using StringFormatting for uint256; + + struct Contracts { + IBoldToken boldToken; + IBorrowerOperations borrowerOperations; + IERC20 collateralToken; + IPriceFeedTestnet priceFeed; + IStabilityPool stabilityPool; + ITroveManagerTester troveManager; + ICollSurplusPool collSurplusPool; + ISystemParams systemParams; + } + + IBoldToken immutable boldToken; + IBorrowerOperations immutable borrowerOperations; + IERC20 collateralToken; + IPriceFeedTestnet immutable priceFeed; + IStabilityPool immutable stabilityPool; + ITroveManagerTester immutable troveManager; + ICollSurplusPool immutable collSurplusPool; + ISystemParams immutable systemParams; + HintHelpers immutable hintHelpers; + + uint256 immutable initialPrice; + mapping(address owner => uint256) troveIndexOf; + + // System params + uint256 immutable ETH_GAS_COMPENSATION; + uint256 immutable MIN_ANNUAL_INTEREST_RATE; + uint256 immutable MIN_BOLD_IN_SP; + uint256 immutable COLL_GAS_COMPENSATION_DIVISOR; + + // Ghost variables + uint256 myBold = 0; + uint256 spBold = 0; + uint256 spColl = 0; + + // Fixtures + uint256[] fixtureDeposited; + + constructor(Contracts memory contracts, HintHelpers hintHelpers_) { + boldToken = contracts.boldToken; + borrowerOperations = contracts.borrowerOperations; + collateralToken = contracts.collateralToken; + priceFeed = contracts.priceFeed; + stabilityPool = contracts.stabilityPool; + troveManager = contracts.troveManager; + collSurplusPool = contracts.collSurplusPool; + systemParams = contracts.systemParams; + hintHelpers = hintHelpers_; + + initialPrice = priceFeed.getPrice(); + + // Initialize system params + ETH_GAS_COMPENSATION = systemParams.ETH_GAS_COMPENSATION(); + MIN_ANNUAL_INTEREST_RATE = systemParams.MIN_ANNUAL_INTEREST_RATE(); + MIN_BOLD_IN_SP = systemParams.MIN_BOLD_IN_SP(); + COLL_GAS_COMPENSATION_DIVISOR = systemParams.COLL_GAS_COMPENSATION_DIVISOR(); + } + + function openTrove(uint256 borrowed) external returns (uint256 debt) { + uint256 i = troveIndexOf[msg.sender]; + vm.assume(troveManager.getTroveStatus(addressToTroveId(msg.sender, i)) != ITroveManager.Status.active); + + borrowed = _bound(borrowed, OPEN_TROVE_BORROWED_MIN, OPEN_TROVE_BORROWED_MAX); + uint256 price = priceFeed.getPrice(); + debt = borrowed + hintHelpers.predictOpenTroveUpfrontFee(0, borrowed, MIN_ANNUAL_INTEREST_RATE); + uint256 coll = debt.mulDivCeil(OPEN_TROVE_ICR, price); + assertEqDecimal(coll * price / debt, OPEN_TROVE_ICR, 18, "Wrong ICR"); + + info("coll = ", coll.decimal(), ", debt = ", debt.decimal()); + logCall("openTrove", borrowed.decimal()); + + deal(address(collateralToken), msg.sender, coll + ETH_GAS_COMPENSATION); + vm.prank(msg.sender); + collateralToken.approve(address(borrowerOperations), coll + ETH_GAS_COMPENSATION); + vm.prank(msg.sender); + uint256 troveId = borrowerOperations.openTrove( + msg.sender, + i, + coll, + borrowed, + 0, + 0, + MIN_ANNUAL_INTEREST_RATE, + type(uint256).max, + address(0), + address(0), + address(0) + ); + (uint256 actualDebt,,,,) = troveManager.getEntireDebtAndColl(troveId); + assertEqDecimal(debt, actualDebt, 18, "Wrong debt"); + + // Sweep funds + vm.prank(msg.sender); + boldToken.transfer(address(this), borrowed); + assertEqDecimal(boldToken.balanceOf(msg.sender), 0, 18, "Incomplete BOLD sweep"); + myBold += borrowed; + + // Use these interesting values as SP deposit amounts later + fixtureDeposited.push(debt); + fixtureDeposited.push(debt + debt / DECIMAL_PRECISION + 1); // See https://github.com/liquity/dev/security/advisories/GHSA-m9f3-hrx8-x2g3 + } + + function provideToSp(uint256 deposited, bool useFixture) external { + vm.assume(myBold > 0); + + uint256 collBefore = collateralToken.balanceOf(msg.sender); + uint256 collGain = stabilityPool.getDepositorCollGain(msg.sender); + uint256 boldGain = stabilityPool.getDepositorYieldGainWithPending(msg.sender); + + // Poor man's fixturing, because Foundry's fixtures don't seem to work under invariant testing + if (useFixture && fixtureDeposited.length > 0) { + info("pulling `deposited` from fixture"); + deposited = fixtureDeposited[_bound(deposited, 0, fixtureDeposited.length - 1)]; + } + + deposited = _bound(deposited, 1, myBold); + + logCall("provideToSp", deposited.decimal(), "false"); + + boldToken.transfer(msg.sender, deposited); + vm.prank(msg.sender); + // Provide to SP and claim Coll and BOLD gains + stabilityPool.provideToSP(deposited, true); + + info("totalBoldDeposits = ", stabilityPool.getTotalBoldDeposits().decimal()); + _log(); + + uint256 collAfter = collateralToken.balanceOf(msg.sender); + assertEqDecimal(collAfter, collBefore + collGain, 18, "Wrong Coll gain"); + + // Sweep BOLD gain + vm.prank(msg.sender); + boldToken.transfer(address(this), boldGain); + assertEqDecimal(boldToken.balanceOf(msg.sender), 0, 18, "Incomplete BOLD sweep"); + myBold += boldGain; + + myBold -= deposited; + spBold += deposited; + spColl -= collGain; + + assertEqDecimal(spBold, stabilityPool.getTotalBoldDeposits(), 18, "Wrong SP BOLD balance"); + assertEqDecimal(spColl, stabilityPool.getCollBalance(), 18, "Wrong SP Coll balance"); + } + + function liquidateMe() external { + vm.assume(troveManager.getTroveIdsCount() > 1); + uint256 troveId = addressToTroveId(msg.sender, troveIndexOf[msg.sender]); + vm.assume(troveManager.getTroveStatus(troveId) == ITroveManager.Status.active); + + (uint256 debt, uint256 coll,,,) = troveManager.getEntireDebtAndColl(troveId); + vm.assume(debt <= (spBold > MIN_BOLD_IN_SP ? spBold - MIN_BOLD_IN_SP : 0)); // only interested in SP offset, no redistribution + + logCall("liquidateMe"); + + priceFeed.setPrice(initialPrice * LIQUIDATION_ICR / OPEN_TROVE_ICR); + + uint256 collBefore = collateralToken.balanceOf(address(this)); + uint256 accountSurplusBefore = collSurplusPool.getCollateral(msg.sender); + uint256 totalBoldDeposits = stabilityPool.getTotalBoldDeposits(); + uint256 boldInSPForOffsets = totalBoldDeposits - LiquityMath._min(MIN_BOLD_IN_SP, totalBoldDeposits); + uint256 collCompensation = troveManager.getCollGasCompensation(coll, debt, boldInSPForOffsets); + // Calc claimable coll based on the remaining coll to liquidate, less the liq. penalty that goes to the SP depositors + uint256 seizedColl = debt * (_100pct + troveManager.get_LIQUIDATION_PENALTY_SP()) / priceFeed.getPrice(); + // The Trove owner bears the gas compensation costs + uint256 claimableColl = coll - seizedColl - collCompensation; + + troveManager.liquidate(troveId); + + priceFeed.setPrice(initialPrice); + + info("totalBoldDeposits = ", stabilityPool.getTotalBoldDeposits().decimal()); + info("P = ", stabilityPool.P().decimal()); + _log(); + + uint256 collAfter = collateralToken.balanceOf(address(this)); + uint256 accountSurplusAfter = collSurplusPool.getCollateral(msg.sender); + // Check liquidator got the compensation + // This is first branch, so coll token is WETH (used for ETH liquidation reserve) + assertEqDecimal(collAfter, collBefore + collCompensation + ETH_GAS_COMPENSATION, 18, "Wrong Coll compensation"); + // Check claimable coll surplus is correct + uint256 accountSurplusDelta = accountSurplusAfter - accountSurplusBefore; + assertEqDecimal(accountSurplusDelta, claimableColl, 18, "Wrong account surplus"); + + ++troveIndexOf[msg.sender]; + + spBold -= debt; + spColl += coll - claimableColl - collCompensation; + + assertEqDecimal(spBold, stabilityPool.getTotalBoldDeposits(), 18, "Wrong SP BOLD balance"); + assertEqDecimal(spColl, stabilityPool.getCollBalance(), 18, "Wrong SP Coll balance"); + } +} From e7b441c37ffeff4072a976b7b15742308aa0edff Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:32:08 -0500 Subject: [PATCH 37/79] test: system params contract --- contracts/test/systemParams.t.sol | 550 ++++++++++++++++++++++++++++++ 1 file changed, 550 insertions(+) create mode 100644 contracts/test/systemParams.t.sol diff --git a/contracts/test/systemParams.t.sol b/contracts/test/systemParams.t.sol new file mode 100644 index 000000000..c5398926d --- /dev/null +++ b/contracts/test/systemParams.t.sol @@ -0,0 +1,550 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./TestContracts/DevTestSetup.sol"; +import {SystemParams} from "../src/SystemParams.sol"; +import {ISystemParams} from "../src/Interfaces/ISystemParams.sol"; + +contract SystemParamsTest is DevTestSetup { + address owner; + address nonOwner; + + event VersionUpdated(uint256 oldVersion, uint256 newVersion); + event MinDebtUpdated(uint256 oldMinDebt, uint256 newMinDebt); + event LiquidationParamsUpdated( + uint256 minLiquidationPenaltySP, + uint256 maxLiquidationPenaltyRedistribution, + uint256 liquidationPenaltySP, + uint256 liquidationPenaltyRedistribution + ); + event GasCompParamsUpdated( + uint256 collGasCompensationDivisor, + uint256 collGasCompensationCap, + uint256 ethGasCompensation + ); + event CollateralParamsUpdated(uint256 ccr, uint256 scr, uint256 mcr, uint256 bcr); + event InterestParamsUpdated( + uint256 minAnnualInterestRate, + uint256 maxAnnualInterestRate, + uint128 maxAnnualBatchManagementFee, + uint256 upfrontInterestPeriod, + uint256 interestRateAdjCooldown, + uint128 minInterestRateChangePeriod, + uint256 maxBatchSharesRatio + ); + event RedemptionParamsUpdated( + uint256 redemptionFeeFloor, + uint256 initialBaseRate, + uint256 redemptionMinuteDecayFactor, + uint256 redemptionBeta, + uint256 urgentRedemptionBonus + ); + event StabilityPoolParamsUpdated(uint256 spYieldSplit, uint256 minBoldInSP); + + function setUp() public override { + super.setUp(); + owner = SystemParams(address(systemParams)).owner(); + nonOwner = address(0x1234); + } + + function testInitialization() public { + SystemParams freshParams = new SystemParams(false); + freshParams.initialize(owner); + + // Check debt parameters + assertEq(freshParams.MIN_DEBT(), 2000e18); + + // Check liquidation parameters + assertEq(freshParams.MIN_LIQUIDATION_PENALTY_SP(), 5 * _1pct); + assertEq(freshParams.MAX_LIQUIDATION_PENALTY_REDISTRIBUTION(), 20 * _1pct); + assertEq(freshParams.LIQUIDATION_PENALTY_SP(), 5e16); + assertEq(freshParams.LIQUIDATION_PENALTY_REDISTRIBUTION(), 10e16); + + // Check gas compensation parameters + assertEq(freshParams.COLL_GAS_COMPENSATION_DIVISOR(), 200); + assertEq(freshParams.COLL_GAS_COMPENSATION_CAP(), 2 ether); + assertEq(freshParams.ETH_GAS_COMPENSATION(), 0.0375 ether); + + // Check collateral parameters + assertEq(freshParams.CCR(), 150 * _1pct); + assertEq(freshParams.SCR(), 110 * _1pct); + assertEq(freshParams.MCR(), 110 * _1pct); + assertEq(freshParams.BCR(), 10 * _1pct); + + // Check interest parameters + assertEq(freshParams.MIN_ANNUAL_INTEREST_RATE(), _1pct / 2); + assertEq(freshParams.MAX_ANNUAL_INTEREST_RATE(), 250 * _1pct); + assertEq(freshParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE(), uint128(_100pct / 10)); + assertEq(freshParams.UPFRONT_INTEREST_PERIOD(), 7 days); + assertEq(freshParams.INTEREST_RATE_ADJ_COOLDOWN(), 7 days); + assertEq(freshParams.MIN_INTEREST_RATE_CHANGE_PERIOD(), 1 hours); + assertEq(freshParams.MAX_BATCH_SHARES_RATIO(), 1e9); + + // Check redemption parameters + assertEq(freshParams.REDEMPTION_FEE_FLOOR(), _1pct / 2); + assertEq(freshParams.INITIAL_BASE_RATE(), _100pct); + assertEq(freshParams.REDEMPTION_MINUTE_DECAY_FACTOR(), 998076443575628800); + assertEq(freshParams.REDEMPTION_BETA(), 1); + assertEq(freshParams.URGENT_REDEMPTION_BONUS(), 2 * _1pct); + + // Check stability pool parameters + assertEq(freshParams.SP_YIELD_SPLIT(), 75 * _1pct); + assertEq(freshParams.MIN_BOLD_IN_SP(), 1e18); + + // Check version + assertEq(freshParams.version(), 1); + + // Check ownership + assertEq(freshParams.owner(), owner); + } + + function testDisableInitializers() public { + SystemParams disabledParams = new SystemParams(true); + + vm.expectRevert(); + disabledParams.initialize(owner); + } + + function testUpdateMinDebt() public { + uint256 oldMinDebt = systemParams.MIN_DEBT(); + uint256 newMinDebt = 3000e18; + + vm.prank(owner); + vm.expectEmit(true, true, false, true); + emit MinDebtUpdated(oldMinDebt, newMinDebt); + systemParams.updateMinDebt(newMinDebt); + + assertEq(systemParams.MIN_DEBT(), newMinDebt); + } + + function testUpdateMinDebtRevertsWhenZero() public { + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidMinDebt.selector); + systemParams.updateMinDebt(0); + } + + function testUpdateMinDebtRevertsWhenTooHigh() public { + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidMinDebt.selector); + systemParams.updateMinDebt(10001e18); + } + + function testUpdateLiquidationParams() public { + uint256 minPenaltySP = 6 * _1pct; + uint256 maxPenaltyRedist = 25 * _1pct; + uint256 penaltySP = 7 * _1pct; + uint256 penaltyRedist = 15 * _1pct; + + vm.prank(owner); + vm.expectEmit(true, true, false, true); + emit LiquidationParamsUpdated(minPenaltySP, maxPenaltyRedist, penaltySP, penaltyRedist); + systemParams.updateLiquidationParams(minPenaltySP, maxPenaltyRedist, penaltySP, penaltyRedist); + + assertEq(systemParams.MIN_LIQUIDATION_PENALTY_SP(), minPenaltySP); + assertEq(systemParams.MAX_LIQUIDATION_PENALTY_REDISTRIBUTION(), maxPenaltyRedist); + assertEq(systemParams.LIQUIDATION_PENALTY_SP(), penaltySP); + assertEq(systemParams.LIQUIDATION_PENALTY_REDISTRIBUTION(), penaltyRedist); + } + + function testUpdateLiquidationParamsRevertsSPPenaltyTooLow() public { + uint256 minPenaltySP = 10 * _1pct; + uint256 penaltySP = 5 * _1pct; + + vm.prank(owner); + vm.expectRevert(ISystemParams.SPPenaltyTooLow.selector); + systemParams.updateLiquidationParams(minPenaltySP, 20 * _1pct, penaltySP, 15 * _1pct); + } + + function testUpdateLiquidationParamsRevertsSPPenaltyGtRedist() public { + uint256 penaltySP = 20 * _1pct; + uint256 penaltyRedist = 15 * _1pct; + + vm.prank(owner); + vm.expectRevert(ISystemParams.SPPenaltyGtRedist.selector); + systemParams.updateLiquidationParams(5 * _1pct, 25 * _1pct, penaltySP, penaltyRedist); + } + + function testUpdateLiquidationParamsRevertsRedistPenaltyTooHigh() public { + uint256 maxPenaltyRedist = 20 * _1pct; + uint256 penaltyRedist = 25 * _1pct; + + vm.prank(owner); + vm.expectRevert(ISystemParams.RedistPenaltyTooHigh.selector); + systemParams.updateLiquidationParams(5 * _1pct, maxPenaltyRedist, 10 * _1pct, penaltyRedist); + } + + function testUpdateGasCompParams() public { + uint256 divisor = 300; + uint256 cap = 3 ether; + uint256 ethComp = 0.05 ether; + + vm.prank(owner); + vm.expectEmit(true, true, false, true); + emit GasCompParamsUpdated(divisor, cap, ethComp); + systemParams.updateGasCompParams(divisor, cap, ethComp); + + assertEq(systemParams.COLL_GAS_COMPENSATION_DIVISOR(), divisor); + assertEq(systemParams.COLL_GAS_COMPENSATION_CAP(), cap); + assertEq(systemParams.ETH_GAS_COMPENSATION(), ethComp); + } + + function testUpdateGasCompParamsRevertsInvalidDivisor() public { + // Test zero divisor + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); + systemParams.updateGasCompParams(0, 2 ether, 0.05 ether); + + // Test divisor too high + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); + systemParams.updateGasCompParams(1001, 2 ether, 0.05 ether); + } + + function testUpdateGasCompParamsRevertsInvalidCap() public { + // Test zero cap + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); + systemParams.updateGasCompParams(200, 0, 0.05 ether); + + // Test cap too high + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); + systemParams.updateGasCompParams(200, 11 ether, 0.05 ether); + } + + function testUpdateGasCompParamsRevertsInvalidETHCompensation() public { + // Test zero ETH compensation + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); + systemParams.updateGasCompParams(200, 2 ether, 0); + + // Test ETH compensation too high + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); + systemParams.updateGasCompParams(200, 2 ether, 1.1 ether); + } + + function testUpdateCollateralParams() public { + uint256 ccr = 160 * _1pct; + uint256 scr = 120 * _1pct; + uint256 mcr = 115 * _1pct; + uint256 bcr = 15 * _1pct; + + vm.prank(owner); + vm.expectEmit(true, true, false, true); + emit CollateralParamsUpdated(ccr, scr, mcr, bcr); + systemParams.updateCollateralParams(ccr, scr, mcr, bcr); + + assertEq(systemParams.CCR(), ccr); + assertEq(systemParams.SCR(), scr); + assertEq(systemParams.MCR(), mcr); + assertEq(systemParams.BCR(), bcr); + } + + function testUpdateCollateralParamsRevertsInvalidCCR() public { + // CCR too low + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidCCR.selector); + systemParams.updateCollateralParams(100 * _1pct, 110 * _1pct, 110 * _1pct, 10 * _1pct); + + // CCR too high + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidCCR.selector); + systemParams.updateCollateralParams(200 * _1pct, 110 * _1pct, 110 * _1pct, 10 * _1pct); + } + + function testUpdateCollateralParamsRevertsInvalidMCR() public { + // MCR too low + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidMCR.selector); + systemParams.updateCollateralParams(150 * _1pct, 110 * _1pct, 100 * _1pct, 10 * _1pct); + + // MCR too high + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidMCR.selector); + systemParams.updateCollateralParams(150 * _1pct, 110 * _1pct, 200 * _1pct, 10 * _1pct); + } + + function testUpdateCollateralParamsRevertsInvalidBCR() public { + // BCR too low + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidBCR.selector); + systemParams.updateCollateralParams(150 * _1pct, 110 * _1pct, 110 * _1pct, 4 * _1pct); + + // BCR too high + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidBCR.selector); + systemParams.updateCollateralParams(150 * _1pct, 110 * _1pct, 110 * _1pct, 50 * _1pct); + } + + function testUpdateCollateralParamsRevertsInvalidSCR() public { + // SCR too low + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidSCR.selector); + systemParams.updateCollateralParams(150 * _1pct, 100 * _1pct, 110 * _1pct, 10 * _1pct); + + // SCR too high + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidSCR.selector); + systemParams.updateCollateralParams(150 * _1pct, 200 * _1pct, 110 * _1pct, 10 * _1pct); + } + + function testUpdateInterestParams() public { + uint256 minRate = 1 * _1pct; + uint256 maxRate = 200 * _1pct; + uint128 maxFee = uint128(5 * _1pct); + uint256 upfrontPeriod = 14 days; + uint256 cooldown = 14 days; + uint128 minChangePeriod = 2 hours; + uint256 maxSharesRatio = 2e9; + + vm.prank(owner); + vm.expectEmit(true, true, false, true); + emit InterestParamsUpdated(minRate, maxRate, maxFee, upfrontPeriod, cooldown, minChangePeriod, maxSharesRatio); + systemParams.updateInterestParams(minRate, maxRate, maxFee, upfrontPeriod, cooldown, minChangePeriod, maxSharesRatio); + + assertEq(systemParams.MIN_ANNUAL_INTEREST_RATE(), minRate); + assertEq(systemParams.MAX_ANNUAL_INTEREST_RATE(), maxRate); + assertEq(systemParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE(), maxFee); + assertEq(systemParams.UPFRONT_INTEREST_PERIOD(), upfrontPeriod); + assertEq(systemParams.INTEREST_RATE_ADJ_COOLDOWN(), cooldown); + assertEq(systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD(), minChangePeriod); + assertEq(systemParams.MAX_BATCH_SHARES_RATIO(), maxSharesRatio); + } + + function testUpdateInterestParamsRevertsMinGtMax() public { + vm.prank(owner); + vm.expectRevert(ISystemParams.MinInterestRateGtMax.selector); + systemParams.updateInterestParams( + 200 * _1pct, // min > max + 100 * _1pct, + uint128(5 * _1pct), + 7 days, + 7 days, + 1 hours, + 1e9 + ); + } + + function testUpdateInterestParamsRevertsMaxTooHigh() public { + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidInterestRateBounds.selector); + systemParams.updateInterestParams( + 1 * _1pct, + 1001 * _1pct, // > 1000% + uint128(5 * _1pct), + 7 days, + 7 days, + 1 hours, + 1e9 + ); + } + + function testUpdateInterestParamsRevertsInvalidFee() public { + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidFeeValue.selector); + systemParams.updateInterestParams( + 1 * _1pct, + 100 * _1pct, + uint128(101 * _1pct), // > 100% + 7 days, + 7 days, + 1 hours, + 1e9 + ); + } + + function testUpdateInterestParamsRevertsInvalidUpfrontPeriod() public { + // Zero upfront period + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidTimeValue.selector); + systemParams.updateInterestParams(1 * _1pct, 100 * _1pct, uint128(5 * _1pct), 0, 7 days, 1 hours, 1e9); + + // Upfront period too long + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidTimeValue.selector); + systemParams.updateInterestParams(1 * _1pct, 100 * _1pct, uint128(5 * _1pct), 366 days, 7 days, 1 hours, 1e9); + } + + function testUpdateInterestParamsRevertsInvalidCooldown() public { + // Zero cooldown + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidTimeValue.selector); + systemParams.updateInterestParams(1 * _1pct, 100 * _1pct, uint128(5 * _1pct), 7 days, 0, 1 hours, 1e9); + + // Cooldown too long + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidTimeValue.selector); + systemParams.updateInterestParams(1 * _1pct, 100 * _1pct, uint128(5 * _1pct), 7 days, 366 days, 1 hours, 1e9); + } + + function testUpdateInterestParamsRevertsInvalidChangePeriod() public { + // Zero change period + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidTimeValue.selector); + systemParams.updateInterestParams(1 * _1pct, 100 * _1pct, uint128(5 * _1pct), 7 days, 7 days, 0, 1e9); + + // Change period too long + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidTimeValue.selector); + systemParams.updateInterestParams(1 * _1pct, 100 * _1pct, uint128(5 * _1pct), 7 days, 7 days, 31 days, 1e9); + } + + function testUpdateRedemptionParams() public { + uint256 feeFloor = 1 * _1pct; + uint256 baseRate = 50 * _1pct; + uint256 decayFactor = 999000000000000000; + uint256 beta = 2; + uint256 urgentBonus = 3 * _1pct; + + vm.prank(owner); + vm.expectEmit(true, true, false, true); + emit RedemptionParamsUpdated(feeFloor, baseRate, decayFactor, beta, urgentBonus); + systemParams.updateRedemptionParams(feeFloor, baseRate, decayFactor, beta, urgentBonus); + + assertEq(systemParams.REDEMPTION_FEE_FLOOR(), feeFloor); + assertEq(systemParams.INITIAL_BASE_RATE(), baseRate); + assertEq(systemParams.REDEMPTION_MINUTE_DECAY_FACTOR(), decayFactor); + assertEq(systemParams.REDEMPTION_BETA(), beta); + assertEq(systemParams.URGENT_REDEMPTION_BONUS(), urgentBonus); + } + + function testUpdateRedemptionParamsRevertsInvalidFeeFloor() public { + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidFeeValue.selector); + systemParams.updateRedemptionParams(101 * _1pct, 50 * _1pct, 999000000000000000, 1, 2 * _1pct); + } + + function testUpdateRedemptionParamsRevertsInvalidBaseRate() public { + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidFeeValue.selector); + systemParams.updateRedemptionParams(1 * _1pct, 1001 * _1pct, 999000000000000000, 1, 2 * _1pct); + } + + function testUpdateRedemptionParamsRevertsInvalidUrgentBonus() public { + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidFeeValue.selector); + systemParams.updateRedemptionParams(1 * _1pct, 50 * _1pct, 999000000000000000, 1, 101 * _1pct); + } + + function testUpdatePoolParams() public { + uint256 yieldSplit = 80 * _1pct; + uint256 minBold = 10e18; + + vm.prank(owner); + vm.expectEmit(true, true, false, true); + emit StabilityPoolParamsUpdated(yieldSplit, minBold); + systemParams.updatePoolParams(yieldSplit, minBold); + + assertEq(systemParams.SP_YIELD_SPLIT(), yieldSplit); + assertEq(systemParams.MIN_BOLD_IN_SP(), minBold); + } + + function testUpdatePoolParamsRevertsInvalidYieldSplit() public { + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidFeeValue.selector); + systemParams.updatePoolParams(101 * _1pct, 1e18); + } + + function testUpdatePoolParamsRevertsZeroMinBold() public { + vm.prank(owner); + vm.expectRevert(ISystemParams.InvalidMinDebt.selector); + systemParams.updatePoolParams(75 * _1pct, 0); + } + + function testUpdateVersion() public { + uint256 oldVersion = systemParams.version(); + uint256 newVersion = 2; + + vm.prank(owner); + vm.expectEmit(true, true, false, true); + emit VersionUpdated(oldVersion, newVersion); + systemParams.updateVersion(newVersion); + + assertEq(systemParams.version(), newVersion); + } + + function testOnlyOwnerCanUpdate() public { + vm.startPrank(nonOwner); + + // Test all update functions revert for non-owner + vm.expectRevert("Ownable: caller is not the owner"); + systemParams.updateMinDebt(3000e18); + + vm.expectRevert("Ownable: caller is not the owner"); + systemParams.updateLiquidationParams(6 * _1pct, 25 * _1pct, 7 * _1pct, 15 * _1pct); + + vm.expectRevert("Ownable: caller is not the owner"); + systemParams.updateGasCompParams(300, 3 ether, 0.05 ether); + + vm.expectRevert("Ownable: caller is not the owner"); + systemParams.updateCollateralParams(160 * _1pct, 120 * _1pct, 115 * _1pct, 15 * _1pct); + + vm.expectRevert("Ownable: caller is not the owner"); + systemParams.updateInterestParams(1 * _1pct, 200 * _1pct, uint128(5 * _1pct), 14 days, 14 days, 2 hours, 2e9); + + vm.expectRevert("Ownable: caller is not the owner"); + systemParams.updateRedemptionParams(1 * _1pct, 50 * _1pct, 999000000000000000, 2, 3 * _1pct); + + vm.expectRevert("Ownable: caller is not the owner"); + systemParams.updatePoolParams(80 * _1pct, 10e18); + + vm.expectRevert("Ownable: caller is not the owner"); + systemParams.updateVersion(2); + + vm.stopPrank(); + } + + function testOwnerCanTransferOwnership() public { + address newOwner = address(0x5678); + + // Transfer ownership + vm.prank(owner); + SystemParams(address(systemParams)).transferOwnership(newOwner); + assertEq(SystemParams(address(systemParams)).owner(), newOwner); + + // New owner can update + vm.prank(newOwner); + systemParams.updateVersion(3); + assertEq(systemParams.version(), 3); + + // Old owner cannot update + vm.prank(owner); + vm.expectRevert("Ownable: caller is not the owner"); + systemParams.updateVersion(4); + } + + function testParameterBoundaryValues() public { + // Test minimum valid values + vm.prank(owner); + systemParams.updateMinDebt(1); // Just above 0 + assertEq(systemParams.MIN_DEBT(), 1); + + vm.prank(owner); + systemParams.updateMinDebt(10000e18); // Exactly at max + assertEq(systemParams.MIN_DEBT(), 10000e18); + + // Test collateral params at boundaries + vm.prank(owner); + systemParams.updateCollateralParams( + _100pct + 1, // CCR just above 100% + _100pct + 1, // SCR just above 100% + _100pct + 1, // MCR just above 100% + 5 * _1pct // BCR at minimum + ); + assertEq(systemParams.CCR(), _100pct + 1); + assertEq(systemParams.BCR(), 5 * _1pct); + + // Test gas comp at boundaries + vm.prank(owner); + systemParams.updateGasCompParams( + 1, // Min divisor + 10 ether, // Max cap + 1 ether // Max ETH comp + ); + assertEq(systemParams.COLL_GAS_COMPENSATION_DIVISOR(), 1); + assertEq(systemParams.COLL_GAS_COMPENSATION_CAP(), 10 ether); + assertEq(systemParams.ETH_GAS_COMPENSATION(), 1 ether); + } +} \ No newline at end of file From c297ae1b6cd409e87d6576bcdeef31480ad11e53 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Mon, 29 Sep 2025 16:36:40 -0500 Subject: [PATCH 38/79] chore: update deployment --- contracts/script/DeployLiquity2.s.sol | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 6714516bd..49dbc54f7 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -166,7 +166,8 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { function _deployAndConnectContracts() internal returns (DeploymentResult memory r) { _deployProxyInfrastructure(r); _deployStableToken(r); - _deployFPMM(r); + // TODO: Fix FPMM deployment - currently failing + //_deployFPMM(r); _deploySystemParams(r); IAddressesRegistry addressesRegistry = new AddressesRegistry(deployer); @@ -233,9 +234,9 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { } function _deploySystemParams(DeploymentResult memory r) internal { - r.systemParams = ISystemParams( - address(new TransparentUpgradeableProxy(address(r.systemParamsImpl), address(r.proxyAdmin), "")) - ); + address systemParamsProxy = address(new TransparentUpgradeableProxy(address(r.systemParamsImpl), address(r.proxyAdmin), "")); + r.systemParams = ISystemParams(systemParamsProxy); + SystemParams(systemParamsProxy).initialize(deployer); } function _deployAndConnectCollateralContracts( @@ -249,6 +250,7 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { contracts.collToken = _collToken; contracts.addressesRegistry = _addressesRegistry; contracts.priceFeed = _priceFeed; + contracts.systemParams = r.systemParams; // TODO: replace with governance timelock on mainnet contracts.interestRouter = IInterestRouter(0x56fD3F2bEE130e9867942D0F463a16fBE49B8d81); @@ -260,11 +262,13 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { ); assert(address(contracts.metadataNFT) == addresses.metadataNFT); - addresses.borrowerOperations = - _computeCreate2Address(type(BorrowerOperations).creationCode, address(contracts.addressesRegistry)); + addresses.borrowerOperations = vm.computeCreate2Address( + SALT, keccak256(getBytecode(type(BorrowerOperations).creationCode, address(contracts.addressesRegistry), address(contracts.systemParams))) + ); addresses.troveNFT = _computeCreate2Address(type(TroveNFT).creationCode, address(contracts.addressesRegistry)); - addresses.activePool = - _computeCreate2Address(type(ActivePool).creationCode, address(contracts.addressesRegistry)); + addresses.activePool = vm.computeCreate2Address( + SALT, keccak256(getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry), address(contracts.systemParams))) + ); addresses.defaultPool = _computeCreate2Address(type(DefaultPool).creationCode, address(contracts.addressesRegistry)); addresses.gasPool = _computeCreate2Address(type(GasPool).creationCode, address(contracts.addressesRegistry)); From c0ff2a324ac6b8bd8a5efbddd0931f6668f919bc Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:18:18 -0500 Subject: [PATCH 39/79] chore: update file permissions --- contracts/foundry.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 1e830c830..e4eb87f0e 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -10,6 +10,7 @@ fs_permissions = [ { access = "read", path = "./utils/assets/" }, { access = "read-write", path = "./utils/assets/test_output" }, { access = "read-write", path = "./deployment-manifest.json" }, + { access = "read-write", path = "./script/deployment-manifest.json" }, { access = "read", path = "./addresses/" } ] From edab12788796926670b13452dcced2f1aeffbaff Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:20:10 -0500 Subject: [PATCH 40/79] chore: add compute address overload --- contracts/script/DeployLiquity2.s.sol | 32 +++++++++++++++++---------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 49dbc54f7..eee38c77b 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -172,8 +172,8 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { IAddressesRegistry addressesRegistry = new AddressesRegistry(deployer); - address troveManagerAddress = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(TroveManager).creationCode, address(addressesRegistry), address(r.systemParams))) + address troveManagerAddress = _computeCreate2Address( + type(TroveManager).creationCode, address(addressesRegistry), address(r.systemParams) ); IERC20Metadata collToken = IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS); @@ -262,12 +262,12 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { ); assert(address(contracts.metadataNFT) == addresses.metadataNFT); - addresses.borrowerOperations = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(BorrowerOperations).creationCode, address(contracts.addressesRegistry), address(contracts.systemParams))) + addresses.borrowerOperations = _computeCreate2Address( + type(BorrowerOperations).creationCode, address(contracts.addressesRegistry), address(contracts.systemParams) ); addresses.troveNFT = _computeCreate2Address(type(TroveNFT).creationCode, address(contracts.addressesRegistry)); - addresses.activePool = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry), address(contracts.systemParams))) + addresses.activePool = _computeCreate2Address( + type(ActivePool).creationCode, address(contracts.addressesRegistry), address(contracts.systemParams) ); addresses.defaultPool = _computeCreate2Address(type(DefaultPool).creationCode, address(contracts.addressesRegistry)); @@ -372,16 +372,15 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry))); } - function _computeCreate2AddressWithPar(bytes memory creationCode, address _addressesRegistry) + function _computeCreate2Address(bytes memory creationCode, address _addressesRegistry, address _systemParams) internal view returns (address) { - return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry))); + return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry, _systemParams))); } - function _getBranchContractsJson(LiquityContracts memory c) internal view returns (string memory) { return string.concat( "{", @@ -433,18 +432,27 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { branches[0] = _getBranchContractsJson(deployed.contracts); - return string.concat( + string memory part1 = string.concat( "{", string.concat('"constants":', _getDeploymentConstants(deployed.contracts.systemParams), ","), string.concat('"collateralRegistry":"', address(deployed.collateralRegistry).toHexString(), '",'), string.concat('"boldToken":"', address(deployed.stableToken).toHexString(), '",'), - string.concat('"hintHelpers":"', address(deployed.hintHelpers).toHexString(), '",'), + string.concat('"hintHelpers":"', address(deployed.hintHelpers).toHexString(), '",') + ); + + string memory part2 = string.concat( string.concat('"stableTokenV3Impl":"', address(deployed.stableTokenV3Impl).toHexString(), '",'), string.concat('"stabilityPoolImpl":"', address(deployed.stabilityPoolImpl).toHexString(), '",'), - string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",'), + string.concat('"systemParamsImpl":"', address(deployed.systemParamsImpl).toHexString(), '",'), + string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",') + ); + + string memory part3 = string.concat( string.concat('"fpmm":"', address(deployed.fpmm).toHexString(), '",'), string.concat('"branches":[', branches.join(","), "]"), "}" ); + + return string.concat(part1, part2, part3); } } From b2888ab8d8465d8640660d77047cf6da249c36eb Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:37:02 -0500 Subject: [PATCH 41/79] feat: set params via constructor --- contracts/src/Interfaces/ISystemParams.sol | 217 +++----------- contracts/src/SystemParams.sol | 315 ++++++--------------- 2 files changed, 130 insertions(+), 402 deletions(-) diff --git a/contracts/src/Interfaces/ISystemParams.sol b/contracts/src/Interfaces/ISystemParams.sol index 3313f8c43..1ee7a0c42 100644 --- a/contracts/src/Interfaces/ISystemParams.sol +++ b/contracts/src/Interfaces/ISystemParams.sol @@ -3,6 +3,53 @@ pragma solidity 0.8.24; interface ISystemParams { + /* ========== PARAMETER STRUCTS ========== */ + + struct DebtParams { + uint256 minDebt; + } + + struct LiquidationParams { + uint256 liquidationPenaltySP; + uint256 liquidationPenaltyRedistribution; + } + + struct GasCompParams { + uint256 collGasCompensationDivisor; + uint256 collGasCompensationCap; + uint256 ethGasCompensation; + } + + struct CollateralParams { + uint256 ccr; + uint256 scr; + uint256 mcr; + uint256 bcr; + } + + struct InterestParams { + uint256 minAnnualInterestRate; + uint256 maxAnnualInterestRate; + uint128 maxAnnualBatchManagementFee; + uint256 upfrontInterestPeriod; + uint256 interestRateAdjCooldown; + uint128 minInterestRateChangePeriod; + uint256 maxBatchSharesRatio; + } + + struct RedemptionParams { + uint256 redemptionFeeFloor; + uint256 initialBaseRate; + uint256 redemptionMinuteDecayFactor; + uint256 redemptionBeta; + uint256 urgentRedemptionBonus; + } + + struct StabilityPoolParams { + uint256 spYieldSplit; + uint256 minBoldInSP; + } + /* ========== ERRORS ========== */ error InvalidMinDebt(); @@ -19,61 +66,6 @@ interface ISystemParams { error SPPenaltyGtRedist(); error RedistPenaltyTooHigh(); - /* ========== EVENTS ========== */ - - /// @notice Emitted when min debt is updated. - event MinDebtUpdated(uint256 oldMinDebt, uint256 newMinDebt); - - /// @notice Emitted when liquidation parameters are updated. - event LiquidationParamsUpdated( - uint256 minLiquidationPenaltySP, - uint256 maxLiquidationPenaltyRedistribution, - uint256 liquidationPenaltySP, - uint256 liquidationPenaltyRedistribution - ); - - /// @notice Emitted when gas compensation parameters are updated. - event GasCompParamsUpdated( - uint256 collGasCompensationDivisor, - uint256 collGasCompensationCap, - uint256 ethGasCompensation - ); - - /// @notice Emitted when collateral parameters are updated. - event CollateralParamsUpdated( - uint256 ccr, - uint256 scr, - uint256 mcr, - uint256 bcr - ); - - /// @notice Emitted when Interest rate parameters are updated. - event InterestParamsUpdated( - uint256 minAnnualInterestRate, - uint256 maxAnnualInterestRate, - uint128 maxAnnualBatchManagementFee, - uint256 upfrontInterestPeriod, - uint256 interestRateAdjCooldown, - uint128 minInterestRateChangePeriod, - uint256 maxBatchSharesRatio - ); - - /// @notice Emitted when redemption parameters are updated. - event RedemptionParamsUpdated( - uint256 redemptionFeeFloor, - uint256 initialBaseRate, - uint256 redemptionMinuteDecayFactor, - uint256 redemptionBeta, - uint256 urgentRedemptionBonus - ); - - /// @notice Emitted when stability pool parameters are updated. - event StabilityPoolParamsUpdated(uint256 spYieldSplit, uint256 minBoldInSP); - - - /// @notice Emitted when the version is updated. - event VersionUpdated(uint256 oldVersion, uint256 newVersion); - /* ========== DEBT PARAMETERS ========== */ /// @notice Minimum amount of net debt a trove must have. @@ -81,15 +73,6 @@ interface ISystemParams { /* ========== LIQUIDATION PARAMETERS ========== */ - // TODO(@bayological): These min and max params are used to validate - // the penalaties are within expected ranges when - // being set. Do we want these to be configurable? - /// @notice Minimum liquidation penalty for troves offset to the SP - function MIN_LIQUIDATION_PENALTY_SP() external view returns (uint256); - - /// @notice Maximum liquidation penalty for troves redistributed. - function MAX_LIQUIDATION_PENALTY_REDISTRIBUTION() external view returns (uint256); - /// @notice Liquidation penalty for troves offset to the SP function LIQUIDATION_PENALTY_SP() external view returns (uint256); @@ -186,110 +169,4 @@ interface ISystemParams { /// @notice Minimum BOLD that must remain in Stability Pool to prevent complete drainage. function MIN_BOLD_IN_SP() external view returns (uint256); - /* ========== VERSION ========== */ - - /// @notice Returns the version of the system params contract. - function version() external view returns (uint256); - - /* ========== FUNCTIONS ========== */ - - /** - * @notice Update the minimum debt - * @param _minDebt The new minimum debt amount - */ - function updateMinDebt( - uint256 _minDebt - ) external; - - /** - * @notice Update the liquidation params. - * @param _minLiquidationPenaltySP The minimum liquidation penalty for stability pool - * @param _maxLiquidationPenaltyRedistribution The maximum liquidation penalty for redistribution - * @param _liquidationPenaltySP The liquidation penalty for stability pool - * @param _liquidationPenaltyRedistribution The liquidation penalty for redistribution - */ - function updateLiquidationParams( - uint256 _minLiquidationPenaltySP, - uint256 _maxLiquidationPenaltyRedistribution, - uint256 _liquidationPenaltySP, - uint256 _liquidationPenaltyRedistribution - ) external; - - /** - * @notice Update gas compensation parameters. - * @param _collGasCompensationDivisor Collateral gas compensation divisor. - * @param _collGasCompensationCap Collateral gas compensation cap. - * @param _ethGasCompensation Amount of ETH to be locked in gas pool on opening troves. - */ - function updateGasCompParams( - uint256 _collGasCompensationDivisor, - uint256 _collGasCompensationCap, - uint256 _ethGasCompensation - ) external; - - /** - * @notice Update the collateral related parameters. - * @param _ccr The critical collateral ratio. - * @param _scr The shutdown collateral ratio. - * @param _mcr The minimum collateral ratio. - * @param _bcr The base collateral ratio. - */ - function updateCollateralParams( - uint256 _ccr, - uint256 _scr, - uint256 _mcr, - uint256 _bcr - ) external; - - /** - * @notice Update interest related parameters. - * @param _minAnnualInterestRate The minimum annual interest rate - * @param _maxAnnualInterestRate The maximum annual interest rate - * @param _maxAnnualBatchManagementFee The maximum annual batch management fee - * @param _upfrontInterestPeriod The upfront interest period - * @param _interestRateAdjCooldown The interest rate adjustment cooldown - * @param _minInterestRateChangePeriod The minimum interest rate change period - * @param _maxBatchSharesRatio The maximum ratio between batch debt and shares to prevent inflation attacks - */ - function updateInterestParams( - uint256 _minAnnualInterestRate, - uint256 _maxAnnualInterestRate, - uint128 _maxAnnualBatchManagementFee, - uint256 _upfrontInterestPeriod, - uint256 _interestRateAdjCooldown, - uint128 _minInterestRateChangePeriod, - uint256 _maxBatchSharesRatio - ) external; - - /** - * @notice Update redemption related parameters. - * @param _redemptionFeeFloor The min redemption fee percentage - * @param _initialBaseRate The initial base rate - * @param _redemptionMinuteDecayFactor Factor to reduce the redemption fee per minute. - * @param _redemptionBeta The redemption beta - * @param _urgentRedemptionBonus The urgent redemption bonus given to redeemers after shutdownn - */ - function updateRedemptionParams( - uint256 _redemptionFeeFloor, - uint256 _initialBaseRate, - uint256 _redemptionMinuteDecayFactor, - uint256 _redemptionBeta, - uint256 _urgentRedemptionBonus - ) external; - - /** - * @notice Update stability pool related parameters. - * @param _spYieldSplit Percentage of minted interest yield allocated to Stability Pool depositors. - * @param _minBoldInSP Minimum BOLD that must remain in Stability Pool to prevent complete drainage. - */ - function updatePoolParams( - uint256 _spYieldSplit, - uint256 _minBoldInSP - ) external; - - /** - * @notice Update the version of the system params contract. - * @param _newVersion The new version number. - */ - function updateVersion(uint256 _newVersion) external; } diff --git a/contracts/src/SystemParams.sol b/contracts/src/SystemParams.sol index 8d421b0b9..d8800da2e 100644 --- a/contracts/src/SystemParams.sol +++ b/contracts/src/SystemParams.sol @@ -2,9 +2,6 @@ pragma solidity 0.8.24; -import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; -import {Initializable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; - import {ISystemParams} from "./Interfaces/ISystemParams.sol"; import {_100pct, _1pct} from "./Dependencies/Constants.sol"; @@ -13,17 +10,13 @@ import {_100pct, _1pct} from "./Dependencies/Constants.sol"; * @author Mento Labs * @notice This contract manages the system-wide parameters for the protocol. */ -contract SystemParams is Initializable, OwnableUpgradeable, ISystemParams { - uint256 public version; - +contract SystemParams is ISystemParams { /* ========== DEBT PARAMETERS ========== */ uint256 public MIN_DEBT; /* ========== LIQUIDATION PARAMETERS ========== */ - uint256 public MIN_LIQUIDATION_PENALTY_SP; - uint256 public MAX_LIQUIDATION_PENALTY_REDISTRIBUTION; uint256 public LIQUIDATION_PENALTY_SP; uint256 public LIQUIDATION_PENALTY_REDISTRIBUTION; @@ -65,247 +58,105 @@ contract SystemParams is Initializable, OwnableUpgradeable, ISystemParams { /* ========== CONSTRUCTOR ========== */ - /** - * @notice Contract constructor - * @param disable Boolean to disable initializers for implementation contract - */ - constructor(bool disable) { - if (disable) { - _disableInitializers(); - } - } - - /* ========== INITIALIZATION ========== */ - - function initialize(address owner_) public initializer { - __Ownable_init(); - transferOwnership(owner_); - - // Debt parameters - MIN_DEBT = 2000e18; - - // Liquidation parameters - MIN_LIQUIDATION_PENALTY_SP = 5 * _1pct; // 5% - MAX_LIQUIDATION_PENALTY_REDISTRIBUTION = 20 * _1pct; // 20% - LIQUIDATION_PENALTY_SP = 5e16; // 5% - LIQUIDATION_PENALTY_REDISTRIBUTION = 10e16; // 10% - - // Gas compensation parameters - COLL_GAS_COMPENSATION_DIVISOR = 200; // dividing by 200 yields 0.5% - COLL_GAS_COMPENSATION_CAP = 2 ether; // Max coll gas compensation capped at 2 ETH - ETH_GAS_COMPENSATION = 0.0375 ether; - - // Collateral parameters - CCR = 150 * _1pct; // 150% - SCR = 110 * _1pct; // 110% - MCR = 110 * _1pct; // 110% - BCR = 10 * _1pct; // 10% - - // Interest parameters - MIN_ANNUAL_INTEREST_RATE = _1pct / 2; // 0.5% - MAX_ANNUAL_INTEREST_RATE = 250 * _1pct; // 250% - MAX_ANNUAL_BATCH_MANAGEMENT_FEE = uint128(_100pct / 10); // 10% - UPFRONT_INTEREST_PERIOD = 7 days; - INTEREST_RATE_ADJ_COOLDOWN = 7 days; - MIN_INTEREST_RATE_CHANGE_PERIOD = 1 hours; // only applies to batch managers / batched Troves - MAX_BATCH_SHARES_RATIO = 1e9; - - // Redemption parameters - REDEMPTION_FEE_FLOOR = _1pct / 2; // 0.5% - INITIAL_BASE_RATE = _100pct; // 100% initial redemption rate - - // Half-life of 6h. 6h = 360 min - // (1/2) = d^360 => d = (1/2)^(1/360) - REDEMPTION_MINUTE_DECAY_FACTOR = 998076443575628800; - REDEMPTION_BETA = 1; - URGENT_REDEMPTION_BONUS = 2 * _1pct; // 2% - - // Stability pool parameters - SP_YIELD_SPLIT = 75 * _1pct; // 75% - MIN_BOLD_IN_SP = 1e18; - - version = 1; - emit VersionUpdated(0, 1); - } - - /* ========== ADMIN FUNCTIONS ========== */ - - /// @inheritdoc ISystemParams - function updateMinDebt(uint256 _minDebt) external onlyOwner { - if (_minDebt == 0 || _minDebt > 10000e18) revert InvalidMinDebt(); - - uint256 oldMinDebt = MIN_DEBT; - MIN_DEBT = _minDebt; - - emit MinDebtUpdated(oldMinDebt, _minDebt); - } - - /// @inheritdoc ISystemParams - function updateLiquidationParams( - uint256 _minLiquidationPenaltySP, - uint256 _maxLiquidationPenaltyRedistribution, - uint256 _liquidationPenaltySP, - uint256 _liquidationPenaltyRedistribution - ) external onlyOwner { - // TODO: Review checks. - if (_liquidationPenaltySP < _minLiquidationPenaltySP) + constructor( + DebtParams memory _debtParams, + LiquidationParams memory _liquidationParams, + GasCompParams memory _gasCompParams, + CollateralParams memory _collateralParams, + InterestParams memory _interestParams, + RedemptionParams memory _redemptionParams, + StabilityPoolParams memory _poolParams + ) { + // Validate debt parameters + if (_debtParams.minDebt == 0 || _debtParams.minDebt > 10000e18) revert InvalidMinDebt(); + + // Validate liquidation parameters + // Hardcoded validation bounds: MIN_LIQUIDATION_PENALTY_SP = 5%, MAX_LIQUIDATION_PENALTY_REDISTRIBUTION = 20% + if (_liquidationParams.liquidationPenaltySP < 5 * _1pct) revert SPPenaltyTooLow(); - if (_liquidationPenaltySP > _liquidationPenaltyRedistribution) + if (_liquidationParams.liquidationPenaltySP > _liquidationParams.liquidationPenaltyRedistribution) revert SPPenaltyGtRedist(); - if ( - _liquidationPenaltyRedistribution > - _maxLiquidationPenaltyRedistribution - ) revert RedistPenaltyTooHigh(); - - MIN_LIQUIDATION_PENALTY_SP = _minLiquidationPenaltySP; - MAX_LIQUIDATION_PENALTY_REDISTRIBUTION = _maxLiquidationPenaltyRedistribution; - LIQUIDATION_PENALTY_SP = _liquidationPenaltySP; - LIQUIDATION_PENALTY_REDISTRIBUTION = _liquidationPenaltyRedistribution; - - emit LiquidationParamsUpdated( - _minLiquidationPenaltySP, - _maxLiquidationPenaltyRedistribution, - _liquidationPenaltySP, - _liquidationPenaltyRedistribution - ); - } + if (_liquidationParams.liquidationPenaltyRedistribution > 20 * _1pct) + revert RedistPenaltyTooHigh(); - /// @inheritdoc ISystemParams - function updateGasCompParams( - uint256 _collGasCompensationDivisor, - uint256 _collGasCompensationCap, - uint256 _ethGasCompensation - ) external onlyOwner { + // Validate gas compensation parameters if ( - _collGasCompensationDivisor == 0 || - _collGasCompensationDivisor > 1000 + _gasCompParams.collGasCompensationDivisor == 0 || + _gasCompParams.collGasCompensationDivisor > 1000 ) revert InvalidGasCompensation(); - if (_collGasCompensationCap == 0 || _collGasCompensationCap > 10 ether) + if (_gasCompParams.collGasCompensationCap == 0 || _gasCompParams.collGasCompensationCap > 10 ether) revert InvalidGasCompensation(); - if (_ethGasCompensation == 0 || _ethGasCompensation > 1 ether) + if (_gasCompParams.ethGasCompensation == 0 || _gasCompParams.ethGasCompensation > 1 ether) revert InvalidGasCompensation(); - COLL_GAS_COMPENSATION_DIVISOR = _collGasCompensationDivisor; - COLL_GAS_COMPENSATION_CAP = _collGasCompensationCap; - ETH_GAS_COMPENSATION = _ethGasCompensation; + // Validate collateral parameters + if (_collateralParams.ccr <= _100pct || _collateralParams.ccr >= 2 * _100pct) revert InvalidCCR(); + if (_collateralParams.mcr <= _100pct || _collateralParams.mcr >= 2 * _100pct) revert InvalidMCR(); + if (_collateralParams.bcr < 5 * _1pct || _collateralParams.bcr >= 50 * _1pct) revert InvalidBCR(); + if (_collateralParams.scr <= _100pct || _collateralParams.scr >= 2 * _100pct) revert InvalidSCR(); - emit GasCompParamsUpdated( - _collGasCompensationDivisor, - _collGasCompensationCap, - _ethGasCompensation - ); - } - - /// @inheritdoc ISystemParams - function updateCollateralParams( - uint256 _ccr, - uint256 _scr, - uint256 _mcr, - uint256 _bcr - ) external onlyOwner { - if (_ccr <= _100pct || _ccr >= 2 * _100pct) revert InvalidCCR(); - if (_mcr <= _100pct || _mcr >= 2 * _100pct) revert InvalidMCR(); - if (_bcr < 5 * _1pct || _bcr >= 50 * _1pct) revert InvalidBCR(); - if (_scr <= _100pct || _scr >= 2 * _100pct) revert InvalidSCR(); - - CCR = _ccr; - SCR = _scr; - MCR = _mcr; - BCR = _bcr; - - emit CollateralParamsUpdated(_ccr, _scr, _mcr, _bcr); - } - - /// @inheritdoc ISystemParams - function updateInterestParams( - uint256 _minAnnualInterestRate, - uint256 _maxAnnualInterestRate, - uint128 _maxAnnualBatchManagementFee, - uint256 _upfrontInterestPeriod, - uint256 _interestRateAdjCooldown, - uint128 _minInterestRateChangePeriod, - uint256 _maxBatchSharesRatio - ) external onlyOwner { - if (_minAnnualInterestRate > _maxAnnualInterestRate) + // Validate interest parameters + if (_interestParams.minAnnualInterestRate > _interestParams.maxAnnualInterestRate) revert MinInterestRateGtMax(); - if (_maxAnnualInterestRate > 10 * _100pct) + if (_interestParams.maxAnnualInterestRate > 10 * _100pct) revert InvalidInterestRateBounds(); // Max 1000% - if (_maxAnnualBatchManagementFee > _100pct) revert InvalidFeeValue(); + if (_interestParams.maxAnnualBatchManagementFee > _100pct) revert InvalidFeeValue(); - if (_upfrontInterestPeriod == 0 || _upfrontInterestPeriod > 365 days) + if (_interestParams.upfrontInterestPeriod == 0 || _interestParams.upfrontInterestPeriod > 365 days) revert InvalidTimeValue(); if ( - _interestRateAdjCooldown == 0 || _interestRateAdjCooldown > 365 days + _interestParams.interestRateAdjCooldown == 0 || _interestParams.interestRateAdjCooldown > 365 days ) revert InvalidTimeValue(); if ( - _minInterestRateChangePeriod == 0 || - _minInterestRateChangePeriod > 30 days + _interestParams.minInterestRateChangePeriod == 0 || + _interestParams.minInterestRateChangePeriod > 30 days ) revert InvalidTimeValue(); - MIN_ANNUAL_INTEREST_RATE = _minAnnualInterestRate; - MAX_ANNUAL_INTEREST_RATE = _maxAnnualInterestRate; - MAX_ANNUAL_BATCH_MANAGEMENT_FEE = _maxAnnualBatchManagementFee; - UPFRONT_INTEREST_PERIOD = _upfrontInterestPeriod; - INTEREST_RATE_ADJ_COOLDOWN = _interestRateAdjCooldown; - MIN_INTEREST_RATE_CHANGE_PERIOD = _minInterestRateChangePeriod; - MAX_BATCH_SHARES_RATIO = _maxBatchSharesRatio; - - emit InterestParamsUpdated( - _minAnnualInterestRate, - _maxAnnualInterestRate, - _maxAnnualBatchManagementFee, - _upfrontInterestPeriod, - _interestRateAdjCooldown, - _minInterestRateChangePeriod, - _maxBatchSharesRatio - ); - } - - /// @inheritdoc ISystemParams - function updateRedemptionParams( - uint256 _redemptionFeeFloor, - uint256 _initialBaseRate, - uint256 _redemptionMinuteDecayFactor, - uint256 _redemptionBeta, - uint256 _urgentRedemptionBonus - ) external onlyOwner { - if (_redemptionFeeFloor > _100pct) revert InvalidFeeValue(); - if (_initialBaseRate > 10 * _100pct) revert InvalidFeeValue(); - if (_urgentRedemptionBonus > _100pct) revert InvalidFeeValue(); - - REDEMPTION_FEE_FLOOR = _redemptionFeeFloor; - INITIAL_BASE_RATE = _initialBaseRate; - REDEMPTION_MINUTE_DECAY_FACTOR = _redemptionMinuteDecayFactor; - REDEMPTION_BETA = _redemptionBeta; - URGENT_REDEMPTION_BONUS = _urgentRedemptionBonus; - - emit RedemptionParamsUpdated( - _redemptionFeeFloor, - _initialBaseRate, - _redemptionMinuteDecayFactor, - _redemptionBeta, - _urgentRedemptionBonus - ); - } - - /// @inheritdoc ISystemParams - function updatePoolParams( - uint256 _spYieldSplit, - uint256 _minBoldInSP - ) external onlyOwner { - if (_spYieldSplit > _100pct) revert InvalidFeeValue(); - if (_minBoldInSP == 0) revert InvalidMinDebt(); - - SP_YIELD_SPLIT = _spYieldSplit; - MIN_BOLD_IN_SP = _minBoldInSP; - - emit StabilityPoolParamsUpdated(_spYieldSplit, _minBoldInSP); - } - - /// @inheritdoc ISystemParams - function updateVersion(uint256 newVersion) external onlyOwner { - uint256 oldVersion = version; - version = newVersion; - emit VersionUpdated(oldVersion, newVersion); + // Validate redemption parameters + if (_redemptionParams.redemptionFeeFloor > _100pct) revert InvalidFeeValue(); + if (_redemptionParams.initialBaseRate > 10 * _100pct) revert InvalidFeeValue(); + if (_redemptionParams.urgentRedemptionBonus > _100pct) revert InvalidFeeValue(); + + // Validate stability pool parameters + if (_poolParams.spYieldSplit > _100pct) revert InvalidFeeValue(); + if (_poolParams.minBoldInSP == 0) revert InvalidMinDebt(); + + // Set debt parameters + MIN_DEBT = _debtParams.minDebt; + + // Set liquidation parameters + LIQUIDATION_PENALTY_SP = _liquidationParams.liquidationPenaltySP; + LIQUIDATION_PENALTY_REDISTRIBUTION = _liquidationParams.liquidationPenaltyRedistribution; + + // Set gas compensation parameters + COLL_GAS_COMPENSATION_DIVISOR = _gasCompParams.collGasCompensationDivisor; + COLL_GAS_COMPENSATION_CAP = _gasCompParams.collGasCompensationCap; + ETH_GAS_COMPENSATION = _gasCompParams.ethGasCompensation; + + // Set collateral parameters + CCR = _collateralParams.ccr; + SCR = _collateralParams.scr; + MCR = _collateralParams.mcr; + BCR = _collateralParams.bcr; + + // Set interest parameters + MIN_ANNUAL_INTEREST_RATE = _interestParams.minAnnualInterestRate; + MAX_ANNUAL_INTEREST_RATE = _interestParams.maxAnnualInterestRate; + MAX_ANNUAL_BATCH_MANAGEMENT_FEE = _interestParams.maxAnnualBatchManagementFee; + UPFRONT_INTEREST_PERIOD = _interestParams.upfrontInterestPeriod; + INTEREST_RATE_ADJ_COOLDOWN = _interestParams.interestRateAdjCooldown; + MIN_INTEREST_RATE_CHANGE_PERIOD = _interestParams.minInterestRateChangePeriod; + MAX_BATCH_SHARES_RATIO = _interestParams.maxBatchSharesRatio; + + // Set redemption parameters + REDEMPTION_FEE_FLOOR = _redemptionParams.redemptionFeeFloor; + INITIAL_BASE_RATE = _redemptionParams.initialBaseRate; + REDEMPTION_MINUTE_DECAY_FACTOR = _redemptionParams.redemptionMinuteDecayFactor; + REDEMPTION_BETA = _redemptionParams.redemptionBeta; + URGENT_REDEMPTION_BONUS = _redemptionParams.urgentRedemptionBonus; + + // Set stability pool parameters + SP_YIELD_SPLIT = _poolParams.spYieldSplit; + MIN_BOLD_IN_SP = _poolParams.minBoldInSP; } } From 85861a9241b2f5e7a5ec0eb7927970471373c08c Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:37:33 -0500 Subject: [PATCH 42/79] feat: store sys params addy --- contracts/src/ActivePool.sol | 2 ++ contracts/src/BorrowerOperations.sol | 3 +++ contracts/src/CollateralRegistry.sol | 2 ++ contracts/src/HintHelpers.sol | 2 ++ contracts/src/TroveManager.sol | 3 +++ 5 files changed, 12 insertions(+) diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index 8ac5e63fb..bc5860e6e 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -29,6 +29,7 @@ contract ActivePool is IActivePool { address public immutable borrowerOperationsAddress; address public immutable troveManagerAddress; address public immutable defaultPoolAddress; + address public immutable systemParamsAddress; IBoldToken public immutable boldToken; @@ -75,6 +76,7 @@ contract ActivePool is IActivePool { event ActivePoolCollBalanceUpdated(uint256 _collBalance); constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) { + systemParamsAddress = address(_systemParams); collToken = _addressesRegistry.collToken(); borrowerOperationsAddress = address(_addressesRegistry.borrowerOperations()); troveManagerAddress = address(_addressesRegistry.troveManager()); diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index e65202398..05a7292d4 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -30,6 +30,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio ISortedTroves internal sortedTroves; // Wrapped ETH for liquidation reserve (gas compensation) IERC20Metadata internal immutable gasToken; + address public immutable systemParamsAddress; // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied uint256 public immutable CCR; @@ -182,6 +183,8 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // This makes impossible to open a trove with zero withdrawn Bold assert(_systemParams.MIN_DEBT() > 0); + systemParamsAddress = address(_systemParams); + collToken = _addressesRegistry.collToken(); gasToken = _addressesRegistry.gasToken(); diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index 53b88a450..1efca9375 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -15,6 +15,7 @@ import "./Interfaces/ICollateralRegistry.sol"; contract CollateralRegistry is ICollateralRegistry { // See: https://github.com/ethereum/solidity/issues/12587 uint256 public immutable totalCollaterals; + address public immutable systemParamsAddress; IERC20Metadata internal immutable token0; IERC20Metadata internal immutable token1; @@ -57,6 +58,7 @@ contract CollateralRegistry is ICollateralRegistry { require(numTokens > 0, "Collateral list cannot be empty"); require(numTokens <= 10, "Collateral list too long"); totalCollaterals = numTokens; + systemParamsAddress = address(_systemParams); boldToken = _boldToken; diff --git a/contracts/src/HintHelpers.sol b/contracts/src/HintHelpers.sol index 302549a0f..5debbb86d 100644 --- a/contracts/src/HintHelpers.sol +++ b/contracts/src/HintHelpers.sol @@ -17,11 +17,13 @@ contract HintHelpers is IHintHelpers { string public constant NAME = "HintHelpers"; ICollateralRegistry public immutable collateralRegistry; + address public immutable systemParamsAddress; uint256 public immutable INTEREST_RATE_ADJ_COOLDOWN; uint256 public immutable UPFRONT_INTEREST_PERIOD; constructor(ICollateralRegistry _collateralRegistry, ISystemParams _systemParams) { + systemParamsAddress = address(_systemParams); collateralRegistry = _collateralRegistry; INTEREST_RATE_ADJ_COOLDOWN = _systemParams.INTEREST_RATE_ADJ_COOLDOWN(); diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index b2505d0b0..5884ec078 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -29,6 +29,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { ICollateralRegistry internal collateralRegistry; // Gas token for liquidation reserve (gas compensation) IERC20Metadata internal immutable gasToken; + address public immutable systemParamsAddress; // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied uint256 public immutable CCR; @@ -196,6 +197,8 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { event CollateralRegistryAddressChanged(address _collateralRegistryAddress); constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) LiquityBase(_addressesRegistry) { + systemParamsAddress = address(_systemParams); + CCR = _systemParams.CCR(); MCR = _systemParams.MCR(); SCR = _systemParams.SCR(); From 12ab46b1c260fd01b52e3b608b04f9fa64e450ea Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:42:06 -0500 Subject: [PATCH 43/79] chore: update tests + deploy scripts --- contracts/script/DeployLiquity2.s.sol | 618 ++++++--- contracts/test/TestContracts/Deployment.t.sol | 75 +- contracts/test/systemParams.t.sol | 1131 ++++++++++------- 3 files changed, 1228 insertions(+), 596 deletions(-) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index eee38c77b..01f024439 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -6,10 +6,10 @@ import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/exten import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; import {IERC20 as IERC20_GOV} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {ProxyAdmin} from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; -import {TransparentUpgradeableProxy} from - "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {TransparentUpgradeableProxy} from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {IFPMMFactory} from "src/Interfaces/IFPMMFactory.sol"; import {SystemParams} from "src/SystemParams.sol"; +import {ISystemParams} from "src/Interfaces/ISystemParams.sol"; import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; import {StringFormatting} from "test/Utils/StringFormatting.sol"; @@ -111,7 +111,6 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { address stabilityPoolImpl; address stableTokenV3Impl; address fpmm; - address systemParamsImpl; } struct DeploymentConfig { @@ -124,15 +123,16 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { string stableTokenSymbol; } - DeploymentConfig internal CONFIG = DeploymentConfig({ - USDm_ALFAJORES_ADDRESS: 0x9E2d4412d0f434cC85500b79447d9323a7416f09, - proxyAdmin: 0xe4DdacCAdb64114215FCe8251B57B2AEB5C2C0E2, - fpmmFactory: 0xd8098494a749a3fDAD2D2e7Fa5272D8f274D8FF6, - fpmmImplementation: 0x0292efcB331C6603eaa29D570d12eB336D6c01d6, - referenceRateFeedID: 0x206B25Ea01E188Ee243131aFdE526bA6E131a016, - stableTokenName: "EUR.m Test", - stableTokenSymbol: "EUR.m" - }); + DeploymentConfig internal CONFIG = + DeploymentConfig({ + USDm_ALFAJORES_ADDRESS: 0x9E2d4412d0f434cC85500b79447d9323a7416f09, + proxyAdmin: 0xe4DdacCAdb64114215FCe8251B57B2AEB5C2C0E2, + fpmmFactory: 0xd8098494a749a3fDAD2D2e7Fa5272D8f274D8FF6, + fpmmImplementation: 0x0292efcB331C6603eaa29D570d12eB336D6c01d6, + referenceRateFeedID: 0x206B25Ea01E188Ee243131aFdE526bA6E131a016, + stableTokenName: "EUR.v2 Test", + stableTokenSymbol: "EUR.v2" + }); function run() external { string memory saltStr = vm.envOr("SALT", block.timestamp.toString()); @@ -144,39 +144,65 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { _log("Deployer: ", deployer.toHexString()); _log("Deployer balance: ", deployer.balance.decimal()); - _log("CREATE2 salt: ", 'keccak256(bytes("', saltStr, '")) = ', uint256(SALT).toHexString()); + _log( + "CREATE2 salt: ", + 'keccak256(bytes("', + saltStr, + '")) = ', + uint256(SALT).toHexString() + ); _log("Chain ID: ", block.chainid.toString()); DeploymentResult memory deployed = _deployAndConnectContracts(); vm.stopBroadcast(); - vm.writeFile("script/deployment-manifest.json", _getManifestJson(deployed)); + vm.writeFile( + "script/deployment-manifest.json", + _getManifestJson(deployed) + ); } // See: https://solidity-by-example.org/app/create2/ - function getBytecode(bytes memory _creationCode, address _addressesRegistry) public pure returns (bytes memory) { + function getBytecode( + bytes memory _creationCode, + address _addressesRegistry + ) public pure returns (bytes memory) { return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry)); } - function getBytecode(bytes memory _creationCode, address _addressesRegistry, address _systemParams) public pure returns (bytes memory) { - return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry, _systemParams)); + function getBytecode( + bytes memory _creationCode, + address _addressesRegistry, + address _systemParams + ) public pure returns (bytes memory) { + return + abi.encodePacked( + _creationCode, + abi.encode(_addressesRegistry, _systemParams) + ); } - function _deployAndConnectContracts() internal returns (DeploymentResult memory r) { + function _deployAndConnectContracts() + internal + returns (DeploymentResult memory r) + { _deployProxyInfrastructure(r); _deployStableToken(r); - // TODO: Fix FPMM deployment - currently failing - //_deployFPMM(r); + // _deployFPMM(r); _deploySystemParams(r); IAddressesRegistry addressesRegistry = new AddressesRegistry(deployer); address troveManagerAddress = _computeCreate2Address( - type(TroveManager).creationCode, address(addressesRegistry), address(r.systemParams) + type(TroveManager).creationCode, + address(addressesRegistry), + address(r.systemParams) ); - IERC20Metadata collToken = IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS); + IERC20Metadata collToken = IERC20Metadata( + CONFIG.USDm_ALFAJORES_ADDRESS + ); IERC20Metadata[] memory collaterals = new IERC20Metadata[](1); collaterals[0] = collToken; @@ -184,59 +210,143 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { ITroveManager[] memory troveManagers = new ITroveManager[](1); troveManagers[0] = ITroveManager(troveManagerAddress); - r.collateralRegistry = new CollateralRegistry(IBoldToken(address(r.stableToken)), collaterals, troveManagers, r.systemParams); + r.collateralRegistry = new CollateralRegistry( + IBoldToken(address(r.stableToken)), + collaterals, + troveManagers, + r.systemParams + ); r.hintHelpers = new HintHelpers(r.collateralRegistry, r.systemParams); r.multiTroveGetter = new MultiTroveGetter(r.collateralRegistry); IPriceFeed priceFeed = new PriceFeedTestnet(); - r.contracts = - _deployAndConnectCollateralContracts(collToken, priceFeed, addressesRegistry, troveManagerAddress, r); + r.contracts = _deployAndConnectCollateralContracts( + collToken, + priceFeed, + addressesRegistry, + troveManagerAddress, + r + ); } function _deployProxyInfrastructure(DeploymentResult memory r) internal { r.proxyAdmin = ProxyAdmin(CONFIG.proxyAdmin); r.stableTokenV3Impl = address(new StableTokenV3{salt: SALT}(true)); r.stabilityPoolImpl = address(new StabilityPool{salt: SALT}(true)); - r.systemParamsImpl = address(new SystemParams{salt: SALT}(true)); assert( - address(r.stableTokenV3Impl) - == vm.computeCreate2Address( - SALT, keccak256(bytes.concat(type(StableTokenV3).creationCode, abi.encode(true))) - ) - ); - assert( - address(r.stabilityPoolImpl) - == vm.computeCreate2Address( - SALT, keccak256(bytes.concat(type(StabilityPool).creationCode, abi.encode(true))) + address(r.stableTokenV3Impl) == + vm.computeCreate2Address( + SALT, + keccak256( + bytes.concat( + type(StableTokenV3).creationCode, + abi.encode(true) + ) + ) ) ); - assert( - address(r.systemParamsImpl) - == vm.computeCreate2Address( - SALT, keccak256(bytes.concat(type(SystemParams).creationCode, abi.encode(true))) + address(r.stabilityPoolImpl) == + vm.computeCreate2Address( + SALT, + keccak256( + bytes.concat( + type(StabilityPool).creationCode, + abi.encode(true) + ) + ) ) ); } function _deployStableToken(DeploymentResult memory r) internal { r.stableToken = IStableTokenV3( - address(new TransparentUpgradeableProxy(address(r.stableTokenV3Impl), address(r.proxyAdmin), "")) + address( + new TransparentUpgradeableProxy( + address(r.stableTokenV3Impl), + address(r.proxyAdmin), + "" + ) + ) ); } function _deployFPMM(DeploymentResult memory r) internal { r.fpmm = IFPMMFactory(CONFIG.fpmmFactory).deployFPMM( - CONFIG.fpmmImplementation, address(r.stableToken), CONFIG.USDm_ALFAJORES_ADDRESS, CONFIG.referenceRateFeedID + CONFIG.fpmmImplementation, + address(r.stableToken), + CONFIG.USDm_ALFAJORES_ADDRESS, + CONFIG.referenceRateFeedID ); } function _deploySystemParams(DeploymentResult memory r) internal { - address systemParamsProxy = address(new TransparentUpgradeableProxy(address(r.systemParamsImpl), address(r.proxyAdmin), "")); - r.systemParams = ISystemParams(systemParamsProxy); - SystemParams(systemParamsProxy).initialize(deployer); + ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({ + minDebt: 2000e18 + }); + + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams + .LiquidationParams({ + liquidationPenaltySP: 5e16, + liquidationPenaltyRedistribution: 10e16 + }); + + ISystemParams.GasCompParams memory gasCompParams = ISystemParams + .GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 2 ether, + ethGasCompensation: 0.0375 ether + }); + + ISystemParams.CollateralParams memory collateralParams = ISystemParams + .CollateralParams({ + ccr: 150 * 1e16, + scr: 110 * 1e16, + mcr: 110 * 1e16, + bcr: 10 * 1e16 + }); + + ISystemParams.InterestParams memory interestParams = ISystemParams + .InterestParams({ + minAnnualInterestRate: 1e18 / 200, + maxAnnualInterestRate: 250 * (1e18 / 100), + maxAnnualBatchManagementFee: uint128(1e18 / 10), + upfrontInterestPeriod: 7 days, + interestRateAdjCooldown: 7 days, + minInterestRateChangePeriod: 1 hours, + maxBatchSharesRatio: 1e9 + }); + + ISystemParams.RedemptionParams memory redemptionParams = ISystemParams + .RedemptionParams({ + redemptionFeeFloor: 1e18 / 200, + initialBaseRate: 1e18, + redemptionMinuteDecayFactor: 998076443575628800, + redemptionBeta: 1, + urgentRedemptionBonus: 2 * 1e16 + }); + + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams + .StabilityPoolParams({ + spYieldSplit: 75 * (1e18 / 100), + minBoldInSP: 1e18 + }); + + r.systemParams = ISystemParams( + address( + new SystemParams{salt: SALT}( + debtParams, + liquidationParams, + gasCompParams, + collateralParams, + interestParams, + redemptionParams, + poolParams + ) + ) + ); } function _deployAndConnectCollateralContracts( @@ -252,34 +362,63 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { contracts.priceFeed = _priceFeed; contracts.systemParams = r.systemParams; // TODO: replace with governance timelock on mainnet - contracts.interestRouter = IInterestRouter(0x56fD3F2bEE130e9867942D0F463a16fBE49B8d81); + contracts.interestRouter = IInterestRouter( + 0x56fD3F2bEE130e9867942D0F463a16fBE49B8d81 + ); addresses.troveManager = _troveManagerAddress; contracts.metadataNFT = deployMetadata(SALT); addresses.metadataNFT = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(MetadataNFT).creationCode, address(initializedFixedAssetReader))) + SALT, + keccak256( + getBytecode( + type(MetadataNFT).creationCode, + address(initializedFixedAssetReader) + ) + ) ); assert(address(contracts.metadataNFT) == addresses.metadataNFT); addresses.borrowerOperations = _computeCreate2Address( - type(BorrowerOperations).creationCode, address(contracts.addressesRegistry), address(contracts.systemParams) + type(BorrowerOperations).creationCode, + address(contracts.addressesRegistry), + address(contracts.systemParams) + ); + addresses.troveNFT = _computeCreate2Address( + type(TroveNFT).creationCode, + address(contracts.addressesRegistry) ); - addresses.troveNFT = _computeCreate2Address(type(TroveNFT).creationCode, address(contracts.addressesRegistry)); addresses.activePool = _computeCreate2Address( - type(ActivePool).creationCode, address(contracts.addressesRegistry), address(contracts.systemParams) + type(ActivePool).creationCode, + address(contracts.addressesRegistry), + address(contracts.systemParams) + ); + addresses.defaultPool = _computeCreate2Address( + type(DefaultPool).creationCode, + address(contracts.addressesRegistry) + ); + addresses.gasPool = _computeCreate2Address( + type(GasPool).creationCode, + address(contracts.addressesRegistry) + ); + addresses.collSurplusPool = _computeCreate2Address( + type(CollSurplusPool).creationCode, + address(contracts.addressesRegistry) + ); + addresses.sortedTroves = _computeCreate2Address( + type(SortedTroves).creationCode, + address(contracts.addressesRegistry) ); - addresses.defaultPool = - _computeCreate2Address(type(DefaultPool).creationCode, address(contracts.addressesRegistry)); - addresses.gasPool = _computeCreate2Address(type(GasPool).creationCode, address(contracts.addressesRegistry)); - addresses.collSurplusPool = - _computeCreate2Address(type(CollSurplusPool).creationCode, address(contracts.addressesRegistry)); - addresses.sortedTroves = - _computeCreate2Address(type(SortedTroves).creationCode, address(contracts.addressesRegistry)); // Deploy StabilityPool proxy - address stabilityPool = - address(new TransparentUpgradeableProxy(address(r.stabilityPoolImpl), address(r.proxyAdmin), "")); + address stabilityPool = address( + new TransparentUpgradeableProxy( + address(r.stabilityPoolImpl), + address(r.proxyAdmin), + "" + ) + ); contracts.stabilityPool = IStabilityPool(stabilityPool); // Set up addresses in registry @@ -288,7 +427,10 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { // Deploy core protocol contracts _deployProtocolContracts(contracts, addresses); - IStabilityPool(stabilityPool).initialize(contracts.addressesRegistry, contracts.systemParams); + IStabilityPool(stabilityPool).initialize( + contracts.addressesRegistry, + contracts.systemParams + ); address[] memory minters = new address[](2); minters[0] = address(contracts.borrowerOperations); @@ -319,42 +461,68 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { LiquityContractAddresses memory addresses, DeploymentResult memory r ) internal { - IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ - collToken: contracts.collToken, - borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), - troveManager: ITroveManager(addresses.troveManager), - troveNFT: ITroveNFT(addresses.troveNFT), - metadataNFT: IMetadataNFT(addresses.metadataNFT), - stabilityPool: contracts.stabilityPool, - priceFeed: contracts.priceFeed, - activePool: IActivePool(addresses.activePool), - defaultPool: IDefaultPool(addresses.defaultPool), - gasPoolAddress: addresses.gasPool, - collSurplusPool: ICollSurplusPool(addresses.collSurplusPool), - sortedTroves: ISortedTroves(addresses.sortedTroves), - interestRouter: contracts.interestRouter, - hintHelpers: r.hintHelpers, - multiTroveGetter: r.multiTroveGetter, - collateralRegistry: r.collateralRegistry, - boldToken: IBoldToken(address(r.stableToken)), - gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS) - }); + IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry + .AddressVars({ + collToken: contracts.collToken, + borrowerOperations: IBorrowerOperations( + addresses.borrowerOperations + ), + troveManager: ITroveManager(addresses.troveManager), + troveNFT: ITroveNFT(addresses.troveNFT), + metadataNFT: IMetadataNFT(addresses.metadataNFT), + stabilityPool: contracts.stabilityPool, + priceFeed: contracts.priceFeed, + activePool: IActivePool(addresses.activePool), + defaultPool: IDefaultPool(addresses.defaultPool), + gasPoolAddress: addresses.gasPool, + collSurplusPool: ICollSurplusPool(addresses.collSurplusPool), + sortedTroves: ISortedTroves(addresses.sortedTroves), + interestRouter: contracts.interestRouter, + hintHelpers: r.hintHelpers, + multiTroveGetter: r.multiTroveGetter, + collateralRegistry: r.collateralRegistry, + boldToken: IBoldToken(address(r.stableToken)), + gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS) + }); contracts.addressesRegistry.setAddresses(addressVars); } - function _deployProtocolContracts(LiquityContracts memory contracts, LiquityContractAddresses memory addresses) - internal - { - contracts.borrowerOperations = new BorrowerOperations{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); - contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); - contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); - contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); - contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); - contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); - contracts.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); - contracts.sortedTroves = new SortedTroves{salt: SALT}(contracts.addressesRegistry); - - assert(address(contracts.borrowerOperations) == addresses.borrowerOperations); + function _deployProtocolContracts( + LiquityContracts memory contracts, + LiquityContractAddresses memory addresses + ) internal { + contracts.borrowerOperations = new BorrowerOperations{salt: SALT}( + contracts.addressesRegistry, + contracts.systemParams + ); + contracts.troveManager = new TroveManager{salt: SALT}( + contracts.addressesRegistry, + contracts.systemParams + ); + contracts.troveNFT = new TroveNFT{salt: SALT}( + contracts.addressesRegistry + ); + contracts.activePool = new ActivePool{salt: SALT}( + contracts.addressesRegistry, + contracts.systemParams + ); + contracts.defaultPool = new DefaultPool{salt: SALT}( + contracts.addressesRegistry + ); + contracts.gasPool = new GasPool{salt: SALT}( + contracts.addressesRegistry + ); + contracts.collSurplusPool = new CollSurplusPool{salt: SALT}( + contracts.addressesRegistry + ); + contracts.sortedTroves = new SortedTroves{salt: SALT}( + contracts.addressesRegistry + ); + + assert( + address(contracts.borrowerOperations) == + addresses.borrowerOperations + ); assert(address(contracts.troveManager) == addresses.troveManager); assert(address(contracts.troveNFT) == addresses.troveNFT); assert(address(contracts.activePool) == addresses.activePool); @@ -364,91 +532,235 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { assert(address(contracts.sortedTroves) == addresses.sortedTroves); } - function _computeCreate2Address(bytes memory creationCode, address _addressesRegistry) - internal - view - returns (address) - { - return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry))); + function _computeCreate2Address( + bytes memory creationCode, + address _addressesRegistry + ) internal view returns (address) { + return + vm.computeCreate2Address( + SALT, + keccak256(getBytecode(creationCode, _addressesRegistry)) + ); } - function _computeCreate2Address(bytes memory creationCode, address _addressesRegistry, address _systemParams) - internal - view - returns (address) - { - return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry, _systemParams))); + function _computeCreate2Address( + bytes memory creationCode, + address _addressesRegistry, + address _systemParams + ) internal view returns (address) { + return + vm.computeCreate2Address( + SALT, + keccak256( + getBytecode(creationCode, _addressesRegistry, _systemParams) + ) + ); } - - function _getBranchContractsJson(LiquityContracts memory c) internal view returns (string memory) { - return string.concat( - "{", + function _getBranchContractsJson( + LiquityContracts memory c + ) internal view returns (string memory) { + return string.concat( - // Avoid stack too deep by chunking concats + "{", string.concat( - string.concat('"collSymbol":"', c.collToken.symbol(), '",'), // purely for human-readability - string.concat('"collToken":"', address(c.collToken).toHexString(), '",'), - string.concat('"addressesRegistry":"', address(c.addressesRegistry).toHexString(), '",'), - string.concat('"activePool":"', address(c.activePool).toHexString(), '",'), - string.concat('"borrowerOperations":"', address(c.borrowerOperations).toHexString(), '",'), - string.concat('"collSurplusPool":"', address(c.collSurplusPool).toHexString(), '",'), - string.concat('"defaultPool":"', address(c.defaultPool).toHexString(), '",'), - string.concat('"sortedTroves":"', address(c.sortedTroves).toHexString(), '",'), - string.concat('"systemParams":"', address(c.systemParams).toHexString(), '",') + // Avoid stack too deep by chunking concats + string.concat( + string.concat( + '"collSymbol":"', + c.collToken.symbol(), + '",' + ), // purely for human-readability + string.concat( + '"collToken":"', + address(c.collToken).toHexString(), + '",' + ), + string.concat( + '"addressesRegistry":"', + address(c.addressesRegistry).toHexString(), + '",' + ), + string.concat( + '"activePool":"', + address(c.activePool).toHexString(), + '",' + ), + string.concat( + '"borrowerOperations":"', + address(c.borrowerOperations).toHexString(), + '",' + ), + string.concat( + '"collSurplusPool":"', + address(c.collSurplusPool).toHexString(), + '",' + ), + string.concat( + '"defaultPool":"', + address(c.defaultPool).toHexString(), + '",' + ), + string.concat( + '"sortedTroves":"', + address(c.sortedTroves).toHexString(), + '",' + ), + string.concat( + '"systemParams":"', + address(c.systemParams).toHexString(), + '",' + ) + ), + string.concat( + string.concat( + '"stabilityPool":"', + address(c.stabilityPool).toHexString(), + '",' + ), + string.concat( + '"troveManager":"', + address(c.troveManager).toHexString(), + '",' + ), + string.concat( + '"troveNFT":"', + address(c.troveNFT).toHexString(), + '",' + ), + string.concat( + '"metadataNFT":"', + address(c.metadataNFT).toHexString(), + '",' + ), + string.concat( + '"priceFeed":"', + address(c.priceFeed).toHexString(), + '",' + ), + string.concat( + '"gasPool":"', + address(c.gasPool).toHexString(), + '",' + ), + string.concat( + '"interestRouter":"', + address(c.interestRouter).toHexString(), + '",' + ) + ) ), - string.concat( - string.concat('"stabilityPool":"', address(c.stabilityPool).toHexString(), '",'), - string.concat('"troveManager":"', address(c.troveManager).toHexString(), '",'), - string.concat('"troveNFT":"', address(c.troveNFT).toHexString(), '",'), - string.concat('"metadataNFT":"', address(c.metadataNFT).toHexString(), '",'), - string.concat('"priceFeed":"', address(c.priceFeed).toHexString(), '",'), - string.concat('"gasPool":"', address(c.gasPool).toHexString(), '",'), - string.concat('"interestRouter":"', address(c.interestRouter).toHexString(), '",') - ) - ), - "}" - ); + "}" + ); } - function _getDeploymentConstants(ISystemParams params) internal view returns (string memory) { - return string.concat( - "{", + function _getDeploymentConstants( + ISystemParams params + ) internal view returns (string memory) { + return string.concat( - string.concat('"ETH_GAS_COMPENSATION":"', params.ETH_GAS_COMPENSATION().toString(), '",'), - string.concat('"INTEREST_RATE_ADJ_COOLDOWN":"', params.INTEREST_RATE_ADJ_COOLDOWN().toString(), '",'), - string.concat('"MAX_ANNUAL_INTEREST_RATE":"', params.MAX_ANNUAL_INTEREST_RATE().toString(), '",'), - string.concat('"MIN_ANNUAL_INTEREST_RATE":"', params.MIN_ANNUAL_INTEREST_RATE().toString(), '",'), - string.concat('"MIN_DEBT":"', params.MIN_DEBT().toString(), '",'), - string.concat('"SP_YIELD_SPLIT":"', params.SP_YIELD_SPLIT().toString(), '",'), - string.concat('"UPFRONT_INTEREST_PERIOD":"', params.UPFRONT_INTEREST_PERIOD().toString(), '"') // no comma - ), - "}" - ); + "{", + string.concat( + string.concat( + '"ETH_GAS_COMPENSATION":"', + params.ETH_GAS_COMPENSATION().toString(), + '",' + ), + string.concat( + '"INTEREST_RATE_ADJ_COOLDOWN":"', + params.INTEREST_RATE_ADJ_COOLDOWN().toString(), + '",' + ), + string.concat( + '"MAX_ANNUAL_INTEREST_RATE":"', + params.MAX_ANNUAL_INTEREST_RATE().toString(), + '",' + ), + string.concat( + '"MIN_ANNUAL_INTEREST_RATE":"', + params.MIN_ANNUAL_INTEREST_RATE().toString(), + '",' + ), + string.concat( + '"MIN_DEBT":"', + params.MIN_DEBT().toString(), + '",' + ), + string.concat( + '"SP_YIELD_SPLIT":"', + params.SP_YIELD_SPLIT().toString(), + '",' + ), + string.concat( + '"UPFRONT_INTEREST_PERIOD":"', + params.UPFRONT_INTEREST_PERIOD().toString(), + '"' + ) // no comma + ), + "}" + ); } - function _getManifestJson(DeploymentResult memory deployed) internal view returns (string memory) { + function _getManifestJson( + DeploymentResult memory deployed + ) internal view returns (string memory) { string[] memory branches = new string[](1); branches[0] = _getBranchContractsJson(deployed.contracts); string memory part1 = string.concat( "{", - string.concat('"constants":', _getDeploymentConstants(deployed.contracts.systemParams), ","), - string.concat('"collateralRegistry":"', address(deployed.collateralRegistry).toHexString(), '",'), - string.concat('"boldToken":"', address(deployed.stableToken).toHexString(), '",'), - string.concat('"hintHelpers":"', address(deployed.hintHelpers).toHexString(), '",') + string.concat( + '"constants":', + _getDeploymentConstants(deployed.contracts.systemParams), + "," + ), + string.concat( + '"collateralRegistry":"', + address(deployed.collateralRegistry).toHexString(), + '",' + ), + string.concat( + '"boldToken":"', + address(deployed.stableToken).toHexString(), + '",' + ), + string.concat( + '"hintHelpers":"', + address(deployed.hintHelpers).toHexString(), + '",' + ) ); string memory part2 = string.concat( - string.concat('"stableTokenV3Impl":"', address(deployed.stableTokenV3Impl).toHexString(), '",'), - string.concat('"stabilityPoolImpl":"', address(deployed.stabilityPoolImpl).toHexString(), '",'), - string.concat('"systemParamsImpl":"', address(deployed.systemParamsImpl).toHexString(), '",'), - string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",') + string.concat( + '"stableTokenV3Impl":"', + address(deployed.stableTokenV3Impl).toHexString(), + '",' + ), + string.concat( + '"stabilityPoolImpl":"', + address(deployed.stabilityPoolImpl).toHexString(), + '",' + ), + string.concat( + '"systemParams":"', + address(deployed.systemParams).toHexString(), + '",' + ), + string.concat( + '"multiTroveGetter":"', + address(deployed.multiTroveGetter).toHexString(), + '",' + ) ); string memory part3 = string.concat( - string.concat('"fpmm":"', address(deployed.fpmm).toHexString(), '",'), + string.concat( + '"fpmm":"', + address(deployed.fpmm).toHexString(), + '",' + ), string.concat('"branches":[', branches.join(","), "]"), "}" ); diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index b290c1923..75dffdbed 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -13,7 +13,6 @@ import "src/HintHelpers.sol"; import "src/MultiTroveGetter.sol"; import "src/SortedTroves.sol"; import "src/StabilityPool.sol"; -import {SystemParams, ISystemParams} from "src/SystemParams.sol"; import "./BorrowerOperationsTester.t.sol"; import "./TroveManagerTester.t.sol"; import "./CollateralRegistryTester.sol"; @@ -23,6 +22,7 @@ import "src/CollateralRegistry.sol"; import "./MockInterestRouter.sol"; import "./PriceFeedTestnet.sol"; import "./MetadataDeployment.sol"; +import "src/SystemParams.sol"; import {WETHTester} from "./WETHTester.sol"; import {ERC20Faucet} from "./ERC20Faucet.sol"; @@ -349,21 +349,64 @@ contract TestDeployer is MetadataDeployment { function deploySystemParamsDev(TroveManagerParams memory params, uint256 index) public returns (ISystemParams) { bytes32 uniqueSalt = keccak256(abi.encodePacked(SALT, index)); - SystemParams systemParams = new SystemParams{salt: uniqueSalt}(false); - systemParams.initialize(address(this)); - - systemParams.updateCollateralParams(params.CCR, params.SCR, params.MCR, params.BCR); - systemParams.updateLiquidationParams(5e16, 20e16, params.LIQUIDATION_PENALTY_SP, params.LIQUIDATION_PENALTY_REDISTRIBUTION); - - return ISystemParams(systemParams); - } - function updateSystemParamsCollateral(ISystemParams systemParams, uint256 ccr, uint256 scr, uint256 mcr, uint256 bcr) public { - systemParams.updateCollateralParams(ccr, scr, mcr, bcr); - } + // Create parameter structs based on constants + ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({ + minDebt: 2000e18 // MIN_DEBT + }); + + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: params.LIQUIDATION_PENALTY_SP, + liquidationPenaltyRedistribution: params.LIQUIDATION_PENALTY_REDISTRIBUTION + }); + + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, // COLL_GAS_COMPENSATION_DIVISOR + collGasCompensationCap: 2 ether, // COLL_GAS_COMPENSATION_CAP + ethGasCompensation: 0.0375 ether // ETH_GAS_COMPENSATION + }); + + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: params.CCR, + scr: params.SCR, + mcr: params.MCR, + bcr: params.BCR + }); + + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: DECIMAL_PRECISION / 200, // MIN_ANNUAL_INTEREST_RATE (0.5%) + maxAnnualInterestRate: 250 * (DECIMAL_PRECISION / 100), // MAX_ANNUAL_INTEREST_RATE (250%) + maxAnnualBatchManagementFee: uint128(DECIMAL_PRECISION / 10), // MAX_ANNUAL_BATCH_MANAGEMENT_FEE (10%) + upfrontInterestPeriod: 7 days, // UPFRONT_INTEREST_PERIOD + interestRateAdjCooldown: 7 days, // INTEREST_RATE_ADJ_COOLDOWN + minInterestRateChangePeriod: 1 hours, // MIN_INTEREST_RATE_CHANGE_PERIOD + maxBatchSharesRatio: 1e9 // MAX_BATCH_SHARES_RATIO + }); - function updateSystemParamsLiquidation(ISystemParams systemParams, uint256 minSP, uint256 maxRedist, uint256 sp, uint256 redist) public { - systemParams.updateLiquidationParams(minSP, maxRedist, sp, redist); + ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ + redemptionFeeFloor: DECIMAL_PRECISION / 200, // REDEMPTION_FEE_FLOOR (0.5%) + initialBaseRate: DECIMAL_PRECISION, // INITIAL_BASE_RATE (100%) + redemptionMinuteDecayFactor: 998076443575628800, // REDEMPTION_MINUTE_DECAY_FACTOR + redemptionBeta: 1, // REDEMPTION_BETA + urgentRedemptionBonus: 2e16 // URGENT_REDEMPTION_BONUS (2%) + }); + + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ + spYieldSplit: 75 * (DECIMAL_PRECISION / 100), // SP_YIELD_SPLIT (75%) + minBoldInSP: 1e18 // MIN_BOLD_IN_SP + }); + + SystemParams systemParams = new SystemParams{salt: uniqueSalt}( + debtParams, + liquidationParams, + gasCompParams, + collateralParams, + interestParams, + redemptionParams, + poolParams + ); + + return ISystemParams(systemParams); } function _deployAndConnectCollateralContractsDev( @@ -716,8 +759,6 @@ contract TestDeployer is MetadataDeployment { } function _deploySystemParamsMainnet() internal returns (ISystemParams) { - SystemParams systemParams = new SystemParams{salt: SALT}(false); - systemParams.initialize(address(this)); - return ISystemParams(systemParams); + return ISystemParams(address(0)); } } diff --git a/contracts/test/systemParams.t.sol b/contracts/test/systemParams.t.sol index c5398926d..0896f75fe 100644 --- a/contracts/test/systemParams.t.sol +++ b/contracts/test/systemParams.t.sol @@ -7,544 +7,823 @@ import {SystemParams} from "../src/SystemParams.sol"; import {ISystemParams} from "../src/Interfaces/ISystemParams.sol"; contract SystemParamsTest is DevTestSetup { - address owner; - address nonOwner; - - event VersionUpdated(uint256 oldVersion, uint256 newVersion); - event MinDebtUpdated(uint256 oldMinDebt, uint256 newMinDebt); - event LiquidationParamsUpdated( - uint256 minLiquidationPenaltySP, - uint256 maxLiquidationPenaltyRedistribution, - uint256 liquidationPenaltySP, - uint256 liquidationPenaltyRedistribution - ); - event GasCompParamsUpdated( - uint256 collGasCompensationDivisor, - uint256 collGasCompensationCap, - uint256 ethGasCompensation - ); - event CollateralParamsUpdated(uint256 ccr, uint256 scr, uint256 mcr, uint256 bcr); - event InterestParamsUpdated( - uint256 minAnnualInterestRate, - uint256 maxAnnualInterestRate, - uint128 maxAnnualBatchManagementFee, - uint256 upfrontInterestPeriod, - uint256 interestRateAdjCooldown, - uint128 minInterestRateChangePeriod, - uint256 maxBatchSharesRatio - ); - event RedemptionParamsUpdated( - uint256 redemptionFeeFloor, - uint256 initialBaseRate, - uint256 redemptionMinuteDecayFactor, - uint256 redemptionBeta, - uint256 urgentRedemptionBonus - ); - event StabilityPoolParamsUpdated(uint256 spYieldSplit, uint256 minBoldInSP); - - function setUp() public override { - super.setUp(); - owner = SystemParams(address(systemParams)).owner(); - nonOwner = address(0x1234); - } - - function testInitialization() public { - SystemParams freshParams = new SystemParams(false); - freshParams.initialize(owner); - - // Check debt parameters - assertEq(freshParams.MIN_DEBT(), 2000e18); - - // Check liquidation parameters - assertEq(freshParams.MIN_LIQUIDATION_PENALTY_SP(), 5 * _1pct); - assertEq(freshParams.MAX_LIQUIDATION_PENALTY_REDISTRIBUTION(), 20 * _1pct); - assertEq(freshParams.LIQUIDATION_PENALTY_SP(), 5e16); - assertEq(freshParams.LIQUIDATION_PENALTY_REDISTRIBUTION(), 10e16); - - // Check gas compensation parameters - assertEq(freshParams.COLL_GAS_COMPENSATION_DIVISOR(), 200); - assertEq(freshParams.COLL_GAS_COMPENSATION_CAP(), 2 ether); - assertEq(freshParams.ETH_GAS_COMPENSATION(), 0.0375 ether); - - // Check collateral parameters - assertEq(freshParams.CCR(), 150 * _1pct); - assertEq(freshParams.SCR(), 110 * _1pct); - assertEq(freshParams.MCR(), 110 * _1pct); - assertEq(freshParams.BCR(), 10 * _1pct); - - // Check interest parameters - assertEq(freshParams.MIN_ANNUAL_INTEREST_RATE(), _1pct / 2); - assertEq(freshParams.MAX_ANNUAL_INTEREST_RATE(), 250 * _1pct); - assertEq(freshParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE(), uint128(_100pct / 10)); - assertEq(freshParams.UPFRONT_INTEREST_PERIOD(), 7 days); - assertEq(freshParams.INTEREST_RATE_ADJ_COOLDOWN(), 7 days); - assertEq(freshParams.MIN_INTEREST_RATE_CHANGE_PERIOD(), 1 hours); - assertEq(freshParams.MAX_BATCH_SHARES_RATIO(), 1e9); - - // Check redemption parameters - assertEq(freshParams.REDEMPTION_FEE_FLOOR(), _1pct / 2); - assertEq(freshParams.INITIAL_BASE_RATE(), _100pct); - assertEq(freshParams.REDEMPTION_MINUTE_DECAY_FACTOR(), 998076443575628800); - assertEq(freshParams.REDEMPTION_BETA(), 1); - assertEq(freshParams.URGENT_REDEMPTION_BONUS(), 2 * _1pct); - - // Check stability pool parameters - assertEq(freshParams.SP_YIELD_SPLIT(), 75 * _1pct); - assertEq(freshParams.MIN_BOLD_IN_SP(), 1e18); - - // Check version - assertEq(freshParams.version(), 1); - - // Check ownership - assertEq(freshParams.owner(), owner); - } - - function testDisableInitializers() public { - SystemParams disabledParams = new SystemParams(true); - - vm.expectRevert(); - disabledParams.initialize(owner); - } - - function testUpdateMinDebt() public { - uint256 oldMinDebt = systemParams.MIN_DEBT(); - uint256 newMinDebt = 3000e18; - - vm.prank(owner); - vm.expectEmit(true, true, false, true); - emit MinDebtUpdated(oldMinDebt, newMinDebt); - systemParams.updateMinDebt(newMinDebt); - - assertEq(systemParams.MIN_DEBT(), newMinDebt); - } - - function testUpdateMinDebtRevertsWhenZero() public { - vm.prank(owner); - vm.expectRevert(ISystemParams.InvalidMinDebt.selector); - systemParams.updateMinDebt(0); - } + function testConstructorSetsAllParametersCorrectly() public { + ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({ + minDebt: 2000e18 + }); + + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, // 5% + liquidationPenaltyRedistribution: 10e16 // 10% + }); + + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 2 ether, + ethGasCompensation: 0.0375 ether + }); + + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 110 * _1pct, + mcr: 110 * _1pct, + bcr: 10 * _1pct + }); + + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: _1pct / 2, // 0.5% + maxAnnualInterestRate: 250 * _1pct, + maxAnnualBatchManagementFee: uint128(_100pct / 10), // 10% + upfrontInterestPeriod: 7 days, + interestRateAdjCooldown: 7 days, + minInterestRateChangePeriod: 1 hours, + maxBatchSharesRatio: 1e9 + }); + + ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ + redemptionFeeFloor: _1pct / 2, // 0.5% + initialBaseRate: _100pct, + redemptionMinuteDecayFactor: 998076443575628800, + redemptionBeta: 1, + urgentRedemptionBonus: 2 * _1pct + }); + + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ + spYieldSplit: 75 * _1pct, + minBoldInSP: 1e18 + }); + + SystemParams params = new SystemParams( + debtParams, + liquidationParams, + gasCompParams, + collateralParams, + interestParams, + redemptionParams, + poolParams + ); + + // Verify all parameters were set correctly + assertEq(params.MIN_DEBT(), 2000e18); + assertEq(params.LIQUIDATION_PENALTY_SP(), 5e16); + assertEq(params.LIQUIDATION_PENALTY_REDISTRIBUTION(), 10e16); + assertEq(params.COLL_GAS_COMPENSATION_DIVISOR(), 200); + assertEq(params.COLL_GAS_COMPENSATION_CAP(), 2 ether); + assertEq(params.ETH_GAS_COMPENSATION(), 0.0375 ether); + assertEq(params.CCR(), 150 * _1pct); + assertEq(params.SCR(), 110 * _1pct); + assertEq(params.MCR(), 110 * _1pct); + assertEq(params.BCR(), 10 * _1pct); + assertEq(params.MIN_ANNUAL_INTEREST_RATE(), _1pct / 2); + assertEq(params.MAX_ANNUAL_INTEREST_RATE(), 250 * _1pct); + assertEq(params.MAX_ANNUAL_BATCH_MANAGEMENT_FEE(), uint128(_100pct / 10)); + assertEq(params.UPFRONT_INTEREST_PERIOD(), 7 days); + assertEq(params.INTEREST_RATE_ADJ_COOLDOWN(), 7 days); + assertEq(params.MIN_INTEREST_RATE_CHANGE_PERIOD(), 1 hours); + assertEq(params.MAX_BATCH_SHARES_RATIO(), 1e9); + assertEq(params.REDEMPTION_FEE_FLOOR(), _1pct / 2); + assertEq(params.INITIAL_BASE_RATE(), _100pct); + assertEq(params.REDEMPTION_MINUTE_DECAY_FACTOR(), 998076443575628800); + assertEq(params.REDEMPTION_BETA(), 1); + assertEq(params.URGENT_REDEMPTION_BONUS(), 2 * _1pct); + assertEq(params.SP_YIELD_SPLIT(), 75 * _1pct); + assertEq(params.MIN_BOLD_IN_SP(), 1e18); + } + + // ========== DEBT VALIDATION TESTS ========== + + function testConstructorRevertsWhenMinDebtIsZero() public { + ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({minDebt: 0}); - function testUpdateMinDebtRevertsWhenTooHigh() public { - vm.prank(owner); vm.expectRevert(ISystemParams.InvalidMinDebt.selector); - systemParams.updateMinDebt(10001e18); + new SystemParams( + debtParams, + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateLiquidationParams() public { - uint256 minPenaltySP = 6 * _1pct; - uint256 maxPenaltyRedist = 25 * _1pct; - uint256 penaltySP = 7 * _1pct; - uint256 penaltyRedist = 15 * _1pct; - - vm.prank(owner); - vm.expectEmit(true, true, false, true); - emit LiquidationParamsUpdated(minPenaltySP, maxPenaltyRedist, penaltySP, penaltyRedist); - systemParams.updateLiquidationParams(minPenaltySP, maxPenaltyRedist, penaltySP, penaltyRedist); + function testConstructorRevertsWhenMinDebtTooHigh() public { + ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({minDebt: 10001e18}); - assertEq(systemParams.MIN_LIQUIDATION_PENALTY_SP(), minPenaltySP); - assertEq(systemParams.MAX_LIQUIDATION_PENALTY_REDISTRIBUTION(), maxPenaltyRedist); - assertEq(systemParams.LIQUIDATION_PENALTY_SP(), penaltySP); - assertEq(systemParams.LIQUIDATION_PENALTY_REDISTRIBUTION(), penaltyRedist); + vm.expectRevert(ISystemParams.InvalidMinDebt.selector); + new SystemParams( + debtParams, + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateLiquidationParamsRevertsSPPenaltyTooLow() public { - uint256 minPenaltySP = 10 * _1pct; - uint256 penaltySP = 5 * _1pct; + // ========== LIQUIDATION VALIDATION TESTS ========== + + function testConstructorRevertsWhenSPPenaltyTooLow() public { + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 4 * _1pct, // Below hardcoded 5% minimum + liquidationPenaltyRedistribution: 10e16 + }); - vm.prank(owner); vm.expectRevert(ISystemParams.SPPenaltyTooLow.selector); - systemParams.updateLiquidationParams(minPenaltySP, 20 * _1pct, penaltySP, 15 * _1pct); + new SystemParams( + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateLiquidationParamsRevertsSPPenaltyGtRedist() public { - uint256 penaltySP = 20 * _1pct; - uint256 penaltyRedist = 15 * _1pct; + function testConstructorRevertsWhenSPPenaltyGreaterThanRedistribution() public { + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 15e16, + liquidationPenaltyRedistribution: 10e16 // SP > Redistribution + }); - vm.prank(owner); vm.expectRevert(ISystemParams.SPPenaltyGtRedist.selector); - systemParams.updateLiquidationParams(5 * _1pct, 25 * _1pct, penaltySP, penaltyRedist); + new SystemParams( + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateLiquidationParamsRevertsRedistPenaltyTooHigh() public { - uint256 maxPenaltyRedist = 20 * _1pct; - uint256 penaltyRedist = 25 * _1pct; + function testConstructorRevertsWhenRedistPenaltyTooHigh() public { + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, + liquidationPenaltyRedistribution: 21 * _1pct // Above hardcoded 20% maximum + }); - vm.prank(owner); vm.expectRevert(ISystemParams.RedistPenaltyTooHigh.selector); - systemParams.updateLiquidationParams(5 * _1pct, maxPenaltyRedist, 10 * _1pct, penaltyRedist); + new SystemParams( + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateGasCompParams() public { - uint256 divisor = 300; - uint256 cap = 3 ether; - uint256 ethComp = 0.05 ether; + // ========== GAS COMPENSATION VALIDATION TESTS ========== - vm.prank(owner); - vm.expectEmit(true, true, false, true); - emit GasCompParamsUpdated(divisor, cap, ethComp); - systemParams.updateGasCompParams(divisor, cap, ethComp); + function testConstructorRevertsWhenGasCompDivisorZero() public { + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 0, + collGasCompensationCap: 2 ether, + ethGasCompensation: 0.0375 ether + }); - assertEq(systemParams.COLL_GAS_COMPENSATION_DIVISOR(), divisor); - assertEq(systemParams.COLL_GAS_COMPENSATION_CAP(), cap); - assertEq(systemParams.ETH_GAS_COMPENSATION(), ethComp); + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + gasCompParams, + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateGasCompParamsRevertsInvalidDivisor() public { - // Test zero divisor - vm.prank(owner); - vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); - systemParams.updateGasCompParams(0, 2 ether, 0.05 ether); + function testConstructorRevertsWhenGasCompDivisorTooHigh() public { + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 1001, + collGasCompensationCap: 2 ether, + ethGasCompensation: 0.0375 ether + }); - // Test divisor too high - vm.prank(owner); vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); - systemParams.updateGasCompParams(1001, 2 ether, 0.05 ether); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + gasCompParams, + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateGasCompParamsRevertsInvalidCap() public { - // Test zero cap - vm.prank(owner); + function testConstructorRevertsWhenGasCompCapZero() public { + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 0, + ethGasCompensation: 0.0375 ether + }); + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); - systemParams.updateGasCompParams(200, 0, 0.05 ether); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + gasCompParams, + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenGasCompCapTooHigh() public { + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 11 ether, + ethGasCompensation: 0.0375 ether + }); - // Test cap too high - vm.prank(owner); vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); - systemParams.updateGasCompParams(200, 11 ether, 0.05 ether); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + gasCompParams, + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateGasCompParamsRevertsInvalidETHCompensation() public { - // Test zero ETH compensation - vm.prank(owner); + function testConstructorRevertsWhenETHGasCompZero() public { + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 2 ether, + ethGasCompensation: 0 + }); + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); - systemParams.updateGasCompParams(200, 2 ether, 0); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + gasCompParams, + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenETHGasCompTooHigh() public { + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 2 ether, + ethGasCompensation: 1.1 ether + }); - // Test ETH compensation too high - vm.prank(owner); vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); - systemParams.updateGasCompParams(200, 2 ether, 1.1 ether); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + gasCompParams, + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateCollateralParams() public { - uint256 ccr = 160 * _1pct; - uint256 scr = 120 * _1pct; - uint256 mcr = 115 * _1pct; - uint256 bcr = 15 * _1pct; + // ========== COLLATERAL VALIDATION TESTS ========== - vm.prank(owner); - vm.expectEmit(true, true, false, true); - emit CollateralParamsUpdated(ccr, scr, mcr, bcr); - systemParams.updateCollateralParams(ccr, scr, mcr, bcr); + function testConstructorRevertsWhenCCRTooLow() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: _100pct, // <= 100% + scr: 110 * _1pct, + mcr: 110 * _1pct, + bcr: 10 * _1pct + }); - assertEq(systemParams.CCR(), ccr); - assertEq(systemParams.SCR(), scr); - assertEq(systemParams.MCR(), mcr); - assertEq(systemParams.BCR(), bcr); + vm.expectRevert(ISystemParams.InvalidCCR.selector); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateCollateralParamsRevertsInvalidCCR() public { - // CCR too low - vm.prank(owner); - vm.expectRevert(ISystemParams.InvalidCCR.selector); - systemParams.updateCollateralParams(100 * _1pct, 110 * _1pct, 110 * _1pct, 10 * _1pct); + function testConstructorRevertsWhenCCRTooHigh() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 2 * _100pct, // >= 200% + scr: 110 * _1pct, + mcr: 110 * _1pct, + bcr: 10 * _1pct + }); - // CCR too high - vm.prank(owner); vm.expectRevert(ISystemParams.InvalidCCR.selector); - systemParams.updateCollateralParams(200 * _1pct, 110 * _1pct, 110 * _1pct, 10 * _1pct); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateCollateralParamsRevertsInvalidMCR() public { - // MCR too low - vm.prank(owner); + function testConstructorRevertsWhenMCRTooLow() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 110 * _1pct, + mcr: _100pct, // <= 100% + bcr: 10 * _1pct + }); + vm.expectRevert(ISystemParams.InvalidMCR.selector); - systemParams.updateCollateralParams(150 * _1pct, 110 * _1pct, 100 * _1pct, 10 * _1pct); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenMCRTooHigh() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 110 * _1pct, + mcr: 2 * _100pct, // >= 200% + bcr: 10 * _1pct + }); - // MCR too high - vm.prank(owner); vm.expectRevert(ISystemParams.InvalidMCR.selector); - systemParams.updateCollateralParams(150 * _1pct, 110 * _1pct, 200 * _1pct, 10 * _1pct); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateCollateralParamsRevertsInvalidBCR() public { - // BCR too low - vm.prank(owner); + function testConstructorRevertsWhenBCRTooLow() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 110 * _1pct, + mcr: 110 * _1pct, + bcr: 4 * _1pct // < 5% + }); + vm.expectRevert(ISystemParams.InvalidBCR.selector); - systemParams.updateCollateralParams(150 * _1pct, 110 * _1pct, 110 * _1pct, 4 * _1pct); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenBCRTooHigh() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 110 * _1pct, + mcr: 110 * _1pct, + bcr: 50 * _1pct // >= 50% + }); - // BCR too high - vm.prank(owner); vm.expectRevert(ISystemParams.InvalidBCR.selector); - systemParams.updateCollateralParams(150 * _1pct, 110 * _1pct, 110 * _1pct, 50 * _1pct); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateCollateralParamsRevertsInvalidSCR() public { - // SCR too low - vm.prank(owner); + function testConstructorRevertsWhenSCRTooLow() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: _100pct, // <= 100% + mcr: 110 * _1pct, + bcr: 10 * _1pct + }); + vm.expectRevert(ISystemParams.InvalidSCR.selector); - systemParams.updateCollateralParams(150 * _1pct, 100 * _1pct, 110 * _1pct, 10 * _1pct); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenSCRTooHigh() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 2 * _100pct, // >= 200% + mcr: 110 * _1pct, + bcr: 10 * _1pct + }); - // SCR too high - vm.prank(owner); vm.expectRevert(ISystemParams.InvalidSCR.selector); - systemParams.updateCollateralParams(150 * _1pct, 200 * _1pct, 110 * _1pct, 10 * _1pct); - } - - function testUpdateInterestParams() public { - uint256 minRate = 1 * _1pct; - uint256 maxRate = 200 * _1pct; - uint128 maxFee = uint128(5 * _1pct); - uint256 upfrontPeriod = 14 days; - uint256 cooldown = 14 days; - uint128 minChangePeriod = 2 hours; - uint256 maxSharesRatio = 2e9; - - vm.prank(owner); - vm.expectEmit(true, true, false, true); - emit InterestParamsUpdated(minRate, maxRate, maxFee, upfrontPeriod, cooldown, minChangePeriod, maxSharesRatio); - systemParams.updateInterestParams(minRate, maxRate, maxFee, upfrontPeriod, cooldown, minChangePeriod, maxSharesRatio); - - assertEq(systemParams.MIN_ANNUAL_INTEREST_RATE(), minRate); - assertEq(systemParams.MAX_ANNUAL_INTEREST_RATE(), maxRate); - assertEq(systemParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE(), maxFee); - assertEq(systemParams.UPFRONT_INTEREST_PERIOD(), upfrontPeriod); - assertEq(systemParams.INTEREST_RATE_ADJ_COOLDOWN(), cooldown); - assertEq(systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD(), minChangePeriod); - assertEq(systemParams.MAX_BATCH_SHARES_RATIO(), maxSharesRatio); - } - - function testUpdateInterestParamsRevertsMinGtMax() public { - vm.prank(owner); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + // ========== INTEREST VALIDATION TESTS ========== + + function testConstructorRevertsWhenMinInterestRateGreaterThanMax() public { + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: 100 * _1pct, + maxAnnualInterestRate: 50 * _1pct, // min > max + maxAnnualBatchManagementFee: uint128(_100pct / 10), + upfrontInterestPeriod: 7 days, + interestRateAdjCooldown: 7 days, + minInterestRateChangePeriod: 1 hours, + maxBatchSharesRatio: 1e9 + }); + vm.expectRevert(ISystemParams.MinInterestRateGtMax.selector); - systemParams.updateInterestParams( - 200 * _1pct, // min > max - 100 * _1pct, - uint128(5 * _1pct), - 7 days, - 7 days, - 1 hours, - 1e9 + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + interestParams, + _getValidRedemptionParams(), + _getValidPoolParams() ); } - function testUpdateInterestParamsRevertsMaxTooHigh() public { - vm.prank(owner); + function testConstructorRevertsWhenMaxInterestRateTooHigh() public { + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: _1pct / 2, + maxAnnualInterestRate: 1001 * _1pct, // > 1000% + maxAnnualBatchManagementFee: uint128(_100pct / 10), + upfrontInterestPeriod: 7 days, + interestRateAdjCooldown: 7 days, + minInterestRateChangePeriod: 1 hours, + maxBatchSharesRatio: 1e9 + }); + vm.expectRevert(ISystemParams.InvalidInterestRateBounds.selector); - systemParams.updateInterestParams( - 1 * _1pct, - 1001 * _1pct, // > 1000% - uint128(5 * _1pct), - 7 days, - 7 days, - 1 hours, - 1e9 + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + interestParams, + _getValidRedemptionParams(), + _getValidPoolParams() ); } - function testUpdateInterestParamsRevertsInvalidFee() public { - vm.prank(owner); + function testConstructorRevertsWhenMaxBatchManagementFeeTooHigh() public { + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: _1pct / 2, + maxAnnualInterestRate: 250 * _1pct, + maxAnnualBatchManagementFee: uint128(_100pct + 1), // > 100% + upfrontInterestPeriod: 7 days, + interestRateAdjCooldown: 7 days, + minInterestRateChangePeriod: 1 hours, + maxBatchSharesRatio: 1e9 + }); + vm.expectRevert(ISystemParams.InvalidFeeValue.selector); - systemParams.updateInterestParams( - 1 * _1pct, - 100 * _1pct, - uint128(101 * _1pct), // > 100% - 7 days, - 7 days, - 1 hours, - 1e9 + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + interestParams, + _getValidRedemptionParams(), + _getValidPoolParams() ); } - function testUpdateInterestParamsRevertsInvalidUpfrontPeriod() public { - // Zero upfront period - vm.prank(owner); - vm.expectRevert(ISystemParams.InvalidTimeValue.selector); - systemParams.updateInterestParams(1 * _1pct, 100 * _1pct, uint128(5 * _1pct), 0, 7 days, 1 hours, 1e9); + function testConstructorRevertsWhenUpfrontInterestPeriodZero() public { + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: _1pct / 2, + maxAnnualInterestRate: 250 * _1pct, + maxAnnualBatchManagementFee: uint128(_100pct / 10), + upfrontInterestPeriod: 0, + interestRateAdjCooldown: 7 days, + minInterestRateChangePeriod: 1 hours, + maxBatchSharesRatio: 1e9 + }); - // Upfront period too long - vm.prank(owner); vm.expectRevert(ISystemParams.InvalidTimeValue.selector); - systemParams.updateInterestParams(1 * _1pct, 100 * _1pct, uint128(5 * _1pct), 366 days, 7 days, 1 hours, 1e9); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + interestParams, + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateInterestParamsRevertsInvalidCooldown() public { - // Zero cooldown - vm.prank(owner); - vm.expectRevert(ISystemParams.InvalidTimeValue.selector); - systemParams.updateInterestParams(1 * _1pct, 100 * _1pct, uint128(5 * _1pct), 7 days, 0, 1 hours, 1e9); + function testConstructorRevertsWhenUpfrontInterestPeriodTooLong() public { + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: _1pct / 2, + maxAnnualInterestRate: 250 * _1pct, + maxAnnualBatchManagementFee: uint128(_100pct / 10), + upfrontInterestPeriod: 366 days, + interestRateAdjCooldown: 7 days, + minInterestRateChangePeriod: 1 hours, + maxBatchSharesRatio: 1e9 + }); - // Cooldown too long - vm.prank(owner); vm.expectRevert(ISystemParams.InvalidTimeValue.selector); - systemParams.updateInterestParams(1 * _1pct, 100 * _1pct, uint128(5 * _1pct), 7 days, 366 days, 1 hours, 1e9); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + interestParams, + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateInterestParamsRevertsInvalidChangePeriod() public { - // Zero change period - vm.prank(owner); - vm.expectRevert(ISystemParams.InvalidTimeValue.selector); - systemParams.updateInterestParams(1 * _1pct, 100 * _1pct, uint128(5 * _1pct), 7 days, 7 days, 0, 1e9); + function testConstructorRevertsWhenInterestRateAdjCooldownZero() public { + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: _1pct / 2, + maxAnnualInterestRate: 250 * _1pct, + maxAnnualBatchManagementFee: uint128(_100pct / 10), + upfrontInterestPeriod: 7 days, + interestRateAdjCooldown: 0, + minInterestRateChangePeriod: 1 hours, + maxBatchSharesRatio: 1e9 + }); - // Change period too long - vm.prank(owner); vm.expectRevert(ISystemParams.InvalidTimeValue.selector); - systemParams.updateInterestParams(1 * _1pct, 100 * _1pct, uint128(5 * _1pct), 7 days, 7 days, 31 days, 1e9); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + interestParams, + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateRedemptionParams() public { - uint256 feeFloor = 1 * _1pct; - uint256 baseRate = 50 * _1pct; - uint256 decayFactor = 999000000000000000; - uint256 beta = 2; - uint256 urgentBonus = 3 * _1pct; + function testConstructorRevertsWhenInterestRateAdjCooldownTooLong() public { + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: _1pct / 2, + maxAnnualInterestRate: 250 * _1pct, + maxAnnualBatchManagementFee: uint128(_100pct / 10), + upfrontInterestPeriod: 7 days, + interestRateAdjCooldown: 366 days, + minInterestRateChangePeriod: 1 hours, + maxBatchSharesRatio: 1e9 + }); - vm.prank(owner); - vm.expectEmit(true, true, false, true); - emit RedemptionParamsUpdated(feeFloor, baseRate, decayFactor, beta, urgentBonus); - systemParams.updateRedemptionParams(feeFloor, baseRate, decayFactor, beta, urgentBonus); - - assertEq(systemParams.REDEMPTION_FEE_FLOOR(), feeFloor); - assertEq(systemParams.INITIAL_BASE_RATE(), baseRate); - assertEq(systemParams.REDEMPTION_MINUTE_DECAY_FACTOR(), decayFactor); - assertEq(systemParams.REDEMPTION_BETA(), beta); - assertEq(systemParams.URGENT_REDEMPTION_BONUS(), urgentBonus); + vm.expectRevert(ISystemParams.InvalidTimeValue.selector); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + interestParams, + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateRedemptionParamsRevertsInvalidFeeFloor() public { - vm.prank(owner); - vm.expectRevert(ISystemParams.InvalidFeeValue.selector); - systemParams.updateRedemptionParams(101 * _1pct, 50 * _1pct, 999000000000000000, 1, 2 * _1pct); - } + function testConstructorRevertsWhenMinInterestRateChangePeriodZero() public { + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: _1pct / 2, + maxAnnualInterestRate: 250 * _1pct, + maxAnnualBatchManagementFee: uint128(_100pct / 10), + upfrontInterestPeriod: 7 days, + interestRateAdjCooldown: 7 days, + minInterestRateChangePeriod: 0, + maxBatchSharesRatio: 1e9 + }); - function testUpdateRedemptionParamsRevertsInvalidBaseRate() public { - vm.prank(owner); - vm.expectRevert(ISystemParams.InvalidFeeValue.selector); - systemParams.updateRedemptionParams(1 * _1pct, 1001 * _1pct, 999000000000000000, 1, 2 * _1pct); + vm.expectRevert(ISystemParams.InvalidTimeValue.selector); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + interestParams, + _getValidRedemptionParams(), + _getValidPoolParams() + ); } - function testUpdateRedemptionParamsRevertsInvalidUrgentBonus() public { - vm.prank(owner); - vm.expectRevert(ISystemParams.InvalidFeeValue.selector); - systemParams.updateRedemptionParams(1 * _1pct, 50 * _1pct, 999000000000000000, 1, 101 * _1pct); - } + function testConstructorRevertsWhenMinInterestRateChangePeriodTooLong() public { + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: _1pct / 2, + maxAnnualInterestRate: 250 * _1pct, + maxAnnualBatchManagementFee: uint128(_100pct / 10), + upfrontInterestPeriod: 7 days, + interestRateAdjCooldown: 7 days, + minInterestRateChangePeriod: 31 days, + maxBatchSharesRatio: 1e9 + }); - function testUpdatePoolParams() public { - uint256 yieldSplit = 80 * _1pct; - uint256 minBold = 10e18; + vm.expectRevert(ISystemParams.InvalidTimeValue.selector); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + interestParams, + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } - vm.prank(owner); - vm.expectEmit(true, true, false, true); - emit StabilityPoolParamsUpdated(yieldSplit, minBold); - systemParams.updatePoolParams(yieldSplit, minBold); + // ========== REDEMPTION VALIDATION TESTS ========== - assertEq(systemParams.SP_YIELD_SPLIT(), yieldSplit); - assertEq(systemParams.MIN_BOLD_IN_SP(), minBold); - } + function testConstructorRevertsWhenRedemptionFeeFloorTooHigh() public { + ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ + redemptionFeeFloor: _100pct + 1, + initialBaseRate: _100pct, + redemptionMinuteDecayFactor: 998076443575628800, + redemptionBeta: 1, + urgentRedemptionBonus: 2 * _1pct + }); - function testUpdatePoolParamsRevertsInvalidYieldSplit() public { - vm.prank(owner); vm.expectRevert(ISystemParams.InvalidFeeValue.selector); - systemParams.updatePoolParams(101 * _1pct, 1e18); - } - - function testUpdatePoolParamsRevertsZeroMinBold() public { - vm.prank(owner); - vm.expectRevert(ISystemParams.InvalidMinDebt.selector); - systemParams.updatePoolParams(75 * _1pct, 0); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + redemptionParams, + _getValidPoolParams() + ); } - function testUpdateVersion() public { - uint256 oldVersion = systemParams.version(); - uint256 newVersion = 2; + function testConstructorRevertsWhenInitialBaseRateTooHigh() public { + ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ + redemptionFeeFloor: _1pct / 2, + initialBaseRate: 1001 * _1pct, // > 1000% + redemptionMinuteDecayFactor: 998076443575628800, + redemptionBeta: 1, + urgentRedemptionBonus: 2 * _1pct + }); - vm.prank(owner); - vm.expectEmit(true, true, false, true); - emit VersionUpdated(oldVersion, newVersion); - systemParams.updateVersion(newVersion); - - assertEq(systemParams.version(), newVersion); + vm.expectRevert(ISystemParams.InvalidFeeValue.selector); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + redemptionParams, + _getValidPoolParams() + ); } - function testOnlyOwnerCanUpdate() public { - vm.startPrank(nonOwner); + function testConstructorRevertsWhenUrgentRedemptionBonusTooHigh() public { + ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ + redemptionFeeFloor: _1pct / 2, + initialBaseRate: _100pct, + redemptionMinuteDecayFactor: 998076443575628800, + redemptionBeta: 1, + urgentRedemptionBonus: _100pct + 1 + }); - // Test all update functions revert for non-owner - vm.expectRevert("Ownable: caller is not the owner"); - systemParams.updateMinDebt(3000e18); - - vm.expectRevert("Ownable: caller is not the owner"); - systemParams.updateLiquidationParams(6 * _1pct, 25 * _1pct, 7 * _1pct, 15 * _1pct); - - vm.expectRevert("Ownable: caller is not the owner"); - systemParams.updateGasCompParams(300, 3 ether, 0.05 ether); - - vm.expectRevert("Ownable: caller is not the owner"); - systemParams.updateCollateralParams(160 * _1pct, 120 * _1pct, 115 * _1pct, 15 * _1pct); + vm.expectRevert(ISystemParams.InvalidFeeValue.selector); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + redemptionParams, + _getValidPoolParams() + ); + } - vm.expectRevert("Ownable: caller is not the owner"); - systemParams.updateInterestParams(1 * _1pct, 200 * _1pct, uint128(5 * _1pct), 14 days, 14 days, 2 hours, 2e9); + // ========== STABILITY POOL VALIDATION TESTS ========== - vm.expectRevert("Ownable: caller is not the owner"); - systemParams.updateRedemptionParams(1 * _1pct, 50 * _1pct, 999000000000000000, 2, 3 * _1pct); + function testConstructorRevertsWhenSPYieldSplitTooHigh() public { + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ + spYieldSplit: _100pct + 1, + minBoldInSP: 1e18 + }); - vm.expectRevert("Ownable: caller is not the owner"); - systemParams.updatePoolParams(80 * _1pct, 10e18); + vm.expectRevert(ISystemParams.InvalidFeeValue.selector); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + poolParams + ); + } - vm.expectRevert("Ownable: caller is not the owner"); - systemParams.updateVersion(2); + function testConstructorRevertsWhenMinBoldInSPZero() public { + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ + spYieldSplit: 75 * _1pct, + minBoldInSP: 0 + }); - vm.stopPrank(); + vm.expectRevert(ISystemParams.InvalidMinDebt.selector); + new SystemParams( + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + poolParams + ); } - function testOwnerCanTransferOwnership() public { - address newOwner = address(0x5678); + // ========== HELPER FUNCTIONS ========== - // Transfer ownership - vm.prank(owner); - SystemParams(address(systemParams)).transferOwnership(newOwner); - assertEq(SystemParams(address(systemParams)).owner(), newOwner); + function _getValidDebtParams() internal pure returns (ISystemParams.DebtParams memory) { + return ISystemParams.DebtParams({minDebt: 2000e18}); + } - // New owner can update - vm.prank(newOwner); - systemParams.updateVersion(3); - assertEq(systemParams.version(), 3); + function _getValidLiquidationParams() internal pure returns (ISystemParams.LiquidationParams memory) { + return ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, + liquidationPenaltyRedistribution: 10e16 + }); + } - // Old owner cannot update - vm.prank(owner); - vm.expectRevert("Ownable: caller is not the owner"); - systemParams.updateVersion(4); + function _getValidGasCompParams() internal pure returns (ISystemParams.GasCompParams memory) { + return ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 2 ether, + ethGasCompensation: 0.0375 ether + }); } - function testParameterBoundaryValues() public { - // Test minimum valid values - vm.prank(owner); - systemParams.updateMinDebt(1); // Just above 0 - assertEq(systemParams.MIN_DEBT(), 1); + function _getValidCollateralParams() internal pure returns (ISystemParams.CollateralParams memory) { + return ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 110 * _1pct, + mcr: 110 * _1pct, + bcr: 10 * _1pct + }); + } - vm.prank(owner); - systemParams.updateMinDebt(10000e18); // Exactly at max - assertEq(systemParams.MIN_DEBT(), 10000e18); + function _getValidInterestParams() internal pure returns (ISystemParams.InterestParams memory) { + return ISystemParams.InterestParams({ + minAnnualInterestRate: _1pct / 2, + maxAnnualInterestRate: 250 * _1pct, + maxAnnualBatchManagementFee: uint128(_100pct / 10), + upfrontInterestPeriod: 7 days, + interestRateAdjCooldown: 7 days, + minInterestRateChangePeriod: 1 hours, + maxBatchSharesRatio: 1e9 + }); + } - // Test collateral params at boundaries - vm.prank(owner); - systemParams.updateCollateralParams( - _100pct + 1, // CCR just above 100% - _100pct + 1, // SCR just above 100% - _100pct + 1, // MCR just above 100% - 5 * _1pct // BCR at minimum - ); - assertEq(systemParams.CCR(), _100pct + 1); - assertEq(systemParams.BCR(), 5 * _1pct); + function _getValidRedemptionParams() internal pure returns (ISystemParams.RedemptionParams memory) { + return ISystemParams.RedemptionParams({ + redemptionFeeFloor: _1pct / 2, + initialBaseRate: _100pct, + redemptionMinuteDecayFactor: 998076443575628800, + redemptionBeta: 1, + urgentRedemptionBonus: 2 * _1pct + }); + } - // Test gas comp at boundaries - vm.prank(owner); - systemParams.updateGasCompParams( - 1, // Min divisor - 10 ether, // Max cap - 1 ether // Max ETH comp - ); - assertEq(systemParams.COLL_GAS_COMPENSATION_DIVISOR(), 1); - assertEq(systemParams.COLL_GAS_COMPENSATION_CAP(), 10 ether); - assertEq(systemParams.ETH_GAS_COMPENSATION(), 1 ether); + function _getValidPoolParams() internal pure returns (ISystemParams.StabilityPoolParams memory) { + return ISystemParams.StabilityPoolParams({ + spYieldSplit: 75 * _1pct, + minBoldInSP: 1e18 + }); } -} \ No newline at end of file +} From b877c0d853bb9ba93aab58e405ea94851d33a60b Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:18:04 -0500 Subject: [PATCH 44/79] style: fmt --- contracts/script/DeployLiquity2.s.sol | 626 ++++++++------------------ 1 file changed, 177 insertions(+), 449 deletions(-) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 01f024439..9c8d83c70 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -6,7 +6,8 @@ import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/exten import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; import {IERC20 as IERC20_GOV} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {ProxyAdmin} from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; -import {TransparentUpgradeableProxy} from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {TransparentUpgradeableProxy} from + "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {IFPMMFactory} from "src/Interfaces/IFPMMFactory.sol"; import {SystemParams} from "src/SystemParams.sol"; import {ISystemParams} from "src/Interfaces/ISystemParams.sol"; @@ -123,16 +124,15 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { string stableTokenSymbol; } - DeploymentConfig internal CONFIG = - DeploymentConfig({ - USDm_ALFAJORES_ADDRESS: 0x9E2d4412d0f434cC85500b79447d9323a7416f09, - proxyAdmin: 0xe4DdacCAdb64114215FCe8251B57B2AEB5C2C0E2, - fpmmFactory: 0xd8098494a749a3fDAD2D2e7Fa5272D8f274D8FF6, - fpmmImplementation: 0x0292efcB331C6603eaa29D570d12eB336D6c01d6, - referenceRateFeedID: 0x206B25Ea01E188Ee243131aFdE526bA6E131a016, - stableTokenName: "EUR.v2 Test", - stableTokenSymbol: "EUR.v2" - }); + DeploymentConfig internal CONFIG = DeploymentConfig({ + USDm_ALFAJORES_ADDRESS: 0x9E2d4412d0f434cC85500b79447d9323a7416f09, + proxyAdmin: 0xe4DdacCAdb64114215FCe8251B57B2AEB5C2C0E2, + fpmmFactory: 0xd8098494a749a3fDAD2D2e7Fa5272D8f274D8FF6, + fpmmImplementation: 0x0292efcB331C6603eaa29D570d12eB336D6c01d6, + referenceRateFeedID: 0x206B25Ea01E188Ee243131aFdE526bA6E131a016, + stableTokenName: "EUR.v2 Test", + stableTokenSymbol: "EUR.v2" + }); function run() external { string memory saltStr = vm.envOr("SALT", block.timestamp.toString()); @@ -144,49 +144,30 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { _log("Deployer: ", deployer.toHexString()); _log("Deployer balance: ", deployer.balance.decimal()); - _log( - "CREATE2 salt: ", - 'keccak256(bytes("', - saltStr, - '")) = ', - uint256(SALT).toHexString() - ); + _log("CREATE2 salt: ", 'keccak256(bytes("', saltStr, '")) = ', uint256(SALT).toHexString()); _log("Chain ID: ", block.chainid.toString()); DeploymentResult memory deployed = _deployAndConnectContracts(); vm.stopBroadcast(); - vm.writeFile( - "script/deployment-manifest.json", - _getManifestJson(deployed) - ); + vm.writeFile("script/deployment-manifest.json", _getManifestJson(deployed)); } // See: https://solidity-by-example.org/app/create2/ - function getBytecode( - bytes memory _creationCode, - address _addressesRegistry - ) public pure returns (bytes memory) { + function getBytecode(bytes memory _creationCode, address _addressesRegistry) public pure returns (bytes memory) { return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry)); } - function getBytecode( - bytes memory _creationCode, - address _addressesRegistry, - address _systemParams - ) public pure returns (bytes memory) { - return - abi.encodePacked( - _creationCode, - abi.encode(_addressesRegistry, _systemParams) - ); + function getBytecode(bytes memory _creationCode, address _addressesRegistry, address _systemParams) + public + pure + returns (bytes memory) + { + return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry, _systemParams)); } - function _deployAndConnectContracts() - internal - returns (DeploymentResult memory r) - { + function _deployAndConnectContracts() internal returns (DeploymentResult memory r) { _deployProxyInfrastructure(r); _deployStableToken(r); // _deployFPMM(r); @@ -194,15 +175,10 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { IAddressesRegistry addressesRegistry = new AddressesRegistry(deployer); - address troveManagerAddress = _computeCreate2Address( - type(TroveManager).creationCode, - address(addressesRegistry), - address(r.systemParams) - ); + address troveManagerAddress = + _computeCreate2Address(type(TroveManager).creationCode, address(addressesRegistry), address(r.systemParams)); - IERC20Metadata collToken = IERC20Metadata( - CONFIG.USDm_ALFAJORES_ADDRESS - ); + IERC20Metadata collToken = IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS); IERC20Metadata[] memory collaterals = new IERC20Metadata[](1); collaterals[0] = collToken; @@ -210,24 +186,15 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { ITroveManager[] memory troveManagers = new ITroveManager[](1); troveManagers[0] = ITroveManager(troveManagerAddress); - r.collateralRegistry = new CollateralRegistry( - IBoldToken(address(r.stableToken)), - collaterals, - troveManagers, - r.systemParams - ); + r.collateralRegistry = + new CollateralRegistry(IBoldToken(address(r.stableToken)), collaterals, troveManagers, r.systemParams); r.hintHelpers = new HintHelpers(r.collateralRegistry, r.systemParams); r.multiTroveGetter = new MultiTroveGetter(r.collateralRegistry); IPriceFeed priceFeed = new PriceFeedTestnet(); - r.contracts = _deployAndConnectCollateralContracts( - collToken, - priceFeed, - addressesRegistry, - troveManagerAddress, - r - ); + r.contracts = + _deployAndConnectCollateralContracts(collToken, priceFeed, addressesRegistry, troveManagerAddress, r); } function _deployProxyInfrastructure(DeploymentResult memory r) internal { @@ -236,103 +203,66 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { r.stabilityPoolImpl = address(new StabilityPool{salt: SALT}(true)); assert( - address(r.stableTokenV3Impl) == - vm.computeCreate2Address( - SALT, - keccak256( - bytes.concat( - type(StableTokenV3).creationCode, - abi.encode(true) - ) - ) + address(r.stableTokenV3Impl) + == vm.computeCreate2Address( + SALT, keccak256(bytes.concat(type(StableTokenV3).creationCode, abi.encode(true))) ) ); assert( - address(r.stabilityPoolImpl) == - vm.computeCreate2Address( - SALT, - keccak256( - bytes.concat( - type(StabilityPool).creationCode, - abi.encode(true) - ) - ) + address(r.stabilityPoolImpl) + == vm.computeCreate2Address( + SALT, keccak256(bytes.concat(type(StabilityPool).creationCode, abi.encode(true))) ) ); } function _deployStableToken(DeploymentResult memory r) internal { r.stableToken = IStableTokenV3( - address( - new TransparentUpgradeableProxy( - address(r.stableTokenV3Impl), - address(r.proxyAdmin), - "" - ) - ) + address(new TransparentUpgradeableProxy(address(r.stableTokenV3Impl), address(r.proxyAdmin), "")) ); } function _deployFPMM(DeploymentResult memory r) internal { r.fpmm = IFPMMFactory(CONFIG.fpmmFactory).deployFPMM( - CONFIG.fpmmImplementation, - address(r.stableToken), - CONFIG.USDm_ALFAJORES_ADDRESS, - CONFIG.referenceRateFeedID + CONFIG.fpmmImplementation, address(r.stableToken), CONFIG.USDm_ALFAJORES_ADDRESS, CONFIG.referenceRateFeedID ); } function _deploySystemParams(DeploymentResult memory r) internal { - ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({ - minDebt: 2000e18 + ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({minDebt: 2000e18}); + + ISystemParams.LiquidationParams memory liquidationParams = + ISystemParams.LiquidationParams({liquidationPenaltySP: 5e16, liquidationPenaltyRedistribution: 10e16}); + + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 2 ether, + ethGasCompensation: 0.0375 ether + }); + + ISystemParams.CollateralParams memory collateralParams = + ISystemParams.CollateralParams({ccr: 150 * 1e16, scr: 110 * 1e16, mcr: 110 * 1e16, bcr: 10 * 1e16}); + + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: 1e18 / 200, + maxAnnualInterestRate: 250 * (1e18 / 100), + maxAnnualBatchManagementFee: uint128(1e18 / 10), + upfrontInterestPeriod: 7 days, + interestRateAdjCooldown: 7 days, + minInterestRateChangePeriod: 1 hours, + maxBatchSharesRatio: 1e9 + }); + + ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ + redemptionFeeFloor: 1e18 / 200, + initialBaseRate: 1e18, + redemptionMinuteDecayFactor: 998076443575628800, + redemptionBeta: 1, + urgentRedemptionBonus: 2 * 1e16 }); - ISystemParams.LiquidationParams memory liquidationParams = ISystemParams - .LiquidationParams({ - liquidationPenaltySP: 5e16, - liquidationPenaltyRedistribution: 10e16 - }); - - ISystemParams.GasCompParams memory gasCompParams = ISystemParams - .GasCompParams({ - collGasCompensationDivisor: 200, - collGasCompensationCap: 2 ether, - ethGasCompensation: 0.0375 ether - }); - - ISystemParams.CollateralParams memory collateralParams = ISystemParams - .CollateralParams({ - ccr: 150 * 1e16, - scr: 110 * 1e16, - mcr: 110 * 1e16, - bcr: 10 * 1e16 - }); - - ISystemParams.InterestParams memory interestParams = ISystemParams - .InterestParams({ - minAnnualInterestRate: 1e18 / 200, - maxAnnualInterestRate: 250 * (1e18 / 100), - maxAnnualBatchManagementFee: uint128(1e18 / 10), - upfrontInterestPeriod: 7 days, - interestRateAdjCooldown: 7 days, - minInterestRateChangePeriod: 1 hours, - maxBatchSharesRatio: 1e9 - }); - - ISystemParams.RedemptionParams memory redemptionParams = ISystemParams - .RedemptionParams({ - redemptionFeeFloor: 1e18 / 200, - initialBaseRate: 1e18, - redemptionMinuteDecayFactor: 998076443575628800, - redemptionBeta: 1, - urgentRedemptionBonus: 2 * 1e16 - }); - - ISystemParams.StabilityPoolParams memory poolParams = ISystemParams - .StabilityPoolParams({ - spYieldSplit: 75 * (1e18 / 100), - minBoldInSP: 1e18 - }); + ISystemParams.StabilityPoolParams memory poolParams = + ISystemParams.StabilityPoolParams({spYieldSplit: 75 * (1e18 / 100), minBoldInSP: 1e18}); r.systemParams = ISystemParams( address( @@ -362,63 +292,34 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { contracts.priceFeed = _priceFeed; contracts.systemParams = r.systemParams; // TODO: replace with governance timelock on mainnet - contracts.interestRouter = IInterestRouter( - 0x56fD3F2bEE130e9867942D0F463a16fBE49B8d81 - ); + contracts.interestRouter = IInterestRouter(0x56fD3F2bEE130e9867942D0F463a16fBE49B8d81); addresses.troveManager = _troveManagerAddress; contracts.metadataNFT = deployMetadata(SALT); addresses.metadataNFT = vm.computeCreate2Address( - SALT, - keccak256( - getBytecode( - type(MetadataNFT).creationCode, - address(initializedFixedAssetReader) - ) - ) + SALT, keccak256(getBytecode(type(MetadataNFT).creationCode, address(initializedFixedAssetReader))) ); assert(address(contracts.metadataNFT) == addresses.metadataNFT); addresses.borrowerOperations = _computeCreate2Address( - type(BorrowerOperations).creationCode, - address(contracts.addressesRegistry), - address(contracts.systemParams) - ); - addresses.troveNFT = _computeCreate2Address( - type(TroveNFT).creationCode, - address(contracts.addressesRegistry) + type(BorrowerOperations).creationCode, address(contracts.addressesRegistry), address(contracts.systemParams) ); + addresses.troveNFT = _computeCreate2Address(type(TroveNFT).creationCode, address(contracts.addressesRegistry)); addresses.activePool = _computeCreate2Address( - type(ActivePool).creationCode, - address(contracts.addressesRegistry), - address(contracts.systemParams) - ); - addresses.defaultPool = _computeCreate2Address( - type(DefaultPool).creationCode, - address(contracts.addressesRegistry) - ); - addresses.gasPool = _computeCreate2Address( - type(GasPool).creationCode, - address(contracts.addressesRegistry) - ); - addresses.collSurplusPool = _computeCreate2Address( - type(CollSurplusPool).creationCode, - address(contracts.addressesRegistry) - ); - addresses.sortedTroves = _computeCreate2Address( - type(SortedTroves).creationCode, - address(contracts.addressesRegistry) + type(ActivePool).creationCode, address(contracts.addressesRegistry), address(contracts.systemParams) ); + addresses.defaultPool = + _computeCreate2Address(type(DefaultPool).creationCode, address(contracts.addressesRegistry)); + addresses.gasPool = _computeCreate2Address(type(GasPool).creationCode, address(contracts.addressesRegistry)); + addresses.collSurplusPool = + _computeCreate2Address(type(CollSurplusPool).creationCode, address(contracts.addressesRegistry)); + addresses.sortedTroves = + _computeCreate2Address(type(SortedTroves).creationCode, address(contracts.addressesRegistry)); // Deploy StabilityPool proxy - address stabilityPool = address( - new TransparentUpgradeableProxy( - address(r.stabilityPoolImpl), - address(r.proxyAdmin), - "" - ) - ); + address stabilityPool = + address(new TransparentUpgradeableProxy(address(r.stabilityPoolImpl), address(r.proxyAdmin), "")); contracts.stabilityPool = IStabilityPool(stabilityPool); // Set up addresses in registry @@ -427,10 +328,7 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { // Deploy core protocol contracts _deployProtocolContracts(contracts, addresses); - IStabilityPool(stabilityPool).initialize( - contracts.addressesRegistry, - contracts.systemParams - ); + IStabilityPool(stabilityPool).initialize(contracts.addressesRegistry, contracts.systemParams); address[] memory minters = new address[](2); minters[0] = address(contracts.borrowerOperations); @@ -461,68 +359,43 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { LiquityContractAddresses memory addresses, DeploymentResult memory r ) internal { - IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry - .AddressVars({ - collToken: contracts.collToken, - borrowerOperations: IBorrowerOperations( - addresses.borrowerOperations - ), - troveManager: ITroveManager(addresses.troveManager), - troveNFT: ITroveNFT(addresses.troveNFT), - metadataNFT: IMetadataNFT(addresses.metadataNFT), - stabilityPool: contracts.stabilityPool, - priceFeed: contracts.priceFeed, - activePool: IActivePool(addresses.activePool), - defaultPool: IDefaultPool(addresses.defaultPool), - gasPoolAddress: addresses.gasPool, - collSurplusPool: ICollSurplusPool(addresses.collSurplusPool), - sortedTroves: ISortedTroves(addresses.sortedTroves), - interestRouter: contracts.interestRouter, - hintHelpers: r.hintHelpers, - multiTroveGetter: r.multiTroveGetter, - collateralRegistry: r.collateralRegistry, - boldToken: IBoldToken(address(r.stableToken)), - gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS) - }); + IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ + collToken: contracts.collToken, + borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), + troveManager: ITroveManager(addresses.troveManager), + troveNFT: ITroveNFT(addresses.troveNFT), + metadataNFT: IMetadataNFT(addresses.metadataNFT), + stabilityPool: contracts.stabilityPool, + priceFeed: contracts.priceFeed, + activePool: IActivePool(addresses.activePool), + defaultPool: IDefaultPool(addresses.defaultPool), + gasPoolAddress: addresses.gasPool, + collSurplusPool: ICollSurplusPool(addresses.collSurplusPool), + sortedTroves: ISortedTroves(addresses.sortedTroves), + interestRouter: contracts.interestRouter, + hintHelpers: r.hintHelpers, + multiTroveGetter: r.multiTroveGetter, + collateralRegistry: r.collateralRegistry, + boldToken: IBoldToken(address(r.stableToken)), + gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS) + }); contracts.addressesRegistry.setAddresses(addressVars); } - function _deployProtocolContracts( - LiquityContracts memory contracts, - LiquityContractAddresses memory addresses - ) internal { - contracts.borrowerOperations = new BorrowerOperations{salt: SALT}( - contracts.addressesRegistry, - contracts.systemParams - ); - contracts.troveManager = new TroveManager{salt: SALT}( - contracts.addressesRegistry, - contracts.systemParams - ); - contracts.troveNFT = new TroveNFT{salt: SALT}( - contracts.addressesRegistry - ); - contracts.activePool = new ActivePool{salt: SALT}( - contracts.addressesRegistry, - contracts.systemParams - ); - contracts.defaultPool = new DefaultPool{salt: SALT}( - contracts.addressesRegistry - ); - contracts.gasPool = new GasPool{salt: SALT}( - contracts.addressesRegistry - ); - contracts.collSurplusPool = new CollSurplusPool{salt: SALT}( - contracts.addressesRegistry - ); - contracts.sortedTroves = new SortedTroves{salt: SALT}( - contracts.addressesRegistry - ); - - assert( - address(contracts.borrowerOperations) == - addresses.borrowerOperations - ); + function _deployProtocolContracts(LiquityContracts memory contracts, LiquityContractAddresses memory addresses) + internal + { + contracts.borrowerOperations = + new BorrowerOperations{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); + contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); + contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); + contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); + contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); + contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); + contracts.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); + contracts.sortedTroves = new SortedTroves{salt: SALT}(contracts.addressesRegistry); + + assert(address(contracts.borrowerOperations) == addresses.borrowerOperations); assert(address(contracts.troveManager) == addresses.troveManager); assert(address(contracts.troveNFT) == addresses.troveNFT); assert(address(contracts.activePool) == addresses.activePool); @@ -532,235 +405,90 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { assert(address(contracts.sortedTroves) == addresses.sortedTroves); } - function _computeCreate2Address( - bytes memory creationCode, - address _addressesRegistry - ) internal view returns (address) { - return - vm.computeCreate2Address( - SALT, - keccak256(getBytecode(creationCode, _addressesRegistry)) - ); + function _computeCreate2Address(bytes memory creationCode, address _addressesRegistry) + internal + view + returns (address) + { + return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry))); } - function _computeCreate2Address( - bytes memory creationCode, - address _addressesRegistry, - address _systemParams - ) internal view returns (address) { - return - vm.computeCreate2Address( - SALT, - keccak256( - getBytecode(creationCode, _addressesRegistry, _systemParams) - ) - ); + function _computeCreate2Address(bytes memory creationCode, address _addressesRegistry, address _systemParams) + internal + view + returns (address) + { + return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry, _systemParams))); } - function _getBranchContractsJson( - LiquityContracts memory c - ) internal view returns (string memory) { - return + function _getBranchContractsJson(LiquityContracts memory c) internal view returns (string memory) { + return string.concat( + "{", string.concat( - "{", + // Avoid stack too deep by chunking concats string.concat( - // Avoid stack too deep by chunking concats - string.concat( - string.concat( - '"collSymbol":"', - c.collToken.symbol(), - '",' - ), // purely for human-readability - string.concat( - '"collToken":"', - address(c.collToken).toHexString(), - '",' - ), - string.concat( - '"addressesRegistry":"', - address(c.addressesRegistry).toHexString(), - '",' - ), - string.concat( - '"activePool":"', - address(c.activePool).toHexString(), - '",' - ), - string.concat( - '"borrowerOperations":"', - address(c.borrowerOperations).toHexString(), - '",' - ), - string.concat( - '"collSurplusPool":"', - address(c.collSurplusPool).toHexString(), - '",' - ), - string.concat( - '"defaultPool":"', - address(c.defaultPool).toHexString(), - '",' - ), - string.concat( - '"sortedTroves":"', - address(c.sortedTroves).toHexString(), - '",' - ), - string.concat( - '"systemParams":"', - address(c.systemParams).toHexString(), - '",' - ) - ), - string.concat( - string.concat( - '"stabilityPool":"', - address(c.stabilityPool).toHexString(), - '",' - ), - string.concat( - '"troveManager":"', - address(c.troveManager).toHexString(), - '",' - ), - string.concat( - '"troveNFT":"', - address(c.troveNFT).toHexString(), - '",' - ), - string.concat( - '"metadataNFT":"', - address(c.metadataNFT).toHexString(), - '",' - ), - string.concat( - '"priceFeed":"', - address(c.priceFeed).toHexString(), - '",' - ), - string.concat( - '"gasPool":"', - address(c.gasPool).toHexString(), - '",' - ), - string.concat( - '"interestRouter":"', - address(c.interestRouter).toHexString(), - '",' - ) - ) + string.concat('"collSymbol":"', c.collToken.symbol(), '",'), // purely for human-readability + string.concat('"collToken":"', address(c.collToken).toHexString(), '",'), + string.concat('"addressesRegistry":"', address(c.addressesRegistry).toHexString(), '",'), + string.concat('"activePool":"', address(c.activePool).toHexString(), '",'), + string.concat('"borrowerOperations":"', address(c.borrowerOperations).toHexString(), '",'), + string.concat('"collSurplusPool":"', address(c.collSurplusPool).toHexString(), '",'), + string.concat('"defaultPool":"', address(c.defaultPool).toHexString(), '",'), + string.concat('"sortedTroves":"', address(c.sortedTroves).toHexString(), '",'), + string.concat('"systemParams":"', address(c.systemParams).toHexString(), '",') ), - "}" - ); + string.concat( + string.concat('"stabilityPool":"', address(c.stabilityPool).toHexString(), '",'), + string.concat('"troveManager":"', address(c.troveManager).toHexString(), '",'), + string.concat('"troveNFT":"', address(c.troveNFT).toHexString(), '",'), + string.concat('"metadataNFT":"', address(c.metadataNFT).toHexString(), '",'), + string.concat('"priceFeed":"', address(c.priceFeed).toHexString(), '",'), + string.concat('"gasPool":"', address(c.gasPool).toHexString(), '",'), + string.concat('"interestRouter":"', address(c.interestRouter).toHexString(), '",') + ) + ), + "}" + ); } - function _getDeploymentConstants( - ISystemParams params - ) internal view returns (string memory) { - return + function _getDeploymentConstants(ISystemParams params) internal view returns (string memory) { + return string.concat( + "{", string.concat( - "{", - string.concat( - string.concat( - '"ETH_GAS_COMPENSATION":"', - params.ETH_GAS_COMPENSATION().toString(), - '",' - ), - string.concat( - '"INTEREST_RATE_ADJ_COOLDOWN":"', - params.INTEREST_RATE_ADJ_COOLDOWN().toString(), - '",' - ), - string.concat( - '"MAX_ANNUAL_INTEREST_RATE":"', - params.MAX_ANNUAL_INTEREST_RATE().toString(), - '",' - ), - string.concat( - '"MIN_ANNUAL_INTEREST_RATE":"', - params.MIN_ANNUAL_INTEREST_RATE().toString(), - '",' - ), - string.concat( - '"MIN_DEBT":"', - params.MIN_DEBT().toString(), - '",' - ), - string.concat( - '"SP_YIELD_SPLIT":"', - params.SP_YIELD_SPLIT().toString(), - '",' - ), - string.concat( - '"UPFRONT_INTEREST_PERIOD":"', - params.UPFRONT_INTEREST_PERIOD().toString(), - '"' - ) // no comma - ), - "}" - ); + string.concat('"ETH_GAS_COMPENSATION":"', params.ETH_GAS_COMPENSATION().toString(), '",'), + string.concat('"INTEREST_RATE_ADJ_COOLDOWN":"', params.INTEREST_RATE_ADJ_COOLDOWN().toString(), '",'), + string.concat('"MAX_ANNUAL_INTEREST_RATE":"', params.MAX_ANNUAL_INTEREST_RATE().toString(), '",'), + string.concat('"MIN_ANNUAL_INTEREST_RATE":"', params.MIN_ANNUAL_INTEREST_RATE().toString(), '",'), + string.concat('"MIN_DEBT":"', params.MIN_DEBT().toString(), '",'), + string.concat('"SP_YIELD_SPLIT":"', params.SP_YIELD_SPLIT().toString(), '",'), + string.concat('"UPFRONT_INTEREST_PERIOD":"', params.UPFRONT_INTEREST_PERIOD().toString(), '"') // no comma + ), + "}" + ); } - function _getManifestJson( - DeploymentResult memory deployed - ) internal view returns (string memory) { + function _getManifestJson(DeploymentResult memory deployed) internal view returns (string memory) { string[] memory branches = new string[](1); branches[0] = _getBranchContractsJson(deployed.contracts); string memory part1 = string.concat( "{", - string.concat( - '"constants":', - _getDeploymentConstants(deployed.contracts.systemParams), - "," - ), - string.concat( - '"collateralRegistry":"', - address(deployed.collateralRegistry).toHexString(), - '",' - ), - string.concat( - '"boldToken":"', - address(deployed.stableToken).toHexString(), - '",' - ), - string.concat( - '"hintHelpers":"', - address(deployed.hintHelpers).toHexString(), - '",' - ) + string.concat('"constants":', _getDeploymentConstants(deployed.contracts.systemParams), ","), + string.concat('"collateralRegistry":"', address(deployed.collateralRegistry).toHexString(), '",'), + string.concat('"boldToken":"', address(deployed.stableToken).toHexString(), '",'), + string.concat('"hintHelpers":"', address(deployed.hintHelpers).toHexString(), '",') ); string memory part2 = string.concat( - string.concat( - '"stableTokenV3Impl":"', - address(deployed.stableTokenV3Impl).toHexString(), - '",' - ), - string.concat( - '"stabilityPoolImpl":"', - address(deployed.stabilityPoolImpl).toHexString(), - '",' - ), - string.concat( - '"systemParams":"', - address(deployed.systemParams).toHexString(), - '",' - ), - string.concat( - '"multiTroveGetter":"', - address(deployed.multiTroveGetter).toHexString(), - '",' - ) + string.concat('"stableTokenV3Impl":"', address(deployed.stableTokenV3Impl).toHexString(), '",'), + string.concat('"stabilityPoolImpl":"', address(deployed.stabilityPoolImpl).toHexString(), '",'), + string.concat('"systemParams":"', address(deployed.systemParams).toHexString(), '",'), + string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",') ); string memory part3 = string.concat( - string.concat( - '"fpmm":"', - address(deployed.fpmm).toHexString(), - '",' - ), + string.concat('"fpmm":"', address(deployed.fpmm).toHexString(), '",'), string.concat('"branches":[', branches.join(","), "]"), "}" ); From 1a9c936ed9a3d8f216ffc7c852f5a70d4ed7b291 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:34:27 -0500 Subject: [PATCH 45/79] feat: read variables from sys params directly --- contracts/src/BorrowerOperations.sol | 30 +++++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 05a7292d4..80164ee01 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -16,6 +16,16 @@ import "./Dependencies/AddRemoveManagers.sol"; import "./Types/LatestTroveData.sol"; import "./Types/LatestBatchData.sol"; +/** + * @dev System parameters pattern: + * Most system parameters are copied from SystemParams to immutable variables at construction for gas optimization. + * However, to reduce contract size, the following parameters are read directly from SystemParams when needed: + * - SCR: Only used in shutdown() function + * - MAX_ANNUAL_BATCH_MANAGEMENT_FEE: Only used in registerBatchManager() + * - MIN_INTEREST_RATE_CHANGE_PERIOD: Only used in registerBatchManager() + * - MAX_BATCH_SHARES_RATIO: Only used in kickFromBatch() + * These are infrequently called operations where the additional ~2500 gas per read is acceptable. + */ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperations { using SafeERC20 for IERC20; @@ -35,9 +45,6 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied uint256 public immutable CCR; - // Shutdown system collateral ratio. If the system's total collateral ratio (TCR) for a given collateral falls below the SCR, - // the protocol triggers the shutdown of the borrow market and permanently disables all borrowing operations except for closing Troves. - uint256 public immutable SCR; bool public hasBeenShutDown; // Minimum collateral ratio for individual troves @@ -48,10 +55,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio uint256 public immutable ETH_GAS_COMPENSATION; uint256 public immutable MIN_DEBT; - uint128 public immutable MAX_ANNUAL_BATCH_MANAGEMENT_FEE; - uint128 public immutable MIN_INTEREST_RATE_CHANGE_PERIOD; uint256 public immutable INTEREST_RATE_ADJ_COOLDOWN; - uint256 public immutable MAX_BATCH_SHARES_RATIO; uint256 public immutable UPFRONT_INTEREST_PERIOD; uint256 public immutable MIN_ANNUAL_INTEREST_RATE; uint256 public immutable MAX_ANNUAL_INTEREST_RATE; @@ -190,15 +194,11 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio gasToken = _addressesRegistry.gasToken(); CCR = _systemParams.CCR(); - SCR = _systemParams.SCR(); MCR = _systemParams.MCR(); BCR = _systemParams.BCR(); ETH_GAS_COMPENSATION = _systemParams.ETH_GAS_COMPENSATION(); MIN_DEBT = _systemParams.MIN_DEBT(); - MAX_ANNUAL_BATCH_MANAGEMENT_FEE = _systemParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE(); - MIN_INTEREST_RATE_CHANGE_PERIOD = _systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD(); - MAX_BATCH_SHARES_RATIO = _systemParams.MAX_BATCH_SHARES_RATIO(); UPFRONT_INTEREST_PERIOD = _systemParams.UPFRONT_INTEREST_PERIOD(); MIN_ANNUAL_INTEREST_RATE = _systemParams.MIN_ANNUAL_INTEREST_RATE(); MAX_ANNUAL_INTEREST_RATE = _systemParams.MAX_ANNUAL_INTEREST_RATE(); @@ -886,8 +886,12 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio _requireInterestRateInRange(_currentInterestRate, _minInterestRate, _maxInterestRate); // Not needed, implicitly checked in the condition above: //_requireValidAnnualInterestRate(_currentInterestRate); - if (_annualManagementFee > MAX_ANNUAL_BATCH_MANAGEMENT_FEE) revert AnnualManagementFeeTooHigh(); - if (_minInterestRateChangePeriod < MIN_INTEREST_RATE_CHANGE_PERIOD) revert MinInterestRateChangePeriodTooLow(); + if (_annualManagementFee > ISystemParams(systemParamsAddress).MAX_ANNUAL_BATCH_MANAGEMENT_FEE()) { + revert AnnualManagementFeeTooHigh(); + } + if (_minInterestRateChangePeriod < ISystemParams(systemParamsAddress).MIN_INTEREST_RATE_CHANGE_PERIOD()) { + revert MinInterestRateChangePeriodTooLow(); + } interestBatchManagers[msg.sender] = InterestBatchManager(_minInterestRate, _maxInterestRate, _minInterestRateChangePeriod); @@ -1111,6 +1115,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio vars.batch = vars.troveManager.getLatestBatchData(vars.batchManager); if (_kick) { + uint256 MAX_BATCH_SHARES_RATIO = ISystemParams(systemParamsAddress).MAX_BATCH_SHARES_RATIO(); if (vars.batch.totalDebtShares * MAX_BATCH_SHARES_RATIO >= vars.batch.entireDebtWithoutRedistribution) { revert BatchSharesRatioTooLow(); } @@ -1251,6 +1256,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // Otherwise, proceed with the TCR check: uint256 TCR = LiquityMath._computeCR(totalColl, totalDebt, price); + uint256 SCR = ISystemParams(systemParamsAddress).SCR(); if (TCR >= SCR) revert TCRNotBelowSCR(); _applyShutdown(); From d047722daf8385a2965cfc9041a7d768f52a563c Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Mon, 6 Oct 2025 10:18:52 -0500 Subject: [PATCH 46/79] chore: read scr from sys params --- contracts/src/BorrowerOperations.sol | 937 +++++++++++++----- .../src/Interfaces/IBorrowerOperations.sol | 1 - 2 files changed, 699 insertions(+), 239 deletions(-) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 80164ee01..7ae2470ee 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -26,7 +26,11 @@ import "./Types/LatestBatchData.sol"; * - MAX_BATCH_SHARES_RATIO: Only used in kickFromBatch() * These are infrequently called operations where the additional ~2500 gas per read is acceptable. */ -contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperations { +contract BorrowerOperations is + LiquityBase, + AddRemoveManagers, + IBorrowerOperations +{ using SafeERC20 for IERC20; // --- Connected contract declarations --- @@ -40,7 +44,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio ISortedTroves internal sortedTroves; // Wrapped ETH for liquidation reserve (gas compensation) IERC20Metadata internal immutable gasToken; - address public immutable systemParamsAddress; + ISystemParams internal immutable systemParams; // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied uint256 public immutable CCR; @@ -61,12 +65,13 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio uint256 public immutable MAX_ANNUAL_INTEREST_RATE; /* - * Mapping from TroveId to individual delegate for interest rate setting. - * - * This address then has the ability to update the borrower’s interest rate, but not change its debt or collateral. - * Useful for instance for cold/hot wallet setups. - */ - mapping(uint256 => InterestIndividualDelegate) private interestIndividualDelegateOf; + * Mapping from TroveId to individual delegate for interest rate setting. + * + * This address then has the ability to update the borrower’s interest rate, but not change its debt or collateral. + * Useful for instance for cold/hot wallet setups. + */ + mapping(uint256 => InterestIndividualDelegate) + private interestIndividualDelegateOf; /* * Mapping from TroveId to granted address for interest rate setting (batch manager). @@ -180,14 +185,14 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio event ShutDown(uint256 _tcr); - constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) - AddRemoveManagers(_addressesRegistry) - LiquityBase(_addressesRegistry) - { + constructor( + IAddressesRegistry _addressesRegistry, + ISystemParams _systemParams + ) AddRemoveManagers(_addressesRegistry) LiquityBase(_addressesRegistry) { // This makes impossible to open a trove with zero withdrawn Bold assert(_systemParams.MIN_DEBT() > 0); - systemParamsAddress = address(_systemParams); + systemParams = _systemParams; collToken = _addressesRegistry.collToken(); @@ -256,29 +261,41 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio ); // Set the stored Trove properties and mint the NFT - troveManager.onOpenTrove(_owner, vars.troveId, vars.change, _annualInterestRate); + troveManager.onOpenTrove( + _owner, + vars.troveId, + vars.change, + _annualInterestRate + ); - sortedTroves.insert(vars.troveId, _annualInterestRate, _upperHint, _lowerHint); + sortedTroves.insert( + vars.troveId, + _annualInterestRate, + _upperHint, + _lowerHint + ); return vars.troveId; } - function openTroveAndJoinInterestBatchManager(OpenTroveAndJoinInterestBatchManagerParams calldata _params) - external - override - returns (uint256) - { + function openTroveAndJoinInterestBatchManager( + OpenTroveAndJoinInterestBatchManagerParams calldata _params + ) external override returns (uint256) { _requireValidInterestBatchManager(_params.interestBatchManager); OpenTroveVars memory vars; vars.troveManager = troveManager; - vars.batch = vars.troveManager.getLatestBatchData(_params.interestBatchManager); + vars.batch = vars.troveManager.getLatestBatchData( + _params.interestBatchManager + ); // We set old weighted values here, as it’s only necessary for batches, so we don’t need to pass them to _openTrove func vars.change.batchAccruedManagementFee = vars.batch.accruedManagementFee; vars.change.oldWeightedRecordedDebt = vars.batch.weightedRecordedDebt; - vars.change.oldWeightedRecordedBatchManagementFee = vars.batch.weightedRecordedBatchManagementFee; + vars.change.oldWeightedRecordedBatchManagementFee = vars + .batch + .weightedRecordedBatchManagementFee; vars.troveId = _openTrove( _params.owner, _params.ownerIndex, @@ -346,35 +363,53 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // --- Checks --- - vars.troveId = uint256(keccak256(abi.encode(msg.sender, _owner, _ownerIndex))); + vars.troveId = uint256( + keccak256(abi.encode(msg.sender, _owner, _ownerIndex)) + ); _requireTroveDoesNotExist(vars.troveManager, vars.troveId); _change.collIncrease = _collAmount; _change.debtIncrease = _boldAmount; // For simplicity, we ignore the fee when calculating the approx. interest rate - _change.newWeightedRecordedDebt = (_batchEntireDebt + _change.debtIncrease) * _annualInterestRate; - - vars.avgInterestRate = vars.activePool.getNewApproxAvgInterestRateFromTroveChange(_change); - _change.upfrontFee = _calcUpfrontFee(_change.debtIncrease, vars.avgInterestRate); + _change.newWeightedRecordedDebt = + (_batchEntireDebt + _change.debtIncrease) * + _annualInterestRate; + + vars.avgInterestRate = vars + .activePool + .getNewApproxAvgInterestRateFromTroveChange(_change); + _change.upfrontFee = _calcUpfrontFee( + _change.debtIncrease, + vars.avgInterestRate + ); _requireUserAcceptsUpfrontFee(_change.upfrontFee, _maxUpfrontFee); vars.entireDebt = _change.debtIncrease + _change.upfrontFee; _requireAtLeastMinDebt(vars.entireDebt); - vars.ICR = LiquityMath._computeCR(_collAmount, vars.entireDebt, vars.price); + vars.ICR = LiquityMath._computeCR( + _collAmount, + vars.entireDebt, + vars.price + ); // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee, and the batch fee if needed if (_interestBatchManager == address(0)) { - _change.newWeightedRecordedDebt = vars.entireDebt * _annualInterestRate; + _change.newWeightedRecordedDebt = + vars.entireDebt * + _annualInterestRate; // ICR is based on the requested Bold amount + upfront fee. _requireICRisAboveMCR(vars.ICR); } else { // old values have been set outside, before calling this function - _change.newWeightedRecordedDebt = (_batchEntireDebt + vars.entireDebt) * _annualInterestRate; + _change.newWeightedRecordedDebt = + (_batchEntireDebt + vars.entireDebt) * + _annualInterestRate; _change.newWeightedRecordedBatchManagementFee = - (_batchEntireDebt + vars.entireDebt) * _batchManagementAnnualFee; + (_batchEntireDebt + vars.entireDebt) * + _batchManagementAnnualFee; // ICR is based on the requested Bold amount + upfront fee. // Troves in a batch have a stronger requirement (MCR+BCR) @@ -390,7 +425,10 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio _setAddManager(vars.troveId, _addManager); _setRemoveManagerAndReceiver(vars.troveId, _removeManager, _receiver); - vars.activePool.mintAggInterestAndAccountForTroveChange(_change, _interestBatchManager); + vars.activePool.mintAggInterestAndAccountForTroveChange( + _change, + _interestBatchManager + ); // Pull coll tokens from sender and move them to the Active Pool _pullCollAndSendToActivePool(vars.activePool, _collAmount); @@ -419,7 +457,10 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } // Withdraw collateral from a trove - function withdrawColl(uint256 _troveId, uint256 _collWithdrawal) external override { + function withdrawColl( + uint256 _troveId, + uint256 _collWithdrawal + ) external override { ITroveManager troveManagerCached = troveManager; _requireTroveIsActive(troveManagerCached, _troveId); @@ -435,7 +476,11 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } // Withdraw Bold tokens from a trove: mint new Bold tokens to the owner, and increase the trove's debt accordingly - function withdrawBold(uint256 _troveId, uint256 _boldAmount, uint256 _maxUpfrontFee) external override { + function withdrawBold( + uint256 _troveId, + uint256 _boldAmount, + uint256 _maxUpfrontFee + ) external override { ITroveManager troveManagerCached = troveManager; _requireTroveIsActive(troveManagerCached, _troveId); @@ -445,7 +490,10 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } // Repay Bold tokens to a Trove: Burn the repaid Bold tokens, and reduce the trove's debt accordingly - function repayBold(uint256 _troveId, uint256 _boldAmount) external override { + function repayBold( + uint256 _troveId, + uint256 _boldAmount + ) external override { ITroveManager troveManagerCached = troveManager; _requireTroveIsActive(troveManagerCached, _troveId); @@ -492,7 +540,13 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio _requireTroveIsActive(troveManagerCached, _troveId); TroveChange memory troveChange; - _initTroveChange(troveChange, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease); + _initTroveChange( + troveChange, + _collChange, + _isCollIncrease, + _boldChange, + _isDebtIncrease + ); _adjustTrove(troveManagerCached, _troveId, troveChange, _maxUpfrontFee); } @@ -510,7 +564,13 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio _requireTroveIsZombie(troveManagerCached, _troveId); TroveChange memory troveChange; - _initTroveChange(troveChange, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease); + _initTroveChange( + troveChange, + _collChange, + _isCollIncrease, + _boldChange, + _isDebtIncrease + ); _adjustTrove(troveManagerCached, _troveId, troveChange, _maxUpfrontFee); troveManagerCached.setTroveStatusToActive(_troveId); @@ -518,7 +578,8 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio address batchManager = interestBatchManagerOf[_troveId]; uint256 batchAnnualInterestRate; if (batchManager != address(0)) { - LatestBatchData memory batch = troveManagerCached.getLatestBatchData(batchManager); + LatestBatchData memory batch = troveManagerCached + .getLatestBatchData(batchManager); batchAnnualInterestRate = batch.annualInterestRate; } _reInsertIntoSortedTroves( @@ -547,9 +608,18 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio _requireSenderIsOwnerOrInterestManager(_troveId); _requireTroveIsActive(troveManagerCached, _troveId); - LatestTroveData memory trove = troveManagerCached.getLatestTroveData(_troveId); - _requireValidDelegateAdjustment(_troveId, trove.lastInterestRateAdjTime, _newAnnualInterestRate); - _requireAnnualInterestRateIsNew(trove.annualInterestRate, _newAnnualInterestRate); + LatestTroveData memory trove = troveManagerCached.getLatestTroveData( + _troveId + ); + _requireValidDelegateAdjustment( + _troveId, + trove.lastInterestRateAdjTime, + _newAnnualInterestRate + ); + _requireAnnualInterestRateIsNew( + trove.annualInterestRate, + _newAnnualInterestRate + ); uint256 newDebt = trove.entireDebt; @@ -560,24 +630,45 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio troveChange.oldWeightedRecordedDebt = trove.weightedRecordedDebt; // Apply upfront fee on premature adjustments. It checks the resulting ICR - if (block.timestamp < trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN) { - newDebt = _applyUpfrontFee(trove.entireColl, newDebt, troveChange, _maxUpfrontFee, false); + if ( + block.timestamp < + trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN + ) { + newDebt = _applyUpfrontFee( + trove.entireColl, + newDebt, + troveChange, + _maxUpfrontFee, + false + ); } // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee troveChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate; - activePool.mintAggInterestAndAccountForTroveChange(troveChange, address(0)); + activePool.mintAggInterestAndAccountForTroveChange( + troveChange, + address(0) + ); - sortedTroves.reInsert(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint); + sortedTroves.reInsert( + _troveId, + _newAnnualInterestRate, + _upperHint, + _lowerHint + ); troveManagerCached.onAdjustTroveInterestRate( - _troveId, trove.entireColl, newDebt, _newAnnualInterestRate, troveChange + _troveId, + trove.entireColl, + newDebt, + _newAnnualInterestRate, + troveChange ); } /* - * _adjustTrove(): Alongside a debt change, this function can perform either a collateral top-up or a collateral withdrawal. - */ + * _adjustTrove(): Alongside a debt change, this function can perform either a collateral top-up or a collateral withdrawal. + */ function _adjustTrove( ITroveManager _troveManager, uint256 _troveId, @@ -591,7 +682,10 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio vars.boldToken = boldToken; vars.price = _requireOraclesLive(); - vars.isBelowCriticalThreshold = _checkBelowCriticalThreshold(vars.price, CCR); + vars.isBelowCriticalThreshold = _checkBelowCriticalThreshold( + vars.price, + CCR + ); // --- Checks --- @@ -601,7 +695,10 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio address receiver = owner; // If it’s a withdrawal, and remove manager privilege is set, a different receiver can be defined if (_troveChange.collDecrease > 0 || _troveChange.debtIncrease > 0) { - receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner); + receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver( + _troveId, + owner + ); } else { // RemoveManager assumes AddManager, so if the former is set, there's no need to check the latter _requireSenderIsOwnerOrAddManager(_troveId, owner); @@ -615,22 +712,37 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // When the adjustment is a debt repayment, check it's a valid amount and that the caller has enough Bold if (_troveChange.debtDecrease > 0) { - uint256 maxRepayment = vars.trove.entireDebt > MIN_DEBT ? vars.trove.entireDebt - MIN_DEBT : 0; + uint256 maxRepayment = vars.trove.entireDebt > MIN_DEBT + ? vars.trove.entireDebt - MIN_DEBT + : 0; if (_troveChange.debtDecrease > maxRepayment) { _troveChange.debtDecrease = maxRepayment; } - _requireSufficientBoldBalance(vars.boldToken, msg.sender, _troveChange.debtDecrease); + _requireSufficientBoldBalance( + vars.boldToken, + msg.sender, + _troveChange.debtDecrease + ); } _requireNonZeroAdjustment(_troveChange); // When the adjustment is a collateral withdrawal, check that it's no more than the Trove's entire collateral if (_troveChange.collDecrease > 0) { - _requireValidCollWithdrawal(vars.trove.entireColl, _troveChange.collDecrease); + _requireValidCollWithdrawal( + vars.trove.entireColl, + _troveChange.collDecrease + ); } - vars.newColl = vars.trove.entireColl + _troveChange.collIncrease - _troveChange.collDecrease; - vars.newDebt = vars.trove.entireDebt + _troveChange.debtIncrease - _troveChange.debtDecrease; + vars.newColl = + vars.trove.entireColl + + _troveChange.collIncrease - + _troveChange.collDecrease; + vars.newDebt = + vars.trove.entireDebt + + _troveChange.debtIncrease - + _troveChange.debtDecrease; address batchManager = interestBatchManagerOf[_troveId]; bool isTroveInBatch = batchManager != address(0); @@ -639,38 +751,68 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio if (isTroveInBatch) { batch = _troveManager.getLatestBatchData(batchManager); - batchFutureDebt = batch.entireDebtWithoutRedistribution + vars.trove.redistBoldDebtGain - + _troveChange.debtIncrease - _troveChange.debtDecrease; + batchFutureDebt = + batch.entireDebtWithoutRedistribution + + vars.trove.redistBoldDebtGain + + _troveChange.debtIncrease - + _troveChange.debtDecrease; - _troveChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain; + _troveChange.appliedRedistBoldDebtGain = vars + .trove + .redistBoldDebtGain; _troveChange.appliedRedistCollGain = vars.trove.redistCollGain; _troveChange.batchAccruedManagementFee = batch.accruedManagementFee; _troveChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; - _troveChange.newWeightedRecordedDebt = batchFutureDebt * batch.annualInterestRate; - _troveChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; - _troveChange.newWeightedRecordedBatchManagementFee = batchFutureDebt * batch.annualManagementFee; + _troveChange.newWeightedRecordedDebt = + batchFutureDebt * + batch.annualInterestRate; + _troveChange.oldWeightedRecordedBatchManagementFee = batch + .weightedRecordedBatchManagementFee; + _troveChange.newWeightedRecordedBatchManagementFee = + batchFutureDebt * + batch.annualManagementFee; } else { - _troveChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain; + _troveChange.appliedRedistBoldDebtGain = vars + .trove + .redistBoldDebtGain; _troveChange.appliedRedistCollGain = vars.trove.redistCollGain; - _troveChange.oldWeightedRecordedDebt = vars.trove.weightedRecordedDebt; - _troveChange.newWeightedRecordedDebt = vars.newDebt * vars.trove.annualInterestRate; + _troveChange.oldWeightedRecordedDebt = vars + .trove + .weightedRecordedDebt; + _troveChange.newWeightedRecordedDebt = + vars.newDebt * + vars.trove.annualInterestRate; } // Pay an upfront fee on debt increases if (_troveChange.debtIncrease > 0) { - uint256 avgInterestRate = vars.activePool.getNewApproxAvgInterestRateFromTroveChange(_troveChange); - _troveChange.upfrontFee = _calcUpfrontFee(_troveChange.debtIncrease, avgInterestRate); - _requireUserAcceptsUpfrontFee(_troveChange.upfrontFee, _maxUpfrontFee); + uint256 avgInterestRate = vars + .activePool + .getNewApproxAvgInterestRateFromTroveChange(_troveChange); + _troveChange.upfrontFee = _calcUpfrontFee( + _troveChange.debtIncrease, + avgInterestRate + ); + _requireUserAcceptsUpfrontFee( + _troveChange.upfrontFee, + _maxUpfrontFee + ); vars.newDebt += _troveChange.upfrontFee; if (isTroveInBatch) { batchFutureDebt += _troveChange.upfrontFee; // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee - _troveChange.newWeightedRecordedDebt = batchFutureDebt * batch.annualInterestRate; - _troveChange.newWeightedRecordedBatchManagementFee = batchFutureDebt * batch.annualManagementFee; + _troveChange.newWeightedRecordedDebt = + batchFutureDebt * + batch.annualInterestRate; + _troveChange.newWeightedRecordedBatchManagementFee = + batchFutureDebt * + batch.annualManagementFee; } else { // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee - _troveChange.newWeightedRecordedDebt = vars.newDebt * vars.trove.annualInterestRate; + _troveChange.newWeightedRecordedDebt = + vars.newDebt * + vars.trove.annualInterestRate; } } @@ -678,10 +820,18 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // Now the max repayment is capped to stay above MIN_DEBT, so this only applies to adjustZombieTrove _requireAtLeastMinDebt(vars.newDebt); - vars.newICR = LiquityMath._computeCR(vars.newColl, vars.newDebt, vars.price); + vars.newICR = LiquityMath._computeCR( + vars.newColl, + vars.newDebt, + vars.price + ); // Check the adjustment satisfies all conditions for the current system mode - _requireValidAdjustmentInCurrentMode(_troveChange, vars, isTroveInBatch); + _requireValidAdjustmentInCurrentMode( + _troveChange, + vars, + isTroveInBatch + ); // --- Effects and interactions --- @@ -696,11 +846,24 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio batch.entireDebtWithoutRedistribution ); } else { - _troveManager.onAdjustTrove(_troveId, vars.newColl, vars.newDebt, _troveChange); + _troveManager.onAdjustTrove( + _troveId, + vars.newColl, + vars.newDebt, + _troveChange + ); } - vars.activePool.mintAggInterestAndAccountForTroveChange(_troveChange, batchManager); - _moveTokensFromAdjustment(receiver, _troveChange, vars.boldToken, vars.activePool); + vars.activePool.mintAggInterestAndAccountForTroveChange( + _troveChange, + batchManager + ); + _moveTokensFromAdjustment( + receiver, + _troveChange, + vars.boldToken, + vars.activePool + ); } function closeTrove(uint256 _troveId) external override { @@ -711,13 +874,22 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // --- Checks --- address owner = troveNFT.ownerOf(_troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner); + address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver( + _troveId, + owner + ); _requireTroveIsOpen(troveManagerCached, _troveId); - LatestTroveData memory trove = troveManagerCached.getLatestTroveData(_troveId); + LatestTroveData memory trove = troveManagerCached.getLatestTroveData( + _troveId + ); // The borrower must repay their entire debt including accrued interest, batch fee and redist. gains - _requireSufficientBoldBalance(boldTokenCached, msg.sender, trove.entireDebt); + _requireSufficientBoldBalance( + boldTokenCached, + msg.sender, + trove.entireDebt + ); TroveChange memory troveChange; troveChange.appliedRedistBoldDebtGain = trove.redistBoldDebtGain; @@ -729,19 +901,24 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio LatestBatchData memory batch; if (batchManager != address(0)) { batch = troveManagerCached.getLatestBatchData(batchManager); - uint256 batchFutureDebt = - batch.entireDebtWithoutRedistribution - (trove.entireDebt - trove.redistBoldDebtGain); + uint256 batchFutureDebt = batch.entireDebtWithoutRedistribution - + (trove.entireDebt - trove.redistBoldDebtGain); troveChange.batchAccruedManagementFee = batch.accruedManagementFee; troveChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; - troveChange.newWeightedRecordedDebt = batchFutureDebt * batch.annualInterestRate; - troveChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; - troveChange.newWeightedRecordedBatchManagementFee = batchFutureDebt * batch.annualManagementFee; + troveChange.newWeightedRecordedDebt = + batchFutureDebt * + batch.annualInterestRate; + troveChange.oldWeightedRecordedBatchManagementFee = batch + .weightedRecordedBatchManagementFee; + troveChange.newWeightedRecordedBatchManagementFee = + batchFutureDebt * + batch.annualManagementFee; } else { troveChange.oldWeightedRecordedDebt = trove.weightedRecordedDebt; // troveChange.newWeightedRecordedDebt = 0; } - (uint256 price,) = priceFeed.fetchPrice(); + (uint256 price, ) = priceFeed.fetchPrice(); uint256 newTCR = _getNewTCRFromTroveChange(troveChange, price); if (!hasBeenShutDown) _requireNewTCRisAboveCCR(newTCR); @@ -759,7 +936,10 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio interestBatchManagerOf[_troveId] = address(0); } - activePoolCached.mintAggInterestAndAccountForTroveChange(troveChange, batchManager); + activePoolCached.mintAggInterestAndAccountForTroveChange( + troveChange, + batchManager + ); // Return ETH gas compensation gasToken.transferFrom(gasPoolAddress, receiver, ETH_GAS_COMPENSATION); @@ -772,14 +952,20 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio _wipeTroveMappings(_troveId); } - function applyPendingDebt(uint256 _troveId, uint256 _lowerHint, uint256 _upperHint) public { + function applyPendingDebt( + uint256 _troveId, + uint256 _lowerHint, + uint256 _upperHint + ) public { _requireIsNotShutDown(); ITroveManager troveManagerCached = troveManager; _requireTroveIsOpen(troveManagerCached, _troveId); - LatestTroveData memory trove = troveManagerCached.getLatestTroveData(_troveId); + LatestTroveData memory trove = troveManagerCached.getLatestTroveData( + _troveId + ); _requireNonZeroDebt(trove.entireDebt); TroveChange memory change; @@ -791,16 +977,23 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio if (batchManager == address(0)) { change.oldWeightedRecordedDebt = trove.weightedRecordedDebt; - change.newWeightedRecordedDebt = trove.entireDebt * trove.annualInterestRate; + change.newWeightedRecordedDebt = + trove.entireDebt * + trove.annualInterestRate; } else { batch = troveManagerCached.getLatestBatchData(batchManager); change.batchAccruedManagementFee = batch.accruedManagementFee; change.oldWeightedRecordedDebt = batch.weightedRecordedDebt; change.newWeightedRecordedDebt = - (batch.entireDebtWithoutRedistribution + trove.redistBoldDebtGain) * batch.annualInterestRate; - change.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; + (batch.entireDebtWithoutRedistribution + + trove.redistBoldDebtGain) * + batch.annualInterestRate; + change.oldWeightedRecordedBatchManagementFee = batch + .weightedRecordedBatchManagementFee; change.newWeightedRecordedBatchManagementFee = - (batch.entireDebtWithoutRedistribution + trove.redistBoldDebtGain) * batch.annualManagementFee; + (batch.entireDebtWithoutRedistribution + + trove.redistBoldDebtGain) * + batch.annualManagementFee; } troveManagerCached.onApplyTroveInterest( @@ -812,22 +1005,31 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio batch.entireDebtWithoutRedistribution, change ); - activePool.mintAggInterestAndAccountForTroveChange(change, batchManager); + activePool.mintAggInterestAndAccountForTroveChange( + change, + batchManager + ); // If the trove was zombie, and now it’s not anymore, put it back in the list - if (_checkTroveIsZombie(troveManagerCached, _troveId) && trove.entireDebt >= MIN_DEBT) { + if ( + _checkTroveIsZombie(troveManagerCached, _troveId) && + trove.entireDebt >= MIN_DEBT + ) { troveManagerCached.setTroveStatusToActive(_troveId); _reInsertIntoSortedTroves( - _troveId, trove.annualInterestRate, _upperHint, _lowerHint, batchManager, batch.annualInterestRate + _troveId, + trove.annualInterestRate, + _upperHint, + _lowerHint, + batchManager, + batch.annualInterestRate ); } } - function getInterestIndividualDelegateOf(uint256 _troveId) - external - view - returns (InterestIndividualDelegate memory) - { + function getInterestIndividualDelegateOf( + uint256 _troveId + ) external view returns (InterestIndividualDelegate memory) { return interestIndividualDelegateOf[_troveId]; } @@ -851,13 +1053,23 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // With the check below, it could only be == _requireOrderedRange(_minInterestRate, _maxInterestRate); - interestIndividualDelegateOf[_troveId] = - InterestIndividualDelegate(_delegate, _minInterestRate, _maxInterestRate, _minInterestRateChangePeriod); + interestIndividualDelegateOf[_troveId] = InterestIndividualDelegate( + _delegate, + _minInterestRate, + _maxInterestRate, + _minInterestRateChangePeriod + ); // Can’t have both individual delegation and batch manager if (interestBatchManagerOf[_troveId] != address(0)) { // Not needed, implicitly checked in removeFromBatch //_requireValidAnnualInterestRate(_newAnnualInterestRate); - removeFromBatch(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee); + removeFromBatch( + _troveId, + _newAnnualInterestRate, + _upperHint, + _lowerHint, + _maxUpfrontFee + ); } } @@ -866,7 +1078,9 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio delete interestIndividualDelegateOf[_troveId]; } - function getInterestBatchManager(address _account) external view returns (InterestBatchManager memory) { + function getInterestBatchManager( + address _account + ) external view returns (InterestBatchManager memory) { return interestBatchManagers[_account]; } @@ -883,20 +1097,37 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio _requireValidAnnualInterestRate(_maxInterestRate); // With the check below, it could only be == _requireOrderedRange(_minInterestRate, _maxInterestRate); - _requireInterestRateInRange(_currentInterestRate, _minInterestRate, _maxInterestRate); + _requireInterestRateInRange( + _currentInterestRate, + _minInterestRate, + _maxInterestRate + ); // Not needed, implicitly checked in the condition above: //_requireValidAnnualInterestRate(_currentInterestRate); - if (_annualManagementFee > ISystemParams(systemParamsAddress).MAX_ANNUAL_BATCH_MANAGEMENT_FEE()) { + if ( + _annualManagementFee > + systemParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE() + ) { revert AnnualManagementFeeTooHigh(); } - if (_minInterestRateChangePeriod < ISystemParams(systemParamsAddress).MIN_INTEREST_RATE_CHANGE_PERIOD()) { + if ( + _minInterestRateChangePeriod < + systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD() + ) { revert MinInterestRateChangePeriodTooLow(); } - interestBatchManagers[msg.sender] = - InterestBatchManager(_minInterestRate, _maxInterestRate, _minInterestRateChangePeriod); + interestBatchManagers[msg.sender] = InterestBatchManager( + _minInterestRate, + _maxInterestRate, + _minInterestRateChangePeriod + ); - troveManager.onRegisterBatchManager(msg.sender, _currentInterestRate, _annualManagementFee); + troveManager.onRegisterBatchManager( + msg.sender, + _currentInterestRate, + _annualManagementFee + ); } function lowerBatchManagementFee(uint256 _newAnnualManagementFee) external { @@ -905,7 +1136,9 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio ITroveManager troveManagerCached = troveManager; - LatestBatchData memory batch = troveManagerCached.getLatestBatchData(msg.sender); + LatestBatchData memory batch = troveManagerCached.getLatestBatchData( + msg.sender + ); if (_newAnnualManagementFee >= batch.annualManagementFee) { revert NewFeeNotLower(); } @@ -922,12 +1155,19 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio TroveChange memory batchChange; batchChange.batchAccruedManagementFee = batch.accruedManagementFee; batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; - batchChange.newWeightedRecordedDebt = batch.entireDebtWithoutRedistribution * batch.annualInterestRate; - batchChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; + batchChange.newWeightedRecordedDebt = + batch.entireDebtWithoutRedistribution * + batch.annualInterestRate; + batchChange.oldWeightedRecordedBatchManagementFee = batch + .weightedRecordedBatchManagementFee; batchChange.newWeightedRecordedBatchManagementFee = - batch.entireDebtWithoutRedistribution * _newAnnualManagementFee; + batch.entireDebtWithoutRedistribution * + _newAnnualManagementFee; - activePool.mintAggInterestAndAccountForTroveChange(batchChange, msg.sender); + activePool.mintAggInterestAndAccountForTroveChange( + batchChange, + msg.sender + ); } function setBatchManagerAnnualInterestRate( @@ -938,15 +1178,23 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio ) external { _requireIsNotShutDown(); _requireValidInterestBatchManager(msg.sender); - _requireInterestRateInBatchManagerRange(msg.sender, _newAnnualInterestRate); + _requireInterestRateInBatchManagerRange( + msg.sender, + _newAnnualInterestRate + ); // Not needed, implicitly checked in the condition above: //_requireValidAnnualInterestRate(_newAnnualInterestRate); ITroveManager troveManagerCached = troveManager; IActivePool activePoolCached = activePool; - LatestBatchData memory batch = troveManagerCached.getLatestBatchData(msg.sender); - _requireBatchInterestRateChangePeriodPassed(msg.sender, uint256(batch.lastInterestRateAdjTime)); + LatestBatchData memory batch = troveManagerCached.getLatestBatchData( + msg.sender + ); + _requireBatchInterestRateChangePeriodPassed( + msg.sender, + uint256(batch.lastInterestRateAdjTime) + ); uint256 newDebt = batch.entireDebtWithoutRedistribution; @@ -954,25 +1202,37 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio batchChange.batchAccruedManagementFee = batch.accruedManagementFee; batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; batchChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate; - batchChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; - batchChange.newWeightedRecordedBatchManagementFee = newDebt * batch.annualManagementFee; + batchChange.oldWeightedRecordedBatchManagementFee = batch + .weightedRecordedBatchManagementFee; + batchChange.newWeightedRecordedBatchManagementFee = + newDebt * + batch.annualManagementFee; // Apply upfront fee on premature adjustments if ( - batch.annualInterestRate != _newAnnualInterestRate - && block.timestamp < batch.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN + batch.annualInterestRate != _newAnnualInterestRate && + block.timestamp < + batch.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN ) { uint256 price = _requireOraclesLive(); - uint256 avgInterestRate = activePoolCached.getNewApproxAvgInterestRateFromTroveChange(batchChange); + uint256 avgInterestRate = activePoolCached + .getNewApproxAvgInterestRateFromTroveChange(batchChange); batchChange.upfrontFee = _calcUpfrontFee(newDebt, avgInterestRate); - _requireUserAcceptsUpfrontFee(batchChange.upfrontFee, _maxUpfrontFee); + _requireUserAcceptsUpfrontFee( + batchChange.upfrontFee, + _maxUpfrontFee + ); newDebt += batchChange.upfrontFee; // Recalculate the batch's weighted terms, now taking into account the upfront fee - batchChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate; - batchChange.newWeightedRecordedBatchManagementFee = newDebt * batch.annualManagementFee; + batchChange.newWeightedRecordedDebt = + newDebt * + _newAnnualInterestRate; + batchChange.newWeightedRecordedBatchManagementFee = + newDebt * + batch.annualManagementFee; // Disallow a premature adjustment if it would result in TCR < CCR // (which includes the case when TCR is already below CCR before the adjustment). @@ -980,15 +1240,27 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio _requireNewTCRisAboveCCR(newTCR); } - activePoolCached.mintAggInterestAndAccountForTroveChange(batchChange, msg.sender); + activePoolCached.mintAggInterestAndAccountForTroveChange( + batchChange, + msg.sender + ); // Check batch is not empty, and then reinsert in sorted list if (!sortedTroves.isEmptyBatch(BatchId.wrap(msg.sender))) { - sortedTroves.reInsertBatch(BatchId.wrap(msg.sender), _newAnnualInterestRate, _upperHint, _lowerHint); + sortedTroves.reInsertBatch( + BatchId.wrap(msg.sender), + _newAnnualInterestRate, + _upperHint, + _lowerHint + ); } troveManagerCached.onSetBatchManagerAnnualInterestRate( - msg.sender, batch.entireCollWithoutRedistribution, newDebt, _newAnnualInterestRate, batchChange.upfrontFee + msg.sender, + batch.entireCollWithoutRedistribution, + newDebt, + _newAnnualInterestRate, + batchChange.upfrontFee ); } @@ -1012,35 +1284,57 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio interestBatchManagerOf[_troveId] = _newBatchManager; // Can’t have both individual delegation and batch manager - if (interestIndividualDelegateOf[_troveId].account != address(0)) delete interestIndividualDelegateOf[_troveId]; + if (interestIndividualDelegateOf[_troveId].account != address(0)) + delete interestIndividualDelegateOf[_troveId]; vars.trove = vars.troveManager.getLatestTroveData(_troveId); vars.newBatch = vars.troveManager.getLatestBatchData(_newBatchManager); TroveChange memory newBatchTroveChange; - newBatchTroveChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain; + newBatchTroveChange.appliedRedistBoldDebtGain = vars + .trove + .redistBoldDebtGain; newBatchTroveChange.appliedRedistCollGain = vars.trove.redistCollGain; - newBatchTroveChange.batchAccruedManagementFee = vars.newBatch.accruedManagementFee; + newBatchTroveChange.batchAccruedManagementFee = vars + .newBatch + .accruedManagementFee; newBatchTroveChange.oldWeightedRecordedDebt = - vars.newBatch.weightedRecordedDebt + vars.trove.weightedRecordedDebt; + vars.newBatch.weightedRecordedDebt + + vars.trove.weightedRecordedDebt; newBatchTroveChange.newWeightedRecordedDebt = - (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualInterestRate; + (vars.newBatch.entireDebtWithoutRedistribution + + vars.trove.entireDebt) * + vars.newBatch.annualInterestRate; // An upfront fee is always charged upon joining a batch to ensure that borrowers can not game the fee logic // and gain free interest rate updates (e.g. if they also manage the batch they joined) // It checks the resulting ICR - vars.trove.entireDebt = - _applyUpfrontFee(vars.trove.entireColl, vars.trove.entireDebt, newBatchTroveChange, _maxUpfrontFee, true); + vars.trove.entireDebt = _applyUpfrontFee( + vars.trove.entireColl, + vars.trove.entireDebt, + newBatchTroveChange, + _maxUpfrontFee, + true + ); // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee newBatchTroveChange.newWeightedRecordedDebt = - (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualInterestRate; + (vars.newBatch.entireDebtWithoutRedistribution + + vars.trove.entireDebt) * + vars.newBatch.annualInterestRate; // Add batch fees - newBatchTroveChange.oldWeightedRecordedBatchManagementFee = vars.newBatch.weightedRecordedBatchManagementFee; + newBatchTroveChange.oldWeightedRecordedBatchManagementFee = vars + .newBatch + .weightedRecordedBatchManagementFee; newBatchTroveChange.newWeightedRecordedBatchManagementFee = - (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualManagementFee; - vars.activePool.mintAggInterestAndAccountForTroveChange(newBatchTroveChange, _newBatchManager); + (vars.newBatch.entireDebtWithoutRedistribution + + vars.trove.entireDebt) * + vars.newBatch.annualManagementFee; + vars.activePool.mintAggInterestAndAccountForTroveChange( + newBatchTroveChange, + _newBatchManager + ); vars.troveManager.onSetInterestBatchManager( ITroveManager.OnSetInterestBatchManagerParams({ @@ -1056,11 +1350,19 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio vars.sortedTroves.remove(_troveId); vars.sortedTroves.insertIntoBatch( - _troveId, BatchId.wrap(_newBatchManager), vars.newBatch.annualInterestRate, _upperHint, _lowerHint + _troveId, + BatchId.wrap(_newBatchManager), + vars.newBatch.annualInterestRate, + _upperHint, + _lowerHint ); } - function kickFromBatch(uint256 _troveId, uint256 _upperHint, uint256 _lowerHint) external override { + function kickFromBatch( + uint256 _troveId, + uint256 _upperHint, + uint256 _lowerHint + ) external override { _removeFromBatch({ _troveId: _troveId, _newAnnualInterestRate: 0, // ignored when kicking @@ -1115,8 +1417,11 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio vars.batch = vars.troveManager.getLatestBatchData(vars.batchManager); if (_kick) { - uint256 MAX_BATCH_SHARES_RATIO = ISystemParams(systemParamsAddress).MAX_BATCH_SHARES_RATIO(); - if (vars.batch.totalDebtShares * MAX_BATCH_SHARES_RATIO >= vars.batch.entireDebtWithoutRedistribution) { + if ( + vars.batch.totalDebtShares * + systemParams.MAX_BATCH_SHARES_RATIO() >= + vars.batch.entireDebtWithoutRedistribution + ) { revert BatchSharesRatioTooLow(); } _newAnnualInterestRate = vars.batch.annualInterestRate; @@ -1128,36 +1433,67 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // Remove trove from Batch in SortedTroves vars.sortedTroves.removeFromBatch(_troveId); // Reinsert as single trove - vars.sortedTroves.insert(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint); + vars.sortedTroves.insert( + _troveId, + _newAnnualInterestRate, + _upperHint, + _lowerHint + ); } vars.batchFutureDebt = - vars.batch.entireDebtWithoutRedistribution - (vars.trove.entireDebt - vars.trove.redistBoldDebtGain); + vars.batch.entireDebtWithoutRedistribution - + (vars.trove.entireDebt - vars.trove.redistBoldDebtGain); - vars.batchChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain; + vars.batchChange.appliedRedistBoldDebtGain = vars + .trove + .redistBoldDebtGain; vars.batchChange.appliedRedistCollGain = vars.trove.redistCollGain; - vars.batchChange.batchAccruedManagementFee = vars.batch.accruedManagementFee; - vars.batchChange.oldWeightedRecordedDebt = vars.batch.weightedRecordedDebt; + vars.batchChange.batchAccruedManagementFee = vars + .batch + .accruedManagementFee; + vars.batchChange.oldWeightedRecordedDebt = vars + .batch + .weightedRecordedDebt; vars.batchChange.newWeightedRecordedDebt = - vars.batchFutureDebt * vars.batch.annualInterestRate + vars.trove.entireDebt * _newAnnualInterestRate; + vars.batchFutureDebt * + vars.batch.annualInterestRate + + vars.trove.entireDebt * + _newAnnualInterestRate; // Apply upfront fee on premature adjustments. It checks the resulting ICR if ( - vars.batch.annualInterestRate != _newAnnualInterestRate - && block.timestamp < vars.trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN + vars.batch.annualInterestRate != _newAnnualInterestRate && + block.timestamp < + vars.trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN ) { - vars.trove.entireDebt = - _applyUpfrontFee(vars.trove.entireColl, vars.trove.entireDebt, vars.batchChange, _maxUpfrontFee, false); + vars.trove.entireDebt = _applyUpfrontFee( + vars.trove.entireColl, + vars.trove.entireDebt, + vars.batchChange, + _maxUpfrontFee, + false + ); } // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee vars.batchChange.newWeightedRecordedDebt = - vars.batchFutureDebt * vars.batch.annualInterestRate + vars.trove.entireDebt * _newAnnualInterestRate; + vars.batchFutureDebt * + vars.batch.annualInterestRate + + vars.trove.entireDebt * + _newAnnualInterestRate; // Add batch fees - vars.batchChange.oldWeightedRecordedBatchManagementFee = vars.batch.weightedRecordedBatchManagementFee; - vars.batchChange.newWeightedRecordedBatchManagementFee = vars.batchFutureDebt * vars.batch.annualManagementFee; - - activePool.mintAggInterestAndAccountForTroveChange(vars.batchChange, vars.batchManager); + vars.batchChange.oldWeightedRecordedBatchManagementFee = vars + .batch + .weightedRecordedBatchManagementFee; + vars.batchChange.newWeightedRecordedBatchManagementFee = + vars.batchFutureDebt * + vars.batch.annualManagementFee; + + activePool.mintAggInterestAndAccountForTroveChange( + vars.batchChange, + vars.batchManager + ); vars.troveManager.onRemoveFromBatch( _troveId, @@ -1183,10 +1519,24 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio address oldBatchManager = _requireIsInBatch(_troveId); _requireNewInterestBatchManager(oldBatchManager, _newBatchManager); - LatestBatchData memory oldBatch = troveManager.getLatestBatchData(oldBatchManager); + LatestBatchData memory oldBatch = troveManager.getLatestBatchData( + oldBatchManager + ); - removeFromBatch(_troveId, oldBatch.annualInterestRate, _removeUpperHint, _removeLowerHint, 0); - setInterestBatchManager(_troveId, _newBatchManager, _addUpperHint, _addLowerHint, _maxUpfrontFee); + removeFromBatch( + _troveId, + oldBatch.annualInterestRate, + _removeUpperHint, + _removeLowerHint, + 0 + ); + setInterestBatchManager( + _troveId, + _newBatchManager, + _addUpperHint, + _addLowerHint, + _maxUpfrontFee + ); } function _applyUpfrontFee( @@ -1198,14 +1548,22 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio ) internal returns (uint256) { uint256 price = _requireOraclesLive(); - uint256 avgInterestRate = activePool.getNewApproxAvgInterestRateFromTroveChange(_troveChange); - _troveChange.upfrontFee = _calcUpfrontFee(_troveEntireDebt, avgInterestRate); + uint256 avgInterestRate = activePool + .getNewApproxAvgInterestRateFromTroveChange(_troveChange); + _troveChange.upfrontFee = _calcUpfrontFee( + _troveEntireDebt, + avgInterestRate + ); _requireUserAcceptsUpfrontFee(_troveChange.upfrontFee, _maxUpfrontFee); _troveEntireDebt += _troveChange.upfrontFee; // ICR is based on the requested Bold amount + upfront fee. - uint256 newICR = LiquityMath._computeCR(_troveEntireColl, _troveEntireDebt, price); + uint256 newICR = LiquityMath._computeCR( + _troveEntireColl, + _troveEntireDebt, + price + ); if (_isTroveInBatch) { _requireICRisAboveMCRPlusBCR(newICR); } else { @@ -1220,7 +1578,10 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio return _troveEntireDebt; } - function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal view returns (uint256) { + function _calcUpfrontFee( + uint256 _debt, + uint256 _avgInterestRate + ) internal view returns (uint256) { return _calcInterest(_debt * _avgInterestRate, UPFRONT_INTEREST_PERIOD); } @@ -1256,8 +1617,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // Otherwise, proceed with the TCR check: uint256 TCR = LiquityMath._computeCR(totalColl, totalDebt, price); - uint256 SCR = ISystemParams(systemParamsAddress).SCR(); - if (TCR >= SCR) revert TCRNotBelowSCR(); + if (TCR >= systemParams.SCR()) revert TCRNotBelowSCR(); _applyShutdown(); @@ -1293,10 +1653,19 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio ) internal { // If it was in a batch, we need to put it back, otherwise we insert it normally if (_batchManager == address(0)) { - sortedTroves.insert(_troveId, _troveAnnualInterestRate, _upperHint, _lowerHint); + sortedTroves.insert( + _troveId, + _troveAnnualInterestRate, + _upperHint, + _lowerHint + ); } else { sortedTroves.insertIntoBatch( - _troveId, BatchId.wrap(_batchManager), _batchAnnualInterestRate, _upperHint, _lowerHint + _troveId, + BatchId.wrap(_batchManager), + _batchAnnualInterestRate, + _upperHint, + _lowerHint ); } } @@ -1317,21 +1686,29 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio if (_troveChange.collIncrease > 0) { // Pull coll tokens from sender and move them to the Active Pool - _pullCollAndSendToActivePool(_activePool, _troveChange.collIncrease); + _pullCollAndSendToActivePool( + _activePool, + _troveChange.collIncrease + ); } else if (_troveChange.collDecrease > 0) { // Pull Coll from Active Pool and decrease its recorded Coll balance _activePool.sendColl(withdrawalReceiver, _troveChange.collDecrease); } } - function _pullCollAndSendToActivePool(IActivePool _activePool, uint256 _amount) internal { + function _pullCollAndSendToActivePool( + IActivePool _activePool, + uint256 _amount + ) internal { // Send Coll tokens from sender to active pool collToken.safeTransferFrom(msg.sender, address(_activePool), _amount); // Make sure Active Pool accountancy is right _activePool.accountForReceivedColl(_amount); } - function checkBatchManagerExists(address _batchManager) external view returns (bool) { + function checkBatchManagerExists( + address _batchManager + ) external view returns (bool) { return interestBatchManagers[_batchManager].maxInterestRate > 0; } @@ -1343,18 +1720,27 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - function _requireNonZeroAdjustment(TroveChange memory _troveChange) internal pure { + function _requireNonZeroAdjustment( + TroveChange memory _troveChange + ) internal pure { if ( - _troveChange.collIncrease == 0 && _troveChange.collDecrease == 0 && _troveChange.debtIncrease == 0 - && _troveChange.debtDecrease == 0 + _troveChange.collIncrease == 0 && + _troveChange.collDecrease == 0 && + _troveChange.debtIncrease == 0 && + _troveChange.debtDecrease == 0 ) { revert ZeroAdjustment(); } } - function _requireSenderIsOwnerOrInterestManager(uint256 _troveId) internal view { + function _requireSenderIsOwnerOrInterestManager( + uint256 _troveId + ) internal view { address owner = troveNFT.ownerOf(_troveId); - if (msg.sender != owner && msg.sender != interestIndividualDelegateOf[_troveId].account) { + if ( + msg.sender != owner && + msg.sender != interestIndividualDelegateOf[_troveId].account + ) { revert NotOwnerNorInterestManager(); } } @@ -1364,15 +1750,19 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio uint256 _lastInterestRateAdjTime, uint256 _annualInterestRate ) internal view { - InterestIndividualDelegate memory individualDelegate = interestIndividualDelegateOf[_troveId]; + InterestIndividualDelegate + memory individualDelegate = interestIndividualDelegateOf[_troveId]; // We have previously checked that sender is either owner or delegate // If it’s owner, this restriction doesn’t apply if (individualDelegate.account == msg.sender) { _requireInterestRateInRange( - _annualInterestRate, individualDelegate.minInterestRate, individualDelegate.maxInterestRate + _annualInterestRate, + individualDelegate.minInterestRate, + individualDelegate.maxInterestRate ); _requireDelegateInterestRateChangePeriodPassed( - _lastInterestRateAdjTime, individualDelegate.minInterestRateChangePeriod + _lastInterestRateAdjTime, + individualDelegate.minInterestRateChangePeriod ); } } @@ -1383,7 +1773,9 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - function _requireIsInBatch(uint256 _troveId) internal view returns (address) { + function _requireIsInBatch( + uint256 _troveId + ) internal view returns (address) { address batchManager = interestBatchManagerOf[_troveId]; if (batchManager == address(0)) { revert TroveNotInBatch(); @@ -1392,34 +1784,52 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio return batchManager; } - function _requireTroveDoesNotExist(ITroveManager _troveManager, uint256 _troveId) internal view { + function _requireTroveDoesNotExist( + ITroveManager _troveManager, + uint256 _troveId + ) internal view { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); if (status != ITroveManager.Status.nonExistent) { revert TroveExists(); } } - function _requireTroveIsOpen(ITroveManager _troveManager, uint256 _troveId) internal view { + function _requireTroveIsOpen( + ITroveManager _troveManager, + uint256 _troveId + ) internal view { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); - if (status != ITroveManager.Status.active && status != ITroveManager.Status.zombie) { + if ( + status != ITroveManager.Status.active && + status != ITroveManager.Status.zombie + ) { revert TroveNotOpen(); } } - function _requireTroveIsActive(ITroveManager _troveManager, uint256 _troveId) internal view { + function _requireTroveIsActive( + ITroveManager _troveManager, + uint256 _troveId + ) internal view { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); if (status != ITroveManager.Status.active) { revert TroveNotActive(); } } - function _requireTroveIsZombie(ITroveManager _troveManager, uint256 _troveId) internal view { + function _requireTroveIsZombie( + ITroveManager _troveManager, + uint256 _troveId + ) internal view { if (!_checkTroveIsZombie(_troveManager, _troveId)) { revert TroveNotZombie(); } } - function _checkTroveIsZombie(ITroveManager _troveManager, uint256 _troveId) internal view returns (bool) { + function _checkTroveIsZombie( + ITroveManager _troveManager, + uint256 _troveId + ) internal view returns (bool) { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); return status == ITroveManager.Status.zombie; } @@ -1430,7 +1840,10 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - function _requireUserAcceptsUpfrontFee(uint256 _fee, uint256 _maxFee) internal pure { + function _requireUserAcceptsUpfrontFee( + uint256 _fee, + uint256 _maxFee + ) internal pure { if (_fee > _maxFee) { revert UpfrontFeeTooHigh(); } @@ -1442,18 +1855,18 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio bool _isTroveInBatch ) internal view { /* - * Below Critical Threshold, it is not permitted: - * - * - Borrowing, unless it brings TCR up to CCR again - * - Collateral withdrawal except accompanied by a debt repayment of at least the same value - * - * In Normal Mode, ensure: - * - * - The adjustment won't pull the TCR below CCR - * - * In Both cases: - * - The new ICR is above MCR, or MCR+BCR if a batched trove - */ + * Below Critical Threshold, it is not permitted: + * + * - Borrowing, unless it brings TCR up to CCR again + * - Collateral withdrawal except accompanied by a debt repayment of at least the same value + * + * In Normal Mode, ensure: + * + * - The adjustment won't pull the TCR below CCR + * + * In Both cases: + * - The new ICR is above MCR, or MCR+BCR if a batched trove + */ if (_isTroveInBatch) { _requireICRisAboveMCRPlusBCR(_vars.newICR); @@ -1463,7 +1876,10 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio uint256 newTCR = _getNewTCRFromTroveChange(_troveChange, _vars.price); if (_vars.isBelowCriticalThreshold) { - _requireNoBorrowingUnlessNewTCRisAboveCCR(_troveChange.debtIncrease, newTCR); + _requireNoBorrowingUnlessNewTCRisAboveCCR( + _troveChange.debtIncrease, + newTCR + ); _requireDebtRepaymentGeCollWithdrawal(_troveChange, _vars.price); } else { // if Normal Mode @@ -1483,14 +1899,23 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - function _requireNoBorrowingUnlessNewTCRisAboveCCR(uint256 _debtIncrease, uint256 _newTCR) internal view { + function _requireNoBorrowingUnlessNewTCRisAboveCCR( + uint256 _debtIncrease, + uint256 _newTCR + ) internal view { if (_debtIncrease > 0 && _newTCR < CCR) { revert TCRBelowCCR(); } } - function _requireDebtRepaymentGeCollWithdrawal(TroveChange memory _troveChange, uint256 _price) internal pure { - if ((_troveChange.debtDecrease * DECIMAL_PRECISION < _troveChange.collDecrease * _price)) { + function _requireDebtRepaymentGeCollWithdrawal( + TroveChange memory _troveChange, + uint256 _price + ) internal pure { + if ( + (_troveChange.debtDecrease * DECIMAL_PRECISION < + _troveChange.collDecrease * _price) + ) { revert RepaymentNotMatchingCollWithdrawal(); } } @@ -1507,22 +1932,28 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - function _requireValidCollWithdrawal(uint256 _currentColl, uint256 _collWithdrawal) internal pure { + function _requireValidCollWithdrawal( + uint256 _currentColl, + uint256 _collWithdrawal + ) internal pure { if (_collWithdrawal > _currentColl) { revert CollWithdrawalTooHigh(); } } - function _requireSufficientBoldBalance(IBoldToken _boldToken, address _borrower, uint256 _debtRepayment) - internal - view - { + function _requireSufficientBoldBalance( + IBoldToken _boldToken, + address _borrower, + uint256 _debtRepayment + ) internal view { if (_boldToken.balanceOf(_borrower) < _debtRepayment) { revert NotEnoughBoldBalance(); } } - function _requireValidAnnualInterestRate(uint256 _annualInterestRate) internal view { + function _requireValidAnnualInterestRate( + uint256 _annualInterestRate + ) internal view { if (_annualInterestRate < MIN_ANNUAL_INTEREST_RATE) { revert InterestRateTooLow(); } @@ -1531,26 +1962,34 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - function _requireAnnualInterestRateIsNew(uint256 _oldAnnualInterestRate, uint256 _newAnnualInterestRate) - internal - pure - { + function _requireAnnualInterestRateIsNew( + uint256 _oldAnnualInterestRate, + uint256 _newAnnualInterestRate + ) internal pure { if (_oldAnnualInterestRate == _newAnnualInterestRate) { revert InterestRateNotNew(); } } - function _requireOrderedRange(uint256 _minInterestRate, uint256 _maxInterestRate) internal pure { + function _requireOrderedRange( + uint256 _minInterestRate, + uint256 _maxInterestRate + ) internal pure { if (_minInterestRate >= _maxInterestRate) revert MinGeMax(); } - function _requireInterestRateInBatchManagerRange(address _interestBatchManagerAddress, uint256 _annualInterestRate) - internal - view - { - InterestBatchManager memory interestBatchManager = interestBatchManagers[_interestBatchManagerAddress]; + function _requireInterestRateInBatchManagerRange( + address _interestBatchManagerAddress, + uint256 _annualInterestRate + ) internal view { + InterestBatchManager + memory interestBatchManager = interestBatchManagers[ + _interestBatchManagerAddress + ]; _requireInterestRateInRange( - _annualInterestRate, interestBatchManager.minInterestRate, interestBatchManager.maxInterestRate + _annualInterestRate, + interestBatchManager.minInterestRate, + interestBatchManager.maxInterestRate ); } @@ -1559,7 +1998,10 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio uint256 _minInterestRate, uint256 _maxInterestRate ) internal pure { - if (_minInterestRate > _annualInterestRate || _annualInterestRate > _maxInterestRate) { + if ( + _minInterestRate > _annualInterestRate || + _annualInterestRate > _maxInterestRate + ) { revert InterestNotInRange(); } } @@ -1568,8 +2010,15 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio address _interestBatchManagerAddress, uint256 _lastInterestRateAdjTime ) internal view { - InterestBatchManager memory interestBatchManager = interestBatchManagers[_interestBatchManagerAddress]; - if (block.timestamp < _lastInterestRateAdjTime + uint256(interestBatchManager.minInterestRateChangePeriod)) { + InterestBatchManager + memory interestBatchManager = interestBatchManagers[ + _interestBatchManagerAddress + ]; + if ( + block.timestamp < + _lastInterestRateAdjTime + + uint256(interestBatchManager.minInterestRateChangePeriod) + ) { revert BatchInterestRateChangePeriodNotPassed(); } } @@ -1578,27 +2027,40 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio uint256 _lastInterestRateAdjTime, uint256 _minInterestRateChangePeriod ) internal view { - if (block.timestamp < _lastInterestRateAdjTime + _minInterestRateChangePeriod) { + if ( + block.timestamp < + _lastInterestRateAdjTime + _minInterestRateChangePeriod + ) { revert DelegateInterestRateChangePeriodNotPassed(); } } - function _requireValidInterestBatchManager(address _interestBatchManagerAddress) internal view { - if (interestBatchManagers[_interestBatchManagerAddress].maxInterestRate == 0) { + function _requireValidInterestBatchManager( + address _interestBatchManagerAddress + ) internal view { + if ( + interestBatchManagers[_interestBatchManagerAddress] + .maxInterestRate == 0 + ) { revert InvalidInterestBatchManager(); } } - function _requireNonExistentInterestBatchManager(address _interestBatchManagerAddress) internal view { - if (interestBatchManagers[_interestBatchManagerAddress].maxInterestRate > 0) { + function _requireNonExistentInterestBatchManager( + address _interestBatchManagerAddress + ) internal view { + if ( + interestBatchManagers[_interestBatchManagerAddress] + .maxInterestRate > 0 + ) { revert BatchManagerExists(); } } - function _requireNewInterestBatchManager(address _oldBatchManagerAddress, address _newBatchManagerAddress) - internal - pure - { + function _requireNewInterestBatchManager( + address _oldBatchManagerAddress, + address _newBatchManagerAddress + ) internal pure { if (_oldBatchManagerAddress == _newBatchManagerAddress) { revert BatchManagerNotNew(); } @@ -1627,11 +2089,10 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // --- ICR and TCR getters --- - function _getNewTCRFromTroveChange(TroveChange memory _troveChange, uint256 _price) - internal - view - returns (uint256 newTCR) - { + function _getNewTCRFromTroveChange( + TroveChange memory _troveChange, + uint256 _price + ) internal view returns (uint256 newTCR) { uint256 totalColl = getEntireBranchColl(); totalColl += _troveChange.collIncrease; totalColl -= _troveChange.collDecrease; diff --git a/contracts/src/Interfaces/IBorrowerOperations.sol b/contracts/src/Interfaces/IBorrowerOperations.sol index 8e8dc38da..7d93cf7e7 100644 --- a/contracts/src/Interfaces/IBorrowerOperations.sol +++ b/contracts/src/Interfaces/IBorrowerOperations.sol @@ -14,7 +14,6 @@ import "./IWETH.sol"; interface IBorrowerOperations is ILiquityBase, IAddRemoveManagers { function CCR() external view returns (uint256); function MCR() external view returns (uint256); - function SCR() external view returns (uint256); function openTrove( address _owner, From d3a79af65595a8043df7e5790b09e07dc061ce15 Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:57:19 -0500 Subject: [PATCH 47/79] feat: re-add global params --- contracts/foundry.toml | 2 +- contracts/script/DeployLiquity2.s.sol | 22 +- contracts/src/BorrowerOperations.sol | 25 +- contracts/src/Dependencies/Constants.sol | 16 ++ contracts/src/HintHelpers.sol | 6 - contracts/src/Interfaces/ISystemParams.sol | 28 -- contracts/src/SystemParams.sol | 41 +-- contracts/src/TroveManager.sol | 5 +- contracts/test/TestContracts/BaseTest.sol | 7 - contracts/test/TestContracts/Deployment.t.sol | 11 +- contracts/test/TestContracts/DevTestSetup.sol | 7 - .../TestContracts/InvariantsTestHandler.t.sol | 36 ++- .../TestContracts/TroveManagerTester.t.sol | 3 - contracts/test/multicollateral.t.sol | 2 - contracts/test/shutdown.t.sol | 12 +- contracts/test/systemParams.t.sol | 247 +----------------- contracts/test/troveNFT.t.sol | 1 - 17 files changed, 70 insertions(+), 401 deletions(-) diff --git a/contracts/foundry.toml b/contracts/foundry.toml index e4eb87f0e..d65024722 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -4,7 +4,7 @@ out = "out" libs = ["lib"] evm_version = 'cancun' optimizer = true -optimizer_runs = 200 +optimizer_runs = 0 ignored_error_codes = [3860, 5574] # contract-size fs_permissions = [ { access = "read", path = "./utils/assets/" }, diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 9c8d83c70..ab3a9ff01 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -11,6 +11,11 @@ import {TransparentUpgradeableProxy} from import {IFPMMFactory} from "src/Interfaces/IFPMMFactory.sol"; import {SystemParams} from "src/SystemParams.sol"; import {ISystemParams} from "src/Interfaces/ISystemParams.sol"; +import { + INTEREST_RATE_ADJ_COOLDOWN, + MAX_ANNUAL_INTEREST_RATE, + UPFRONT_INTEREST_PERIOD +} from "src/Dependencies/Constants.sol"; import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; import {StringFormatting} from "test/Utils/StringFormatting.sol"; @@ -244,21 +249,14 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { ISystemParams.CollateralParams({ccr: 150 * 1e16, scr: 110 * 1e16, mcr: 110 * 1e16, bcr: 10 * 1e16}); ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ - minAnnualInterestRate: 1e18 / 200, - maxAnnualInterestRate: 250 * (1e18 / 100), - maxAnnualBatchManagementFee: uint128(1e18 / 10), - upfrontInterestPeriod: 7 days, - interestRateAdjCooldown: 7 days, - minInterestRateChangePeriod: 1 hours, - maxBatchSharesRatio: 1e9 + minAnnualInterestRate: 1e18 / 200 }); ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ redemptionFeeFloor: 1e18 / 200, initialBaseRate: 1e18, redemptionMinuteDecayFactor: 998076443575628800, - redemptionBeta: 1, - urgentRedemptionBonus: 2 * 1e16 + redemptionBeta: 1 }); ISystemParams.StabilityPoolParams memory poolParams = @@ -456,12 +454,12 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { "{", string.concat( string.concat('"ETH_GAS_COMPENSATION":"', params.ETH_GAS_COMPENSATION().toString(), '",'), - string.concat('"INTEREST_RATE_ADJ_COOLDOWN":"', params.INTEREST_RATE_ADJ_COOLDOWN().toString(), '",'), - string.concat('"MAX_ANNUAL_INTEREST_RATE":"', params.MAX_ANNUAL_INTEREST_RATE().toString(), '",'), + string.concat('"INTEREST_RATE_ADJ_COOLDOWN":"', INTEREST_RATE_ADJ_COOLDOWN.toString(), '",'), + string.concat('"MAX_ANNUAL_INTEREST_RATE":"', MAX_ANNUAL_INTEREST_RATE.toString(), '",'), string.concat('"MIN_ANNUAL_INTEREST_RATE":"', params.MIN_ANNUAL_INTEREST_RATE().toString(), '",'), string.concat('"MIN_DEBT":"', params.MIN_DEBT().toString(), '",'), string.concat('"SP_YIELD_SPLIT":"', params.SP_YIELD_SPLIT().toString(), '",'), - string.concat('"UPFRONT_INTEREST_PERIOD":"', params.UPFRONT_INTEREST_PERIOD().toString(), '"') // no comma + string.concat('"UPFRONT_INTEREST_PERIOD":"', UPFRONT_INTEREST_PERIOD.toString(), '"') // no comma ), "}" ); diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 7ae2470ee..b8bb6822c 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -19,12 +19,8 @@ import "./Types/LatestBatchData.sol"; /** * @dev System parameters pattern: * Most system parameters are copied from SystemParams to immutable variables at construction for gas optimization. - * However, to reduce contract size, the following parameters are read directly from SystemParams when needed: + * However, to reduce contract size, the following parameter is read directly from SystemParams when needed: * - SCR: Only used in shutdown() function - * - MAX_ANNUAL_BATCH_MANAGEMENT_FEE: Only used in registerBatchManager() - * - MIN_INTEREST_RATE_CHANGE_PERIOD: Only used in registerBatchManager() - * - MAX_BATCH_SHARES_RATIO: Only used in kickFromBatch() - * These are infrequently called operations where the additional ~2500 gas per read is acceptable. */ contract BorrowerOperations is LiquityBase, @@ -59,10 +55,7 @@ contract BorrowerOperations is uint256 public immutable ETH_GAS_COMPENSATION; uint256 public immutable MIN_DEBT; - uint256 public immutable INTEREST_RATE_ADJ_COOLDOWN; - uint256 public immutable UPFRONT_INTEREST_PERIOD; uint256 public immutable MIN_ANNUAL_INTEREST_RATE; - uint256 public immutable MAX_ANNUAL_INTEREST_RATE; /* * Mapping from TroveId to individual delegate for interest rate setting. @@ -204,10 +197,7 @@ contract BorrowerOperations is ETH_GAS_COMPENSATION = _systemParams.ETH_GAS_COMPENSATION(); MIN_DEBT = _systemParams.MIN_DEBT(); - UPFRONT_INTEREST_PERIOD = _systemParams.UPFRONT_INTEREST_PERIOD(); MIN_ANNUAL_INTEREST_RATE = _systemParams.MIN_ANNUAL_INTEREST_RATE(); - MAX_ANNUAL_INTEREST_RATE = _systemParams.MAX_ANNUAL_INTEREST_RATE(); - INTEREST_RATE_ADJ_COOLDOWN = _systemParams.INTEREST_RATE_ADJ_COOLDOWN(); troveManager = _addressesRegistry.troveManager(); gasPoolAddress = _addressesRegistry.gasPoolAddress(); @@ -1104,16 +1094,10 @@ contract BorrowerOperations is ); // Not needed, implicitly checked in the condition above: //_requireValidAnnualInterestRate(_currentInterestRate); - if ( - _annualManagementFee > - systemParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE() - ) { + if (_annualManagementFee > MAX_ANNUAL_BATCH_MANAGEMENT_FEE) { revert AnnualManagementFeeTooHigh(); } - if ( - _minInterestRateChangePeriod < - systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD() - ) { + if (_minInterestRateChangePeriod < MIN_INTEREST_RATE_CHANGE_PERIOD) { revert MinInterestRateChangePeriodTooLow(); } @@ -1418,8 +1402,7 @@ contract BorrowerOperations is if (_kick) { if ( - vars.batch.totalDebtShares * - systemParams.MAX_BATCH_SHARES_RATIO() >= + vars.batch.totalDebtShares * MAX_BATCH_SHARES_RATIO >= vars.batch.entireDebtWithoutRedistribution ) { revert BatchSharesRatioTooLow(); diff --git a/contracts/src/Dependencies/Constants.sol b/contracts/src/Dependencies/Constants.sol index d9f9bef55..59d4d502e 100644 --- a/contracts/src/Dependencies/Constants.sol +++ b/contracts/src/Dependencies/Constants.sol @@ -11,3 +11,19 @@ uint256 constant _1pct = DECIMAL_PRECISION / 100; uint256 constant ONE_MINUTE = 1 minutes; uint256 constant ONE_YEAR = 365 days; + +// Interest rate parameters +uint256 constant MAX_ANNUAL_INTEREST_RATE = 250 * _1pct; // 250% +uint128 constant MAX_ANNUAL_BATCH_MANAGEMENT_FEE = uint128(_100pct / 10); // 10% +uint128 constant MIN_INTEREST_RATE_CHANGE_PERIOD = 1 hours; +uint256 constant UPFRONT_INTEREST_PERIOD = 7 days; +uint256 constant INTEREST_RATE_ADJ_COOLDOWN = 7 days; + +// Batch parameters +uint256 constant MAX_BATCH_SHARES_RATIO = 1e9; + +// Redemption parameters +uint256 constant URGENT_REDEMPTION_BONUS = 1 * _1pct; // 1% + +// Liquidation parameters +uint256 constant MAX_LIQUIDATION_PENALTY_REDISTRIBUTION = 20 * _1pct; // 20% diff --git a/contracts/src/HintHelpers.sol b/contracts/src/HintHelpers.sol index 5debbb86d..a9665290e 100644 --- a/contracts/src/HintHelpers.sol +++ b/contracts/src/HintHelpers.sol @@ -19,15 +19,9 @@ contract HintHelpers is IHintHelpers { ICollateralRegistry public immutable collateralRegistry; address public immutable systemParamsAddress; - uint256 public immutable INTEREST_RATE_ADJ_COOLDOWN; - uint256 public immutable UPFRONT_INTEREST_PERIOD; - constructor(ICollateralRegistry _collateralRegistry, ISystemParams _systemParams) { systemParamsAddress = address(_systemParams); collateralRegistry = _collateralRegistry; - - INTEREST_RATE_ADJ_COOLDOWN = _systemParams.INTEREST_RATE_ADJ_COOLDOWN(); - UPFRONT_INTEREST_PERIOD = _systemParams.UPFRONT_INTEREST_PERIOD(); } /* getApproxHint() - return id of a Trove that is, on average, (length / numTrials) positions away in the diff --git a/contracts/src/Interfaces/ISystemParams.sol b/contracts/src/Interfaces/ISystemParams.sol index 1ee7a0c42..949008beb 100644 --- a/contracts/src/Interfaces/ISystemParams.sol +++ b/contracts/src/Interfaces/ISystemParams.sol @@ -29,12 +29,6 @@ interface ISystemParams { struct InterestParams { uint256 minAnnualInterestRate; - uint256 maxAnnualInterestRate; - uint128 maxAnnualBatchManagementFee; - uint256 upfrontInterestPeriod; - uint256 interestRateAdjCooldown; - uint128 minInterestRateChangePeriod; - uint256 maxBatchSharesRatio; } struct RedemptionParams { @@ -42,7 +36,6 @@ interface ISystemParams { uint256 initialBaseRate; uint256 redemptionMinuteDecayFactor; uint256 redemptionBeta; - uint256 urgentRedemptionBonus; } struct StabilityPoolParams { @@ -126,24 +119,6 @@ interface ISystemParams { /// @notice Min annual interest rate for a trove. function MIN_ANNUAL_INTEREST_RATE() external view returns (uint256); - /// @notice Max annual interest rate for a trove. - function MAX_ANNUAL_INTEREST_RATE() external view returns (uint256); - - /// @notice Max fee that batch managers can charge. - function MAX_ANNUAL_BATCH_MANAGEMENT_FEE() external view returns (uint128); - - /// @notice Time period for which interest is charged upfront to prevent rate gaming. - function UPFRONT_INTEREST_PERIOD() external view returns (uint256); - - /// @notice Wait time in between interest rate adjustments. - function INTEREST_RATE_ADJ_COOLDOWN() external view returns (uint256); - - /// @notice Minimum time in between interest rate changes triggered by a batch Manager. - function MIN_INTEREST_RATE_CHANGE_PERIOD() external view returns (uint128); - - /// @notice Maximum ratio between batch debt and shares to prevent inflation attacks. - function MAX_BATCH_SHARES_RATIO() external view returns (uint256); - /* ========== REDEMPTION PARAMETERS ========== */ /// @notice Minimum redemption fee percentage. @@ -158,9 +133,6 @@ interface ISystemParams { /// @notice Divisor controlling base rate sensitivity to redemption volume (higher = less sensitive). function REDEMPTION_BETA() external view returns (uint256); - /// @notice Extra collateral bonus given to redeemers during urgent redemptions after shutdown. - function URGENT_REDEMPTION_BONUS() external view returns (uint256); - /* ========== STABILITY POOL PARAMETERS ========== */ /// @notice Percentage of minted interest yield allocated to Stability Pool depositors. diff --git a/contracts/src/SystemParams.sol b/contracts/src/SystemParams.sol index d8800da2e..c6721071d 100644 --- a/contracts/src/SystemParams.sol +++ b/contracts/src/SystemParams.sol @@ -3,7 +3,12 @@ pragma solidity 0.8.24; import {ISystemParams} from "./Interfaces/ISystemParams.sol"; -import {_100pct, _1pct} from "./Dependencies/Constants.sol"; +import { + _100pct, + _1pct, + MAX_LIQUIDATION_PENALTY_REDISTRIBUTION, + MAX_ANNUAL_INTEREST_RATE +} from "./Dependencies/Constants.sol"; /** * @title System Parameters @@ -36,12 +41,6 @@ contract SystemParams is ISystemParams { /* ========== INTEREST PARAMETERS ========== */ uint256 public MIN_ANNUAL_INTEREST_RATE; - uint256 public MAX_ANNUAL_INTEREST_RATE; - uint128 public MAX_ANNUAL_BATCH_MANAGEMENT_FEE; - uint256 public UPFRONT_INTEREST_PERIOD; - uint256 public INTEREST_RATE_ADJ_COOLDOWN; - uint128 public MIN_INTEREST_RATE_CHANGE_PERIOD; - uint256 public MAX_BATCH_SHARES_RATIO; /* ========== REDEMPTION PARAMETERS ========== */ @@ -49,7 +48,6 @@ contract SystemParams is ISystemParams { uint256 public INITIAL_BASE_RATE; uint256 public REDEMPTION_MINUTE_DECAY_FACTOR; uint256 public REDEMPTION_BETA; - uint256 public URGENT_REDEMPTION_BONUS; /* ========== STABILITY POOL PARAMETERS ========== */ @@ -71,12 +69,12 @@ contract SystemParams is ISystemParams { if (_debtParams.minDebt == 0 || _debtParams.minDebt > 10000e18) revert InvalidMinDebt(); // Validate liquidation parameters - // Hardcoded validation bounds: MIN_LIQUIDATION_PENALTY_SP = 5%, MAX_LIQUIDATION_PENALTY_REDISTRIBUTION = 20% + // Hardcoded validation bounds: MIN_LIQUIDATION_PENALTY_SP = 5% if (_liquidationParams.liquidationPenaltySP < 5 * _1pct) revert SPPenaltyTooLow(); if (_liquidationParams.liquidationPenaltySP > _liquidationParams.liquidationPenaltyRedistribution) revert SPPenaltyGtRedist(); - if (_liquidationParams.liquidationPenaltyRedistribution > 20 * _1pct) + if (_liquidationParams.liquidationPenaltyRedistribution > MAX_LIQUIDATION_PENALTY_REDISTRIBUTION) revert RedistPenaltyTooHigh(); // Validate gas compensation parameters @@ -96,26 +94,12 @@ contract SystemParams is ISystemParams { if (_collateralParams.scr <= _100pct || _collateralParams.scr >= 2 * _100pct) revert InvalidSCR(); // Validate interest parameters - if (_interestParams.minAnnualInterestRate > _interestParams.maxAnnualInterestRate) + if (_interestParams.minAnnualInterestRate > MAX_ANNUAL_INTEREST_RATE) revert MinInterestRateGtMax(); - if (_interestParams.maxAnnualInterestRate > 10 * _100pct) - revert InvalidInterestRateBounds(); // Max 1000% - if (_interestParams.maxAnnualBatchManagementFee > _100pct) revert InvalidFeeValue(); - - if (_interestParams.upfrontInterestPeriod == 0 || _interestParams.upfrontInterestPeriod > 365 days) - revert InvalidTimeValue(); - if ( - _interestParams.interestRateAdjCooldown == 0 || _interestParams.interestRateAdjCooldown > 365 days - ) revert InvalidTimeValue(); - if ( - _interestParams.minInterestRateChangePeriod == 0 || - _interestParams.minInterestRateChangePeriod > 30 days - ) revert InvalidTimeValue(); // Validate redemption parameters if (_redemptionParams.redemptionFeeFloor > _100pct) revert InvalidFeeValue(); if (_redemptionParams.initialBaseRate > 10 * _100pct) revert InvalidFeeValue(); - if (_redemptionParams.urgentRedemptionBonus > _100pct) revert InvalidFeeValue(); // Validate stability pool parameters if (_poolParams.spYieldSplit > _100pct) revert InvalidFeeValue(); @@ -141,19 +125,12 @@ contract SystemParams is ISystemParams { // Set interest parameters MIN_ANNUAL_INTEREST_RATE = _interestParams.minAnnualInterestRate; - MAX_ANNUAL_INTEREST_RATE = _interestParams.maxAnnualInterestRate; - MAX_ANNUAL_BATCH_MANAGEMENT_FEE = _interestParams.maxAnnualBatchManagementFee; - UPFRONT_INTEREST_PERIOD = _interestParams.upfrontInterestPeriod; - INTEREST_RATE_ADJ_COOLDOWN = _interestParams.interestRateAdjCooldown; - MIN_INTEREST_RATE_CHANGE_PERIOD = _interestParams.minInterestRateChangePeriod; - MAX_BATCH_SHARES_RATIO = _interestParams.maxBatchSharesRatio; // Set redemption parameters REDEMPTION_FEE_FLOOR = _redemptionParams.redemptionFeeFloor; INITIAL_BASE_RATE = _redemptionParams.initialBaseRate; REDEMPTION_MINUTE_DECAY_FACTOR = _redemptionParams.redemptionMinuteDecayFactor; REDEMPTION_BETA = _redemptionParams.redemptionBeta; - URGENT_REDEMPTION_BONUS = _redemptionParams.urgentRedemptionBonus; // Set stability pool parameters SP_YIELD_SPLIT = _poolParams.spYieldSplit; diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 5884ec078..b75674867 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -14,6 +14,7 @@ import "./Interfaces/ICollateralRegistry.sol"; import "./Interfaces/IWETH.sol"; import "./Interfaces/ISystemParams.sol"; import "./Dependencies/LiquityBase.sol"; +import "./Dependencies/Constants.sol"; contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // --- Connected contract declarations --- @@ -50,8 +51,6 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 internal immutable MIN_BOLD_IN_SP; uint256 internal immutable ETH_GAS_COMPENSATION; uint256 internal immutable MIN_DEBT; - uint256 internal immutable URGENT_REDEMPTION_BONUS; - uint256 internal immutable MAX_BATCH_SHARES_RATIO; // --- Data structures --- @@ -209,8 +208,6 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { MIN_BOLD_IN_SP = _systemParams.MIN_BOLD_IN_SP(); ETH_GAS_COMPENSATION = _systemParams.ETH_GAS_COMPENSATION(); MIN_DEBT = _systemParams.MIN_DEBT(); - URGENT_REDEMPTION_BONUS = _systemParams.URGENT_REDEMPTION_BONUS(); - MAX_BATCH_SHARES_RATIO = _systemParams.MAX_BATCH_SHARES_RATIO(); troveNFT = _addressesRegistry.troveNFT(); borrowerOperations = _addressesRegistry.borrowerOperations(); diff --git a/contracts/test/TestContracts/BaseTest.sol b/contracts/test/TestContracts/BaseTest.sol index 35eb3d422..ebdee4ade 100644 --- a/contracts/test/TestContracts/BaseTest.sol +++ b/contracts/test/TestContracts/BaseTest.sol @@ -32,22 +32,15 @@ contract BaseTest is TestAccounts, Logging, TroveId { uint256 SCR; uint256 LIQUIDATION_PENALTY_SP; uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; - uint256 UPFRONT_INTEREST_PERIOD; - uint128 MIN_INTEREST_RATE_CHANGE_PERIOD; uint256 MIN_DEBT; uint256 SP_YIELD_SPLIT; uint256 MIN_ANNUAL_INTEREST_RATE; - uint256 INTEREST_RATE_ADJ_COOLDOWN; uint256 ETH_GAS_COMPENSATION; uint256 COLL_GAS_COMPENSATION_DIVISOR; - uint256 MAX_ANNUAL_INTEREST_RATE; uint256 MIN_BOLD_IN_SP; uint256 INITIAL_BASE_RATE; uint256 REDEMPTION_FEE_FLOOR; - uint256 MAX_BATCH_SHARES_RATIO; - uint128 MAX_ANNUAL_BATCH_MANAGEMENT_FEE; uint256 REDEMPTION_MINUTE_DECAY_FACTOR; - uint256 URGENT_REDEMPTION_BONUS; // Core contracts IAddressesRegistry addressesRegistry; diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index 75dffdbed..d7b5a3a42 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -374,21 +374,14 @@ contract TestDeployer is MetadataDeployment { }); ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ - minAnnualInterestRate: DECIMAL_PRECISION / 200, // MIN_ANNUAL_INTEREST_RATE (0.5%) - maxAnnualInterestRate: 250 * (DECIMAL_PRECISION / 100), // MAX_ANNUAL_INTEREST_RATE (250%) - maxAnnualBatchManagementFee: uint128(DECIMAL_PRECISION / 10), // MAX_ANNUAL_BATCH_MANAGEMENT_FEE (10%) - upfrontInterestPeriod: 7 days, // UPFRONT_INTEREST_PERIOD - interestRateAdjCooldown: 7 days, // INTEREST_RATE_ADJ_COOLDOWN - minInterestRateChangePeriod: 1 hours, // MIN_INTEREST_RATE_CHANGE_PERIOD - maxBatchSharesRatio: 1e9 // MAX_BATCH_SHARES_RATIO + minAnnualInterestRate: DECIMAL_PRECISION / 200 // MIN_ANNUAL_INTEREST_RATE (0.5%) }); ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ redemptionFeeFloor: DECIMAL_PRECISION / 200, // REDEMPTION_FEE_FLOOR (0.5%) initialBaseRate: DECIMAL_PRECISION, // INITIAL_BASE_RATE (100%) redemptionMinuteDecayFactor: 998076443575628800, // REDEMPTION_MINUTE_DECAY_FACTOR - redemptionBeta: 1, // REDEMPTION_BETA - urgentRedemptionBonus: 2e16 // URGENT_REDEMPTION_BONUS (2%) + redemptionBeta: 1 // REDEMPTION_BETA }); ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ diff --git a/contracts/test/TestContracts/DevTestSetup.sol b/contracts/test/TestContracts/DevTestSetup.sol index e0c719afb..4a77690e1 100644 --- a/contracts/test/TestContracts/DevTestSetup.sol +++ b/contracts/test/TestContracts/DevTestSetup.sol @@ -82,19 +82,12 @@ contract DevTestSetup is BaseTest { MIN_DEBT = systemParams.MIN_DEBT(); SP_YIELD_SPLIT = systemParams.SP_YIELD_SPLIT(); MIN_ANNUAL_INTEREST_RATE = systemParams.MIN_ANNUAL_INTEREST_RATE(); - INTEREST_RATE_ADJ_COOLDOWN = systemParams.INTEREST_RATE_ADJ_COOLDOWN(); ETH_GAS_COMPENSATION = systemParams.ETH_GAS_COMPENSATION(); COLL_GAS_COMPENSATION_DIVISOR = systemParams.COLL_GAS_COMPENSATION_DIVISOR(); - MAX_ANNUAL_INTEREST_RATE = systemParams.MAX_ANNUAL_INTEREST_RATE(); MIN_BOLD_IN_SP = systemParams.MIN_BOLD_IN_SP(); REDEMPTION_FEE_FLOOR = systemParams.REDEMPTION_FEE_FLOOR(); INITIAL_BASE_RATE = systemParams.INITIAL_BASE_RATE(); - MAX_BATCH_SHARES_RATIO = systemParams.MAX_BATCH_SHARES_RATIO(); - MAX_ANNUAL_BATCH_MANAGEMENT_FEE = systemParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE(); REDEMPTION_MINUTE_DECAY_FACTOR = systemParams.REDEMPTION_MINUTE_DECAY_FACTOR(); - URGENT_REDEMPTION_BONUS = systemParams.URGENT_REDEMPTION_BONUS(); - MIN_INTEREST_RATE_CHANGE_PERIOD = systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD(); - UPFRONT_INTEREST_PERIOD = systemParams.UPFRONT_INTEREST_PERIOD(); } function _setupForWithdrawCollGainToTrove() internal returns (uint256, uint256, uint256) { diff --git a/contracts/test/TestContracts/InvariantsTestHandler.t.sol b/contracts/test/TestContracts/InvariantsTestHandler.t.sol index 00bc582b2..2a765e291 100644 --- a/contracts/test/TestContracts/InvariantsTestHandler.t.sol +++ b/contracts/test/TestContracts/InvariantsTestHandler.t.sol @@ -26,7 +26,18 @@ import {BaseMultiCollateralTest} from "./BaseMultiCollateralTest.sol"; import {TestDeployer} from "./Deployment.t.sol"; import {ISystemParams} from "src/Interfaces/ISystemParams.sol"; -import { _100pct, _1pct, DECIMAL_PRECISION, ONE_MINUTE, ONE_YEAR } from "src/Dependencies/Constants.sol"; +import { + _100pct, + _1pct, + DECIMAL_PRECISION, + ONE_MINUTE, + ONE_YEAR, + MAX_ANNUAL_INTEREST_RATE, + MAX_ANNUAL_BATCH_MANAGEMENT_FEE, + MIN_INTEREST_RATE_CHANGE_PERIOD, + INTEREST_RATE_ADJ_COOLDOWN, + URGENT_REDEMPTION_BONUS +} from "src/Dependencies/Constants.sol"; uint256 constant TIME_DELTA_MIN = 0; uint256 constant TIME_DELTA_MAX = ONE_YEAR; @@ -357,28 +368,24 @@ contract InvariantsTestHandler is Assertions, BaseHandler, BaseMultiCollateralTe // Urgent redemption transient state UrgentRedemptionTransientState _urgentRedemption; + uint256 constant BATCH_MANAGEMENT_FEE_MIN = 0; + uint256 constant BATCH_MANAGEMENT_FEE_MAX = MAX_ANNUAL_BATCH_MANAGEMENT_FEE + 1; // Sometimes try too high + uint256 constant INTEREST_RATE_MAX = MAX_ANNUAL_INTEREST_RATE + 1; // Sometimes try rates exceeding the max + uint256 constant RATE_CHANGE_PERIOD_MIN = MIN_INTEREST_RATE_CHANGE_PERIOD - 1; // Sometimes try too low + // System params-based variables uint256 immutable INTEREST_RATE_MIN; - uint256 immutable INTEREST_RATE_MAX; - uint256 immutable BATCH_MANAGEMENT_FEE_MIN; - uint256 immutable BATCH_MANAGEMENT_FEE_MAX; - uint256 immutable RATE_CHANGE_PERIOD_MIN; uint256 immutable RATE_CHANGE_PERIOD_MAX = TIME_DELTA_MAX; uint256 immutable ETH_GAS_COMPENSATION; uint256 immutable MIN_ANNUAL_INTEREST_RATE; uint256 immutable MIN_DEBT; - uint256 immutable INTEREST_RATE_ADJ_COOLDOWN; uint256 immutable MIN_BOLD_IN_SP; - uint256 immutable MAX_ANNUAL_INTEREST_RATE; - uint128 immutable MAX_ANNUAL_BATCH_MANAGEMENT_FEE; - uint128 immutable MIN_INTEREST_RATE_CHANGE_PERIOD; uint256 immutable REDEMPTION_MINUTE_DECAY_FACTOR; uint256 immutable REDEMPTION_BETA; uint256 immutable REDEMPTION_FEE_FLOOR; uint256 immutable SP_YIELD_SPLIT; uint256 immutable COLL_GAS_COMPENSATION_DIVISOR; uint256 immutable COLL_GAS_COMPENSATION_CAP; - uint256 immutable URGENT_REDEMPTION_BONUS; constructor(Contracts memory contracts, bool assumeNoExpectedFailures) { _functionCaller = new FunctionCaller(); @@ -398,26 +405,17 @@ contract InvariantsTestHandler is Assertions, BaseHandler, BaseMultiCollateralTe // Set system params-based variables INTEREST_RATE_MIN = systemParams.MIN_ANNUAL_INTEREST_RATE() - 1; // Sometimes try rates lower than the min - INTEREST_RATE_MAX = systemParams.MAX_ANNUAL_INTEREST_RATE() + 1; // Sometimes try rates exceeding the max - BATCH_MANAGEMENT_FEE_MIN = 0; - BATCH_MANAGEMENT_FEE_MAX = systemParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE() + 1; // Sometimes try too high - RATE_CHANGE_PERIOD_MIN = systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD() - 1; // Sometimes try too low _baseRate = systemParams.INITIAL_BASE_RATE(); ETH_GAS_COMPENSATION = systemParams.ETH_GAS_COMPENSATION(); MIN_ANNUAL_INTEREST_RATE = systemParams.MIN_ANNUAL_INTEREST_RATE(); MIN_DEBT = systemParams.MIN_DEBT(); - INTEREST_RATE_ADJ_COOLDOWN = systemParams.INTEREST_RATE_ADJ_COOLDOWN(); MIN_BOLD_IN_SP = systemParams.MIN_BOLD_IN_SP(); - MAX_ANNUAL_INTEREST_RATE = systemParams.MAX_ANNUAL_INTEREST_RATE(); - MAX_ANNUAL_BATCH_MANAGEMENT_FEE = systemParams.MAX_ANNUAL_BATCH_MANAGEMENT_FEE(); - MIN_INTEREST_RATE_CHANGE_PERIOD = systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD(); REDEMPTION_MINUTE_DECAY_FACTOR = systemParams.REDEMPTION_MINUTE_DECAY_FACTOR(); REDEMPTION_BETA = systemParams.REDEMPTION_BETA(); REDEMPTION_FEE_FLOOR = systemParams.REDEMPTION_FEE_FLOOR(); SP_YIELD_SPLIT = systemParams.SP_YIELD_SPLIT(); COLL_GAS_COMPENSATION_DIVISOR = systemParams.COLL_GAS_COMPENSATION_DIVISOR(); COLL_GAS_COMPENSATION_CAP = systemParams.COLL_GAS_COMPENSATION_CAP(); - URGENT_REDEMPTION_BONUS = systemParams.URGENT_REDEMPTION_BONUS(); } ////////////////////////////////////////////// diff --git a/contracts/test/TestContracts/TroveManagerTester.t.sol b/contracts/test/TestContracts/TroveManagerTester.t.sol index 626f194c2..1b6f141e3 100644 --- a/contracts/test/TestContracts/TroveManagerTester.t.sol +++ b/contracts/test/TestContracts/TroveManagerTester.t.sol @@ -17,12 +17,9 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { // Extra buffer of collateral ratio to join a batch or adjust a trove inside a batch (on top of MCR) uint256 public immutable BCR; - uint256 public immutable UPFRONT_INTEREST_PERIOD; - constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) TroveManager(_addressesRegistry, _systemParams) { BCR = _systemParams.BCR(); - UPFRONT_INTEREST_PERIOD = _systemParams.UPFRONT_INTEREST_PERIOD(); } // Single liquidation function. Closes the trove if its ICR is lower than the minimum collateral ratio. diff --git a/contracts/test/multicollateral.t.sol b/contracts/test/multicollateral.t.sol index b95be7829..a4feab804 100644 --- a/contracts/test/multicollateral.t.sol +++ b/contracts/test/multicollateral.t.sol @@ -111,8 +111,6 @@ contract MulticollateralTest is DevTestSetup { } systemParams = contractsArray[0].systemParams; - UPFRONT_INTEREST_PERIOD = systemParams.UPFRONT_INTEREST_PERIOD(); - URGENT_REDEMPTION_BONUS = systemParams.URGENT_REDEMPTION_BONUS(); REDEMPTION_FEE_FLOOR = systemParams.REDEMPTION_FEE_FLOOR(); INITIAL_BASE_RATE = systemParams.INITIAL_BASE_RATE(); } diff --git a/contracts/test/shutdown.t.sol b/contracts/test/shutdown.t.sol index ab0656464..1d7c73439 100644 --- a/contracts/test/shutdown.t.sol +++ b/contracts/test/shutdown.t.sol @@ -43,9 +43,6 @@ contract ShutdownTest is DevTestSetup { // Initialize SystemParams-based variables systemParams = contractsArray[0].systemParams; - UPFRONT_INTEREST_PERIOD = systemParams.UPFRONT_INTEREST_PERIOD(); - URGENT_REDEMPTION_BONUS = systemParams.URGENT_REDEMPTION_BONUS(); - MIN_INTEREST_RATE_CHANGE_PERIOD = systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD(); SP_YIELD_SPLIT = systemParams.SP_YIELD_SPLIT(); // Set price feeds contractsArray[0].priceFeed.setPrice(2000e18); @@ -84,9 +81,6 @@ contract ShutdownTest is DevTestSetup { systemParams = contractsArray[0].systemParams; MCR = troveManager.get_MCR(); SCR = troveManager.get_SCR(); - UPFRONT_INTEREST_PERIOD = systemParams.UPFRONT_INTEREST_PERIOD(); - MIN_INTEREST_RATE_CHANGE_PERIOD = systemParams.MIN_INTEREST_RATE_CHANGE_PERIOD(); - URGENT_REDEMPTION_BONUS = systemParams.URGENT_REDEMPTION_BONUS(); } function openMulticollateralTroveNoHints100pctWithIndex( @@ -432,13 +426,13 @@ contract ShutdownTest is DevTestSetup { // Min not reached vm.startPrank(A); - vm.expectRevert(abi.encodeWithSelector(TroveManager.MinCollNotReached.selector, 102e16)); - troveManager.urgentRedemption(1000e18, uintToArray(troveId), 103e16); + vm.expectRevert(abi.encodeWithSelector(TroveManager.MinCollNotReached.selector, 101e16)); + troveManager.urgentRedemption(1000e18, uintToArray(troveId), 102e16); vm.stopPrank(); // Min just reached vm.startPrank(A); - troveManager.urgentRedemption(1000e18, uintToArray(troveId), 102e16); + troveManager.urgentRedemption(1000e18, uintToArray(troveId), 101e16); vm.stopPrank(); } diff --git a/contracts/test/systemParams.t.sol b/contracts/test/systemParams.t.sol index 0896f75fe..ffd31e1e9 100644 --- a/contracts/test/systemParams.t.sol +++ b/contracts/test/systemParams.t.sol @@ -31,21 +31,14 @@ contract SystemParamsTest is DevTestSetup { }); ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ - minAnnualInterestRate: _1pct / 2, // 0.5% - maxAnnualInterestRate: 250 * _1pct, - maxAnnualBatchManagementFee: uint128(_100pct / 10), // 10% - upfrontInterestPeriod: 7 days, - interestRateAdjCooldown: 7 days, - minInterestRateChangePeriod: 1 hours, - maxBatchSharesRatio: 1e9 + minAnnualInterestRate: _1pct / 2 // 0.5% }); ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ redemptionFeeFloor: _1pct / 2, // 0.5% initialBaseRate: _100pct, redemptionMinuteDecayFactor: 998076443575628800, - redemptionBeta: 1, - urgentRedemptionBonus: 2 * _1pct + redemptionBeta: 1 }); ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ @@ -75,17 +68,10 @@ contract SystemParamsTest is DevTestSetup { assertEq(params.MCR(), 110 * _1pct); assertEq(params.BCR(), 10 * _1pct); assertEq(params.MIN_ANNUAL_INTEREST_RATE(), _1pct / 2); - assertEq(params.MAX_ANNUAL_INTEREST_RATE(), 250 * _1pct); - assertEq(params.MAX_ANNUAL_BATCH_MANAGEMENT_FEE(), uint128(_100pct / 10)); - assertEq(params.UPFRONT_INTEREST_PERIOD(), 7 days); - assertEq(params.INTEREST_RATE_ADJ_COOLDOWN(), 7 days); - assertEq(params.MIN_INTEREST_RATE_CHANGE_PERIOD(), 1 hours); - assertEq(params.MAX_BATCH_SHARES_RATIO(), 1e9); assertEq(params.REDEMPTION_FEE_FLOOR(), _1pct / 2); assertEq(params.INITIAL_BASE_RATE(), _100pct); assertEq(params.REDEMPTION_MINUTE_DECAY_FACTOR(), 998076443575628800); assertEq(params.REDEMPTION_BETA(), 1); - assertEq(params.URGENT_REDEMPTION_BONUS(), 2 * _1pct); assertEq(params.SP_YIELD_SPLIT(), 75 * _1pct); assertEq(params.MIN_BOLD_IN_SP(), 1e18); } @@ -460,13 +446,7 @@ contract SystemParamsTest is DevTestSetup { function testConstructorRevertsWhenMinInterestRateGreaterThanMax() public { ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ - minAnnualInterestRate: 100 * _1pct, - maxAnnualInterestRate: 50 * _1pct, // min > max - maxAnnualBatchManagementFee: uint128(_100pct / 10), - upfrontInterestPeriod: 7 days, - interestRateAdjCooldown: 7 days, - minInterestRateChangePeriod: 1 hours, - maxBatchSharesRatio: 1e9 + minAnnualInterestRate: 300 * _1pct // min > MAX (250%) }); vm.expectRevert(ISystemParams.MinInterestRateGtMax.selector); @@ -481,189 +461,6 @@ contract SystemParamsTest is DevTestSetup { ); } - function testConstructorRevertsWhenMaxInterestRateTooHigh() public { - ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ - minAnnualInterestRate: _1pct / 2, - maxAnnualInterestRate: 1001 * _1pct, // > 1000% - maxAnnualBatchManagementFee: uint128(_100pct / 10), - upfrontInterestPeriod: 7 days, - interestRateAdjCooldown: 7 days, - minInterestRateChangePeriod: 1 hours, - maxBatchSharesRatio: 1e9 - }); - - vm.expectRevert(ISystemParams.InvalidInterestRateBounds.selector); - new SystemParams( - _getValidDebtParams(), - _getValidLiquidationParams(), - _getValidGasCompParams(), - _getValidCollateralParams(), - interestParams, - _getValidRedemptionParams(), - _getValidPoolParams() - ); - } - - function testConstructorRevertsWhenMaxBatchManagementFeeTooHigh() public { - ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ - minAnnualInterestRate: _1pct / 2, - maxAnnualInterestRate: 250 * _1pct, - maxAnnualBatchManagementFee: uint128(_100pct + 1), // > 100% - upfrontInterestPeriod: 7 days, - interestRateAdjCooldown: 7 days, - minInterestRateChangePeriod: 1 hours, - maxBatchSharesRatio: 1e9 - }); - - vm.expectRevert(ISystemParams.InvalidFeeValue.selector); - new SystemParams( - _getValidDebtParams(), - _getValidLiquidationParams(), - _getValidGasCompParams(), - _getValidCollateralParams(), - interestParams, - _getValidRedemptionParams(), - _getValidPoolParams() - ); - } - - function testConstructorRevertsWhenUpfrontInterestPeriodZero() public { - ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ - minAnnualInterestRate: _1pct / 2, - maxAnnualInterestRate: 250 * _1pct, - maxAnnualBatchManagementFee: uint128(_100pct / 10), - upfrontInterestPeriod: 0, - interestRateAdjCooldown: 7 days, - minInterestRateChangePeriod: 1 hours, - maxBatchSharesRatio: 1e9 - }); - - vm.expectRevert(ISystemParams.InvalidTimeValue.selector); - new SystemParams( - _getValidDebtParams(), - _getValidLiquidationParams(), - _getValidGasCompParams(), - _getValidCollateralParams(), - interestParams, - _getValidRedemptionParams(), - _getValidPoolParams() - ); - } - - function testConstructorRevertsWhenUpfrontInterestPeriodTooLong() public { - ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ - minAnnualInterestRate: _1pct / 2, - maxAnnualInterestRate: 250 * _1pct, - maxAnnualBatchManagementFee: uint128(_100pct / 10), - upfrontInterestPeriod: 366 days, - interestRateAdjCooldown: 7 days, - minInterestRateChangePeriod: 1 hours, - maxBatchSharesRatio: 1e9 - }); - - vm.expectRevert(ISystemParams.InvalidTimeValue.selector); - new SystemParams( - _getValidDebtParams(), - _getValidLiquidationParams(), - _getValidGasCompParams(), - _getValidCollateralParams(), - interestParams, - _getValidRedemptionParams(), - _getValidPoolParams() - ); - } - - function testConstructorRevertsWhenInterestRateAdjCooldownZero() public { - ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ - minAnnualInterestRate: _1pct / 2, - maxAnnualInterestRate: 250 * _1pct, - maxAnnualBatchManagementFee: uint128(_100pct / 10), - upfrontInterestPeriod: 7 days, - interestRateAdjCooldown: 0, - minInterestRateChangePeriod: 1 hours, - maxBatchSharesRatio: 1e9 - }); - - vm.expectRevert(ISystemParams.InvalidTimeValue.selector); - new SystemParams( - _getValidDebtParams(), - _getValidLiquidationParams(), - _getValidGasCompParams(), - _getValidCollateralParams(), - interestParams, - _getValidRedemptionParams(), - _getValidPoolParams() - ); - } - - function testConstructorRevertsWhenInterestRateAdjCooldownTooLong() public { - ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ - minAnnualInterestRate: _1pct / 2, - maxAnnualInterestRate: 250 * _1pct, - maxAnnualBatchManagementFee: uint128(_100pct / 10), - upfrontInterestPeriod: 7 days, - interestRateAdjCooldown: 366 days, - minInterestRateChangePeriod: 1 hours, - maxBatchSharesRatio: 1e9 - }); - - vm.expectRevert(ISystemParams.InvalidTimeValue.selector); - new SystemParams( - _getValidDebtParams(), - _getValidLiquidationParams(), - _getValidGasCompParams(), - _getValidCollateralParams(), - interestParams, - _getValidRedemptionParams(), - _getValidPoolParams() - ); - } - - function testConstructorRevertsWhenMinInterestRateChangePeriodZero() public { - ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ - minAnnualInterestRate: _1pct / 2, - maxAnnualInterestRate: 250 * _1pct, - maxAnnualBatchManagementFee: uint128(_100pct / 10), - upfrontInterestPeriod: 7 days, - interestRateAdjCooldown: 7 days, - minInterestRateChangePeriod: 0, - maxBatchSharesRatio: 1e9 - }); - - vm.expectRevert(ISystemParams.InvalidTimeValue.selector); - new SystemParams( - _getValidDebtParams(), - _getValidLiquidationParams(), - _getValidGasCompParams(), - _getValidCollateralParams(), - interestParams, - _getValidRedemptionParams(), - _getValidPoolParams() - ); - } - - function testConstructorRevertsWhenMinInterestRateChangePeriodTooLong() public { - ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ - minAnnualInterestRate: _1pct / 2, - maxAnnualInterestRate: 250 * _1pct, - maxAnnualBatchManagementFee: uint128(_100pct / 10), - upfrontInterestPeriod: 7 days, - interestRateAdjCooldown: 7 days, - minInterestRateChangePeriod: 31 days, - maxBatchSharesRatio: 1e9 - }); - - vm.expectRevert(ISystemParams.InvalidTimeValue.selector); - new SystemParams( - _getValidDebtParams(), - _getValidLiquidationParams(), - _getValidGasCompParams(), - _getValidCollateralParams(), - interestParams, - _getValidRedemptionParams(), - _getValidPoolParams() - ); - } // ========== REDEMPTION VALIDATION TESTS ========== @@ -672,8 +469,7 @@ contract SystemParamsTest is DevTestSetup { redemptionFeeFloor: _100pct + 1, initialBaseRate: _100pct, redemptionMinuteDecayFactor: 998076443575628800, - redemptionBeta: 1, - urgentRedemptionBonus: 2 * _1pct + redemptionBeta: 1 }); vm.expectRevert(ISystemParams.InvalidFeeValue.selector); @@ -693,29 +489,7 @@ contract SystemParamsTest is DevTestSetup { redemptionFeeFloor: _1pct / 2, initialBaseRate: 1001 * _1pct, // > 1000% redemptionMinuteDecayFactor: 998076443575628800, - redemptionBeta: 1, - urgentRedemptionBonus: 2 * _1pct - }); - - vm.expectRevert(ISystemParams.InvalidFeeValue.selector); - new SystemParams( - _getValidDebtParams(), - _getValidLiquidationParams(), - _getValidGasCompParams(), - _getValidCollateralParams(), - _getValidInterestParams(), - redemptionParams, - _getValidPoolParams() - ); - } - - function testConstructorRevertsWhenUrgentRedemptionBonusTooHigh() public { - ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ - redemptionFeeFloor: _1pct / 2, - initialBaseRate: _100pct, - redemptionMinuteDecayFactor: 998076443575628800, - redemptionBeta: 1, - urgentRedemptionBonus: _100pct + 1 + redemptionBeta: 1 }); vm.expectRevert(ISystemParams.InvalidFeeValue.selector); @@ -800,13 +574,7 @@ contract SystemParamsTest is DevTestSetup { function _getValidInterestParams() internal pure returns (ISystemParams.InterestParams memory) { return ISystemParams.InterestParams({ - minAnnualInterestRate: _1pct / 2, - maxAnnualInterestRate: 250 * _1pct, - maxAnnualBatchManagementFee: uint128(_100pct / 10), - upfrontInterestPeriod: 7 days, - interestRateAdjCooldown: 7 days, - minInterestRateChangePeriod: 1 hours, - maxBatchSharesRatio: 1e9 + minAnnualInterestRate: _1pct / 2 }); } @@ -815,8 +583,7 @@ contract SystemParamsTest is DevTestSetup { redemptionFeeFloor: _1pct / 2, initialBaseRate: _100pct, redemptionMinuteDecayFactor: 998076443575628800, - redemptionBeta: 1, - urgentRedemptionBonus: 2 * _1pct + redemptionBeta: 1 }); } diff --git a/contracts/test/troveNFT.t.sol b/contracts/test/troveNFT.t.sol index c7a113511..03ef11ee0 100644 --- a/contracts/test/troveNFT.t.sol +++ b/contracts/test/troveNFT.t.sol @@ -110,7 +110,6 @@ contract troveNFTTest is DevTestSetup { } systemParams = contractsArray[0].systemParams; - UPFRONT_INTEREST_PERIOD = systemParams.UPFRONT_INTEREST_PERIOD(); troveIds = new uint256[](NUM_VARIANTS); From de9602406a3ff71f100432d0c023f5cf8fecfd17 Mon Sep 17 00:00:00 2001 From: Mouradif Date: Wed, 8 Oct 2025 18:11:13 +0200 Subject: [PATCH 48/79] feat: immutable system params --- contracts/src/SystemParams.sol | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/contracts/src/SystemParams.sol b/contracts/src/SystemParams.sol index c6721071d..8e69363a9 100644 --- a/contracts/src/SystemParams.sol +++ b/contracts/src/SystemParams.sol @@ -18,41 +18,41 @@ import { contract SystemParams is ISystemParams { /* ========== DEBT PARAMETERS ========== */ - uint256 public MIN_DEBT; + uint256 immutable public MIN_DEBT; /* ========== LIQUIDATION PARAMETERS ========== */ - uint256 public LIQUIDATION_PENALTY_SP; - uint256 public LIQUIDATION_PENALTY_REDISTRIBUTION; + uint256 immutable public LIQUIDATION_PENALTY_SP; + uint256 immutable public LIQUIDATION_PENALTY_REDISTRIBUTION; /* ========== GAS COMPENSATION PARAMETERS ========== */ - uint256 public COLL_GAS_COMPENSATION_DIVISOR; - uint256 public COLL_GAS_COMPENSATION_CAP; - uint256 public ETH_GAS_COMPENSATION; + uint256 immutable public COLL_GAS_COMPENSATION_DIVISOR; + uint256 immutable public COLL_GAS_COMPENSATION_CAP; + uint256 immutable public ETH_GAS_COMPENSATION; /* ========== COLLATERAL PARAMETERS ========== */ - uint256 public CCR; - uint256 public SCR; - uint256 public MCR; - uint256 public BCR; + uint256 immutable public CCR; + uint256 immutable public SCR; + uint256 immutable public MCR; + uint256 immutable public BCR; /* ========== INTEREST PARAMETERS ========== */ - uint256 public MIN_ANNUAL_INTEREST_RATE; + uint256 immutable public MIN_ANNUAL_INTEREST_RATE; /* ========== REDEMPTION PARAMETERS ========== */ - uint256 public REDEMPTION_FEE_FLOOR; - uint256 public INITIAL_BASE_RATE; - uint256 public REDEMPTION_MINUTE_DECAY_FACTOR; - uint256 public REDEMPTION_BETA; + uint256 immutable public REDEMPTION_FEE_FLOOR; + uint256 immutable public INITIAL_BASE_RATE; + uint256 immutable public REDEMPTION_MINUTE_DECAY_FACTOR; + uint256 immutable public REDEMPTION_BETA; /* ========== STABILITY POOL PARAMETERS ========== */ - uint256 public SP_YIELD_SPLIT; - uint256 public MIN_BOLD_IN_SP; + uint256 immutable public SP_YIELD_SPLIT; + uint256 immutable public MIN_BOLD_IN_SP; /* ========== CONSTRUCTOR ========== */ From f2aa92053ae6d0a3d51befd89a822c8e51d759c1 Mon Sep 17 00:00:00 2001 From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:42:52 +0200 Subject: [PATCH 49/79] feat: draft price feed contract --- contracts/script/DeployLiquity2.s.sol | 5 +- contracts/src/AddressesRegistry.sol | 4 ++ contracts/src/BorrowerOperations.sol | 21 ++------ .../src/Interfaces/IAddressesRegistry.sol | 2 + contracts/src/PriceFeeds/MentoPriceFeed.sol | 50 +++++++++++++++++++ contracts/test/TestContracts/Deployment.t.sol | 8 ++- 6 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 contracts/src/PriceFeeds/MentoPriceFeed.sol diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index ab3a9ff01..fae9549ab 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -375,7 +375,10 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { multiTroveGetter: r.multiTroveGetter, collateralRegistry: r.collateralRegistry, boldToken: IBoldToken(address(r.stableToken)), - gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS) + gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS), + // TODO: set liquidity strategy + liquidityStrategy: address(0), + watchdogAddress: address(0) }); contracts.addressesRegistry.setAddresses(addressVars); } diff --git a/contracts/src/AddressesRegistry.sol b/contracts/src/AddressesRegistry.sol index ab6003511..4f3a940ed 100644 --- a/contracts/src/AddressesRegistry.sol +++ b/contracts/src/AddressesRegistry.sol @@ -25,6 +25,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { IBoldToken public boldToken; IERC20Metadata public gasToken; address public liquidityStrategy; + address public watchdogAddress; event CollTokenAddressChanged(address _collTokenAddress); event BorrowerOperationsAddressChanged(address _borrowerOperationsAddress); @@ -45,6 +46,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { event BoldTokenAddressChanged(address _boldTokenAddress); event GasTokenAddressChanged(address _gasTokenAddress); event LiquidityStrategyAddressChanged(address _liquidityStrategyAddress); + event WatchdogAddressChanged(address _watchdogAddress); constructor(address _owner) Ownable(_owner) {} @@ -68,6 +70,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { boldToken = _vars.boldToken; gasToken = _vars.gasToken; liquidityStrategy = _vars.liquidityStrategy; + watchdogAddress = _vars.watchdogAddress; emit CollTokenAddressChanged(address(_vars.collToken)); emit BorrowerOperationsAddressChanged( @@ -92,6 +95,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { emit BoldTokenAddressChanged(address(_vars.boldToken)); emit GasTokenAddressChanged(address(_vars.gasToken)); emit LiquidityStrategyAddressChanged(address(_vars.liquidityStrategy)); + emit WatchdogAddressChanged(address(_vars.watchdogAddress)); _renounceOwnership(); } diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index b8bb6822c..f9554be0d 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -349,7 +349,7 @@ contract BorrowerOperations is vars.activePool = activePool; vars.boldToken = boldToken; - vars.price = _requireOraclesLive(); + (vars.price, ) = priceFeed.fetchPrice(); // --- Checks --- @@ -671,7 +671,7 @@ contract BorrowerOperations is vars.activePool = activePool; vars.boldToken = boldToken; - vars.price = _requireOraclesLive(); + (vars.price,) = priceFeed.fetchPrice(); vars.isBelowCriticalThreshold = _checkBelowCriticalThreshold( vars.price, CCR @@ -1198,7 +1198,7 @@ contract BorrowerOperations is block.timestamp < batch.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN ) { - uint256 price = _requireOraclesLive(); + (uint256 price, ) = priceFeed.fetchPrice(); uint256 avgInterestRate = activePoolCached .getNewApproxAvgInterestRateFromTroveChange(batchChange); @@ -1529,7 +1529,7 @@ contract BorrowerOperations is uint256 _maxUpfrontFee, bool _isTroveInBatch ) internal returns (uint256) { - uint256 price = _requireOraclesLive(); + (uint256 price, ) = priceFeed.fetchPrice(); uint256 avgInterestRate = activePool .getNewApproxAvgInterestRateFromTroveChange(_troveChange); @@ -1594,9 +1594,7 @@ contract BorrowerOperations is uint256 totalColl = getEntireBranchColl(); uint256 totalDebt = getEntireBranchDebt(); - (uint256 price, bool newOracleFailureDetected) = priceFeed.fetchPrice(); - // If the oracle failed, the above call to PriceFeed will have shut this branch down - if (newOracleFailureDetected) return; + (uint256 price,) = priceFeed.fetchPrice(); // Otherwise, proceed with the TCR check: uint256 TCR = LiquityMath._computeCR(totalColl, totalDebt, price); @@ -2061,15 +2059,6 @@ contract BorrowerOperations is } } - function _requireOraclesLive() internal returns (uint256) { - (uint256 price, bool newOracleFailureDetected) = priceFeed.fetchPrice(); - if (newOracleFailureDetected) { - revert NewOracleFailureDetected(); - } - - return price; - } - // --- ICR and TCR getters --- function _getNewTCRFromTroveChange( diff --git a/contracts/src/Interfaces/IAddressesRegistry.sol b/contracts/src/Interfaces/IAddressesRegistry.sol index 971f52f48..bf6705e12 100644 --- a/contracts/src/Interfaces/IAddressesRegistry.sol +++ b/contracts/src/Interfaces/IAddressesRegistry.sol @@ -39,6 +39,7 @@ interface IAddressesRegistry { IBoldToken boldToken; IERC20Metadata gasToken; address liquidityStrategy; + address watchdogAddress; } function collToken() external view returns (IERC20Metadata); @@ -60,6 +61,7 @@ interface IAddressesRegistry { function boldToken() external view returns (IBoldToken); function gasToken() external view returns (IERC20Metadata); function liquidityStrategy() external view returns (address); + function watchdogAddress() external view returns (address); function setAddresses(AddressVars memory _vars) external; } diff --git a/contracts/src/PriceFeeds/MentoPriceFeed.sol b/contracts/src/PriceFeeds/MentoPriceFeed.sol new file mode 100644 index 000000000..f36742bfc --- /dev/null +++ b/contracts/src/PriceFeeds/MentoPriceFeed.sol @@ -0,0 +1,50 @@ +pragma solidity 0.8.24; + +import "../Interfaces/IPriceFeed.sol"; +import "../BorrowerOperations.sol"; + +interface IOracleAdapter { + function getFXRateIfValid(address rateFeedID) external view returns (uint256 numerator, uint256 denominator); +} + +contract MentoPriceFeed is IPriceFeed { + + IOracleAdapter public oracleAdapter; + address public rateFeedID; + address public watchdogAddress; + IBorrowerOperations public borrowerOperations; + + uint256 public lastGoodPrice; + bool public isShutdown; + + constructor(address _oracleAdapterAddress, address _rateFeedID, address _borrowerOperationsAddress, address _watchdogAddress) { + oracleAdapter = IOracleAdapter(_oracleAdapterAddress); + rateFeedID = _rateFeedID; + borrowerOperations = IBorrowerOperations(_borrowerOperationsAddress); + watchdogAddress = _watchdogAddress; + } + + function fetchPrice() public returns (uint256, bool) { + if (isShutdown) { + return (lastGoodPrice, false); + } + + (uint256 numerator, ) = oracleAdapter.getFXRateIfValid(rateFeedID); + + lastGoodPrice = numerator; + + return (numerator, false); + } + + function fetchRedemptionPrice() external returns (uint256, bool) { + return fetchPrice(); + } + + function shutdown() external { + require(!isShutdown, "MentoPriceFeed: already shutdown"); + require(msg.sender == watchdogAddress, "MentoPriceFeed: not authorized"); + + isShutdown = true; + borrowerOperations.shutdownFromOracleFailure(); + } +} diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index d7b5a3a42..a15f77fa6 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -479,7 +479,9 @@ contract TestDeployer is MetadataDeployment { collToken: _collToken, gasToken: _gasToken, // TODO: add liquidity strategy - liquidityStrategy: makeAddr("liquidityStrategy") + liquidityStrategy: makeAddr("liquidityStrategy"), + // TODO: add watchdog address + watchdogAddress: makeAddr("watchdog") }); contracts.addressesRegistry.setAddresses(addressVars); @@ -680,7 +682,9 @@ contract TestDeployer is MetadataDeployment { collToken: _params.collToken, gasToken: _params.gasToken, // TODO: add liquidity strategy - liquidityStrategy: makeAddr("liquidityStrategy") + liquidityStrategy: makeAddr("liquidityStrategy"), + // TODO: add watchdog address + watchdogAddress: makeAddr("watchdog") }); contracts.addressesRegistry.setAddresses(addressVars); From d7bbffd28aafa88e330696fb9fc8c3b83cab063b Mon Sep 17 00:00:00 2001 From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Date: Wed, 8 Oct 2025 21:04:47 +0200 Subject: [PATCH 50/79] refactor: rename price feed contract --- .../src/PriceFeeds/{MentoPriceFeed.sol => FXPriceFeed.sol} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename contracts/src/PriceFeeds/{MentoPriceFeed.sol => FXPriceFeed.sol} (97%) diff --git a/contracts/src/PriceFeeds/MentoPriceFeed.sol b/contracts/src/PriceFeeds/FXPriceFeed.sol similarity index 97% rename from contracts/src/PriceFeeds/MentoPriceFeed.sol rename to contracts/src/PriceFeeds/FXPriceFeed.sol index f36742bfc..4824c95a1 100644 --- a/contracts/src/PriceFeeds/MentoPriceFeed.sol +++ b/contracts/src/PriceFeeds/FXPriceFeed.sol @@ -7,7 +7,7 @@ interface IOracleAdapter { function getFXRateIfValid(address rateFeedID) external view returns (uint256 numerator, uint256 denominator); } -contract MentoPriceFeed is IPriceFeed { +contract FXPriceFeed is IPriceFeed { IOracleAdapter public oracleAdapter; address public rateFeedID; From 7fc695e8e443a962cc2c096651dca468791b193c Mon Sep 17 00:00:00 2001 From: Mouradif Date: Tue, 14 Oct 2025 16:37:05 +0200 Subject: [PATCH 51/79] feat: test for stability scaling --- contracts/foundry.toml | 4 +-- contracts/test/stabilityScaling.t.sol | 48 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 contracts/test/stabilityScaling.t.sol diff --git a/contracts/foundry.toml b/contracts/foundry.toml index d65024722..376bdfce0 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -4,7 +4,7 @@ out = "out" libs = ["lib"] evm_version = 'cancun' optimizer = true -optimizer_runs = 0 +optimizer_runs = 1 ignored_error_codes = [3860, 5574] # contract-size fs_permissions = [ { access = "read", path = "./utils/assets/" }, @@ -45,4 +45,4 @@ no_storage_caching = true [lint] # Excludes info/notes from 'forge build' and 'forge lint' output per default as it's quite noisy -severity = ["high", "med", "low"] \ No newline at end of file +severity = ["high", "med", "low"] diff --git a/contracts/test/stabilityScaling.t.sol b/contracts/test/stabilityScaling.t.sol new file mode 100644 index 000000000..56a2d4a17 --- /dev/null +++ b/contracts/test/stabilityScaling.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./TestContracts/DevTestSetup.sol"; +import {mulDivCeil} from "./Utils/Math.sol"; + +contract StabilityScalingTest is DevTestSetup { + address liquidityStrategy; + + function setUp() override public { + super.setUp(); + liquidityStrategy = addressesRegistry.liquidityStrategy(); + deal(address(collToken), liquidityStrategy, 1e60); + vm.prank(liquidityStrategy); + collToken.approve(address(stabilityPool), type(uint256).max); + } + + function _setupStabilityPool(uint256 magnitude) internal { + uint256 amount = 1e18 * 10 ** magnitude; + deal(address(boldToken), A, amount); + deal(address(boldToken), B, amount); + deal(address(boldToken), C, amount); + makeSPDepositAndClaim(A, amount); + makeSPDepositAndClaim(B, amount); + makeSPDepositAndClaim(C, amount); + } + + function _topUpStabilityPoolBold(uint256 amount) internal { + deal(address(boldToken), D, amount); + makeSPDepositAndClaim(D, amount); + } + + function testStabilityScaling_canScaleALot() public { + _setupStabilityPool(13); + + for (uint256 i = 0; i < 5000; ++i) { + uint256 spBalance = boldToken.balanceOf(address(stabilityPool)); + uint256 boldOut = spBalance / 2; + uint256 collIn = boldOut / 2000; + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collIn, boldOut); + _topUpStabilityPoolBold(boldOut); + } + + assertEq(stabilityPool.currentScale(), 167); + } +} From 9133e29c1dfc503c656d8c861f6ea5ad2d34905b Mon Sep 17 00:00:00 2001 From: Bayological <6872903+bayological@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:39:02 +0100 Subject: [PATCH 52/79] feat: upgradeable system params (#10) --- contracts/script/DeployLiquity2.s.sol | 42 +++++++++----- contracts/src/ActivePool.sol | 12 ++-- contracts/src/BorrowerOperations.sol | 57 +++++++------------ contracts/src/CollateralRegistry.sol | 22 +++---- contracts/src/Interfaces/IStabilityPool.sol | 2 +- contracts/src/Interfaces/ISystemParams.sol | 1 + contracts/src/StabilityPool.sol | 18 +++--- contracts/src/SystemParams.sol | 15 ++++- contracts/src/TroveManager.sol | 53 ++++------------- .../BorrowerOperationsTester.t.sol | 2 +- contracts/test/TestContracts/Deployment.t.sol | 19 +++++-- .../TestContracts/TroveManagerTester.t.sol | 22 +++---- contracts/test/systemParams.t.sol | 52 ++++++++--------- 13 files changed, 145 insertions(+), 172 deletions(-) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index fae9549ab..dccccddc5 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -116,6 +116,7 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { ISystemParams systemParams; address stabilityPoolImpl; address stableTokenV3Impl; + address systemParamsImpl; address fpmm; } @@ -205,7 +206,9 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { function _deployProxyInfrastructure(DeploymentResult memory r) internal { r.proxyAdmin = ProxyAdmin(CONFIG.proxyAdmin); r.stableTokenV3Impl = address(new StableTokenV3{salt: SALT}(true)); - r.stabilityPoolImpl = address(new StabilityPool{salt: SALT}(true)); + r.stabilityPoolImpl = address(new StabilityPool{salt: SALT}(true, r.systemParams)); + + _deploySystemParamsImpl(r); assert( address(r.stableTokenV3Impl) @@ -216,7 +219,7 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { assert( address(r.stabilityPoolImpl) == vm.computeCreate2Address( - SALT, keccak256(bytes.concat(type(StabilityPool).creationCode, abi.encode(true))) + SALT, keccak256(bytes.concat(type(StabilityPool).creationCode, abi.encode(true, r.systemParams))) ) ); } @@ -233,7 +236,7 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { ); } - function _deploySystemParams(DeploymentResult memory r) internal { + function _deploySystemParamsImpl(DeploymentResult memory r) internal { ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({minDebt: 2000e18}); ISystemParams.LiquidationParams memory liquidationParams = @@ -262,21 +265,29 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({spYieldSplit: 75 * (1e18 / 100), minBoldInSP: 1e18}); - r.systemParams = ISystemParams( - address( - new SystemParams{salt: SALT}( - debtParams, - liquidationParams, - gasCompParams, - collateralParams, - interestParams, - redemptionParams, - poolParams - ) + r.systemParamsImpl = address( + new SystemParams{salt: SALT}( + true, // disableInitializers for implementation + debtParams, + liquidationParams, + gasCompParams, + collateralParams, + interestParams, + redemptionParams, + poolParams ) ); } + function _deploySystemParams(DeploymentResult memory r) internal { + address systemParamsProxy = address( + new TransparentUpgradeableProxy(address(r.systemParamsImpl), address(r.proxyAdmin), "") + ); + + r.systemParams = ISystemParams(systemParamsProxy); + r.systemParams.initialize(); + } + function _deployAndConnectCollateralContracts( IERC20Metadata _collToken, IPriceFeed _priceFeed, @@ -326,7 +337,7 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { // Deploy core protocol contracts _deployProtocolContracts(contracts, addresses); - IStabilityPool(stabilityPool).initialize(contracts.addressesRegistry, contracts.systemParams); + IStabilityPool(stabilityPool).initialize(contracts.addressesRegistry); address[] memory minters = new address[](2); minters[0] = address(contracts.borrowerOperations); @@ -484,6 +495,7 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { string memory part2 = string.concat( string.concat('"stableTokenV3Impl":"', address(deployed.stableTokenV3Impl).toHexString(), '",'), string.concat('"stabilityPoolImpl":"', address(deployed.stabilityPoolImpl).toHexString(), '",'), + string.concat('"systemParamsImpl":"', address(deployed.systemParamsImpl).toHexString(), '",'), string.concat('"systemParams":"', address(deployed.systemParams).toHexString(), '",'), string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",') ); diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index bc5860e6e..d6b0baa25 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -29,15 +29,13 @@ contract ActivePool is IActivePool { address public immutable borrowerOperationsAddress; address public immutable troveManagerAddress; address public immutable defaultPoolAddress; - address public immutable systemParamsAddress; + ISystemParams public immutable systemParams; IBoldToken public immutable boldToken; IInterestRouter public immutable interestRouter; IBoldRewardsReceiver public immutable stabilityPool; - uint256 public immutable SP_YIELD_SPLIT; - uint256 internal collBalance; // deposited coll tracker // Aggregate recorded debt tracker. Updated whenever a Trove's debt is touched AND whenever the aggregate pending interest is minted. @@ -76,7 +74,7 @@ contract ActivePool is IActivePool { event ActivePoolCollBalanceUpdated(uint256 _collBalance); constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) { - systemParamsAddress = address(_systemParams); + systemParams = _systemParams; collToken = _addressesRegistry.collToken(); borrowerOperationsAddress = address(_addressesRegistry.borrowerOperations()); troveManagerAddress = address(_addressesRegistry.troveManager()); @@ -85,8 +83,6 @@ contract ActivePool is IActivePool { interestRouter = _addressesRegistry.interestRouter(); boldToken = _addressesRegistry.boldToken(); - SP_YIELD_SPLIT = _systemParams.SP_YIELD_SPLIT(); - emit CollTokenAddressChanged(address(collToken)); emit BorrowerOperationsAddressChanged(borrowerOperationsAddress); emit TroveManagerAddressChanged(troveManagerAddress); @@ -120,7 +116,7 @@ contract ActivePool is IActivePool { } function calcPendingSPYield() external view returns (uint256) { - return calcPendingAggInterest() * SP_YIELD_SPLIT / DECIMAL_PRECISION; + return calcPendingAggInterest() * systemParams.SP_YIELD_SPLIT() / DECIMAL_PRECISION; } function calcPendingAggBatchManagementFee() public view returns (uint256) { @@ -257,7 +253,7 @@ contract ActivePool is IActivePool { // Mint part of the BOLD interest to the SP and part to the router for LPs. if (mintedAmount > 0) { - uint256 spYield = SP_YIELD_SPLIT * mintedAmount / DECIMAL_PRECISION; + uint256 spYield = systemParams.SP_YIELD_SPLIT() * mintedAmount / DECIMAL_PRECISION; uint256 remainderToLPs = mintedAmount - spYield; boldToken.mint(address(interestRouter), remainderToLPs); diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index f9554be0d..e70d0d3c4 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -40,23 +40,10 @@ contract BorrowerOperations is ISortedTroves internal sortedTroves; // Wrapped ETH for liquidation reserve (gas compensation) IERC20Metadata internal immutable gasToken; - ISystemParams internal immutable systemParams; - - // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied - uint256 public immutable CCR; + ISystemParams public immutable systemParams; bool public hasBeenShutDown; - // Minimum collateral ratio for individual troves - uint256 public immutable MCR; - - // Extra buffer of collateral ratio to join a batch or adjust a trove inside a batch (on top of MCR) - uint256 public immutable BCR; - - uint256 public immutable ETH_GAS_COMPENSATION; - uint256 public immutable MIN_DEBT; - uint256 public immutable MIN_ANNUAL_INTEREST_RATE; - /* * Mapping from TroveId to individual delegate for interest rate setting. * @@ -191,14 +178,6 @@ contract BorrowerOperations is gasToken = _addressesRegistry.gasToken(); - CCR = _systemParams.CCR(); - MCR = _systemParams.MCR(); - BCR = _systemParams.BCR(); - - ETH_GAS_COMPENSATION = _systemParams.ETH_GAS_COMPENSATION(); - MIN_DEBT = _systemParams.MIN_DEBT(); - MIN_ANNUAL_INTEREST_RATE = _systemParams.MIN_ANNUAL_INTEREST_RATE(); - troveManager = _addressesRegistry.troveManager(); gasPoolAddress = _addressesRegistry.gasPoolAddress(); collSurplusPool = _addressesRegistry.collSurplusPool(); @@ -215,6 +194,14 @@ contract BorrowerOperations is collToken.approve(address(activePool), type(uint256).max); } + function CCR() external view override returns (uint256) { + return systemParams.CCR(); + } + + function MCR() external view override returns (uint256) { + return systemParams.MCR(); + } + // --- Borrower Trove Operations --- function openTrove( @@ -425,7 +412,7 @@ contract BorrowerOperations is // Mint the requested _boldAmount to the borrower and mint the gas comp to the GasPool vars.boldToken.mint(msg.sender, _boldAmount); - gasToken.transferFrom(msg.sender, gasPoolAddress, ETH_GAS_COMPENSATION); + gasToken.transferFrom(msg.sender, gasPoolAddress, systemParams.ETH_GAS_COMPENSATION()); return vars.troveId; } @@ -674,7 +661,7 @@ contract BorrowerOperations is (vars.price,) = priceFeed.fetchPrice(); vars.isBelowCriticalThreshold = _checkBelowCriticalThreshold( vars.price, - CCR + systemParams.CCR() ); // --- Checks --- @@ -702,8 +689,8 @@ contract BorrowerOperations is // When the adjustment is a debt repayment, check it's a valid amount and that the caller has enough Bold if (_troveChange.debtDecrease > 0) { - uint256 maxRepayment = vars.trove.entireDebt > MIN_DEBT - ? vars.trove.entireDebt - MIN_DEBT + uint256 maxRepayment = vars.trove.entireDebt > systemParams.MIN_DEBT() + ? vars.trove.entireDebt - systemParams.MIN_DEBT() : 0; if (_troveChange.debtDecrease > maxRepayment) { _troveChange.debtDecrease = maxRepayment; @@ -932,7 +919,7 @@ contract BorrowerOperations is ); // Return ETH gas compensation - gasToken.transferFrom(gasPoolAddress, receiver, ETH_GAS_COMPENSATION); + gasToken.transferFrom(gasPoolAddress, receiver, systemParams.ETH_GAS_COMPENSATION()); // Burn the remainder of the Trove's entire debt from the user boldTokenCached.burn(msg.sender, trove.entireDebt); @@ -1000,10 +987,10 @@ contract BorrowerOperations is batchManager ); - // If the trove was zombie, and now it’s not anymore, put it back in the list + // If the trove was zombie, and now it's not anymore, put it back in the list if ( _checkTroveIsZombie(troveManagerCached, _troveId) && - trove.entireDebt >= MIN_DEBT + trove.entireDebt >= systemParams.MIN_DEBT() ) { troveManagerCached.setTroveStatusToActive(_troveId); _reInsertIntoSortedTroves( @@ -1869,13 +1856,13 @@ contract BorrowerOperations is } function _requireICRisAboveMCR(uint256 _newICR) internal view { - if (_newICR < MCR) { + if (_newICR < systemParams.MCR()) { revert ICRBelowMCR(); } } function _requireICRisAboveMCRPlusBCR(uint256 _newICR) internal view { - if (_newICR < MCR + BCR) { + if (_newICR < systemParams.MCR() + systemParams.BCR()) { revert ICRBelowMCRPlusBCR(); } } @@ -1884,7 +1871,7 @@ contract BorrowerOperations is uint256 _debtIncrease, uint256 _newTCR ) internal view { - if (_debtIncrease > 0 && _newTCR < CCR) { + if (_debtIncrease > 0 && _newTCR < systemParams.CCR()) { revert TCRBelowCCR(); } } @@ -1902,13 +1889,13 @@ contract BorrowerOperations is } function _requireNewTCRisAboveCCR(uint256 _newTCR) internal view { - if (_newTCR < CCR) { + if (_newTCR < systemParams.CCR()) { revert TCRBelowCCR(); } } function _requireAtLeastMinDebt(uint256 _debt) internal view { - if (_debt < MIN_DEBT) { + if (_debt < systemParams.MIN_DEBT()) { revert DebtBelowMin(); } } @@ -1935,7 +1922,7 @@ contract BorrowerOperations is function _requireValidAnnualInterestRate( uint256 _annualInterestRate ) internal view { - if (_annualInterestRate < MIN_ANNUAL_INTEREST_RATE) { + if (_annualInterestRate < systemParams.MIN_ANNUAL_INTEREST_RATE()) { revert InterestRateTooLow(); } if (_annualInterestRate > MAX_ANNUAL_INTEREST_RATE) { diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index 1efca9375..b62a3afa9 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -15,7 +15,6 @@ import "./Interfaces/ICollateralRegistry.sol"; contract CollateralRegistry is ICollateralRegistry { // See: https://github.com/ethereum/solidity/issues/12587 uint256 public immutable totalCollaterals; - address public immutable systemParamsAddress; IERC20Metadata internal immutable token0; IERC20Metadata internal immutable token1; @@ -39,12 +38,9 @@ contract CollateralRegistry is ICollateralRegistry { ITroveManager internal immutable troveManager8; ITroveManager internal immutable troveManager9; + ISystemParams public immutable systemParams; IBoldToken public immutable boldToken; - uint256 public immutable REDEMPTION_BETA; - uint256 public immutable REDEMPTION_MINUTE_DECAY_FACTOR; - uint256 public immutable REDEMPTION_FEE_FLOOR; - uint256 public baseRate; // The timestamp of the latest fee operation (redemption or new Bold issuance) @@ -58,7 +54,7 @@ contract CollateralRegistry is ICollateralRegistry { require(numTokens > 0, "Collateral list cannot be empty"); require(numTokens <= 10, "Collateral list too long"); totalCollaterals = numTokens; - systemParamsAddress = address(_systemParams); + systemParams = _systemParams; boldToken = _boldToken; @@ -84,13 +80,9 @@ contract CollateralRegistry is ICollateralRegistry { troveManager8 = numTokens > 8 ? _troveManagers[8] : ITroveManager(address(0)); troveManager9 = numTokens > 9 ? _troveManagers[9] : ITroveManager(address(0)); - REDEMPTION_BETA = _systemParams.REDEMPTION_BETA(); - REDEMPTION_MINUTE_DECAY_FACTOR = _systemParams.REDEMPTION_MINUTE_DECAY_FACTOR(); - REDEMPTION_FEE_FLOOR = _systemParams.REDEMPTION_FEE_FLOOR(); - // Initialize the baseRate state variable baseRate = _systemParams.INITIAL_BASE_RATE(); - emit BaseRateUpdated(_systemParams.INITIAL_BASE_RATE()); + emit BaseRateUpdated(baseRate); } struct RedemptionTotals { @@ -231,7 +223,7 @@ contract CollateralRegistry is ICollateralRegistry { // get the fraction of total supply that was redeemed uint256 redeemedBoldFraction = _redeemAmount * DECIMAL_PRECISION / _totalBoldSupply; - uint256 newBaseRate = decayedBaseRate + redeemedBoldFraction / REDEMPTION_BETA; + uint256 newBaseRate = decayedBaseRate + redeemedBoldFraction / systemParams.REDEMPTION_BETA(); newBaseRate = LiquityMath._min(newBaseRate, DECIMAL_PRECISION); // cap baseRate at a maximum of 100% return newBaseRate; @@ -239,14 +231,14 @@ contract CollateralRegistry is ICollateralRegistry { function _calcDecayedBaseRate() internal view returns (uint256) { uint256 minutesPassed = _minutesPassedSinceLastFeeOp(); - uint256 decayFactor = LiquityMath._decPow(REDEMPTION_MINUTE_DECAY_FACTOR, minutesPassed); + uint256 decayFactor = LiquityMath._decPow(systemParams.REDEMPTION_MINUTE_DECAY_FACTOR(), minutesPassed); return baseRate * decayFactor / DECIMAL_PRECISION; } function _calcRedemptionRate(uint256 _baseRate) internal view returns (uint256) { return LiquityMath._min( - REDEMPTION_FEE_FLOOR + _baseRate, + systemParams.REDEMPTION_FEE_FLOOR() + _baseRate, DECIMAL_PRECISION // cap at a maximum of 100% ); } @@ -316,7 +308,7 @@ contract CollateralRegistry is ICollateralRegistry { function _requireValidMaxFeePercentage(uint256 _maxFeePercentage) internal view { require( - _maxFeePercentage >= REDEMPTION_FEE_FLOOR && _maxFeePercentage <= DECIMAL_PRECISION, + _maxFeePercentage >= systemParams.REDEMPTION_FEE_FLOOR() && _maxFeePercentage <= DECIMAL_PRECISION, "Max fee percentage must be between 0.5% and 100%" ); } diff --git a/contracts/src/Interfaces/IStabilityPool.sol b/contracts/src/Interfaces/IStabilityPool.sol index f695a59f7..8723c7188 100644 --- a/contracts/src/Interfaces/IStabilityPool.sol +++ b/contracts/src/Interfaces/IStabilityPool.sol @@ -31,7 +31,7 @@ import "./ISystemParams.sol"; * */ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { - function initialize(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) external; + function initialize(IAddressesRegistry _addressesRegistry) external; function boldToken() external view returns (IBoldToken); function troveManager() external view returns (ITroveManager); diff --git a/contracts/src/Interfaces/ISystemParams.sol b/contracts/src/Interfaces/ISystemParams.sol index 949008beb..cfa7173e6 100644 --- a/contracts/src/Interfaces/ISystemParams.sol +++ b/contracts/src/Interfaces/ISystemParams.sol @@ -141,4 +141,5 @@ interface ISystemParams { /// @notice Minimum BOLD that must remain in Stability Pool to prevent complete drainage. function MIN_BOLD_IN_SP() external view returns (uint256); + function initialize() external; } diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 6ea19b52a..563ee89d0 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -180,10 +180,10 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi // Each time the scale of P shifts by SCALE_FACTOR, the scale is incremented by 1 uint256 public currentScale; - uint256 public MIN_BOLD_IN_SP; - address public liquidityStrategy; + ISystemParams public immutable systemParams; + /* Coll Gain sum 'S': During its lifetime, each deposit d_t earns an Coll gain of ( d_t * [S - S_t] )/P_t, where S_t * is the depositor's snapshot of S taken at the time t when the deposit was made. * @@ -203,13 +203,15 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi * Call this with disable=false during testing, when used without a proxy. */ /// @custom:oz-upgrades-unsafe-allow constructor - constructor(bool disable) { + constructor(bool disable, ISystemParams _systemParams) { if (disable) { _disableInitializers(); } + + systemParams = _systemParams; } - function initialize(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) external initializer { + function initialize(IAddressesRegistry _addressesRegistry) external initializer { __LiquityBase_init(_addressesRegistry); collToken = _addressesRegistry.collToken(); @@ -217,8 +219,6 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi boldToken = _addressesRegistry.boldToken(); liquidityStrategy = _addressesRegistry.liquidityStrategy(); - MIN_BOLD_IN_SP = _systemParams.MIN_BOLD_IN_SP(); - emit TroveManagerAddressChanged(address(troveManager)); emit BoldTokenAddressChanged(address(boldToken)); } @@ -340,7 +340,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi _sendBoldtoDepositor(msg.sender, boldToWithdraw + yieldGainToSend); _sendCollGainToDepositor(collToSend); - require(newTotalBoldDeposits >= MIN_BOLD_IN_SP, "Withdrawal must leave totalBoldDeposits >= MIN_BOLD_IN_SP"); + require(newTotalBoldDeposits >= systemParams.MIN_BOLD_IN_SP(), "Withdrawal must leave totalBoldDeposits >= MIN_BOLD_IN_SP"); } function _getNewStashedCollAndCollToSend(address _depositor, uint256 _currentCollGain, bool _doClaim) @@ -386,7 +386,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi // When total deposits is very small, B is not updated. In this case, the BOLD issued is held // until the total deposits reach 1 BOLD (remains in the balance of the SP). - if (totalBoldDeposits < MIN_BOLD_IN_SP) { + if (totalBoldDeposits < systemParams.MIN_BOLD_IN_SP()) { yieldGainsPending = accumulatedYieldGains; return; } @@ -543,7 +543,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi } function getDepositorYieldGainWithPending(address _depositor) external view override returns (uint256) { - if (totalBoldDeposits < MIN_BOLD_IN_SP) return 0; + if (totalBoldDeposits < systemParams.MIN_BOLD_IN_SP()) return 0; uint256 initialDeposit = deposits[_depositor].initialValue; if (initialDeposit == 0) return 0; diff --git a/contracts/src/SystemParams.sol b/contracts/src/SystemParams.sol index 8e69363a9..6602a964e 100644 --- a/contracts/src/SystemParams.sol +++ b/contracts/src/SystemParams.sol @@ -9,13 +9,14 @@ import { MAX_LIQUIDATION_PENALTY_REDISTRIBUTION, MAX_ANNUAL_INTEREST_RATE } from "./Dependencies/Constants.sol"; +import "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; /** * @title System Parameters * @author Mento Labs * @notice This contract manages the system-wide parameters for the protocol. */ -contract SystemParams is ISystemParams { +contract SystemParams is ISystemParams, Initializable { /* ========== DEBT PARAMETERS ========== */ uint256 immutable public MIN_DEBT; @@ -57,6 +58,7 @@ contract SystemParams is ISystemParams { /* ========== CONSTRUCTOR ========== */ constructor( + bool disableInitializers, DebtParams memory _debtParams, LiquidationParams memory _liquidationParams, GasCompParams memory _gasCompParams, @@ -65,6 +67,10 @@ contract SystemParams is ISystemParams { RedemptionParams memory _redemptionParams, StabilityPoolParams memory _poolParams ) { + if (disableInitializers) { + _disableInitializers(); + } + // Validate debt parameters if (_debtParams.minDebt == 0 || _debtParams.minDebt > 10000e18) revert InvalidMinDebt(); @@ -136,4 +142,11 @@ contract SystemParams is ISystemParams { SP_YIELD_SPLIT = _poolParams.spYieldSplit; MIN_BOLD_IN_SP = _poolParams.minBoldInSP; } + + /* + * Initializes proxy storage + * All parameters are immutable from constructor. This function + * only marks initialization complete for proxy pattern. + */ + function initialize() external initializer {} } diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index b75674867..c8628bfad 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -30,27 +30,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { ICollateralRegistry internal collateralRegistry; // Gas token for liquidation reserve (gas compensation) IERC20Metadata internal immutable gasToken; - address public immutable systemParamsAddress; - - // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied - uint256 public immutable CCR; - - // Minimum collateral ratio for individual troves - uint256 internal immutable MCR; - // Shutdown system collateral ratio. If the system's total collateral ratio (TCR) for a given collateral falls below the SCR, - // the protocol triggers the shutdown of the borrow market and permanently disables all borrowing operations except for closing Troves. - uint256 internal immutable SCR; - - // Liquidation penalty for troves offset to the SP - uint256 internal immutable LIQUIDATION_PENALTY_SP; - // Liquidation penalty for troves redistributed - uint256 internal immutable LIQUIDATION_PENALTY_REDISTRIBUTION; - - uint256 internal immutable COLL_GAS_COMPENSATION_DIVISOR; - uint256 internal immutable COLL_GAS_COMPENSATION_CAP; - uint256 internal immutable MIN_BOLD_IN_SP; - uint256 internal immutable ETH_GAS_COMPENSATION; - uint256 internal immutable MIN_DEBT; + ISystemParams immutable systemParams; // --- Data structures --- @@ -196,18 +176,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { event CollateralRegistryAddressChanged(address _collateralRegistryAddress); constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) LiquityBase(_addressesRegistry) { - systemParamsAddress = address(_systemParams); - - CCR = _systemParams.CCR(); - MCR = _systemParams.MCR(); - SCR = _systemParams.SCR(); - LIQUIDATION_PENALTY_SP = _systemParams.LIQUIDATION_PENALTY_SP(); - LIQUIDATION_PENALTY_REDISTRIBUTION = _systemParams.LIQUIDATION_PENALTY_REDISTRIBUTION(); - COLL_GAS_COMPENSATION_DIVISOR = _systemParams.COLL_GAS_COMPENSATION_DIVISOR(); - COLL_GAS_COMPENSATION_CAP = _systemParams.COLL_GAS_COMPENSATION_CAP(); - MIN_BOLD_IN_SP = _systemParams.MIN_BOLD_IN_SP(); - ETH_GAS_COMPENSATION = _systemParams.ETH_GAS_COMPENSATION(); - MIN_DEBT = _systemParams.MIN_DEBT(); + systemParams = _systemParams; troveNFT = _addressesRegistry.troveNFT(); borrowerOperations = _addressesRegistry.borrowerOperations(); @@ -350,7 +319,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // Return the amount of Coll to be drawn from a trove's collateral and sent as gas compensation. function _getCollGasCompensation(uint256 _coll) internal view returns (uint256) { // _entireDebt should never be zero, but we add the condition defensively to avoid an unexpected revert - return LiquityMath._min(_coll / COLL_GAS_COMPENSATION_DIVISOR, COLL_GAS_COMPENSATION_CAP); + return LiquityMath._min(_coll / systemParams.COLL_GAS_COMPENSATION_DIVISOR(), systemParams.COLL_GAS_COMPENSATION_CAP()); } /* In a full liquidation, returns the values for a trove's coll and debt to be offset, and coll and debt to be @@ -392,7 +361,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 collToOffset = collSPPortion - collGasCompensation; (collToSendToSP, collSurplus) = - _getCollPenaltyAndSurplus(collToOffset, debtToOffset, LIQUIDATION_PENALTY_SP, _price); + _getCollPenaltyAndSurplus(collToOffset, debtToOffset, systemParams.LIQUIDATION_PENALTY_SP(), _price); } // Redistribution @@ -403,7 +372,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { (collToRedistribute, collSurplus) = _getCollPenaltyAndSurplus( collRedistributionPortion + collSurplus, // Coll surplus from offset can be eaten up by red. penalty debtToRedistribute, - LIQUIDATION_PENALTY_REDISTRIBUTION, // _penaltyRatio + systemParams.LIQUIDATION_PENALTY_REDISTRIBUTION(), // _penaltyRatio _price ); } @@ -447,7 +416,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // - If the SP has total deposits >= 1e18, we leave 1e18 in it untouched. // - If it has 0 < x < 1e18 total deposits, we leave x in it. uint256 totalBoldDeposits = stabilityPoolCached.getTotalBoldDeposits(); - uint256 boldToLeaveInSP = LiquityMath._min(MIN_BOLD_IN_SP, totalBoldDeposits); + uint256 boldToLeaveInSP = LiquityMath._min(systemParams.MIN_BOLD_IN_SP(), totalBoldDeposits); uint256 boldInSPForOffsets = totalBoldDeposits - boldToLeaveInSP; // Perform the appropriate liquidation sequence - tally values and obtain their totals. @@ -513,7 +482,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 ICR = getCurrentICR(troveId, _price); - if (ICR < MCR) { + if (ICR < systemParams.MCR()) { LiquidationValues memory singleLiquidation; LatestTroveData memory trove; @@ -537,7 +506,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { ) internal view { // Tally all the values with their respective running totals totals.collGasCompensation += _singleLiquidation.collGasCompensation; - totals.ETHGasCompensation += ETH_GAS_COMPENSATION; + totals.ETHGasCompensation += systemParams.ETH_GAS_COMPENSATION(); troveChange.debtDecrease += _trove.entireDebt; troveChange.collDecrease += _trove.entireColl; troveChange.appliedRedistBoldDebtGain += _trove.redistBoldDebtGain; @@ -708,7 +677,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 newDebt = _applySingleRedemption(_defaultPool, _singleRedemption, isTroveInBatch); // Make Trove zombie if it's tiny (and it wasn’t already), in order to prevent griefing future (normal, sequential) redemptions - if (newDebt < MIN_DEBT) { + if (newDebt < systemParams.MIN_DEBT()) { if (!_singleRedemption.isZombieTrove) { Troves[_singleRedemption.troveId].status = Status.zombie; if (isTroveInBatch) { @@ -726,7 +695,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } } // Note: technically, it could happen that the Trove pointed to by `lastZombieTroveId` ends up with - // newDebt >= MIN_DEBT thanks to BOLD debt redistribution, which means it _could_ be made active again, + // newDebt >= systemParams.MIN_DEBT() thanks to BOLD debt redistribution, which means it _could_ be made active again, // however we don't do that here, as it would require hints for re-insertion into `SortedTroves`. } @@ -1238,7 +1207,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { (uint256 price,) = priceFeed.fetchPrice(); // It's redeemable if the TCR is above the shutdown threshold, and branch has not been shut down. // Use the normal price for the TCR check. - bool redeemable = _getTCR(price) >= SCR && shutdownTime == 0; + bool redeemable = _getTCR(price) >= systemParams.SCR() && shutdownTime == 0; return (unbackedPortion, price, redeemable); } diff --git a/contracts/test/TestContracts/BorrowerOperationsTester.t.sol b/contracts/test/TestContracts/BorrowerOperationsTester.t.sol index 2886879fe..ba19c73f2 100644 --- a/contracts/test/TestContracts/BorrowerOperationsTester.t.sol +++ b/contracts/test/TestContracts/BorrowerOperationsTester.t.sol @@ -13,7 +13,7 @@ contract BorrowerOperationsTester is BorrowerOperations, IBorrowerOperationsTest constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) BorrowerOperations(_addressesRegistry, _systemParams) {} function get_CCR() external view returns (uint256) { - return CCR; + return systemParams.CCR(); } function getCollToken() external view returns (IERC20) { diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index a15f77fa6..c23376eb5 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -177,6 +177,10 @@ contract TestDeployer is MetadataDeployment { return abi.encodePacked(_creationCode, abi.encode(_disable)); } + function getBytecode(bytes memory _creationCode, bool _disable, address _systemParams) public pure returns (bytes memory) { + return abi.encodePacked(_creationCode, abi.encode(_disable, _systemParams)); + } + function getAddress(address _deployer, bytes memory _bytecode, bytes32 _salt) public pure returns (address) { bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), _deployer, _salt, keccak256(_bytecode))); @@ -390,6 +394,7 @@ contract TestDeployer is MetadataDeployment { }); SystemParams systemParams = new SystemParams{salt: uniqueSalt}( + false, debtParams, liquidationParams, gasCompParams, @@ -399,6 +404,8 @@ contract TestDeployer is MetadataDeployment { poolParams ); + systemParams.initialize(); + return ISystemParams(systemParams); } @@ -441,7 +448,7 @@ contract TestDeployer is MetadataDeployment { ); bytes32 stabilityPoolSalt = keccak256(abi.encodePacked(address(contracts.addressesRegistry))); addresses.stabilityPool = - getAddress(address(this), getBytecode(type(StabilityPool).creationCode, bool(false)), stabilityPoolSalt); + getAddress(address(this), getBytecode(type(StabilityPool).creationCode, bool(false), address(_systemParams)), stabilityPoolSalt); addresses.activePool = getAddress( address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry), address(_systemParams)), SALT ); @@ -488,7 +495,7 @@ contract TestDeployer is MetadataDeployment { contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.troveManager = new TroveManagerTester{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); - contracts.stabilityPool = new StabilityPool{salt: stabilityPoolSalt}(false); + contracts.stabilityPool = new StabilityPool{salt: stabilityPoolSalt}(false, _systemParams); contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.pools.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); contracts.pools.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); @@ -505,7 +512,7 @@ contract TestDeployer is MetadataDeployment { assert(address(contracts.pools.collSurplusPool) == addresses.collSurplusPool); assert(address(contracts.sortedTroves) == addresses.sortedTroves); - contracts.stabilityPool.initialize(contracts.addressesRegistry, _systemParams); + contracts.stabilityPool.initialize(contracts.addressesRegistry); // Connect contracts _boldToken.setBranchAddresses( @@ -641,7 +648,7 @@ contract TestDeployer is MetadataDeployment { address(this), getBytecode(type(TroveNFT).creationCode, address(contracts.addressesRegistry)), SALT ); addresses.stabilityPool = - getAddress(address(this), getBytecode(type(StabilityPool).creationCode, false), stabilityPoolSalt); + getAddress(address(this), getBytecode(type(StabilityPool).creationCode, false, address(_systemParams)), stabilityPoolSalt); addresses.activePool = getAddress( address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry), address(_systemParams)), SALT ); @@ -691,7 +698,7 @@ contract TestDeployer is MetadataDeployment { contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); - contracts.stabilityPool = new StabilityPool{salt: stabilityPoolSalt}(false); + contracts.stabilityPool = new StabilityPool{salt: stabilityPoolSalt}(false, _systemParams); contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); @@ -708,7 +715,7 @@ contract TestDeployer is MetadataDeployment { assert(address(contracts.collSurplusPool) == addresses.collSurplusPool); assert(address(contracts.sortedTroves) == addresses.sortedTroves); - contracts.stabilityPool.initialize(contracts.addressesRegistry, _systemParams); + contracts.stabilityPool.initialize(contracts.addressesRegistry); // Connect contracts _params.boldToken.setBranchAddresses( diff --git a/contracts/test/TestContracts/TroveManagerTester.t.sol b/contracts/test/TestContracts/TroveManagerTester.t.sol index 1b6f141e3..6c337ec70 100644 --- a/contracts/test/TestContracts/TroveManagerTester.t.sol +++ b/contracts/test/TestContracts/TroveManagerTester.t.sol @@ -14,12 +14,8 @@ for testing the parent's internal functions. */ contract TroveManagerTester is ITroveManagerTester, TroveManager { uint256 constant STALE_TROVE_DURATION = 90 days; - // Extra buffer of collateral ratio to join a batch or adjust a trove inside a batch (on top of MCR) - uint256 public immutable BCR; - constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) TroveManager(_addressesRegistry, _systemParams) { - BCR = _systemParams.BCR(); } // Single liquidation function. Closes the trove if its ICR is lower than the minimum collateral ratio. @@ -30,27 +26,27 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { } function get_CCR() external view returns (uint256) { - return CCR; + return systemParams.CCR(); } function get_MCR() external view returns (uint256) { - return MCR; + return systemParams.MCR(); } function get_BCR() external view returns (uint256) { - return BCR; + return systemParams.BCR(); } function get_SCR() external view returns (uint256) { - return SCR; + return systemParams.SCR(); } function get_LIQUIDATION_PENALTY_SP() external view returns (uint256) { - return LIQUIDATION_PENALTY_SP; + return systemParams.LIQUIDATION_PENALTY_SP(); } function get_LIQUIDATION_PENALTY_REDISTRIBUTION() external view returns (uint256) { - return LIQUIDATION_PENALTY_REDISTRIBUTION; + return systemParams.LIQUIDATION_PENALTY_REDISTRIBUTION(); } function getBoldToken() external view returns (IBoldToken) { @@ -98,7 +94,7 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { } function checkBelowCriticalThreshold(uint256 _price) external view override returns (bool) { - return _checkBelowCriticalThreshold(_price, CCR); + return _checkBelowCriticalThreshold(_price, systemParams.CCR()); } function computeICR(uint256 _coll, uint256 _debt, uint256 _price) external pure returns (uint256) { @@ -122,11 +118,11 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { } function getETHGasCompensation() external view returns (uint256) { - return ETH_GAS_COMPENSATION; + return systemParams.ETH_GAS_COMPENSATION(); } function get_MIN_DEBT() external view returns (uint256) { - return MIN_DEBT; + return systemParams.MIN_DEBT(); } /* diff --git a/contracts/test/systemParams.t.sol b/contracts/test/systemParams.t.sol index ffd31e1e9..fa187120a 100644 --- a/contracts/test/systemParams.t.sol +++ b/contracts/test/systemParams.t.sol @@ -46,7 +46,7 @@ contract SystemParamsTest is DevTestSetup { minBoldInSP: 1e18 }); - SystemParams params = new SystemParams( + SystemParams params = new SystemParams(false, debtParams, liquidationParams, gasCompParams, @@ -82,7 +82,7 @@ contract SystemParamsTest is DevTestSetup { ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({minDebt: 0}); vm.expectRevert(ISystemParams.InvalidMinDebt.selector); - new SystemParams( + new SystemParams(false, debtParams, _getValidLiquidationParams(), _getValidGasCompParams(), @@ -97,7 +97,7 @@ contract SystemParamsTest is DevTestSetup { ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({minDebt: 10001e18}); vm.expectRevert(ISystemParams.InvalidMinDebt.selector); - new SystemParams( + new SystemParams(false, debtParams, _getValidLiquidationParams(), _getValidGasCompParams(), @@ -117,7 +117,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.SPPenaltyTooLow.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), liquidationParams, _getValidGasCompParams(), @@ -135,7 +135,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.SPPenaltyGtRedist.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), liquidationParams, _getValidGasCompParams(), @@ -153,7 +153,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.RedistPenaltyTooHigh.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), liquidationParams, _getValidGasCompParams(), @@ -174,7 +174,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), gasCompParams, @@ -193,7 +193,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), gasCompParams, @@ -212,7 +212,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), gasCompParams, @@ -231,7 +231,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), gasCompParams, @@ -250,7 +250,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), gasCompParams, @@ -269,7 +269,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), gasCompParams, @@ -291,7 +291,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidCCR.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), _getValidGasCompParams(), @@ -311,7 +311,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidCCR.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), _getValidGasCompParams(), @@ -331,7 +331,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidMCR.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), _getValidGasCompParams(), @@ -351,7 +351,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidMCR.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), _getValidGasCompParams(), @@ -371,7 +371,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidBCR.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), _getValidGasCompParams(), @@ -391,7 +391,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidBCR.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), _getValidGasCompParams(), @@ -411,7 +411,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidSCR.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), _getValidGasCompParams(), @@ -431,7 +431,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidSCR.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), _getValidGasCompParams(), @@ -450,7 +450,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.MinInterestRateGtMax.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), _getValidGasCompParams(), @@ -473,7 +473,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidFeeValue.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), _getValidGasCompParams(), @@ -493,7 +493,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidFeeValue.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), _getValidGasCompParams(), @@ -513,7 +513,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidFeeValue.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), _getValidGasCompParams(), @@ -531,7 +531,7 @@ contract SystemParamsTest is DevTestSetup { }); vm.expectRevert(ISystemParams.InvalidMinDebt.selector); - new SystemParams( + new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), _getValidGasCompParams(), @@ -593,4 +593,4 @@ contract SystemParamsTest is DevTestSetup { minBoldInSP: 1e18 }); } -} +} \ No newline at end of file From c95cee05415699a9f2d4c8b8d0a7c24a1c8f91b0 Mon Sep 17 00:00:00 2001 From: baroooo Date: Wed, 15 Oct 2025 16:46:47 +0200 Subject: [PATCH 53/79] test: two scaling simulations --- contracts/test/stabilityScaling.t.sol | 329 +++++++++++++++++++++++--- 1 file changed, 302 insertions(+), 27 deletions(-) diff --git a/contracts/test/stabilityScaling.t.sol b/contracts/test/stabilityScaling.t.sol index 56a2d4a17..7737040a9 100644 --- a/contracts/test/stabilityScaling.t.sol +++ b/contracts/test/stabilityScaling.t.sol @@ -3,46 +3,321 @@ pragma solidity 0.8.24; import "./TestContracts/DevTestSetup.sol"; -import {mulDivCeil} from "./Utils/Math.sol"; contract StabilityScalingTest is DevTestSetup { address liquidityStrategy; - function setUp() override public { - super.setUp(); - liquidityStrategy = addressesRegistry.liquidityStrategy(); - deal(address(collToken), liquidityStrategy, 1e60); - vm.prank(liquidityStrategy); - collToken.approve(address(stabilityPool), type(uint256).max); + function setUp() public override { + super.setUp(); + liquidityStrategy = addressesRegistry.liquidityStrategy(); + deal(address(collToken), liquidityStrategy, 1e60); + vm.prank(liquidityStrategy); + collToken.approve(address(stabilityPool), type(uint256).max); } function _setupStabilityPool(uint256 magnitude) internal { - uint256 amount = 1e18 * 10 ** magnitude; - deal(address(boldToken), A, amount); - deal(address(boldToken), B, amount); - deal(address(boldToken), C, amount); - makeSPDepositAndClaim(A, amount); - makeSPDepositAndClaim(B, amount); - makeSPDepositAndClaim(C, amount); + uint256 amount = 1e18 * 10 ** magnitude; + deal(address(boldToken), A, amount); + deal(address(boldToken), B, amount); + makeSPDepositAndClaim(A, amount); + makeSPDepositAndClaim(B, amount); } function _topUpStabilityPoolBold(uint256 amount) internal { - deal(address(boldToken), D, amount); - makeSPDepositAndClaim(D, amount); + deal(address(boldToken), D, amount); + makeSPDepositAndClaim(D, amount); } - function testStabilityScaling_canScaleALot() public { - _setupStabilityPool(13); + function _openLiquidatableTroves( + uint256 numTroves, + uint256 collAmount, + uint256 debtAmount, + uint256 interestRate, + uint256 liquidationPrice + ) internal returns (uint256[] memory) { + uint256[] memory troveIds = new uint256[](numTroves); - for (uint256 i = 0; i < 5000; ++i) { - uint256 spBalance = boldToken.balanceOf(address(stabilityPool)); - uint256 boldOut = spBalance / 2; - uint256 collIn = boldOut / 2000; - vm.prank(liquidityStrategy); - stabilityPool.swapCollateralForStable(collIn, boldOut); - _topUpStabilityPoolBold(boldOut); - } + // Set initial price high enough for troves to be opened safely + priceFeed.setPrice(4000e18); + + // Open troves for additional accounts + for (uint256 i = 0; i < numTroves; i++) { + address account = vm.addr(1000 + i); // Generate unique addresses + + // Fund and approve collateral + giveAndApproveColl(account, collAmount); + + deal(address(WETH), account, 1000e18); + + vm.startPrank(account); + WETH.approve(address(borrowerOperations), type(uint256).max); + vm.stopPrank(); + + // Open trove + uint256 troveId = openTroveNoHints100pct( + account, + collAmount, + debtAmount, + interestRate + ); + troveIds[i] = troveId; + } + + // Drop price to make troves liquidatable + if (liquidationPrice > 0) { + priceFeed.setPrice(liquidationPrice); + } + + return troveIds; + } + + function _openLiquidatableTrovesDefault( + uint256 numTroves + ) internal returns (uint256[] memory) { + uint256 collAmount = 1 ether; + uint256 debtAmount = 2000e18; + uint256 interestRate = 5e16; // 5% + uint256 liquidationPrice = 2000e18; // Makes troves liquidatable at 2000 price + + return + _openLiquidatableTroves( + numTroves, + collAmount, + debtAmount, + interestRate, + liquidationPrice + ); + } + + function testWithdrawalsWithLargeNumberOfScaleChanges() public { + // Users each deposit 1M bold to SP + _setupStabilityPool(6); + + uint256 previousScale = stabilityPool.currentScale(); + uint256 previousP = stabilityPool.P(); + uint256 previousTotalDeposits = stabilityPool.getTotalBoldDeposits(); + + // Open 200 liquidatable troves + uint256[] memory troveIds = _openLiquidatableTrovesDefault(200); + // liquidate 50 of them + for (uint256 j = 0; j < 50; j++) { + liquidate(D, troveIds[j]); + } + // Scale won't change + assertEq(stabilityPool.currentScale(), previousScale); + // P should decrease + assertGt(previousP, stabilityPool.P()); + // Total deposits should decrease + assertGt(previousTotalDeposits, stabilityPool.getTotalBoldDeposits()); + + // Update previous values + previousScale = stabilityPool.currentScale(); + previousP = stabilityPool.P(); + previousTotalDeposits = stabilityPool.getTotalBoldDeposits(); + + // Simulate rebalance 1000 times + for (uint256 i = 1; i <= 1000; i++) { + uint boldBalance = stabilityPool.getTotalBoldDeposits(); + uint256 stableOut = (boldBalance) / 2; + // Swap out half of the bold in SP + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(stableOut / 2000, stableOut); + // Top up SP with the same amount of bold swapped out + _topUpStabilityPoolBold(stableOut); + } + + // Scale will increase + assertGt(stabilityPool.currentScale(), previousScale); + // Total deposits will stay the same because we top up SP with the same amount of bold swapped out + assertEq(previousTotalDeposits, stabilityPool.getTotalBoldDeposits()); + // Update previous values + previousScale = stabilityPool.currentScale(); + previousP = stabilityPool.P(); + previousTotalDeposits = stabilityPool.getTotalBoldDeposits(); + + // Liquidate the remaining troves - 1 + for (uint256 j = 50; j < 199; j++) { + liquidate(D, troveIds[j]); + } + // Scale won't change + assertEq(stabilityPool.currentScale(), previousScale); + // P should decrease + assertGt(previousP, stabilityPool.P()); + // Total deposits should decrease + assertGt(previousTotalDeposits, stabilityPool.getTotalBoldDeposits()); + + // Withdraw A and B from SP to observe the withdrawals + uint256 boldBalanceA = boldToken.balanceOf(A); + uint256 collBalanceA = collToken.balanceOf(A); + + // A deposited 1M bold to SP initially + assertEq(stabilityPool.deposits(A), 1_000_000e18); + + vm.startPrank(A); + // A tries to withdraw all their bold from SP + stabilityPool.withdrawFromSP(stabilityPool.deposits(A), true); + vm.stopPrank(); + + uint256 boldWithdrawA = boldToken.balanceOf(A) - boldBalanceA; + uint256 collWithdrawA = collToken.balanceOf(A) - collBalanceA; + + // A's full bold position is diminished by the rebalances and liquidations + // There is a small amount of bold left because of the Yield gain + assertLt(boldWithdrawA, 500e18); + + // A's coll gain is a combination of rebalance and liquidations + // In exchange for the 1M bold they lost, they should receive almost the same amount of collateral + // * because we executed swaps and liquidations at the same price (2000e18) + assertApproxEqAbs(collWithdrawA, 1_000_000e18 / 2000, 1e18); + } + + function testHowScaleChangesAffectsCollGains() public { + _setupStabilityPool(6); + + uint256 previousScale = stabilityPool.currentScale(); + uint256 previousCollGain = stabilityPool.getDepositorCollGain(A); + uint256 previousDeposit = stabilityPool.getCompoundedBoldDeposit(A); + + // Open 200 liquidatable troves + uint256[] memory troveIds = _openLiquidatableTrovesDefault(200); + + // liquidate 50 of them + for (uint256 j = 0; j < 50; j++) { + liquidate(D, troveIds[j]); + } + + // 50 troves were liquidated, each with 2000e18 debt + // Half of the total debt is offsetted by A's deposit since it had the half of the total deposits + uint256 deptOffset = (50 * 2_000e18) / 2; + // Liquidation price is 2000e18, so each trove has 1 ether of collateral + uint256 collGain = (50 * 1 ether) / 2; + assertApproxEqRel( + stabilityPool.getCompoundedBoldDeposit(A), + previousDeposit - deptOffset, + 1e16 + ); + assertApproxEqRel( + stabilityPool.getDepositorCollGain(A), + collGain - previousCollGain, + 1e16 + ); + + previousScale = stabilityPool.currentScale(); + previousCollGain = stabilityPool.getDepositorCollGain(A); + previousDeposit = stabilityPool.getCompoundedBoldDeposit(A); + + // Simulate rebalance until the edge of a scale change + for (uint256 i = 1; i <= 29; i++) { + uint boldBalance = stabilityPool.getTotalBoldDeposits(); + uint256 stableOut = (boldBalance) / 2; + // Swap out half of the bold in SP + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(stableOut / 2000, stableOut); + // Top up SP with the same amount of bold swapped out + _topUpStabilityPoolBold(stableOut); + } + + assertEq(stabilityPool.currentScale(), previousScale); + + // at this point, initial depositors almost lost all their deposits + assertLt(stabilityPool.getCompoundedBoldDeposit(A), 1e18); + // their positions moved to collateral almost completely =~ 500e18 + assertApproxEqRel( + stabilityPool.getDepositorCollGain(A), + 1_000_000e18 / 2000, + 1e16 + ); + + // Deposit again + _setupStabilityPool(6); + + previousScale = stabilityPool.currentScale(); + previousCollGain = stabilityPool.getDepositorCollGain(A); + previousDeposit = stabilityPool.getCompoundedBoldDeposit(A); + + // Simulate rebalance until scale changes + for (uint256 i = 1; i <= 10; i++) { + uint boldBalance = stabilityPool.getTotalBoldDeposits(); + uint256 stableOut = (boldBalance) / 10; + // Swap out half of the bold in SP + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(stableOut / 2000, stableOut); + // Top up SP with the same amount of bold swapped out + if (stabilityPool.currentScale() > previousScale) { + break; + } + } + + previousCollGain = stabilityPool.getDepositorCollGain(A); + previousDeposit = stabilityPool.getCompoundedBoldDeposit(A); + + // liquidate 50 more troves + // coll gains will be received for a positon that is opened on 1 scale above + for (uint256 j = 50; j < 100; j++) { + liquidate(D, troveIds[j]); + } + // depositors should have less deposits and more coll gain + assertLt(stabilityPool.getCompoundedBoldDeposit(A), previousDeposit); + assertGt(stabilityPool.getDepositorCollGain(A), previousCollGain); + + previousScale = stabilityPool.currentScale(); + + + // Simulate rebalance until scale changes + for (uint256 i = 1; i <= 100; i++) { + uint boldBalance = stabilityPool.getTotalBoldDeposits(); + uint256 stableOut = (boldBalance) / 2; + // Swap out half of the bold in SP + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(stableOut / 2000, stableOut); + // Top up SP with the same amount of bold swapped out + _topUpStabilityPoolBold(stableOut); + if (stabilityPool.currentScale() > previousScale) { + break; + } + } + + + previousCollGain = stabilityPool.getDepositorCollGain(A); + previousDeposit = stabilityPool.getCompoundedBoldDeposit(A); + + // liquidate 50 more troves + // coll gains will be received for a positon that is opened on 2 scale above + for (uint256 j = 100; j < 150; j++) { + liquidate(D, troveIds[j]); + } + // depositors should have less deposits and more coll gain + // changes will be minimal since the share of the depositor is small now + assertLt(stabilityPool.getCompoundedBoldDeposit(A), previousDeposit); + assertGt(stabilityPool.getDepositorCollGain(A), previousCollGain); + + previousScale = stabilityPool.currentScale(); + + // Simulate rebalance until scale changes + for (uint256 i = 1; i <= 100; i++) { + uint boldBalance = stabilityPool.getTotalBoldDeposits(); + uint256 stableOut = (boldBalance) / 2; + // Swap out half of the bold in SP + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(stableOut / 2000, stableOut); + // Top up SP with the same amount of bold swapped out + _topUpStabilityPoolBold(stableOut); + if (stabilityPool.currentScale() > previousScale) { + break; + } + } + + previousCollGain = stabilityPool.getDepositorCollGain(A); + previousDeposit = stabilityPool.getCompoundedBoldDeposit(A); - assertEq(stabilityPool.currentScale(), 167); + // liquidate 50 more troves + // positions will stop gaining coll because they are opened on 2 scale above + // and now too small to gain any coll + for (uint256 j = 150; j < 199; j++) { + liquidate(D, troveIds[j]); + } + // depositors should have less deposits and coll gain should be the same + assertLt(stabilityPool.getCompoundedBoldDeposit(A), previousDeposit); + assertEq(stabilityPool.getDepositorCollGain(A), previousCollGain); } } From c5b76721ccb62d9a1ebb2973142be3f2b8d4fe36 Mon Sep 17 00:00:00 2001 From: Mouradif Date: Wed, 15 Oct 2025 17:24:28 +0200 Subject: [PATCH 54/79] feat: add test with a big depositor getting only coll gain --- contracts/test/stabilityScaling.t.sol | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/contracts/test/stabilityScaling.t.sol b/contracts/test/stabilityScaling.t.sol index 7737040a9..12e513f7f 100644 --- a/contracts/test/stabilityScaling.t.sol +++ b/contracts/test/stabilityScaling.t.sol @@ -320,4 +320,27 @@ contract StabilityScalingTest is DevTestSetup { assertLt(stabilityPool.getCompoundedBoldDeposit(A), previousDeposit); assertEq(stabilityPool.getDepositorCollGain(A), previousCollGain); } + + function testStabilityScaling_hugeDepositorDoesntLooseBold() public { + address depositor = makeAddr("earthUsdReserve"); + uint256 depositAmount = 2.417 ether * 1e12; // Total USD in circulation: 2.417 T + deal(address(boldToken), depositor, depositAmount); + makeSPDepositAndClaim(depositor, depositAmount); + + for (uint256 i = 0; i < 9; ++i) { + uint256 spBalance = boldToken.balanceOf(address(stabilityPool)); + uint256 boldOut = spBalance - 1_000e18; + uint256 collIn = boldOut / 2000; + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collIn, boldOut); + _topUpStabilityPoolBold(boldOut); + } + + uint256 collGain = stabilityPool.getDepositorCollGain(depositor); + uint256 compoundedBold = stabilityPool.getCompoundedBoldDeposit(depositor); + assertEq(stabilityPool.currentScale(), 9); + + assertEq(compoundedBold, 0); + assertApproxEqAbs(collGain, depositAmount / 2000, 1); + } } From d3cdb2a083e7d95c75817b79206f77c5bd1f0215 Mon Sep 17 00:00:00 2001 From: baroooo Date: Thu, 16 Oct 2025 12:50:24 +0200 Subject: [PATCH 55/79] chore: revert foundry changes --- contracts/foundry.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 376bdfce0..d65024722 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -4,7 +4,7 @@ out = "out" libs = ["lib"] evm_version = 'cancun' optimizer = true -optimizer_runs = 1 +optimizer_runs = 0 ignored_error_codes = [3860, 5574] # contract-size fs_permissions = [ { access = "read", path = "./utils/assets/" }, @@ -45,4 +45,4 @@ no_storage_caching = true [lint] # Excludes info/notes from 'forge build' and 'forge lint' output per default as it's quite noisy -severity = ["high", "med", "low"] +severity = ["high", "med", "low"] \ No newline at end of file From 86fba7781570e6800f786f4d9bf4e5175f559381 Mon Sep 17 00:00:00 2001 From: nvtaveras <4562733+nvtaveras@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:06:52 +0200 Subject: [PATCH 56/79] feat: OracleAdapter integration (#9) --- contracts/script/DeployLiquity2.s.sol | 17 +- contracts/src/AddressesRegistry.sol | 6 +- contracts/src/BorrowerOperations.sol | 12 +- .../src/Interfaces/IAddressesRegistry.sol | 4 +- .../src/Interfaces/IMainnetPriceFeed.sol | 16 - contracts/src/Interfaces/IOracleAdapter.sol | 19 + contracts/src/Interfaces/IPriceFeed.sol | 4 +- contracts/src/Interfaces/IRETHPriceFeed.sol | 9 - contracts/src/Interfaces/IRETHToken.sol | 7 - contracts/src/Interfaces/IWSTETH.sol | 12 - contracts/src/Interfaces/IWSTETHPriceFeed.sol | 9 - .../src/PriceFeeds/CompositePriceFeed.sol | 119 - contracts/src/PriceFeeds/FXPriceFeed.sol | 130 +- .../src/PriceFeeds/MainnetPriceFeedBase.sol | 125 - contracts/src/PriceFeeds/RETHPriceFeed.sol | 101 - contracts/src/PriceFeeds/WETHPriceFeed.sol | 46 - contracts/src/PriceFeeds/WSTETHPriceFeed.sol | 91 - contracts/src/TroveManager.sol | 9 +- contracts/test/FXPriceFeed.t.sol | 276 ++ .../Interfaces/LiquityV1/IPriceFeedV1.sol | 6 - contracts/test/OracleMainnet.t.sol | 2518 ----------------- contracts/test/TestContracts/BaseTest.sol | 4 +- contracts/test/TestContracts/Deployment.t.sol | 296 +- .../test/TestContracts/GasGuzzlerOracle.sol | 47 - .../test/TestContracts/GasGuzzlerToken.sol | 28 - .../Interfaces/IMockFXPriceFeed.sol | 12 + .../Interfaces/IPriceFeedMock.sol | 9 - .../Interfaces/IPriceFeedTestnet.sol | 10 - .../test/TestContracts/MockFXPriceFeed.sol | 38 + .../test/TestContracts/PriceFeedMock.sol | 33 - .../test/TestContracts/PriceFeedTestnet.sol | 46 - .../test/TestContracts/RETHTokenMock.sol | 18 - .../SPInvariantsTestHandler.t.sol | 6 +- .../test/TestContracts/WSTETHTokenMock.sol | 31 - contracts/test/basicOps.t.sol | 143 +- contracts/test/interestRateAggregate.t.sol | 16 +- contracts/test/liquidationCosts.t.sol | 4 +- contracts/test/liquidations.t.sol | 8 +- contracts/test/liquidationsLST.t.sol | 2 +- 39 files changed, 647 insertions(+), 3640 deletions(-) delete mode 100644 contracts/src/Interfaces/IMainnetPriceFeed.sol create mode 100644 contracts/src/Interfaces/IOracleAdapter.sol delete mode 100644 contracts/src/Interfaces/IRETHPriceFeed.sol delete mode 100644 contracts/src/Interfaces/IRETHToken.sol delete mode 100644 contracts/src/Interfaces/IWSTETH.sol delete mode 100644 contracts/src/Interfaces/IWSTETHPriceFeed.sol delete mode 100644 contracts/src/PriceFeeds/CompositePriceFeed.sol delete mode 100644 contracts/src/PriceFeeds/MainnetPriceFeedBase.sol delete mode 100644 contracts/src/PriceFeeds/RETHPriceFeed.sol delete mode 100644 contracts/src/PriceFeeds/WETHPriceFeed.sol delete mode 100644 contracts/src/PriceFeeds/WSTETHPriceFeed.sol create mode 100644 contracts/test/FXPriceFeed.t.sol delete mode 100644 contracts/test/Interfaces/LiquityV1/IPriceFeedV1.sol delete mode 100644 contracts/test/OracleMainnet.t.sol delete mode 100644 contracts/test/TestContracts/GasGuzzlerOracle.sol delete mode 100644 contracts/test/TestContracts/GasGuzzlerToken.sol create mode 100644 contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol delete mode 100644 contracts/test/TestContracts/Interfaces/IPriceFeedMock.sol delete mode 100644 contracts/test/TestContracts/Interfaces/IPriceFeedTestnet.sol create mode 100644 contracts/test/TestContracts/MockFXPriceFeed.sol delete mode 100644 contracts/test/TestContracts/PriceFeedMock.sol delete mode 100644 contracts/test/TestContracts/PriceFeedTestnet.sol delete mode 100644 contracts/test/TestContracts/RETHTokenMock.sol delete mode 100644 contracts/test/TestContracts/WSTETHTokenMock.sol diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index dccccddc5..bd406d0af 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -39,7 +39,7 @@ import "src/StabilityPool.sol"; import "src/CollateralRegistry.sol"; import "src/tokens/StableTokenV3.sol"; import "src/Interfaces/IStableTokenV3.sol"; -import "test/TestContracts/PriceFeedTestnet.sol"; +import "test/TestContracts/MockFXPriceFeed.sol"; import "test/TestContracts/MetadataDeployment.sol"; import "test/Utils/Logging.sol"; import "test/Utils/StringEquality.sol"; @@ -125,9 +125,12 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { address proxyAdmin; address fpmmFactory; address fpmmImplementation; + address liquidityStrategy; + address oracleAdapter; address referenceRateFeedID; string stableTokenName; string stableTokenSymbol; + address watchdog; } DeploymentConfig internal CONFIG = DeploymentConfig({ @@ -135,9 +138,12 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { proxyAdmin: 0xe4DdacCAdb64114215FCe8251B57B2AEB5C2C0E2, fpmmFactory: 0xd8098494a749a3fDAD2D2e7Fa5272D8f274D8FF6, fpmmImplementation: 0x0292efcB331C6603eaa29D570d12eB336D6c01d6, + liquidityStrategy: address(123), // TODO: set liquidity strategy + oracleAdapter: address(234), // TODO: set oracle adapter address referenceRateFeedID: 0x206B25Ea01E188Ee243131aFdE526bA6E131a016, stableTokenName: "EUR.v2 Test", - stableTokenSymbol: "EUR.v2" + stableTokenSymbol: "EUR.v2", + watchdog: address(345) // TODO: set watchdog address }); function run() external { @@ -197,7 +203,8 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { r.hintHelpers = new HintHelpers(r.collateralRegistry, r.systemParams); r.multiTroveGetter = new MultiTroveGetter(r.collateralRegistry); - IPriceFeed priceFeed = new PriceFeedTestnet(); + // TODO: replace with real price feed + IPriceFeed priceFeed = new MockFXPriceFeed(); r.contracts = _deployAndConnectCollateralContracts(collToken, priceFeed, addressesRegistry, troveManagerAddress, r); @@ -387,9 +394,7 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { collateralRegistry: r.collateralRegistry, boldToken: IBoldToken(address(r.stableToken)), gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS), - // TODO: set liquidity strategy - liquidityStrategy: address(0), - watchdogAddress: address(0) + liquidityStrategy: CONFIG.liquidityStrategy }); contracts.addressesRegistry.setAddresses(addressVars); } diff --git a/contracts/src/AddressesRegistry.sol b/contracts/src/AddressesRegistry.sol index 4f3a940ed..275003668 100644 --- a/contracts/src/AddressesRegistry.sol +++ b/contracts/src/AddressesRegistry.sol @@ -25,7 +25,6 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { IBoldToken public boldToken; IERC20Metadata public gasToken; address public liquidityStrategy; - address public watchdogAddress; event CollTokenAddressChanged(address _collTokenAddress); event BorrowerOperationsAddressChanged(address _borrowerOperationsAddress); @@ -46,7 +45,6 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { event BoldTokenAddressChanged(address _boldTokenAddress); event GasTokenAddressChanged(address _gasTokenAddress); event LiquidityStrategyAddressChanged(address _liquidityStrategyAddress); - event WatchdogAddressChanged(address _watchdogAddress); constructor(address _owner) Ownable(_owner) {} @@ -70,7 +68,6 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { boldToken = _vars.boldToken; gasToken = _vars.gasToken; liquidityStrategy = _vars.liquidityStrategy; - watchdogAddress = _vars.watchdogAddress; emit CollTokenAddressChanged(address(_vars.collToken)); emit BorrowerOperationsAddressChanged( @@ -95,8 +92,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { emit BoldTokenAddressChanged(address(_vars.boldToken)); emit GasTokenAddressChanged(address(_vars.gasToken)); emit LiquidityStrategyAddressChanged(address(_vars.liquidityStrategy)); - emit WatchdogAddressChanged(address(_vars.watchdogAddress)); _renounceOwnership(); } -} +} \ No newline at end of file diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index e70d0d3c4..d6224efb5 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -336,7 +336,7 @@ contract BorrowerOperations is vars.activePool = activePool; vars.boldToken = boldToken; - (vars.price, ) = priceFeed.fetchPrice(); + vars.price = priceFeed.fetchPrice(); // --- Checks --- @@ -658,7 +658,7 @@ contract BorrowerOperations is vars.activePool = activePool; vars.boldToken = boldToken; - (vars.price,) = priceFeed.fetchPrice(); + vars.price = priceFeed.fetchPrice(); vars.isBelowCriticalThreshold = _checkBelowCriticalThreshold( vars.price, systemParams.CCR() @@ -895,7 +895,7 @@ contract BorrowerOperations is // troveChange.newWeightedRecordedDebt = 0; } - (uint256 price, ) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 newTCR = _getNewTCRFromTroveChange(troveChange, price); if (!hasBeenShutDown) _requireNewTCRisAboveCCR(newTCR); @@ -1185,7 +1185,7 @@ contract BorrowerOperations is block.timestamp < batch.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN ) { - (uint256 price, ) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 avgInterestRate = activePoolCached .getNewApproxAvgInterestRateFromTroveChange(batchChange); @@ -1516,7 +1516,7 @@ contract BorrowerOperations is uint256 _maxUpfrontFee, bool _isTroveInBatch ) internal returns (uint256) { - (uint256 price, ) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 avgInterestRate = activePool .getNewApproxAvgInterestRateFromTroveChange(_troveChange); @@ -1581,7 +1581,7 @@ contract BorrowerOperations is uint256 totalColl = getEntireBranchColl(); uint256 totalDebt = getEntireBranchDebt(); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); // Otherwise, proceed with the TCR check: uint256 TCR = LiquityMath._computeCR(totalColl, totalDebt, price); diff --git a/contracts/src/Interfaces/IAddressesRegistry.sol b/contracts/src/Interfaces/IAddressesRegistry.sol index bf6705e12..cb3644fbb 100644 --- a/contracts/src/Interfaces/IAddressesRegistry.sol +++ b/contracts/src/Interfaces/IAddressesRegistry.sol @@ -39,7 +39,6 @@ interface IAddressesRegistry { IBoldToken boldToken; IERC20Metadata gasToken; address liquidityStrategy; - address watchdogAddress; } function collToken() external view returns (IERC20Metadata); @@ -61,7 +60,6 @@ interface IAddressesRegistry { function boldToken() external view returns (IBoldToken); function gasToken() external view returns (IERC20Metadata); function liquidityStrategy() external view returns (address); - function watchdogAddress() external view returns (address); function setAddresses(AddressVars memory _vars) external; -} +} \ No newline at end of file diff --git a/contracts/src/Interfaces/IMainnetPriceFeed.sol b/contracts/src/Interfaces/IMainnetPriceFeed.sol deleted file mode 100644 index ee8580236..000000000 --- a/contracts/src/Interfaces/IMainnetPriceFeed.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT -import "../Interfaces/IPriceFeed.sol"; -import "../Dependencies/AggregatorV3Interface.sol"; - -pragma solidity ^0.8.0; - -interface IMainnetPriceFeed is IPriceFeed { - enum PriceSource { - primary, - ETHUSDxCanonical, - lastGoodPrice - } - - function ethUsdOracle() external view returns (AggregatorV3Interface, uint256, uint8); - function priceSource() external view returns (PriceSource); -} diff --git a/contracts/src/Interfaces/IOracleAdapter.sol b/contracts/src/Interfaces/IOracleAdapter.sol new file mode 100644 index 000000000..0347012a6 --- /dev/null +++ b/contracts/src/Interfaces/IOracleAdapter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +/** + * @title IOracleAdapter + * @notice Interface for the Oracle Adapter contract that provides FX rate data + */ +interface IOracleAdapter { + /** + * @notice Returns the exchange rate for a given rate feed ID + * with 18 decimals of precision if considered valid, based on + * FX market hours, trading mode, and recent rate, otherwise reverts + * @param rateFeedID The address of the rate feed + * @return numerator The numerator of the rate + * @return denominator The denominator of the rate + */ + function getFXRateIfValid(address rateFeedID) external view returns (uint256 numerator, uint256 denominator); +} \ No newline at end of file diff --git a/contracts/src/Interfaces/IPriceFeed.sol b/contracts/src/Interfaces/IPriceFeed.sol index ca49b06cf..c9bb33a1a 100644 --- a/contracts/src/Interfaces/IPriceFeed.sol +++ b/contracts/src/Interfaces/IPriceFeed.sol @@ -3,7 +3,5 @@ pragma solidity ^0.8.0; interface IPriceFeed { - function fetchPrice() external returns (uint256, bool); - function fetchRedemptionPrice() external returns (uint256, bool); - function lastGoodPrice() external view returns (uint256); + function fetchPrice() external returns (uint256); } diff --git a/contracts/src/Interfaces/IRETHPriceFeed.sol b/contracts/src/Interfaces/IRETHPriceFeed.sol deleted file mode 100644 index 862bf6874..000000000 --- a/contracts/src/Interfaces/IRETHPriceFeed.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -import "./IMainnetPriceFeed.sol"; -import "../Dependencies/AggregatorV3Interface.sol"; - -pragma solidity ^0.8.0; - -interface IRETHPriceFeed is IMainnetPriceFeed { - function rEthEthOracle() external view returns (AggregatorV3Interface, uint256, uint8); -} diff --git a/contracts/src/Interfaces/IRETHToken.sol b/contracts/src/Interfaces/IRETHToken.sol deleted file mode 100644 index df5e840be..000000000 --- a/contracts/src/Interfaces/IRETHToken.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -interface IRETHToken { - function getExchangeRate() external view returns (uint256); -} diff --git a/contracts/src/Interfaces/IWSTETH.sol b/contracts/src/Interfaces/IWSTETH.sol deleted file mode 100644 index eab84d38d..000000000 --- a/contracts/src/Interfaces/IWSTETH.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -interface IWSTETH { - function wrap(uint256 _stETHAmount) external returns (uint256); - function unwrap(uint256 _wstETHAmount) external returns (uint256); - function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256); - function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256); - function stEthPerToken() external view returns (uint256); - function tokensPerStEth() external view returns (uint256); -} diff --git a/contracts/src/Interfaces/IWSTETHPriceFeed.sol b/contracts/src/Interfaces/IWSTETHPriceFeed.sol deleted file mode 100644 index cfb20934e..000000000 --- a/contracts/src/Interfaces/IWSTETHPriceFeed.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -import "./IMainnetPriceFeed.sol"; -import "../Dependencies/AggregatorV3Interface.sol"; - -pragma solidity ^0.8.0; - -interface IWSTETHPriceFeed is IMainnetPriceFeed { - function stEthUsdOracle() external view returns (AggregatorV3Interface, uint256, uint8); -} diff --git a/contracts/src/PriceFeeds/CompositePriceFeed.sol b/contracts/src/PriceFeeds/CompositePriceFeed.sol deleted file mode 100644 index 995727d14..000000000 --- a/contracts/src/PriceFeeds/CompositePriceFeed.sol +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "../Dependencies/LiquityMath.sol"; -import "./MainnetPriceFeedBase.sol"; - -// import "forge-std/console2.sol"; - -// The CompositePriceFeed is used for feeds that incorporate both a market price oracle (e.g. STETH-USD, or RETH-ETH) -// and an LST canonical rate (e.g. WSTETH:STETH, or RETH:ETH). -abstract contract CompositePriceFeed is MainnetPriceFeedBase { - address public rateProviderAddress; - - constructor( - address _ethUsdOracleAddress, - address _rateProviderAddress, - uint256 _ethUsdStalenessThreshold, - address _borrowerOperationsAddress - ) MainnetPriceFeedBase(_ethUsdOracleAddress, _ethUsdStalenessThreshold, _borrowerOperationsAddress) { - // Store rate provider - rateProviderAddress = _rateProviderAddress; - } - - // Returns: - // - The price, using the current price calculation - // - A bool that is true if: - // --- a) the system was not shut down prior to this call, and - // --- b) an oracle or exchange rate contract failed during this call. - function fetchPrice() public returns (uint256, bool) { - // If branch is live and the primary oracle setup has been working, try to use it - if (priceSource == PriceSource.primary) return _fetchPricePrimary(false); - - return _fetchPriceDuringShutdown(); - } - - function fetchRedemptionPrice() external returns (uint256, bool) { - // If branch is live and the primary oracle setup has been working, try to use it - if (priceSource == PriceSource.primary) return _fetchPricePrimary(true); - - return _fetchPriceDuringShutdown(); - } - - function _shutDownAndSwitchToETHUSDxCanonical(address _failedOracleAddr, uint256 _ethUsdPrice) - internal - returns (uint256) - { - // Shut down the branch - borrowerOperations.shutdownFromOracleFailure(); - - priceSource = PriceSource.ETHUSDxCanonical; - - emit ShutDownFromOracleFailure(_failedOracleAddr); - return _fetchPriceETHUSDxCanonical(_ethUsdPrice); - } - - function _fetchPriceDuringShutdown() internal returns (uint256, bool) { - // When branch is already shut down and using ETH-USD * canonical_rate, try to use that - if (priceSource == PriceSource.ETHUSDxCanonical) { - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - //... but if the ETH-USD oracle *also* fails here, switch to using the lastGoodPrice - if (ethUsdOracleDown) { - // No need to shut down, since branch already is shut down - priceSource = PriceSource.lastGoodPrice; - return (lastGoodPrice, false); - } else { - return (_fetchPriceETHUSDxCanonical(ethUsdPrice), false); - } - } - - // Otherwise when branch is shut down and already using the lastGoodPrice, continue with it - assert(priceSource == PriceSource.lastGoodPrice); - return (lastGoodPrice, false); - } - - // Only called if the primary LST oracle has failed, branch has shut down, - // and we've switched to using: ETH-USD * canonical_rate. - function _fetchPriceETHUSDxCanonical(uint256 _ethUsdPrice) internal returns (uint256) { - assert(priceSource == PriceSource.ETHUSDxCanonical); - // Get the underlying_per_LST canonical rate directly from the LST contract - (uint256 lstRate, bool exchangeRateIsDown) = _getCanonicalRate(); - - // If the exchange rate contract is down, switch to (and return) lastGoodPrice. - if (exchangeRateIsDown) { - priceSource = PriceSource.lastGoodPrice; - return lastGoodPrice; - } - - // Calculate the canonical LST-USD price: USD_per_LST = USD_per_ETH * underlying_per_LST - uint256 lstUsdCanonicalPrice = _ethUsdPrice * lstRate / 1e18; - - uint256 bestPrice = LiquityMath._min(lstUsdCanonicalPrice, lastGoodPrice); - - lastGoodPrice = bestPrice; - - return bestPrice; - } - - function _withinDeviationThreshold(uint256 _priceToCheck, uint256 _referencePrice, uint256 _deviationThreshold) - internal - pure - returns (bool) - { - // Calculate the price deviation of the oracle market price relative to the canonical price - uint256 max = _referencePrice * (DECIMAL_PRECISION + _deviationThreshold) / 1e18; - uint256 min = _referencePrice * (DECIMAL_PRECISION - _deviationThreshold) / 1e18; - - return _priceToCheck >= min && _priceToCheck <= max; - } - - // An individual Pricefeed instance implements _fetchPricePrimary according to the data sources it uses. Returns: - // - The price - // - A bool indicating whether a new oracle failure or exchange rate failure was detected in the call - function _fetchPricePrimary(bool _isRedemption) internal virtual returns (uint256, bool); - - // Returns the LST exchange rate and a bool indicating whether the exchange rate failed to return a valid rate. - // Implementation depends on the specific LST. - function _getCanonicalRate() internal view virtual returns (uint256, bool); -} diff --git a/contracts/src/PriceFeeds/FXPriceFeed.sol b/contracts/src/PriceFeeds/FXPriceFeed.sol index 4824c95a1..dbbe13637 100644 --- a/contracts/src/PriceFeeds/FXPriceFeed.sol +++ b/contracts/src/PriceFeeds/FXPriceFeed.sol @@ -1,50 +1,142 @@ +// SPDX-License-Identifier: MIT + pragma solidity 0.8.24; +import "../Interfaces/IOracleAdapter.sol"; import "../Interfaces/IPriceFeed.sol"; -import "../BorrowerOperations.sol"; +import "../Interfaces/IBorrowerOperations.sol"; -interface IOracleAdapter { - function getFXRateIfValid(address rateFeedID) external view returns (uint256 numerator, uint256 denominator); -} +import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; +/** + * @title FXPriceFeed + * @author Mento Labs + * @notice A contract that fetches the price of an FX rate from an OracleAdapter. + * Implements emergency shutdown functionality to handle oracle failures. + */ +contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { -contract FXPriceFeed is IPriceFeed { + /* ==================== State Variables ==================== */ + /// @notice The OracleAdapter contract that provides FX rate data IOracleAdapter public oracleAdapter; + + /// @notice The identifier address for the specific rate feed to query address public rateFeedID; + + /// @notice The watchdog contract address authorized to trigger emergency shutdown address public watchdogAddress; + + /// @notice The BorrowerOperations contract IBorrowerOperations public borrowerOperations; - uint256 public lastGoodPrice; + /// @notice The last valid price returned by the OracleAdapter + uint256 public lastValidPrice; + + /// @notice Whether the contract has been shutdown due to an oracle failure bool public isShutdown; - constructor(address _oracleAdapterAddress, address _rateFeedID, address _borrowerOperationsAddress, address _watchdogAddress) { + /// @notice Thrown when the attempting to shutdown the contract when it is already shutdown + error IsShutDown(); + /// @notice Thrown when a non-watchdog address attempts to shutdown the contract + error CallerNotWatchdog(); + /// @notice Thrown when a zero address is provided as a parameter + error ZeroAddress(); + + /// @notice Emitted when the watchdog address is updated + /// @param _oldWatchdogAddress The previous watchdog address + /// @param _newWatchdogAddress The new watchdog address + event WatchdogAddressUpdated(address indexed _oldWatchdogAddress, address indexed _newWatchdogAddress); + + /// @notice Emitted when the contract is shutdown due to oracle failure + event FXPriceFeedShutdown(); + + /** + * @notice Contract constructor + * @param disableInitializers Boolean to disable initializers for implementation contract + */ + constructor(bool disableInitializers) { + if (disableInitializers) { + _disableInitializers(); + } + } + + /** + * @notice Initializes the FXPriceFeed contract + * @param _oracleAdapterAddress The address of the OracleAdapter contract + * @param _rateFeedID The address of the rate feed ID + * @param _borrowerOperationsAddress The address of the BorrowerOperations contract + * @param _watchdogAddress The address of the watchdog contract + * @param _initialOwner The address of the initial owner + */ + function initialize( + address _oracleAdapterAddress, + address _rateFeedID, + address _borrowerOperationsAddress, + address _watchdogAddress, + address _initialOwner + ) external initializer { + if (_oracleAdapterAddress == address(0)) revert ZeroAddress(); + if (_rateFeedID == address(0)) revert ZeroAddress(); + if (_borrowerOperationsAddress == address(0)) revert ZeroAddress(); + if (_watchdogAddress == address(0)) revert ZeroAddress(); + if (_initialOwner == address(0)) revert ZeroAddress(); + oracleAdapter = IOracleAdapter(_oracleAdapterAddress); rateFeedID = _rateFeedID; borrowerOperations = IBorrowerOperations(_borrowerOperationsAddress); watchdogAddress = _watchdogAddress; + + fetchPrice(); + + _transferOwnership(_initialOwner); + } + + /** + * @notice Sets the watchdog address + * @param _newWatchdogAddress The address of the new watchdog contract + */ + function setWatchdogAddress(address _newWatchdogAddress) external onlyOwner { + if (_newWatchdogAddress == address(0)) revert ZeroAddress(); + + address oldWatchdogAddress = watchdogAddress; + watchdogAddress = _newWatchdogAddress; + + emit WatchdogAddressUpdated(oldWatchdogAddress, _newWatchdogAddress); } - function fetchPrice() public returns (uint256, bool) { + /** + * @notice Fetches the price of the FX rate, if valid + * @dev If the contract is shutdown due to oracle failure, the last valid price is returned + * @return The price of the FX rate + */ + function fetchPrice() public returns (uint256) { if (isShutdown) { - return (lastGoodPrice, false); + return lastValidPrice; } - (uint256 numerator, ) = oracleAdapter.getFXRateIfValid(rateFeedID); + // Denominator is always 1e18, so we only use the numerator as the price + (uint256 price, ) = oracleAdapter.getFXRateIfValid(rateFeedID); - lastGoodPrice = numerator; + lastValidPrice = price; - return (numerator, false); - } - - function fetchRedemptionPrice() external returns (uint256, bool) { - return fetchPrice(); + return price; } + /** + * @notice Shuts down the price feed contract due to oracle failure + * @dev Can only be called by the authorized watchdog address. + * Once shutdown: + * - The contract will only return the last valid price + * - The BorrowerOperations and TroveManager contracts are notified to shut down the collateral branch + * - The shutdown state is permanent and cannot be reversed + */ function shutdown() external { - require(!isShutdown, "MentoPriceFeed: already shutdown"); - require(msg.sender == watchdogAddress, "MentoPriceFeed: not authorized"); + if (isShutdown) revert IsShutDown(); + if (msg.sender != watchdogAddress) revert CallerNotWatchdog(); isShutdown = true; borrowerOperations.shutdownFromOracleFailure(); + + emit FXPriceFeedShutdown(); } -} +} \ No newline at end of file diff --git a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol deleted file mode 100644 index f2f8f2412..000000000 --- a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "../Dependencies/AggregatorV3Interface.sol"; -import "../Interfaces/IMainnetPriceFeed.sol"; -import "../BorrowerOperations.sol"; - -// import "forge-std/console2.sol"; - -abstract contract MainnetPriceFeedBase is IMainnetPriceFeed { - // Determines where the PriceFeed sources data from. Possible states: - // - primary: Uses the primary price calcuation, which depends on the specific feed - // - ETHUSDxCanonical: Uses Chainlink's ETH-USD multiplied by the LST' canonical rate - // - lastGoodPrice: the last good price recorded by this PriceFeed. - PriceSource public priceSource; - - // Last good price tracker for the derived USD price - uint256 public lastGoodPrice; - - struct Oracle { - AggregatorV3Interface aggregator; - uint256 stalenessThreshold; - uint8 decimals; - } - - struct ChainlinkResponse { - uint80 roundId; - int256 answer; - uint256 timestamp; - bool success; - } - - error InsufficientGasForExternalCall(); - - event ShutDownFromOracleFailure(address _failedOracleAddr); - - Oracle public ethUsdOracle; - - IBorrowerOperations borrowerOperations; - - constructor(address _ethUsdOracleAddress, uint256 _ethUsdStalenessThreshold, address _borrowOperationsAddress) { - // Store ETH-USD oracle - ethUsdOracle.aggregator = AggregatorV3Interface(_ethUsdOracleAddress); - ethUsdOracle.stalenessThreshold = _ethUsdStalenessThreshold; - ethUsdOracle.decimals = ethUsdOracle.aggregator.decimals(); - - borrowerOperations = IBorrowerOperations(_borrowOperationsAddress); - - assert(ethUsdOracle.decimals == 8); - } - - function _getOracleAnswer(Oracle memory _oracle) internal view returns (uint256, bool) { - ChainlinkResponse memory chainlinkResponse = _getCurrentChainlinkResponse(_oracle.aggregator); - - uint256 scaledPrice; - bool oracleIsDown; - // Check oracle is serving an up-to-date and sensible price. If not, shut down this collateral branch. - if (!_isValidChainlinkPrice(chainlinkResponse, _oracle.stalenessThreshold)) { - oracleIsDown = true; - } else { - scaledPrice = _scaleChainlinkPriceTo18decimals(chainlinkResponse.answer, _oracle.decimals); - } - - return (scaledPrice, oracleIsDown); - } - - function _shutDownAndSwitchToLastGoodPrice(address _failedOracleAddr) internal returns (uint256) { - // Shut down the branch - borrowerOperations.shutdownFromOracleFailure(); - - priceSource = PriceSource.lastGoodPrice; - - emit ShutDownFromOracleFailure(_failedOracleAddr); - return lastGoodPrice; - } - - function _getCurrentChainlinkResponse(AggregatorV3Interface _aggregator) - internal - view - returns (ChainlinkResponse memory chainlinkResponse) - { - uint256 gasBefore = gasleft(); - - // Try to get latest price data: - try _aggregator.latestRoundData() returns ( - uint80 roundId, int256 answer, uint256, /* startedAt */ uint256 updatedAt, uint80 /* answeredInRound */ - ) { - // If call to Chainlink succeeds, return the response and success = true - chainlinkResponse.roundId = roundId; - chainlinkResponse.answer = answer; - chainlinkResponse.timestamp = updatedAt; - chainlinkResponse.success = true; - - return chainlinkResponse; - } catch { - // Require that enough gas was provided to prevent an OOG revert in the call to Chainlink - // causing a shutdown. Instead, just revert. Slightly conservative, as it includes gas used - // in the check itself. - if (gasleft() <= gasBefore / 64) revert InsufficientGasForExternalCall(); - - // If call to Chainlink aggregator reverts, return a zero response with success = false - return chainlinkResponse; - } - } - - // False if: - // - Call to Chainlink aggregator reverts - // - price is too stale, i.e. older than the oracle's staleness threshold - // - Price answer is 0 or negative - function _isValidChainlinkPrice(ChainlinkResponse memory chainlinkResponse, uint256 _stalenessThreshold) - internal - view - returns (bool) - { - return chainlinkResponse.success && block.timestamp - chainlinkResponse.timestamp < _stalenessThreshold - && chainlinkResponse.answer > 0; - } - - // Trust assumption: Chainlink won't change the decimal precision on any feed used in v2 after deployment - function _scaleChainlinkPriceTo18decimals(int256 _price, uint256 _decimals) internal pure returns (uint256) { - // Scale an int price to a uint with 18 decimals - return uint256(_price) * 10 ** (18 - _decimals); - } -} diff --git a/contracts/src/PriceFeeds/RETHPriceFeed.sol b/contracts/src/PriceFeeds/RETHPriceFeed.sol deleted file mode 100644 index 30b513aa6..000000000 --- a/contracts/src/PriceFeeds/RETHPriceFeed.sol +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./CompositePriceFeed.sol"; -import "../Interfaces/IRETHToken.sol"; -import "../Interfaces/IRETHPriceFeed.sol"; - -// import "forge-std/console2.sol"; - -contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { - constructor( - address _ethUsdOracleAddress, - address _rEthEthOracleAddress, - address _rEthTokenAddress, - uint256 _ethUsdStalenessThreshold, - uint256 _rEthEthStalenessThreshold, - address _borrowerOperationsAddress - ) - CompositePriceFeed(_ethUsdOracleAddress, _rEthTokenAddress, _ethUsdStalenessThreshold, _borrowerOperationsAddress) - { - // Store RETH-ETH oracle - rEthEthOracle.aggregator = AggregatorV3Interface(_rEthEthOracleAddress); - rEthEthOracle.stalenessThreshold = _rEthEthStalenessThreshold; - rEthEthOracle.decimals = rEthEthOracle.aggregator.decimals(); - - _fetchPricePrimary(false); - - // Check the oracle didn't already fail - assert(priceSource == PriceSource.primary); - } - - Oracle public rEthEthOracle; - - uint256 public constant RETH_ETH_DEVIATION_THRESHOLD = 2e16; // 2% - - function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { - assert(priceSource == PriceSource.primary); - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - (uint256 rEthEthPrice, bool rEthEthOracleDown) = _getOracleAnswer(rEthEthOracle); - (uint256 ethPerReth, bool exchangeRateIsDown) = _getCanonicalRate(); - - // If either the ETH-USD feed or exchange rate is down, shut down and switch to the last good price - // seen by the system since we need both for primary and fallback price calcs - if (ethUsdOracleDown) { - return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true); - } - if (exchangeRateIsDown) { - return (_shutDownAndSwitchToLastGoodPrice(rateProviderAddress), true); - } - // If the ETH-USD feed is live but the RETH-ETH oracle is down, shutdown and substitute RETH-ETH with the canonical rate - if (rEthEthOracleDown) { - return (_shutDownAndSwitchToETHUSDxCanonical(address(rEthEthOracle.aggregator), ethUsdPrice), true); - } - - // Otherwise, use the primary price calculation: - - // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 rEthUsdMarketPrice = ethUsdPrice * rEthEthPrice / 1e18; - - // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 rEthUsdCanonicalPrice = ethUsdPrice * ethPerReth / 1e18; - - uint256 rEthUsdPrice; - - // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb - if ( - _isRedemption - && _withinDeviationThreshold(rEthUsdMarketPrice, rEthUsdCanonicalPrice, RETH_ETH_DEVIATION_THRESHOLD) - ) { - rEthUsdPrice = LiquityMath._max(rEthUsdMarketPrice, rEthUsdCanonicalPrice); - } else { - // Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation. - // Assumes a deviation between market <> canonical of >2% represents a legitimate market price difference. - rEthUsdPrice = LiquityMath._min(rEthUsdMarketPrice, rEthUsdCanonicalPrice); - } - - lastGoodPrice = rEthUsdPrice; - - return (rEthUsdPrice, false); - } - - function _getCanonicalRate() internal view override returns (uint256, bool) { - uint256 gasBefore = gasleft(); - - try IRETHToken(rateProviderAddress).getExchangeRate() returns (uint256 ethPerReth) { - // If rate is 0, return true - if (ethPerReth == 0) return (0, true); - - return (ethPerReth, false); - } catch { - // Require that enough gas was provided to prevent an OOG revert in the external call - // causing a shutdown. Instead, just revert. Slightly conservative, as it includes gas used - // in the check itself. - if (gasleft() <= gasBefore / 64) revert InsufficientGasForExternalCall(); - - // If call to exchange rate reverts, return true - return (0, true); - } - } -} diff --git a/contracts/src/PriceFeeds/WETHPriceFeed.sol b/contracts/src/PriceFeeds/WETHPriceFeed.sol deleted file mode 100644 index b2eb60420..000000000 --- a/contracts/src/PriceFeeds/WETHPriceFeed.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./MainnetPriceFeedBase.sol"; - -// import "forge-std/console2.sol"; - -contract WETHPriceFeed is MainnetPriceFeedBase { - constructor(address _ethUsdOracleAddress, uint256 _ethUsdStalenessThreshold, address _borrowerOperationsAddress) - MainnetPriceFeedBase(_ethUsdOracleAddress, _ethUsdStalenessThreshold, _borrowerOperationsAddress) - { - _fetchPricePrimary(); - - // Check the oracle didn't already fail - assert(priceSource == PriceSource.primary); - } - - function fetchPrice() public returns (uint256, bool) { - // If branch is live and the primary oracle setup has been working, try to use it - if (priceSource == PriceSource.primary) return _fetchPricePrimary(); - - // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it - assert(priceSource == PriceSource.lastGoodPrice); - return (lastGoodPrice, false); - } - - function fetchRedemptionPrice() external returns (uint256, bool) { - // Use same price for redemption as all other ops in WETH branch - return fetchPrice(); - } - - // _fetchPricePrimary returns: - // - The price - // - A bool indicating whether a new oracle failure was detected in the call - function _fetchPricePrimary() internal returns (uint256, bool) { - assert(priceSource == PriceSource.primary); - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - - // If the ETH-USD Chainlink response was invalid in this transaction, return the last good ETH-USD price calculated - if (ethUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true); - - lastGoodPrice = ethUsdPrice; - return (ethUsdPrice, false); - } -} diff --git a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol b/contracts/src/PriceFeeds/WSTETHPriceFeed.sol deleted file mode 100644 index f18b92d97..000000000 --- a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./CompositePriceFeed.sol"; -import "../Interfaces/IWSTETH.sol"; -import "../Interfaces/IWSTETHPriceFeed.sol"; - -// import "forge-std/console2.sol"; - -contract WSTETHPriceFeed is CompositePriceFeed, IWSTETHPriceFeed { - Oracle public stEthUsdOracle; - - uint256 public constant STETH_USD_DEVIATION_THRESHOLD = 1e16; // 1% - - constructor( - address _ethUsdOracleAddress, - address _stEthUsdOracleAddress, - address _wstEthTokenAddress, - uint256 _ethUsdStalenessThreshold, - uint256 _stEthUsdStalenessThreshold, - address _borrowerOperationsAddress - ) - CompositePriceFeed(_ethUsdOracleAddress, _wstEthTokenAddress, _ethUsdStalenessThreshold, _borrowerOperationsAddress) - { - stEthUsdOracle.aggregator = AggregatorV3Interface(_stEthUsdOracleAddress); - stEthUsdOracle.stalenessThreshold = _stEthUsdStalenessThreshold; - stEthUsdOracle.decimals = stEthUsdOracle.aggregator.decimals(); - - _fetchPricePrimary(false); - - // Check the oracle didn't already fail - assert(priceSource == PriceSource.primary); - } - - function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { - assert(priceSource == PriceSource.primary); - (uint256 stEthUsdPrice, bool stEthUsdOracleDown) = _getOracleAnswer(stEthUsdOracle); - (uint256 stEthPerWstEth, bool exchangeRateIsDown) = _getCanonicalRate(); - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - - // - If exchange rate or ETH-USD is down, shut down and switch to last good price. Reasoning: - // - Exchange rate is used in all price calcs - // - ETH-USD is used in the fallback calc, and for redemptions in the primary price calc - if (exchangeRateIsDown) { - return (_shutDownAndSwitchToLastGoodPrice(rateProviderAddress), true); - } - if (ethUsdOracleDown) { - return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true); - } - - // If the STETH-USD feed is down, shut down and try to substitute it with the ETH-USD price - if (stEthUsdOracleDown) { - return (_shutDownAndSwitchToETHUSDxCanonical(address(stEthUsdOracle.aggregator), ethUsdPrice), true); - } - - // Otherwise, use the primary price calculation: - uint256 wstEthUsdPrice; - - if (_isRedemption && _withinDeviationThreshold(stEthUsdPrice, ethUsdPrice, STETH_USD_DEVIATION_THRESHOLD)) { - // If it's a redemption and within 1%, take the max of (STETH-USD, ETH-USD) to mitigate unwanted redemption arb and convert to WSTETH-USD - wstEthUsdPrice = LiquityMath._max(stEthUsdPrice, ethUsdPrice) * stEthPerWstEth / 1e18; - } else { - // Otherwise, just calculate WSTETH-USD price: USD_per_WSTETH = USD_per_STETH * STETH_per_WSTETH - wstEthUsdPrice = stEthUsdPrice * stEthPerWstEth / 1e18; - } - - lastGoodPrice = wstEthUsdPrice; - - return (wstEthUsdPrice, false); - } - - function _getCanonicalRate() internal view override returns (uint256, bool) { - uint256 gasBefore = gasleft(); - - try IWSTETH(rateProviderAddress).stEthPerToken() returns (uint256 stEthPerWstEth) { - // If rate is 0, return true - if (stEthPerWstEth == 0) return (0, true); - - return (stEthPerWstEth, false); - } catch { - // Require that enough gas was provided to prevent an OOG revert in the external call - // causing a shutdown. Instead, just revert. Slightly conservative, as it includes gas used - // in the check itself. - if (gasleft() <= gasBefore / 64) revert InsufficientGasForExternalCall(); - - // If call to exchange rate reverted for another reason, return true - return (0, true); - } - } -} diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index c8628bfad..e269a7c62 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -411,7 +411,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { TroveChange memory troveChange; LiquidationValues memory totals; - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); // - If the SP has total deposits >= 1e18, we leave 1e18 in it untouched. // - If it has 0 < x < 1e18 total deposits, we leave x in it. @@ -756,8 +756,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } vars.lastBatchUpdatedInterest = address(0); - // Get the price to use for the redemption collateral calculations - (uint256 redemptionPrice,) = priceFeed.fetchRedemptionPrice(); + uint256 redemptionPrice = priceFeed.fetchPrice(); // Loop through the Troves starting from the one with lowest interest rate until _amount of Bold is exchanged for collateral if (_maxIterations == 0) _maxIterations = type(uint256).max; @@ -865,7 +864,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { TroveChange memory totalsTroveChange; // Use the standard fetchPrice here, since if branch has shut down we don't worry about small redemption arbs - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 remainingBold = _boldAmount; for (uint256 i = 0; i < _troveIds.length; i++) { @@ -1204,7 +1203,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 spSize = stabilityPool.getTotalBoldDeposits(); uint256 unbackedPortion = totalDebt > spSize ? totalDebt - spSize : 0; - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); // It's redeemable if the TCR is above the shutdown threshold, and branch has not been shut down. // Use the normal price for the TCR check. bool redeemable = _getTCR(price) >= systemParams.SCR() && shutdownTime == 0; diff --git a/contracts/test/FXPriceFeed.t.sol b/contracts/test/FXPriceFeed.t.sol new file mode 100644 index 000000000..086095556 --- /dev/null +++ b/contracts/test/FXPriceFeed.t.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "./TestContracts/DevTestSetup.sol"; +import "../src/PriceFeeds/FXPriceFeed.sol"; + +import { Test } from "forge-std/Test.sol"; + +contract MockBorrowerOperations { + bool public _isShutdown; + + function shutdownFromOracleFailure() external { + _isShutdown = true; + } + + function shutdownCalled() external view returns (bool) { + return _isShutdown; + } +} + +contract MockOracleAdapter { + uint256 numerator; + uint256 denominator; + + function setFXRate(uint256 _numerator, uint256 _denominator) external { + numerator = _numerator; + denominator = _denominator; + } + + function getFXRateIfValid(address) external view returns (uint256, uint256) { + return (numerator, denominator); + } +} + + +contract FXPriceFeedTest is Test { + + event WatchdogAddressUpdated(address indexed _oldWatchdogAddress, address indexed _newWatchdogAddress); + event FXPriceFeedShutdown(); + + FXPriceFeed public fxPriceFeed; + MockOracleAdapter public mockOracleAdapter; + MockBorrowerOperations public mockBorrowerOperations; + + address public rateFeedID = makeAddr("rateFeedID"); + address public watchdog = makeAddr("watchdog"); + address public owner = makeAddr("owner"); + + uint256 constant mockRateNumerator = 1200 * 1e18; // 1.2 USD per unit + uint256 constant mockRateDenominator = 1e18; + + modifier initialized() { + vm.startPrank(owner); + fxPriceFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + address(mockBorrowerOperations), + watchdog, + owner + ); + vm.stopPrank(); + _; + } + + function setUp() public { + mockOracleAdapter = new MockOracleAdapter(); + mockOracleAdapter.setFXRate(mockRateNumerator, mockRateDenominator); + + mockBorrowerOperations = new MockBorrowerOperations(); + + fxPriceFeed = new FXPriceFeed(false); + } + + function test_constructor_whenDisableInitializersTrue_shouldDisableInitialization() public { + FXPriceFeed newFeed = new FXPriceFeed(true); + + vm.expectRevert(); + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + address(mockBorrowerOperations), + watchdog, + owner + ); + } + + function test_initialize_whenOracleAdapterAddressIsZero_shouldRevert() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + newFeed.initialize( + address(0), + rateFeedID, + address(mockBorrowerOperations), + watchdog, + owner + ); + } + + function test_initialize_whenRateFeedIDIsZero_shouldRevert() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + newFeed.initialize( + address(mockOracleAdapter), + address(0), + address(mockBorrowerOperations), + watchdog, + owner + ); + } + + function test_initialize_whenBorrowerOperationsAddressIsZero_shouldRevert() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + address(0), + watchdog, + owner + ); + } + + function test_initialize_whenWatchdogAddressIsZero_shouldRevert() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + address(mockBorrowerOperations), + address(0), + owner + ); + } + + function test_initialize_whenInitialOwnerIsZero_shouldRevert() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + address(mockBorrowerOperations), + watchdog, + address(0) + ); + } + + function test_initialize_whenAllParametersValid_shouldSucceed() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + mockOracleAdapter.setFXRate(5e18, 1e18); + + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + address(mockBorrowerOperations), + watchdog, + owner + ); + + assertEq(address(newFeed.oracleAdapter()), address(mockOracleAdapter)); + assertEq(newFeed.rateFeedID(), rateFeedID); + assertEq(address(newFeed.borrowerOperations()), address(mockBorrowerOperations)); + assertEq(newFeed.watchdogAddress(), watchdog); + assertEq(newFeed.owner(), owner); + assertEq(newFeed.lastValidPrice(), 5e18); + } + + function test_initialize_whenCalledTwice_shouldRevert() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + address(mockBorrowerOperations), + watchdog, + owner + ); + + vm.expectRevert("Initializable: contract is already initialized"); + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + address(mockBorrowerOperations), + watchdog, + owner + ); + } + + function test_setWatchdogAddress_whenCalledByNonOwner_shouldRevert() initialized public { + address notOwner = makeAddr("notOwner"); + address newWatchdog = makeAddr("newWatchdog"); + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + fxPriceFeed.setWatchdogAddress(newWatchdog); + vm.stopPrank(); + } + + function test_setWatchdogAddress_whenNewAddressIsZero_shouldRevert() initialized public { + vm.prank(owner); + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + fxPriceFeed.setWatchdogAddress(address(0)); + vm.stopPrank(); + } + + function test_setWatchdogAddress_whenCalledByOwner_shouldSucceed() initialized public { + address newWatchdog = makeAddr("newWatchdog"); + + vm.prank(owner); + vm.expectEmit(); + emit WatchdogAddressUpdated(watchdog, newWatchdog); + fxPriceFeed.setWatchdogAddress(newWatchdog); + vm.stopPrank(); + + assertEq(fxPriceFeed.watchdogAddress(), newWatchdog); + } + + function test_fetchPrice_whenNotShutdown_shouldReturnOraclePrice() initialized public { + uint256 price = fxPriceFeed.fetchPrice(); + + assertEq(price, mockRateNumerator); + assertEq(fxPriceFeed.lastValidPrice(), mockRateNumerator); + } + + function test_fetchPrice_whenShutdown_shouldReturnLastValidPrice() initialized public { + uint256 initialPrice = fxPriceFeed.fetchPrice(); + assertEq(initialPrice, mockRateNumerator); + + vm.prank(watchdog); + fxPriceFeed.shutdown(); + vm.stopPrank(); + + mockOracleAdapter.setFXRate(2 * mockRateNumerator, 2 * mockRateDenominator); + + uint256 priceAfterShutdown = fxPriceFeed.fetchPrice(); + + assertEq(priceAfterShutdown, initialPrice); + assertEq(fxPriceFeed.lastValidPrice(), initialPrice); + } + + function test_shutdown_whenCalledByNonWatchdog_shouldRevert() initialized public { + address notWatchdog = makeAddr("notWatchdog"); + + vm.prank(notWatchdog); + vm.expectRevert(FXPriceFeed.CallerNotWatchdog.selector); + fxPriceFeed.shutdown(); + vm.stopPrank(); + } + + function test_shutdown_whenCalledByWatchdog_shouldShutdown() initialized public { + assertEq(fxPriceFeed.isShutdown(), false); + assertEq(mockBorrowerOperations.shutdownCalled(), false); + + vm.prank(watchdog); + vm.expectEmit(); + emit FXPriceFeedShutdown(); + fxPriceFeed.shutdown(); + vm.stopPrank(); + + assertTrue(fxPriceFeed.isShutdown()); + assertTrue(mockBorrowerOperations.shutdownCalled()); + } + + function test_shutdown_whenAlreadyShutdown_shouldRevert() initialized public { + vm.prank(watchdog); + fxPriceFeed.shutdown(); + vm.expectRevert(FXPriceFeed.IsShutDown.selector); + fxPriceFeed.shutdown(); + vm.stopPrank(); + } +} diff --git a/contracts/test/Interfaces/LiquityV1/IPriceFeedV1.sol b/contracts/test/Interfaces/LiquityV1/IPriceFeedV1.sol deleted file mode 100644 index e2d790c24..000000000 --- a/contracts/test/Interfaces/LiquityV1/IPriceFeedV1.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface IPriceFeedV1 { - function fetchPrice() external returns (uint256); -} diff --git a/contracts/test/OracleMainnet.t.sol b/contracts/test/OracleMainnet.t.sol deleted file mode 100644 index b21e78a04..000000000 --- a/contracts/test/OracleMainnet.t.sol +++ /dev/null @@ -1,2518 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "src/PriceFeeds/WSTETHPriceFeed.sol"; -import "src/PriceFeeds/MainnetPriceFeedBase.sol"; -import "src/PriceFeeds/RETHPriceFeed.sol"; -import "src/PriceFeeds/WETHPriceFeed.sol"; - -import "./TestContracts/Accounts.sol"; -import "./TestContracts/ChainlinkOracleMock.sol"; -import "./TestContracts/GasGuzzlerOracle.sol"; -import "./TestContracts/GasGuzzlerToken.sol"; -import "./TestContracts/RETHTokenMock.sol"; -import "./TestContracts/WSTETHTokenMock.sol"; -import "./TestContracts/Deployment.t.sol"; - -import "src/Dependencies/AggregatorV3Interface.sol"; -import "src/Interfaces/IRETHPriceFeed.sol"; -import "src/Interfaces/IWSTETHPriceFeed.sol"; - -import "src/Interfaces/IRETHToken.sol"; -import "src/Interfaces/IWSTETH.sol"; - -import "forge-std/Test.sol"; -import "lib/forge-std/src/console2.sol"; - -contract OraclesMainnet is TestAccounts { - AggregatorV3Interface ethOracle; - AggregatorV3Interface stethOracle; - AggregatorV3Interface rethOracle; - - ChainlinkOracleMock mockOracle; - GasGuzzlerToken gasGuzzlerToken; - GasGuzzlerOracle gasGuzzlerOracle; - - IMainnetPriceFeed wethPriceFeed; - IRETHPriceFeed rethPriceFeed; - IWSTETHPriceFeed wstethPriceFeed; - - IRETHToken rethToken; - IWSTETH wstETH; - - RETHTokenMock mockRethToken; - WSTETHTokenMock mockWstethToken; - - TestDeployer.LiquityContracts[] contractsArray; - CollateralRegistryTester collateralRegistry; - IBoldToken boldToken; - - struct StoredOracle { - AggregatorV3Interface aggregator; - uint256 stalenessThreshold; - uint256 decimals; - } - - struct Vars { - uint256 numCollaterals; - uint256 initialColl; - uint256 price; - uint256 coll; - uint256 debtRequest; - uint256 debt_B; - uint256 debt_C; - uint256 debt_D; - uint256 ICR_A; - uint256 ICR_B; - uint256 ICR_C; - uint256 ICR_D; - uint256 redemptionICR_A; - uint256 redemptionICR_B; - uint256 redemptionICR_C; - uint256 redemptionICR_D; - uint256 troveId_A; - uint256 troveId_B; - uint256 troveId_C; - uint256 troveId_D; - int256 newEthPrice; - uint256 systemPrice; - uint256 newSystemPrice; - uint256 newSystemRedemptionPrice; - int256 ethPerRethMarket; - int256 usdPerEthMarket; - uint256 ethPerRethLST; - LatestTroveData troveDataBefore_A; - LatestTroveData troveDataBefore_B; - LatestTroveData troveDataBefore_C; - LatestTroveData troveDataBefore_D; - LatestTroveData troveDataAfter_A; - LatestTroveData troveDataAfter_B; - LatestTroveData troveDataAfter_C; - LatestTroveData troveDataAfter_D; - } - - function setUp() public { - try vm.envString("MAINNET_RPC_URL") returns (string memory rpcUrl) { - vm.createSelectFork(rpcUrl); - } catch { - vm.skip(true); - } - - Vars memory vars; - - accounts = new Accounts(); - createAccounts(); - - (A, B, C, D, E, F) = - (accountsList[0], accountsList[1], accountsList[2], accountsList[3], accountsList[4], accountsList[5]); - - vars.numCollaterals = 3; - TestDeployer.TroveManagerParams memory tmParams = - TestDeployer.TroveManagerParams(150e16, 110e16, 10e16, 110e16, 5e16, 10e16); - TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = - new TestDeployer.TroveManagerParams[](vars.numCollaterals); - for (uint256 i = 0; i < troveManagerParamsArray.length; i++) { - troveManagerParamsArray[i] = tmParams; - } - - TestDeployer deployer = new TestDeployer(); - TestDeployer.DeploymentResultMainnet memory result = - deployer.deployAndConnectContractsMainnet(troveManagerParamsArray); - collateralRegistry = result.collateralRegistry; - boldToken = result.boldToken; - - ethOracle = AggregatorV3Interface(result.externalAddresses.ETHOracle); - rethOracle = AggregatorV3Interface(result.externalAddresses.RETHOracle); - stethOracle = AggregatorV3Interface(result.externalAddresses.STETHOracle); - - mockOracle = new ChainlinkOracleMock(); - gasGuzzlerToken = new GasGuzzlerToken(); - gasGuzzlerOracle = new GasGuzzlerOracle(); - - rethToken = IRETHToken(result.externalAddresses.RETHToken); - - wstETH = IWSTETH(result.externalAddresses.WSTETHToken); - - mockRethToken = new RETHTokenMock(); - mockWstethToken = new WSTETHTokenMock(); - - // Record contracts - for (uint256 c = 0; c < vars.numCollaterals; c++) { - contractsArray.push(result.contractsArray[c]); - } - - // Give all users all collaterals - vars.initialColl = 1000_000e18; - for (uint256 i = 0; i < 6; i++) { - for (uint256 j = 0; j < vars.numCollaterals; j++) { - deal(address(contractsArray[j].collToken), accountsList[i], vars.initialColl); - vm.startPrank(accountsList[i]); - // Approve all Borrower Ops to use the user's WETH funds - contractsArray[0].collToken.approve(address(contractsArray[j].borrowerOperations), type(uint256).max); - // Approve Borrower Ops in LST branches to use the user's respective LST funds - contractsArray[j].collToken.approve(address(contractsArray[j].borrowerOperations), type(uint256).max); - vm.stopPrank(); - } - - vm.startPrank(accountsList[i]); - } - - wethPriceFeed = IMainnetPriceFeed(address(contractsArray[0].priceFeed)); - rethPriceFeed = IRETHPriceFeed(address(contractsArray[1].priceFeed)); - wstethPriceFeed = IWSTETHPriceFeed(address(contractsArray[2].priceFeed)); - - // log some current blockchain state - // console2.log(block.timestamp, "block.timestamp"); - // console2.log(block.number, "block.number"); - // console2.log(ethOracle.decimals(), "ETHUSD decimals"); - // console2.log(rethOracle.decimals(), "RETHETH decimals"); - // console2.log(stethOracle.decimals(), "STETHETH decimals"); - - // Artificially decay the base rate so we start with a low redemption rate. - // Normally, we would just wait for it to decay "naturally" (with `vm.warp`), but we can't do that here, - // as it would result in all the oracles going stale. - collateralRegistry.setBaseRate(0); - } - - function _getLatestAnswerFromOracle(AggregatorV3Interface _oracle) internal view returns (uint256) { - (, int256 answer,,,) = _oracle.latestRoundData(); - - uint256 decimals = _oracle.decimals(); - assertLe(decimals, 18); - // Convert to uint and scale up to 18 decimals - return uint256(answer) * 10 ** (18 - decimals); - } - - function redeem(address _from, uint256 _boldAmount) public { - vm.startPrank(_from); - collateralRegistry.redeemCollateral(_boldAmount, MAX_UINT256, 1e18); - vm.stopPrank(); - } - - function etchStaleMockToEthOracle(bytes memory _mockOracleCode) internal { - // Etch the mock code to the ETH-USD oracle address - vm.etch(address(ethOracle), _mockOracleCode); - ChainlinkOracleMock mock = ChainlinkOracleMock(address(ethOracle)); - mock.setDecimals(8); - // Fake ETH-USD price of 2000 USD - mock.setPrice(2000e8); - // Make it stale - mock.setUpdatedAt(block.timestamp - 7 days); - } - - function etchStaleMockToRethOracle(bytes memory _mockOracleCode) internal { - // Etch the mock code to the RETH-ETH oracle address - vm.etch(address(rethOracle), _mockOracleCode); - // Wrap so we can use the mock's setters - ChainlinkOracleMock mock = ChainlinkOracleMock(address(rethOracle)); - mock.setDecimals(18); - // Set 1 RETH = 1 ETH - mock.setPrice(1e18); - // Make it stale - mock.setUpdatedAt(block.timestamp - 7 days); - } - - function etchStaleMockToStethOracle(bytes memory _mockOracleCode) internal { - // Etch the mock code to the STETH-USD oracle address - vm.etch(address(stethOracle), _mockOracleCode); - // Wrap so we can use the mock's setters - ChainlinkOracleMock mock = ChainlinkOracleMock(address(stethOracle)); - mock.setDecimals(8); - // Set 1 STETH = 2000 USD - mock.setPrice(2000e8); - // Make it stale - mock.setUpdatedAt(block.timestamp - 7 days); - } - - function etchMockToEthOracle() internal returns (ChainlinkOracleMock) { - // Etch the mock code to the ETH-USD oracle address - vm.etch(address(ethOracle), address(mockOracle).code); - ChainlinkOracleMock mock = ChainlinkOracleMock(address(ethOracle)); - mock.setDecimals(8); - mock.setPrice(0); - // Make it current - mock.setUpdatedAt(block.timestamp); - - return mock; - } - - function etchMockToRethOracle() internal returns (ChainlinkOracleMock) { - // Etch the mock code to the ETH-USD oracle address - vm.etch(address(rethOracle), address(mockOracle).code); - ChainlinkOracleMock mock = ChainlinkOracleMock(address(rethOracle)); - mock.setDecimals(18); - mock.setPrice(0); - // Make it current - mock.setUpdatedAt(block.timestamp); - - return mock; - } - - function etchMockToStethOracle() internal returns (ChainlinkOracleMock) { - // Etch the mock code to the ETH-USD oracle address - vm.etch(address(stethOracle), address(mockOracle).code); - ChainlinkOracleMock mock = ChainlinkOracleMock(address(stethOracle)); - mock.setDecimals(8); - mock.setPrice(0); - // Make it current - mock.setUpdatedAt(block.timestamp); - - return mock; - } - - function etchGasGuzzlerToEthOracle(bytes memory _mockOracleCode) internal { - // Etch the mock code to the ETH-USD oracle address - vm.etch(address(ethOracle), _mockOracleCode); - GasGuzzlerOracle mock = GasGuzzlerOracle(address(ethOracle)); - mock.setDecimals(8); - // Fake ETH-USD price of 2000 USD - mock.setPrice(2000e8); - mock.setUpdatedAt(block.timestamp); - } - - function etchGasGuzzlerToRethOracle(bytes memory _mockOracleCode) internal { - // Etch the mock code to the RETH-ETH oracle address - vm.etch(address(rethOracle), _mockOracleCode); - // Wrap so we can use the mock's setters - GasGuzzlerOracle mock = GasGuzzlerOracle(address(rethOracle)); - mock.setDecimals(18); - // Set 1 RETH = 1.1 ETH - mock.setPrice(11e17); - mock.setUpdatedAt(block.timestamp); - } - - function etchGasGuzzlerToStethOracle(bytes memory _mockOracleCode) internal { - // Etch the mock code to the STETH-USD oracle address - vm.etch(address(stethOracle), _mockOracleCode); - // Wrap so we can use the mock's setters - GasGuzzlerOracle mock = GasGuzzlerOracle(address(stethOracle)); - mock.setDecimals(8); - // Set 1 STETH = 2000 USD - mock.setPrice(2000e8); - mock.setUpdatedAt(block.timestamp); - } - - function etchMockToRethToken() internal { - vm.etch(address(rethToken), address(mockRethToken).code); - RETHTokenMock mock = RETHTokenMock(address(rethToken)); - mock.setExchangeRate(0); - } - - function etchGasGuzzlerMockToRethToken(bytes memory _mockTokenCode) internal { - // Etch the mock code to the RETH token address - vm.etch(address(rethToken), _mockTokenCode); - } - - function etchGasGuzzlerMockToWstethToken(bytes memory _mockTokenCode) internal { - // Etch the mock code to the RETH token address - vm.etch(address(wstETH), _mockTokenCode); - } - - // --- lastGoodPrice set on deployment --- - - function testSetLastGoodPriceOnDeploymentWETH() public view { - uint256 lastGoodPriceWeth = wethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPriceWeth, 0); - - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - assertEq(lastGoodPriceWeth, latestAnswerEthUsd); - } - - function testSetLastGoodPriceOnDeploymentRETH() public view { - uint256 lastGoodPriceReth = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPriceReth, 0); - - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 expectedMarketPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 rate = rethToken.getExchangeRate(); - assertGt(rate, 1e18); - - uint256 expectedCanonicalPrice = rate * latestAnswerEthUsd / 1e18; - - uint256 expectedPrice = LiquityMath._min(expectedMarketPrice, expectedCanonicalPrice); - - assertEq(lastGoodPriceReth, expectedPrice); - } - - function testSetLastGoodPriceOnDeploymentWSTETH() public view { - uint256 lastGoodPriceWsteth = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPriceWsteth, 0); - - uint256 latestAnswerStethUsd = _getLatestAnswerFromOracle(stethOracle); - uint256 stethWstethExchangeRate = wstETH.stEthPerToken(); - - uint256 expectedStoredPrice = latestAnswerStethUsd * stethWstethExchangeRate / 1e18; - - assertEq(lastGoodPriceWsteth, expectedStoredPrice); - } - - // --- fetchPrice --- - - function testFetchPriceReturnsCorrectPriceWETH() public { - (uint256 fetchedEthUsdPrice,) = wethPriceFeed.fetchPrice(); - assertGt(fetchedEthUsdPrice, 0); - - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - assertEq(fetchedEthUsdPrice, latestAnswerEthUsd); - } - - function testFetchPriceReturnsCorrectPriceRETH() public { - (uint256 fetchedRethUsdPrice,) = rethPriceFeed.fetchPrice(); - assertGt(fetchedRethUsdPrice, 0); - - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 expectedMarketPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 rate = rethToken.getExchangeRate(); - assertGt(rate, 1e18); - - uint256 expectedCanonicalPrice = rate * latestAnswerEthUsd / 1e18; - - uint256 expectedPrice = LiquityMath._min(expectedMarketPrice, expectedCanonicalPrice); - - assertEq(fetchedRethUsdPrice, expectedPrice); - } - - function testFetchPriceReturnsCorrectPriceWSTETH() public { - (uint256 fetchedStethUsdPrice,) = wstethPriceFeed.fetchPrice(); - assertGt(fetchedStethUsdPrice, 0); - - uint256 latestAnswerStethUsd = _getLatestAnswerFromOracle(stethOracle); - uint256 stethWstethExchangeRate = wstETH.stEthPerToken(); - - uint256 expectedFetchedPrice = latestAnswerStethUsd * stethWstethExchangeRate / 1e18; - - assertEq(fetchedStethUsdPrice, expectedFetchedPrice); - } - - // --- Thresholds set at deployment --- - - function testEthUsdStalenessThresholdSetWETH() public view { - (, uint256 storedEthUsdStaleness,) = wethPriceFeed.ethUsdOracle(); - assertEq(storedEthUsdStaleness, _24_HOURS); - } - - function testEthUsdStalenessThresholdSetRETH() public view { - (, uint256 storedEthUsdStaleness,) = rethPriceFeed.ethUsdOracle(); - assertEq(storedEthUsdStaleness, _24_HOURS); - } - - function testRethEthStalenessThresholdSetRETH() public view { - (, uint256 storedRethEthStaleness,) = rethPriceFeed.rEthEthOracle(); - assertEq(storedRethEthStaleness, _48_HOURS); - } - - function testStethUsdStalenessThresholdSetWSTETH() public view { - (, uint256 storedStEthUsdStaleness,) = wstethPriceFeed.stEthUsdOracle(); - assertEq(storedStEthUsdStaleness, _24_HOURS); - } - - // --- LST exchange rates and market price oracle sanity checks --- - - function testRETHExchangeRateBetween1And2() public { - uint256 rate = rethToken.getExchangeRate(); - assertGt(rate, 1e18); - assertLt(rate, 2e18); - } - - function testWSTETHExchangeRateBetween1And2() public { - uint256 rate = wstETH.stEthPerToken(); - assertGt(rate, 1e18); - assertLt(rate, 2e18); - } - - function testRETHOracleAnswerBetween1And2() public { - uint256 answer = _getLatestAnswerFromOracle(rethOracle); - assertGt(answer, 1e18); - assertLt(answer, 2e18); - } - - function testSTETHOracleAnswerWithin1PctOfETHOracleAnswer() public { - uint256 stethUsd = _getLatestAnswerFromOracle(stethOracle); - uint256 ethUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 relativeDelta; - - if (stethUsd > ethUsd) { - relativeDelta = (stethUsd - ethUsd) * 1e18 / ethUsd; - } else { - relativeDelta = (ethUsd - stethUsd) * 1e18 / stethUsd; - } - - assertLt(relativeDelta, 1e16); - } - - // // --- Basic actions --- - - function testOpenTroveWETH() public { - uint256 price = _getLatestAnswerFromOracle(ethOracle); - - uint256 coll = 5 ether; - uint256 debtRequest = coll * price / 2 / 1e18; - - uint256 trovesCount = contractsArray[0].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 0); - - vm.startPrank(A); - contractsArray[0].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - trovesCount = contractsArray[0].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - } - - function testOpenTroveRETH() public { - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 calcdRethUsdPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdRethUsdPrice / 2 / 1e18; - - uint256 trovesCount = contractsArray[1].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 0); - - vm.startPrank(A); - contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - trovesCount = contractsArray[1].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - } - - function testOpenTroveWSTETH() public { - uint256 latestAnswerStethUsd = _getLatestAnswerFromOracle(stethOracle); - uint256 wstethStethExchangeRate = wstETH.stEthPerToken(); - - uint256 calcdWstethUsdPrice = latestAnswerStethUsd * wstethStethExchangeRate / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdWstethUsdPrice / 2 / 1e18; - - uint256 trovesCount = contractsArray[2].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 0); - - vm.startPrank(A); - contractsArray[2].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - trovesCount = contractsArray[2].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - } - - // --- Oracle manipulation tests --- - - function testManipulatedChainlinkReturnsStalePrice() public { - // Replace the ETH Oracle's code with the mock oracle's code that returns a stale price - etchStaleMockToEthOracle(address(mockOracle).code); - - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - - // Confirm it's stale - assertEq(updatedAt, block.timestamp - 7 days); - } - - function testManipulatedChainlinkReturns2kUsdPrice() public { - // Replace the ETH Oracle's code with the mock oracle's code that returns a stale price - etchStaleMockToEthOracle(address(mockOracle).code); - - uint256 price = _getLatestAnswerFromOracle(ethOracle); - assertEq(price, 2000e18); - } - - function testOpenTroveWETHWithStalePriceReverts() public { - Vars memory vars; - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - assertFalse(contractsArray[0].borrowerOperations.hasBeenShutDown()); - - vars.price = _getLatestAnswerFromOracle(ethOracle); - vars.coll = 5 ether; - vars.debtRequest = vars.coll * vars.price / 2 / 1e18; - - vm.startPrank(A); - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[0].borrowerOperations.openTrove( - A, 0, vars.coll, vars.debtRequest, 0, 0, 5e16, vars.debtRequest, address(0), address(0), address(0) - ); - } - - function testAdjustTroveWETHWithStalePriceReverts() public { - uint256 price = _getLatestAnswerFromOracle(ethOracle); - - uint256 coll = 5 ether; - uint256 debtRequest = coll * price / 2 / 1e18; - - vm.startPrank(A); - uint256 troveId = contractsArray[0].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // confirm Trove was opened - uint256 trovesCount = contractsArray[0].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - - // Replace oracle with a stale oracle - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Try to adjust Trove - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[0].borrowerOperations.adjustTrove(troveId, 0, false, 1 wei, true, 1e18); - } - - function testOpenTroveWSTETHWithStalePriceReverts() public { - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - assertFalse(contractsArray[2].borrowerOperations.hasBeenShutDown()); - - uint256 price = _getLatestAnswerFromOracle(stethOracle); - - uint256 coll = 5 ether; - uint256 debtRequest = coll * price / 2 / 1e18; - - vm.startPrank(A); - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[2].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - } - - function testAdjustTroveWSTETHWithStalePriceReverts() public { - uint256 price = _getLatestAnswerFromOracle(stethOracle); - - uint256 coll = 5 ether; - uint256 debtRequest = coll * price / 2 / 1e18; - - vm.startPrank(A); - uint256 troveId = contractsArray[2].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // confirm Trove was opened - uint256 trovesCount = contractsArray[2].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - - // Replace oracle with a stale oracle - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Try to adjust Trove - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[2].borrowerOperations.adjustTrove(troveId, 0, false, 1 wei, true, 1e18); - } - - function testOpenTroveRETHWithStaleRETHPriceReverts() public { - // Make only RETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - assertFalse(contractsArray[1].borrowerOperations.hasBeenShutDown()); - - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - uint256 calcdRethUsdPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdRethUsdPrice / 2 / 1e18; - - vm.startPrank(A); - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - } - - function testAdjustTroveRETHWithStaleRETHPriceReverts() public { - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - uint256 calcdRethUsdPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdRethUsdPrice / 2 / 1e18; - - vm.startPrank(A); - uint256 troveId = contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // confirm Trove was opened - uint256 trovesCount = contractsArray[1].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - - // Make only RETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Try to adjust Trove - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[1].borrowerOperations.adjustTrove(troveId, 0, false, 1 wei, true, 1e18); - } - - function testOpenTroveRETHWithStaleETHPriceReverts() public { - // Make only ETH oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - assertFalse(contractsArray[1].borrowerOperations.hasBeenShutDown()); - - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - uint256 calcdRethUsdPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdRethUsdPrice / 2 / 1e18; - - vm.startPrank(A); - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - } - - function testAdjustTroveRETHWithStaleETHPriceReverts() public { - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - uint256 calcdRethUsdPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdRethUsdPrice / 2 / 1e18; - - vm.startPrank(A); - uint256 troveId = contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // confirm Trove was opened - uint256 trovesCount = contractsArray[1].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - - // Make only ETH oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // // Try to adjust Trove - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[1].borrowerOperations.adjustTrove(troveId, 0, false, 1 wei, true, 1e18); - } - - // --- WETH shutdown --- - - function testWETHPriceFeedShutsDownWhenETHUSDOracleFails() public { - // Fetch price - (uint256 price, bool ethUsdFailed) = wethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check oracle call didn't fail - assertFalse(ethUsdFailed); - - // Check branch is live, not shut down - assertEq(contractsArray[0].troveManager.shutdownTime(), 0); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, ethUsdFailed) = wethPriceFeed.fetchPrice(); - - // Check oracle call failed this time - assertTrue(ethUsdFailed); - - // Confirm the branch is now shutdown - assertEq(contractsArray[0].troveManager.shutdownTime(), block.timestamp); - } - - function testWETHPriceFeedReturnsLastGoodPriceWhenETHUSDOracleFails() public { - // Fetch price - wethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - assertGt(mockPrice, 0, "mockPrice 0"); - // Confirm the lastGoodPrice is not coincidentally equal to the mock oracle's price - assertNotEq(lastGoodPrice1, uint256(mockPrice)); - - // Fetch price again - (uint256 price, bool ethUsdFailed) = wethPriceFeed.fetchPrice(); - - // Check oracle call failed this time - assertTrue(ethUsdFailed); - - // Confirm the PriceFeed's returned price equals the lastGoodPrice - assertEq(price, lastGoodPrice1, "current price != lastGoodPrice"); - - // Confirm the stored lastGoodPrice has not changed - assertEq(wethPriceFeed.lastGoodPrice(), lastGoodPrice1, "lastGoodPrice not same"); - } - - // --- RETH shutdown --- - - function testRETHPriceFeedShutsDownWhenETHUSDOracleFails() public { - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check oracle call didn't fail - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[1].troveManager.shutdownTime(), 0); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the branch is now shutdown - assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); - } - - function testRETHPriceFeedShutsDownWhenExchangeRateFails() public { - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check oracle call didn't fail - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[1].troveManager.shutdownTime(), 0); - - // Make the exchange rate 0 - etchMockToRethToken(); - uint256 rate = rethToken.getExchangeRate(); - assertEq(rate, 0, "rate not zero"); - - // Fetch price again - (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check a call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the branch is now shutdown - assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp, "timestamps not equal"); - } - - function testRETHPriceFeedReturnsLastGoodPriceWhenETHUSDOracleFails() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - assertGt(mockPrice, 0, "mockPrice 0"); - - // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the PriceFeed's returned price equals the lastGoodPrice - assertEq(price, lastGoodPrice1); - - // Confirm the stored lastGoodPrice has not changed - assertEq(rethPriceFeed.lastGoodPrice(), lastGoodPrice1); - } - - function testRETHPriceFeedReturnsLastGoodPriceWhenExchangeRateFails() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Make the exchange rate 0 - etchMockToRethToken(); - uint256 rate = rethToken.getExchangeRate(); - assertEq(rate, 0); - - // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the PriceFeed's returned price equals the lastGoodPrice - assertEq(price, lastGoodPrice1); - - // Confirm the stored lastGoodPrice has not changed - assertEq(rethPriceFeed.lastGoodPrice(), lastGoodPrice1); - } - - function testRETHPriceSourceIsLastGoodPriceWhenETHUSDFails() public { - // Fetch price - rethPriceFeed.fetchPrice(); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - assertTrue(oracleFailedWhileBranchLive); - - // Check using lastGoodPrice - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - } - - function testRETHPriceFeedShutsDownWhenRETHETHOracleFails() public { - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check oracle call didn't fail - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[1].troveManager.shutdownTime(), 0); - - // Make the RETH-ETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the branch is now shutdown - assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); - } - - function testFetchPriceReturnsMinETHUSDxCanonicalAndLastGoodPriceWhenRETHETHOracleFails() public { - // Make the RETH-ETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Calc expected price i.e. ETH-USD x canonical - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = rethToken.getExchangeRate(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - - uint256 expectedPrice = LiquityMath._min(rethPriceFeed.lastGoodPrice(), ethUsdPrice * exchangeRate / 1e18); - - assertEq(price, expectedPrice, "price not expected price"); - } - - function testRETHPriceSourceIsETHUSDxCanonicalWhenRETHETHFails() public { - // Fetch price - rethPriceFeed.fetchPrice(); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Make the RETH-ETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - assertTrue(oracleFailedWhileBranchLive); - - // Check using canonical - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); - } - - function testRETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenETHUSDOracleFails() public { - // Make the RETH-USD oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary), "not using primary"); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive, "primary oracle calc didnt fail"); - - // Check using ETHUSDxCanonical - assertEq( - uint8(rethPriceFeed.priceSource()), - uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical), - "not using ethusdxcanonical" - ); - - uint256 lastGoodPrice = rethPriceFeed.lastGoodPrice(); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Calc expected price if didnt fail, i.e. ETH-USD x canonical - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = rethToken.getExchangeRate(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; - - // These should differ since the mock oracle's price should not equal the previous real price - assertNotEq(priceIfDidntFail, lastGoodPrice, "price if didnt fail == lastGoodPrice"); - - // Now fetch the price - (price, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // This should be false, since the branch is already shutdown and not live - assertFalse(oracleFailedWhileBranchLive); - - // Confirm the returned price is the last good price - assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); - } - - function testRETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenExchangeRateFails() public { - // Make the RETH-USD oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary), "not using primary"); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive, "primary oracle calc didnt fail"); - - // Check using ETHUSDxCanonical - assertEq( - uint8(rethPriceFeed.priceSource()), - uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical), - "not using ethusdxcanonical" - ); - - uint256 lastGoodPrice = rethPriceFeed.lastGoodPrice(); - - // Calc expected price if didnt fail, i.e. ETH-USD x canonical - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = rethToken.getExchangeRate(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - - // Make the exchange rate return 0 - etchMockToRethToken(); - uint256 rate = rethToken.getExchangeRate(); - assertEq(rate, 0, "mock rate non-zero"); - - // Now fetch the price - (price, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // This should be false, since the branch is already shutdown and not live - assertFalse(oracleFailedWhileBranchLive); - - // Confirm the returned price is the last good price - assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); - // Check we've switched to lastGoodPrice source - assertEq( - uint8(rethPriceFeed.priceSource()), - uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice), - "not using lastGoodPrice" - ); - } - - function testRETHWhenUsingETHUSDxCanonicalReturnsMinOfLastGoodPriceAndETHUSDxCanonical() public { - // Make the RETH-ETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Check using ETHUSDxCanonical - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); - - // Make lastGoodPrice tiny, and below ETHUSDxCanonical - vm.store( - address(rethPriceFeed), - bytes32(uint256(1)), // 1st storage slot where lastGoodPrice is stored - bytes32(uint256(1)) // make lastGoodPrice equal to 1 wei - ); - assertEq(rethPriceFeed.lastGoodPrice(), 1); - - // Fetch the price again - (price,) = rethPriceFeed.fetchPrice(); - - // Check price was lastGoodPrice - assertEq(price, rethPriceFeed.lastGoodPrice()); - - // Now make lastGoodPrice massive, and greater than ETHUSDxCanonical - vm.store( - address(rethPriceFeed), - bytes32(uint256(1)), // 1st storage slot where lastGoodPrice is stored - bytes32(uint256(1e27)) // make lastGoodPrice equal to 1e27 i.e. 1 billion (with 18 decimal digits) - ); - assertEq(rethPriceFeed.lastGoodPrice(), 1e27); - - // Fetch the price again - (price,) = rethPriceFeed.fetchPrice(); - - // Check price is expected ETH-USDxCanonical - // Calc expected price if didnt fail, i.e. - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = rethToken.getExchangeRate(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; - - assertEq(price, priceIfDidntFail, "price not equal expected"); - } - - function testRETHPriceFeedShutsDownWhenBothOraclesFail() public { - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check oracle call didn't fail - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[1].troveManager.shutdownTime(), 0); - - // Make the RETH-ETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the branch is now shutdown - assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); - } - - function testRETHPriceFeedReturnsLastGoodPriceWhenBothOraclesFail() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Make the RETH-ETH oracle stale too - etchStaleMockToRethOracle(address(mockOracle).code); - (, mockPrice,, updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the PriceFeed's returned price equals the lastGoodPrice - assertEq(price, lastGoodPrice1); - - // Confirm the stored lastGoodPrice has not changed - assertEq(rethPriceFeed.lastGoodPrice(), lastGoodPrice1); - } - - function testRETHPriceSourceIsLastGoodPriceWhenBothOraclesFail() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Make the RETH-ETH oracle stale too - etchStaleMockToRethOracle(address(mockOracle).code); - (, mockPrice,, updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - rethPriceFeed.fetchPrice(); - - // Check using lastGoodPrice - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - } - - // --- WSTETH shutdown --- - - function testWSTETHPriceFeedShutsDownWhenExchangeRateFails() public { - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check oracle call didn't fail - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[1].troveManager.shutdownTime(), 0); - - // Make the exchange rate 0 - vm.etch(address(wstETH), address(mockWstethToken).code); - uint256 rate = wstETH.stEthPerToken(); - assertEq(rate, 0); - - // Fetch price again - (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check a call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the branch is now shutdown - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp, "timestamps not equal"); - } - - function testWSTETHPriceFeedReturnsLastGoodPriceWhenExchangeRateFails() public { - // Fetch price - wstethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Make the exchange rate 0 - vm.etch(address(wstETH), address(mockWstethToken).code); - uint256 rate = wstETH.stEthPerToken(); - assertEq(rate, 0); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check a call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the PriceFeed's returned price equals the lastGoodPrice - assertEq(price, lastGoodPrice1); - - // Confirm the stored lastGoodPrice has not changed - assertEq(wstethPriceFeed.lastGoodPrice(), lastGoodPrice1); - } - - function testWSTETHPriceSourceIsLastGoodPricePriceWhenETHUSDOracleFails() public { - // Fetch price - (uint256 price1,) = wstethPriceFeed.fetchPrice(); - assertGt(price1, 0, "price is 0"); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - assertGt(mockPrice, 0, "mockPrice 0"); - - // Fetch price again - (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check ncall failed - assertTrue(oracleFailedWhileBranchLive); - - // Check using lastGoodPrice - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - } - - function testWSTETHPriceFeedReturnsLastGoodPriceWhenETHUSDOracleFails() public { - // Fetch price - (uint256 price1,) = wstethPriceFeed.fetchPrice(); - assertGt(price1, 0, "price is 0"); - - uint256 lastGoodPriceBeforeFail = wstethPriceFeed.lastGoodPrice(); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - assertGt(mockPrice, 0, "mockPrice 0"); - - // Fetch price again - (uint256 price2, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check oracle failed in this call - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the PriceFeed's returned price equals the stored lastGoodPrice - assertEq(price2, lastGoodPriceBeforeFail); - // Confirm the stored last good price didn't change - assertEq(lastGoodPriceBeforeFail, wstethPriceFeed.lastGoodPrice()); - } - - function testWSTETHPriceDoesShutsDownWhenETHUSDOracleFails() public { - // Fetch price - (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), 0); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc did fail - assertTrue(oracleFailedWhileBranchLive); - - // Confirm branch is shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); - } - - function testWSTETHPriceShutdownWhenSTETHUSDOracleFails() public { - // Fetch price - (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), 0); - - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that this time the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Confirm branch is now shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); - } - - function testFetchPriceReturnsMinETHUSDxCanonicalAndLastGoodPriceWhenSTETHUSDOracleFails() public { - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Calc expected price i.e. ETH-USD x canonical - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = wstETH.stEthPerToken(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - - uint256 expectedPrice = LiquityMath._min(wstethPriceFeed.lastGoodPrice(), ethUsdPrice * exchangeRate / 1e18); - - assertEq(price, expectedPrice, "price not expected price"); - } - - function testSTETHPriceSourceIsETHUSDxCanonicalWhenSTETHUSDOracleFails() public { - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price - (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Check using ETHUSDxCanonical - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); - } - - function testSTETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenETHUSDOracleFails() public { - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Check using ETHUSDxCanonical - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); - - uint256 lastGoodPrice = wstethPriceFeed.lastGoodPrice(); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Calc expected price if didnt fail, i.e. ETH-USD x canonical - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = wstETH.stEthPerToken(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; - - // These should differ since the mock oracle's price should not equal the previous real price - assertNotEq(priceIfDidntFail, lastGoodPrice, "price if didnt fail == lastGoodPrice"); - - // Now fetch the price - (price, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check using lastGoodPrice - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - - // This should be false, since the branch is already shutdown and not live - assertFalse(oracleFailedWhileBranchLive); - - // Confirm the returned price is the last good price - assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); - } - - function testSTETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenExchangeRateFails() public { - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq( - uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary), "not using primary" - ); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive, "primary oracle calc didnt fail"); - - // Check using ETHUSDxCanonical - assertEq( - uint8(wstethPriceFeed.priceSource()), - uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical), - "not using ethusdxcanonical" - ); - - uint256 lastGoodPrice = wstethPriceFeed.lastGoodPrice(); - - // Make the exchange rate return 0 - vm.etch(address(wstETH), address(mockWstethToken).code); - uint256 rate = wstETH.stEthPerToken(); - assertEq(rate, 0, "mock rate non-zero"); - - // Now fetch the price - (price, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // This should be false, since the branch is already shutdown and not live - assertFalse(oracleFailedWhileBranchLive); - - // Confirm the returned price is the last good price - assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); - // Check we've switched to lastGoodPrice source - assertEq( - uint8(wstethPriceFeed.priceSource()), - uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice), - "not using lastGoodPrice" - ); - } - - function testSTETHWhenUsingETHUSDxCanonicalRemainsShutDownWhenETHUSDOracleFails() public { - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Check branch is live, not shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), 0); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Check using ETHUSDxCanonical - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); - - // Check branch is now shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Now fetch the price again - (price, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check using lastGoodPrice - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - - // Check branch is still down - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); - } - - function testSTETHWhenUsingETHUSDxCanonicalReturnsMinOfLastGoodPriceAndETHUSDxCanonical() public { - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Check using ETHUSDxCanonical - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); - - // Make lastGoodPrice tiny, and below ETHUSDxCanonical - vm.store( - address(wstethPriceFeed), - bytes32(uint256(1)), // 1st storage slot where lastGoodPrice is stored - bytes32(uint256(1)) // make lastGoodPrice equal to 1 wei - ); - assertEq(wstethPriceFeed.lastGoodPrice(), 1); - - // Fetch the price again - (price,) = wstethPriceFeed.fetchPrice(); - - // Check price was lastGoodPrice - assertEq(price, wstethPriceFeed.lastGoodPrice()); - - // Now make lastGoodPrice massive, and greater than ETHUSDxCanonical - vm.store( - address(wstethPriceFeed), - bytes32(uint256(1)), // 1st storage slot where lastGoodPrice is stored - bytes32(uint256(1e27)) // make lastGoodPrice equal to 1e27 i.e. 1 billion (with 18 decimal digits) - ); - assertEq(wstethPriceFeed.lastGoodPrice(), 1e27); - - // Fetch the price again - (price,) = wstethPriceFeed.fetchPrice(); - - // Check price is expected ETH-USDxCanonical - // Calc expected price if didnt fail, i.e. - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = wstETH.stEthPerToken(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; - - assertEq(price, priceIfDidntFail); - } - - function testWSTETHPriceShutdownWhenBothOraclesFail() public { - // Fetch price - (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), 0); - - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (, mockPrice,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that this time the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Confirm branch is now shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); - } - - function testWSTETHPriceFeedReturnsLastGoodPriceWhenBothOraclesFail() public { - // Fetch price - wstethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (, mockPrice,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the PriceFeed's returned price equals the lastGoodPrice - assertEq(price, lastGoodPrice1); - - // Confirm the stored lastGoodPrice has not changed - assertEq(wstethPriceFeed.lastGoodPrice(), lastGoodPrice1); - } - - function testWSTETHPriceSourceIsLastGoodPriceWhenBothOraclesFail() public { - // Fetch price - wstethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (, mockPrice,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - wstethPriceFeed.fetchPrice(); - - // Check using lastGoodPrice - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - } - - // --- redemptions --- - - function testNormalWETHRedemptionDoesNotHitShutdownBranch() public { - // Fetch price - wethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(wethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[0].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, bool oracleFailedWhileBranchLive) = wethPriceFeed.fetchPrice(); - assertTrue(oracleFailedWhileBranchLive); - // Confirm branch shutdown - assertEq(contractsArray[0].troveManager.shutdownTime(), block.timestamp); - - uint256 totalBoldRedeemAmount = 100e18; - uint256 branch0DebtBefore = contractsArray[0].activePool.getBoldDebt(); - assertGt(branch0DebtBefore, 0); - - uint256 boldBalBefore_A = boldToken.balanceOf(A); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm A lost no BOLD - assertEq(boldToken.balanceOf(A), boldBalBefore_A); - - // Confirm WETH branch did not get redeemed from - assertEq(contractsArray[0].activePool.getBoldDebt(), branch0DebtBefore); - } - - function testNormalRETHRedemptionDoesNotHitShutdownBranch() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Make the RETH-ETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - assertTrue(oracleFailedWhileBranchLive); - // Confirm RETH branch shutdown - assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); - - uint256 totalBoldRedeemAmount = 100e18; - uint256 branch1DebtBefore = contractsArray[1].activePool.getBoldDebt(); - assertGt(branch1DebtBefore, 0); - - uint256 boldBalBefore_A = boldToken.balanceOf(A); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm A lost no BOLD - assertEq(boldToken.balanceOf(A), boldBalBefore_A); - - // Confirm RETH branch did not get redeemed from - assertEq(contractsArray[1].activePool.getBoldDebt(), branch1DebtBefore); - } - - function testNormalWSTETHRedemptionDoesNotHitShutdownBranch() public { - // Fetch price - wstethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[2].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - assertTrue(oracleFailedWhileBranchLive); - // Confirm RETH branch shutdown - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); - - uint256 totalBoldRedeemAmount = 100e18; - uint256 branch2DebtBefore = contractsArray[2].activePool.getBoldDebt(); - assertGt(branch2DebtBefore, 0); - - uint256 boldBalBefore_A = boldToken.balanceOf(A); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm A lost no BOLD - assertEq(boldToken.balanceOf(A), boldBalBefore_A); - - // Confirm RETH branch did not get redeemed from - assertEq(contractsArray[2].activePool.getBoldDebt(), branch2DebtBefore); - } - - function testRedemptionOfWETHUsesETHUSDMarketforPrimaryPrice() public { - // Fetch price - wethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(wethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[0].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Expected price used for primary calc: ETH-USD market price - uint256 expectedPrice = _getLatestAnswerFromOracle(ethOracle); - assertGt(expectedPrice, 0); - - // Calc expected fee based on price - uint256 totalBoldRedeemAmount = 100e18; - uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; - assertGt(totalCorrespondingColl, 0); - - uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) - * DECIMAL_PRECISION / totalBoldRedeemAmount; - assertGt(redemptionFeePct, 0); - - uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; - - uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; - assertGt(expectedCollDelta, 0); - - uint256 branch0DebtBefore = contractsArray[0].activePool.getBoldDebt(); - assertGt(branch0DebtBefore, 0); - uint256 A_collBefore = contractsArray[0].collToken.balanceOf(A); - assertGt(A_collBefore, 0); - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm WETH branch got redeemed from - assertEq(contractsArray[0].activePool.getBoldDebt(), branch0DebtBefore - totalBoldRedeemAmount); - - // Confirm the received amount coll is the expected amount (i.e. used the expected price) - assertEq(contractsArray[0].collToken.balanceOf(A), A_collBefore + expectedCollDelta); - } - - function testRedemptionOfWSTETHUsesMaxETHUSDMarketandWSTETHUSDMarketForPrimaryPriceWhenWithin1pct() public { - // Fetch price - wstethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[2].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Expected price used for primary calc: ETH-USD market price - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 stethUsdPrice = _getLatestAnswerFromOracle(stethOracle); - assertNotEq(ethUsdPrice, stethUsdPrice, "raw prices equal"); - // Check STETH-USD is within 1ct of ETH-USD - uint256 max = (1e18 + 1e16) * ethUsdPrice / 1e18; - uint256 min = (1e18 - 1e16) * ethUsdPrice / 1e18; - assertGe(stethUsdPrice, min); - assertLe(stethUsdPrice, max); - - // USD_per_WSTETH = USD_per_STETH(or_per_ETH) * STETH_per_WSTETH - uint256 expectedPrice = LiquityMath._max(ethUsdPrice, stethUsdPrice) * wstETH.stEthPerToken() / 1e18; - assertGt(expectedPrice, 0, "expected price not 0"); - - // Calc expected fee based on price - uint256 totalBoldRedeemAmount = 100e18; - uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; - assertGt(totalCorrespondingColl, 0, "coll not 0"); - - uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) - * DECIMAL_PRECISION / totalBoldRedeemAmount; - assertGt(redemptionFeePct, 0, "fee not 0"); - - uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; - - uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; - assertGt(expectedCollDelta, 0, "delta not 0"); - - uint256 branch2DebtBefore = contractsArray[2].activePool.getBoldDebt(); - assertGt(branch2DebtBefore, 0); - uint256 A_collBefore = contractsArray[2].collToken.balanceOf(A); - assertGt(A_collBefore, 0); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm WSTETH branch got redeemed from - assertEq(contractsArray[2].activePool.getBoldDebt(), branch2DebtBefore - totalBoldRedeemAmount); - - // Confirm the received amount coll is the expected amount (i.e. used the expected price) - assertEq(contractsArray[2].collToken.balanceOf(A), A_collBefore + expectedCollDelta); - } - - function testRedemptionOfWSTETHUsesMinETHUSDMarketandWSTETHUSDMarketForPrimaryPriceWhenNotWithin1pct() public { - // Fetch price - console.log("test::first wsteth pricefeed call"); - wstethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[2].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Get the raw ETH-USD price (at 8 decimals) for comparison - (, int256 rawEthUsdPrice,,,) = ethOracle.latestRoundData(); - assertGt(rawEthUsdPrice, 0, "eth-usd price not 0"); - - // Replace the STETH-USD Oracle's code with the mock oracle's code - etchStaleMockToStethOracle(address(mockOracle).code); - ChainlinkOracleMock mock = ChainlinkOracleMock(address(stethOracle)); - // Reduce STETH-USD price to 90% of ETH-USD price. Use 8 decimal precision on the oracle. - mock.setPrice(int256(rawEthUsdPrice * 90e6 / 1e8)); - // Make it fresh - mock.setUpdatedAt(block.timestamp); - // STETH-USD price has 8 decimals - mock.setDecimals(8); - - assertEq(contractsArray[2].troveManager.shutdownTime(), 0, "is shutdown"); - - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 stethUsdPrice = _getLatestAnswerFromOracle(stethOracle); - console.log(stethUsdPrice, "test stehUsdPrice after replacement"); - console.log(ethUsdPrice, "test ethUsdPrice after replacement"); - console.log(ethUsdPrice * 90e16 / 1e18, "test ethUsdPrice * 90e16 / 1e18"); - - // Confirm that STETH-USD is lower than ETH-USD - assertLt(stethUsdPrice, ethUsdPrice, "steth-usd not < eth-usd"); - - // USD_per_STETH = USD_per_STETH * STETH_per_WSTETH - // Use STETH-USD as expected price since it is out of range of ETH-USD - uint256 expectedPrice = stethUsdPrice * wstETH.stEthPerToken() / 1e18; - assertGt(expectedPrice, 0, "expected price not 0"); - - // Calc expected fee based on price - uint256 totalBoldRedeemAmount = 100e18; - uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; - assertGt(totalCorrespondingColl, 0, "coll not 0"); - - uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) - * DECIMAL_PRECISION / totalBoldRedeemAmount; - assertGt(redemptionFeePct, 0, "fee not 0"); - - uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; - - uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; - assertGt(expectedCollDelta, 0, "delta not 0"); - - uint256 branch2DebtBefore = contractsArray[2].activePool.getBoldDebt(); - assertGt(branch2DebtBefore, 0); - uint256 A_collBefore = contractsArray[2].collToken.balanceOf(A); - assertGt(A_collBefore, 0); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - assertEq(contractsArray[2].troveManager.shutdownTime(), 0, "is shutdown"); - - // Confirm WSTETH branch got redeemed from - assertEq( - contractsArray[2].activePool.getBoldDebt(), - branch2DebtBefore - totalBoldRedeemAmount, - "remaining branch debt wrong" - ); - - // Confirm the received amount coll is the expected amount (i.e. used the expected price) - assertEq( - contractsArray[2].collToken.balanceOf(A), A_collBefore + expectedCollDelta, "remaining branch coll wrong" - ); - } - - function testRedemptionOfRETHUsesMaxCanonicalAndMarketforPrimaryPriceWhenWithin2pct() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Expected price used for primary calc: ETH-USD market price - uint256 canonicalRethRate = rethToken.getExchangeRate(); - uint256 marketRethPrice = _getLatestAnswerFromOracle(rethOracle); - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - assertNotEq(canonicalRethRate, marketRethPrice, "raw price and rate equal"); - - // Check market is within 2pct of max; - uint256 max = (1e18 + 2e16) * canonicalRethRate / 1e18; - uint256 min = (1e18 - 2e16) * canonicalRethRate / 1e18; - assertGe(marketRethPrice, min); - assertLe(marketRethPrice, max); - - // USD_per_WSTETH = USD_per_STETH(or_per_ETH) * STETH_per_WSTETH - uint256 expectedPrice = LiquityMath._max(canonicalRethRate, marketRethPrice) * ethUsdPrice / 1e18; - assertGt(expectedPrice, 0, "expected price not 0"); - - // Calc expected fee based on price - uint256 totalBoldRedeemAmount = 100e18; - uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; - assertGt(totalCorrespondingColl, 0, "coll not 0"); - - uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) - * DECIMAL_PRECISION / totalBoldRedeemAmount; - assertGt(redemptionFeePct, 0, "fee not 0"); - - uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; - - uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; - assertGt(expectedCollDelta, 0, "delta not 0"); - - uint256 branch1DebtBefore = contractsArray[1].activePool.getBoldDebt(); - assertGt(branch1DebtBefore, 0); - uint256 A_collBefore = contractsArray[1].collToken.balanceOf(A); - assertGt(A_collBefore, 0); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm RETH branch got redeemed from - assertEq(contractsArray[1].activePool.getBoldDebt(), branch1DebtBefore - totalBoldRedeemAmount); - - // Confirm the received amount coll is the expected amount (i.e. used the expected price) - assertEq(contractsArray[1].collToken.balanceOf(A), A_collBefore + expectedCollDelta); - } - - function testRedemptionOfRETHUsesMinCanonicalAndMarketforPrimaryPriceWhenDeviationGreaterThan2pct() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - vm.stopPrank(); - - // Replace the RETH Oracle's code with the mock oracle's code - etchStaleMockToRethOracle(address(mockOracle).code); - ChainlinkOracleMock mock = ChainlinkOracleMock(address(rethOracle)); - // Set ETH_per_RETH market price to 0.95 - mock.setPrice(95e16); - // Make it fresh - mock.setUpdatedAt(block.timestamp); - // RETH-ETH price has 18 decimals - mock.setDecimals(18); - - (, int256 price,,,) = rethOracle.latestRoundData(); - // Confirm that RETH oracle now returns the artificial low price - assertEq(price, 95e16, "reth-eth price not 0.95"); - - // // Expected price used for primary calc: ETH-USD market price - uint256 canonicalRethRate = rethToken.getExchangeRate(); - uint256 marketRethPrice = _getLatestAnswerFromOracle(rethOracle); - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - assertNotEq(canonicalRethRate, marketRethPrice, "raw price and rate equal"); - - // Check market is not within 2pct of canonical - uint256 min = (1e18 - 2e16) * canonicalRethRate / 1e18; - assertLe(marketRethPrice, min, "market reth-eth price not < min"); - - // USD_per_WSTETH = USD_per_STETH(or_per_ETH) * STETH_per_WSTETH - uint256 expectedPrice = LiquityMath._min(canonicalRethRate, marketRethPrice) * ethUsdPrice / 1e18; - assertGt(expectedPrice, 0, "expected price not 0"); - - // Calc expected fee based on price, i.e. the minimum - uint256 totalBoldRedeemAmount = 100e18; - uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; - assertGt(totalCorrespondingColl, 0, "coll not 0"); - - uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) - * DECIMAL_PRECISION / totalBoldRedeemAmount; - assertGt(redemptionFeePct, 0, "fee not 0"); - - uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; - - uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; - assertGt(expectedCollDelta, 0, "delta not 0"); - - uint256 branch1DebtBefore = contractsArray[1].activePool.getBoldDebt(); - assertGt(branch1DebtBefore, 0); - uint256 A_collBefore = contractsArray[1].collToken.balanceOf(A); - assertGt(A_collBefore, 0); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm RETH branch got redeemed from - assertEq( - contractsArray[1].activePool.getBoldDebt(), - branch1DebtBefore - totalBoldRedeemAmount, - "active debt != branch - redeemed" - ); - - // Confirm the received amount coll is the expected amount (i.e. used the expected price) - assertEq(contractsArray[1].collToken.balanceOf(A), A_collBefore + expectedCollDelta, "A's coll didn't change"); - } - - // --- Low gas market oracle reverts --- - - function testRevertLowGasSTETHOracle() public { - // Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k - (bool success,) = address(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(success); - - // Etch gas guzzler to the oracle - etchGasGuzzlerToStethOracle(address(gasGuzzlerOracle).code); - - // After etching the gas guzzler to the oracle, confirm the same call with 500k gas now reverts due to OOG - vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); - (bool revertAsExpected,) = address(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(revertAsExpected); - } - - function testRevertLowGasRETHOracle() public { - // Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k - (bool success,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(success); - - // Etch gas guzzler to the oracle - etchGasGuzzlerToRethOracle(address(gasGuzzlerOracle).code); - - // After etching the gas guzzler to the oracle, confirm the same call with 500k gas now reverts due to OOG - vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); - (bool revertAsExpected,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(revertAsExpected); - } - - function testRevertLowGasETHOracle() public { - // Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k - (bool success,) = address(wethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(success); - - // Etch gas guzzler to the oracle - etchGasGuzzlerToEthOracle(address(gasGuzzlerOracle).code); - - // After etching the gas guzzler to the oracle, confirm the same call with 500k gas now reverts due to OOG - vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); - (bool revertAsExpected,) = address(wethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(revertAsExpected); - } - - // --- Test with a gas guzzler token, and confirm revert --- - - function testRevertLowGasWSTETHToken() public { - // Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k - (bool success,) = address(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(success); - - // Etch gas guzzler to the LST - etchGasGuzzlerMockToWstethToken(address(gasGuzzlerToken).code); - - // After etching the gas guzzler to the LST, confirm the same call with 500k gas now reverts due to OOG - vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); - (bool revertsAsExpected,) = address(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(revertsAsExpected); - } - - function testRevertLowGasRETHToken() public { - // Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k - (bool success,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(success); - - // Etch gas guzzler to the LST - etchGasGuzzlerMockToRethToken(address(gasGuzzlerToken).code); - - // After etching the gas guzzler to the LST, confirm the same call with 500k gas now reverts due to OOG - vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); - (bool revertsAsExpected,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(revertsAsExpected); - } - - /* - function testTMCodeSize() public { - uint256 codeSize = address(contractsArray[0].troveManager).code.length; - uint256 left = 24576 - codeSize; - console.log(codeSize, "TM contract size"); - console.log(left, "space left in TM"); - } - */ - - function testRETHRedemptionOnlyHitsTrovesAtICRGte100() public { - Vars memory vars; - // Set two mock market oracles: RETH-ETH, and ETH-USD - ChainlinkOracleMock mockRETHOracle = etchMockToRethOracle(); - ChainlinkOracleMock mockETHOracle = etchMockToEthOracle(); - - vars.usdPerEthMarket = 2000e8; // 2000 usd, 8 decimal - // Set 1 ETH = 2000 USD on market oracle - mockETHOracle.setPrice(vars.usdPerEthMarket); - - vars.ethPerRethLST = rethToken.getExchangeRate(); - - // Make market RETH price 1% lower than LST exchange rate - vars.ethPerRethMarket = int256(vars.ethPerRethLST) * 99 / 100; - mockRETHOracle.setPrice(vars.ethPerRethMarket); - - console.log(_getLatestAnswerFromOracle(rethOracle), "reth oracle latest answer"); - console.log(_getLatestAnswerFromOracle(ethOracle), "eth oracle latest answer"); - - // Open annchor Trove with high CR and 51m BOLD - vm.startPrank(A); - vars.troveId_A = contractsArray[1].borrowerOperations.openTrove( - A, 0, 1000_000 ether, 51_000_000e18, 0, 0, 5e16, 51_000_000e18, address(0), address(0), address(0) - ); - vm.stopPrank(); - - // Get the calculated RETH-USD price directly from the system - (vars.systemPrice,) = contractsArray[1].priceFeed.fetchPrice(); - - // Open 3 Troves with ICRs clustered together - vars.coll = 10 ether; - vars.debt_B = 10000e18 + 1e18; - vars.debt_C = 10000e18; - vars.debt_D = 10000e18 - 1e18; - - vm.startPrank(B); - vars.troveId_B = contractsArray[1].borrowerOperations.openTrove( - B, 0, vars.coll, vars.debt_B, 0, 0, 5e15, vars.debt_B, address(0), address(0), address(0) - ); - vm.stopPrank(); - - vm.startPrank(C); - vars.troveId_C = contractsArray[1].borrowerOperations.openTrove( - C, 0, vars.coll, vars.debt_C, 0, 0, 5e15, vars.debt_C, address(0), address(0), address(0) - ); - vm.stopPrank(); - - vm.startPrank(D); - vars.troveId_D = contractsArray[1].borrowerOperations.openTrove( - D, 0, vars.coll, vars.debt_D, 0, 0, 5e15, vars.debt_D, address(0), address(0), address(0) - ); - vm.stopPrank(); - - vars.ICR_C = contractsArray[1].troveManager.getCurrentICR(vars.troveId_C, vars.systemPrice); - - // Check ICRs are clustered - // console.log(contractsArray[1].troveManager.getCurrentICR(vars.troveId_A, vars.systemPrice), "A ICR"); - // console.log(contractsArray[1].troveManager.getCurrentICR(vars.troveId_A, vars.systemPrice), "A ICR"); - // console.log(contractsArray[1].troveManager.getCurrentICR(vars.troveId_B, vars.systemPrice), "B ICR"); - // console.log(contractsArray[1].troveManager.getCurrentICR(vars.troveId_C, vars.systemPrice), "C ICR"); - // console.log(contractsArray[1].troveManager.getCurrentICR(vars.troveId_D, vars.systemPrice), "D ICR"); - - // Scale the price down such that C has ICR ~100%, ceil division - vars.newEthPrice = vars.usdPerEthMarket * 1e18 / int256(vars.ICR_C) + 1; - mockETHOracle.setPrice(vars.newEthPrice); - - // Get new system price from PriceFeed - (vars.newSystemPrice,) = contractsArray[1].priceFeed.fetchPrice(); - // Calculate the new redemption price, given RETH-ETH market price is 1% greater than exchange rate - vars.newSystemRedemptionPrice = vars.newSystemPrice * 100 / 99; - - // Confirm ICR ordering - vars.ICR_A = contractsArray[1].troveManager.getCurrentICR(vars.troveId_A, vars.newSystemPrice); - vars.ICR_B = contractsArray[1].troveManager.getCurrentICR(vars.troveId_B, vars.newSystemPrice); - vars.ICR_C = contractsArray[1].troveManager.getCurrentICR(vars.troveId_C, vars.newSystemPrice); - vars.ICR_D = contractsArray[1].troveManager.getCurrentICR(vars.troveId_D, vars.newSystemPrice); - - // console.log(vars.ICR_A, "A ICR after price drop"); - // console.log(vars.ICR_B, "B ICR after price drop"); - // console.log(vars.ICR_C, "C ICR after price drop"); - // console.log(vars.ICR_D, "D ICR after price drop"); - - assertLt(vars.ICR_B, 1e18, "B ICR not < 100%"); - assertGt(vars.ICR_C, 1e18, "C ICR not > 100%"); - assertGt(vars.ICR_D, 1e18, "D ICR not > 100%"); - assertGt(vars.ICR_A, vars.ICR_D, "A ICR not > D ICR"); - - // TODO: Confirm that if we used the *redemption* price to calc ICRs, all ICRs would be > 100% - vars.redemptionICR_A = - contractsArray[1].troveManager.getCurrentICR(vars.troveId_A, vars.newSystemRedemptionPrice); - vars.redemptionICR_B = - contractsArray[1].troveManager.getCurrentICR(vars.troveId_B, vars.newSystemRedemptionPrice); - vars.redemptionICR_C = - contractsArray[1].troveManager.getCurrentICR(vars.troveId_C, vars.newSystemRedemptionPrice); - vars.redemptionICR_D = - contractsArray[1].troveManager.getCurrentICR(vars.troveId_D, vars.newSystemRedemptionPrice); - - // console.log(vars.redemptionICR_A, " vars.redemptionICR_A"); - // console.log(vars.redemptionICR_B, " vars.redemptionICR_B"); - // console.log(vars.redemptionICR_C, " vars.redemptionICR_C"); - // console.log(vars.redemptionICR_D, " vars.redemptionICR_D"); - - assertGe(vars.redemptionICR_A, 1e18, "A ICR not > 100%"); - assertGe(vars.redemptionICR_B, 1e18, "B ICR not > 100%"); - assertGe(vars.redemptionICR_C, 1e18, "C ICR not > 100%"); - assertGe(vars.redemptionICR_D, 1e18, "D ICR not > 100%"); - - // A deposits 25m to WETH and STETH SPs, making them fully backed - - // so A's redemption should now fully hits the RETH branch - vm.startPrank(A); - contractsArray[0].stabilityPool.provideToSP(25_000_000e18, false); - contractsArray[2].stabilityPool.provideToSP(25_000_000e18, false); - - vars.troveDataBefore_A = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_A); - vars.troveDataBefore_B = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_B); - vars.troveDataBefore_C = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_C); - vars.troveDataBefore_D = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_D); - - // A redeems. Expect redemption to hit Troves C, D, A and skip B - collateralRegistry.redeemCollateral(50000e18, 100, 1e18); - - vars.troveDataAfter_A = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_A); - vars.troveDataAfter_B = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_B); - vars.troveDataAfter_C = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_C); - vars.troveDataAfter_D = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_D); - - // Expect B's Trove to be untouched - assertEq(vars.troveDataAfter_B.entireDebt, vars.troveDataBefore_B.entireDebt, "B's debt not same after redeem"); - assertEq(vars.troveDataAfter_B.entireColl, vars.troveDataBefore_B.entireColl, "B's coll not same after redeem"); - - // Expect A, C and D to have been redeemed - assertLt(vars.troveDataAfter_A.entireDebt, vars.troveDataBefore_A.entireDebt, "A's debt not lower after redeem"); - assertLt(vars.troveDataAfter_A.entireColl, vars.troveDataBefore_A.entireColl, "A's coll not lower after redeem"); - // console.log(vars.troveDataAfter_A.entireDebt, "A after"); - // console.log(vars.troveDataBefore_A.entireDebt, "A before"); - assertLt(vars.troveDataAfter_C.entireDebt, vars.troveDataBefore_C.entireDebt, "C's debt not lower after redeem"); - assertLt(vars.troveDataAfter_C.entireColl, vars.troveDataBefore_C.entireColl, "C's coll not lower after redeem"); - assertLt(vars.troveDataAfter_D.entireDebt, vars.troveDataBefore_D.entireDebt, "D's debt not lower after redeem"); - assertLt(vars.troveDataAfter_D.entireColl, vars.troveDataBefore_D.entireColl, "D's coll not lower after redeem"); - - // console.log(vars.troveDataAfter_D.entireDebt, "D after"); - // console.log(vars.troveDataBefore_D.entireDebt, "D before"); - } - - function testSTETHRedemptionOnlyHitsTrovesAtICRGte100() public { - Vars memory vars; - // Set two mock market oracles: STETH-USD, and ETH-USD - ChainlinkOracleMock mockSTETHOracle = etchMockToStethOracle(); - ChainlinkOracleMock mockETHOracle = etchMockToEthOracle(); - - vars.usdPerEthMarket = 2000e8; // 2000 usd, 8 decimal - // Set 1 ETH = 2000 USD on market oracle - mockETHOracle.setPrice(vars.usdPerEthMarket); - - // Make market STETH-USD price 0.5% greater than market ETH-USD price - mockSTETHOracle.setPrice(vars.usdPerEthMarket * 1005 / 1000); - - console.log(_getLatestAnswerFromOracle(stethOracle), "steth oracle latest answer"); - console.log(_getLatestAnswerFromOracle(ethOracle), "eth oracle latest answer"); - - // Open anchor Trove with high CR and 51m BOLD - vm.startPrank(A); - vars.troveId_A = contractsArray[2].borrowerOperations.openTrove( - A, 0, 1000_000 ether, 51_000_000e18, 0, 0, 5e16, 51_000_000e18, address(0), address(0), address(0) - ); - vm.stopPrank(); - - // Get the calculated WSTETH-USD price directly from the system - (vars.systemPrice,) = contractsArray[2].priceFeed.fetchPrice(); - - // Open 3 Troves with ICRs clustered together - vars.coll = 10 ether; - vars.debt_B = 10000e18 + 1e18; - vars.debt_C = 10000e18; - vars.debt_D = 10000e18 - 1e18; - - vm.startPrank(B); - vars.troveId_B = contractsArray[2].borrowerOperations.openTrove( - B, 0, vars.coll, vars.debt_B, 0, 0, 5e15, vars.debt_B, address(0), address(0), address(0) - ); - vm.stopPrank(); - - vm.startPrank(C); - vars.troveId_C = contractsArray[2].borrowerOperations.openTrove( - C, 0, vars.coll, vars.debt_C, 0, 0, 5e15, vars.debt_C, address(0), address(0), address(0) - ); - vm.stopPrank(); - - vm.startPrank(D); - vars.troveId_D = contractsArray[2].borrowerOperations.openTrove( - D, 0, vars.coll, vars.debt_D, 0, 0, 5e15, vars.debt_D, address(0), address(0), address(0) - ); - vm.stopPrank(); - - vars.ICR_C = contractsArray[2].troveManager.getCurrentICR(vars.troveId_C, vars.systemPrice); - - // Check ICRs are clustered - console.log(contractsArray[2].troveManager.getCurrentICR(vars.troveId_A, vars.systemPrice), "A ICR"); - console.log(contractsArray[2].troveManager.getCurrentICR(vars.troveId_B, vars.systemPrice), "B ICR"); - console.log(contractsArray[2].troveManager.getCurrentICR(vars.troveId_C, vars.systemPrice), "C ICR"); - console.log(contractsArray[2].troveManager.getCurrentICR(vars.troveId_D, vars.systemPrice), "D ICR"); - - // Scale the ETH-USD price down such that C has ICR ~100% - vars.newEthPrice = vars.usdPerEthMarket * 1e18 / int256(vars.ICR_C) + 10; - mockETHOracle.setPrice(vars.newEthPrice); - // Scale STETH-USD price down too, keep it 0.5% above ETH-USD - mockSTETHOracle.setPrice(vars.newEthPrice * 1005 / 1000); - - // Get new system price from PriceFeed - (vars.newSystemPrice,) = contractsArray[2].priceFeed.fetchPrice(); - vars.newSystemRedemptionPrice = vars.newSystemPrice * 1005 / 1000; - - // console.log(_getLatestAnswerFromOracle(stethOracle), "steth oracle latest answer after price drop"); - // console.log(_getLatestAnswerFromOracle(ethOracle), "eth oracle latest answer after price drop"); - - // Confirm ICR ordering - vars.ICR_A = contractsArray[2].troveManager.getCurrentICR(vars.troveId_A, vars.newSystemPrice); - vars.ICR_B = contractsArray[2].troveManager.getCurrentICR(vars.troveId_B, vars.newSystemPrice); - vars.ICR_C = contractsArray[2].troveManager.getCurrentICR(vars.troveId_C, vars.newSystemPrice); - vars.ICR_D = contractsArray[2].troveManager.getCurrentICR(vars.troveId_D, vars.newSystemPrice); - - // console.log(vars.ICR_A, "A ICR after price drop"); - // console.log(vars.ICR_B, "B ICR after price drop"); - // console.log(vars.ICR_C, "C ICR after price drop"); - // console.log(vars.ICR_D, "D ICR after price drop"); - - assertLt(vars.ICR_B, 1e18, "B ICR not < 100%"); - assertGt(vars.ICR_C, 1e18, "C ICR not > 100%"); - assertGt(vars.ICR_D, 1e18, "D ICR not > 100%"); - assertGt(vars.ICR_A, vars.ICR_D, "A ICR not > D ICR"); - - // TODO: Confirm that if we used the *redemption* price to calc ICRs, all ICRs would be > 100% - vars.redemptionICR_A = - contractsArray[2].troveManager.getCurrentICR(vars.troveId_A, vars.newSystemRedemptionPrice); - vars.redemptionICR_B = - contractsArray[2].troveManager.getCurrentICR(vars.troveId_B, vars.newSystemRedemptionPrice); - vars.redemptionICR_C = - contractsArray[2].troveManager.getCurrentICR(vars.troveId_C, vars.newSystemRedemptionPrice); - vars.redemptionICR_D = - contractsArray[2].troveManager.getCurrentICR(vars.troveId_D, vars.newSystemRedemptionPrice); - - // console.log(vars.redemptionICR_A, " vars.redemptionICR_A"); - // console.log(vars.redemptionICR_B, " vars.redemptionICR_B"); - // console.log(vars.redemptionICR_C, " vars.redemptionICR_C"); - // console.log(vars.redemptionICR_D, " vars.redemptionICR_D"); - - assertGe(vars.redemptionICR_A, 1e18, "A ICR not > 100%"); - assertGe(vars.redemptionICR_B, 1e18, "B ICR not > 100%"); - assertGe(vars.redemptionICR_C, 1e18, "C ICR not > 100%"); - assertGe(vars.redemptionICR_D, 1e18, "D ICR not > 100%"); - - // A deposits 25m to WETH and RETH SPs, making them fully backed - - // so A's redemption should now fully hit the STETH branch - vm.startPrank(A); - contractsArray[0].stabilityPool.provideToSP(25_000_000e18, false); - contractsArray[1].stabilityPool.provideToSP(25_000_000e18, false); - - vars.troveDataBefore_A = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_A); - vars.troveDataBefore_B = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_B); - vars.troveDataBefore_C = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_C); - vars.troveDataBefore_D = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_D); - - // A redeems. Expect redemption to hit Troves C, D, A and skip B - collateralRegistry.redeemCollateral(50000e18, 100, 1e18); - - vars.troveDataAfter_A = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_A); - vars.troveDataAfter_B = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_B); - vars.troveDataAfter_C = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_C); - vars.troveDataAfter_D = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_D); - - // Expect B's Trove to be untouched - assertEq(vars.troveDataAfter_B.entireDebt, vars.troveDataBefore_B.entireDebt, "B's debt not same after redeem"); - assertEq(vars.troveDataAfter_B.entireColl, vars.troveDataBefore_B.entireColl, "B's coll not same after redeem"); - - // Expect A, C and D to have been redeemed - assertLt(vars.troveDataAfter_A.entireDebt, vars.troveDataBefore_A.entireDebt, "A's debt not lower after redeem"); - assertLt(vars.troveDataAfter_A.entireColl, vars.troveDataBefore_A.entireColl, "A's coll not lower after redeem"); - // console.log(vars.troveDataAfter_A.entireDebt, "A after"); - // console.log(vars.troveDataBefore_A.entireDebt, "A before"); - assertLt(vars.troveDataAfter_C.entireDebt, vars.troveDataBefore_C.entireDebt, "C's debt not lower after redeem"); - assertLt(vars.troveDataAfter_C.entireColl, vars.troveDataBefore_C.entireColl, "C's coll not lower after redeem"); - assertLt(vars.troveDataAfter_D.entireDebt, vars.troveDataBefore_D.entireDebt, "D's debt not lower after redeem"); - assertLt(vars.troveDataAfter_D.entireColl, vars.troveDataBefore_D.entireColl, "D's coll not lower after redeem"); - // console.log(vars.troveDataAfter_D.entireDebt, "D after"); - // console.log(vars.troveDataBefore_D.entireDebt, "D before"); - } - - // - More basic actions tests (adjust, close, etc) - // - liq tests (manipulate aggregator stored price) -} diff --git a/contracts/test/TestContracts/BaseTest.sol b/contracts/test/TestContracts/BaseTest.sol index ebdee4ade..84f1e7361 100644 --- a/contracts/test/TestContracts/BaseTest.sol +++ b/contracts/test/TestContracts/BaseTest.sol @@ -12,7 +12,7 @@ import "src/Interfaces/IStabilityPool.sol"; import "./BorrowerOperationsTester.t.sol"; import "./TroveManagerTester.t.sol"; import "src/Interfaces/ICollateralRegistry.sol"; -import "./PriceFeedTestnet.sol"; +import "./MockFXPriceFeed.sol"; import "src/Interfaces/IInterestRouter.sol"; import "src/GasPool.sol"; import "src/HintHelpers.sol"; @@ -55,7 +55,7 @@ contract BaseTest is TestAccounts, Logging, TroveId { IMetadataNFT metadataNFT; IBoldToken boldToken; ICollateralRegistry collateralRegistry; - IPriceFeedTestnet priceFeed; + IMockFXPriceFeed priceFeed; GasPool gasPool; IInterestRouter mockInterestRouter; IERC20 collToken; diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index c23376eb5..f6aa000d5 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -20,26 +20,20 @@ import "src/TroveNFT.sol"; import "src/NFTMetadata/MetadataNFT.sol"; import "src/CollateralRegistry.sol"; import "./MockInterestRouter.sol"; -import "./PriceFeedTestnet.sol"; +import "./MockFXPriceFeed.sol"; import "./MetadataDeployment.sol"; import "src/SystemParams.sol"; import {WETHTester} from "./WETHTester.sol"; import {ERC20Faucet} from "./ERC20Faucet.sol"; -import "src/PriceFeeds/WETHPriceFeed.sol"; -import "src/PriceFeeds/WSTETHPriceFeed.sol"; -import "src/PriceFeeds/RETHPriceFeed.sol"; - import "forge-std/console2.sol"; uint256 constant _24_HOURS = 86400; uint256 constant _48_HOURS = 172800; -// TODO: Split dev and mainnet contract TestDeployer is MetadataDeployment { IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); - IWETH constant WETH_MAINNET = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); bytes32 constant SALT = keccak256("LiquityV2"); @@ -57,7 +51,7 @@ contract TestDeployer is MetadataDeployment { IStabilityPool stabilityPool; ITroveManagerTester troveManager; // Tester ITroveNFT troveNFT; - IPriceFeedTestnet priceFeed; // Tester + IMockFXPriceFeed priceFeed; // Tester IInterestRouter interestRouter; IERC20Metadata collToken; LiquityContractsDevPools pools; @@ -115,41 +109,6 @@ contract TestDeployer is MetadataDeployment { uint256 i; } - struct DeploymentResultMainnet { - LiquityContracts[] contractsArray; - ExternalAddresses externalAddresses; - CollateralRegistryTester collateralRegistry; - IBoldToken boldToken; - HintHelpers hintHelpers; - MultiTroveGetter multiTroveGetter; - ISystemParams systemParams; - } - - struct DeploymentVarsMainnet { - OracleParams oracleParams; - uint256 numCollaterals; - IERC20Metadata[] collaterals; - IAddressesRegistry[] addressesRegistries; - ITroveManager[] troveManagers; - IPriceFeed[] priceFeeds; - bytes bytecode; - address boldTokenAddress; - uint256 i; - } - - struct DeploymentParamsMainnet { - uint256 branch; - IERC20Metadata collToken; - IPriceFeed priceFeed; - IBoldToken boldToken; - ICollateralRegistry collateralRegistry; - IERC20Metadata gasToken; - IAddressesRegistry addressesRegistry; - address troveManagerAddress; - IHintHelpers hintHelpers; - IMultiTroveGetter multiTroveGetter; - } - struct ExternalAddresses { address ETHOracle; address STETHOracle; @@ -426,7 +385,7 @@ contract TestDeployer is MetadataDeployment { // Deploy all contracts, using testers for TM and PriceFeed contracts.addressesRegistry = _addressesRegistry; - contracts.priceFeed = new PriceFeedTestnet(); + contracts.priceFeed = new MockFXPriceFeed(); contracts.interestRouter = new MockInterestRouter(); // Deploy Metadata @@ -486,9 +445,7 @@ contract TestDeployer is MetadataDeployment { collToken: _collToken, gasToken: _gasToken, // TODO: add liquidity strategy - liquidityStrategy: makeAddr("liquidityStrategy"), - // TODO: add watchdog address - watchdogAddress: makeAddr("watchdog") + liquidityStrategy: makeAddr("liquidityStrategy") }); contracts.addressesRegistry.setAddresses(addressVars); @@ -522,247 +479,4 @@ contract TestDeployer is MetadataDeployment { address(contracts.activePool) ); } - - // Creates individual PriceFeed contracts based on oracle addresses. - // Still uses mock collaterals rather than real mainnet WETH and LST addresses. - - function deployAndConnectContractsMainnet(TroveManagerParams[] memory _troveManagerParamsArray) - public - returns (DeploymentResultMainnet memory result) - { - DeploymentVarsMainnet memory vars; - - result.externalAddresses.ETHOracle = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; - result.externalAddresses.RETHOracle = 0x536218f9E9Eb48863970252233c8F271f554C2d0; - result.externalAddresses.STETHOracle = 0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8; - result.externalAddresses.WSTETHToken = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; - - result.externalAddresses.RETHToken = 0xae78736Cd615f374D3085123A210448E74Fc6393; - - vars.oracleParams.ethUsdStalenessThreshold = _24_HOURS; - vars.oracleParams.stEthUsdStalenessThreshold = _24_HOURS; - vars.oracleParams.rEthEthStalenessThreshold = _48_HOURS; - - // Deploy System Params - // TODO(@bayological): Implement - result.systemParams = _deploySystemParamsMainnet(); - - // Colls: WETH, WSTETH, RETH - vars.numCollaterals = 3; - result.contractsArray = new LiquityContracts[](vars.numCollaterals); - vars.collaterals = new IERC20Metadata[](vars.numCollaterals); - vars.addressesRegistries = new IAddressesRegistry[](vars.numCollaterals); - vars.troveManagers = new ITroveManager[](vars.numCollaterals); - address troveManagerAddress; - - // Deploy Bold - vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(address(this))); - vars.boldTokenAddress = getAddress(address(this), vars.bytecode, SALT); - result.boldToken = new BoldToken{salt: SALT}(address(this)); - assert(address(result.boldToken) == vars.boldTokenAddress); - - // WETH - IWETH WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - vars.collaterals[0] = WETH; - (vars.addressesRegistries[0], troveManagerAddress) = - _deployAddressesRegistryMainnet(result.systemParams, _troveManagerParamsArray[0]); - vars.troveManagers[0] = ITroveManager(troveManagerAddress); - - // RETH - vars.collaterals[1] = IERC20Metadata(0xae78736Cd615f374D3085123A210448E74Fc6393); - (vars.addressesRegistries[1], troveManagerAddress) = - _deployAddressesRegistryMainnet(result.systemParams, _troveManagerParamsArray[1]); - vars.troveManagers[1] = ITroveManager(troveManagerAddress); - - // WSTETH - vars.collaterals[2] = IERC20Metadata(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); - (vars.addressesRegistries[2], troveManagerAddress) = - _deployAddressesRegistryMainnet(result.systemParams, _troveManagerParamsArray[2]); - vars.troveManagers[2] = ITroveManager(troveManagerAddress); - - // Deploy registry and register the TMs - result.collateralRegistry = new CollateralRegistryTester(result.boldToken, vars.collaterals, vars.troveManagers, result.systemParams); - - result.hintHelpers = new HintHelpers(result.collateralRegistry, result.systemParams); - result.multiTroveGetter = new MultiTroveGetter(result.collateralRegistry); - - // Deploy each set of core contracts - for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { - DeploymentParamsMainnet memory params; - params.branch = vars.i; - params.collToken = vars.collaterals[vars.i]; - params.boldToken = result.boldToken; - params.collateralRegistry = result.collateralRegistry; - params.gasToken = vars.collaterals[0]; - params.addressesRegistry = vars.addressesRegistries[vars.i]; - params.troveManagerAddress = address(vars.troveManagers[vars.i]); - params.hintHelpers = result.hintHelpers; - params.multiTroveGetter = result.multiTroveGetter; - result.contractsArray[vars.i] = - _deployAndConnectCollateralContractsMainnet(params, result.externalAddresses, vars.oracleParams, result.systemParams); - } - - result.boldToken.setCollateralRegistry(address(result.collateralRegistry)); - } - - function _deployAddressesRegistryMainnet(ISystemParams _systemParams, TroveManagerParams memory _troveManagerParams) - internal - returns (IAddressesRegistry, address) - { - IAddressesRegistry addressesRegistry = new AddressesRegistry(address(this)); - address troveManagerAddress = - getAddress(address(this), getBytecode(type(TroveManager).creationCode, address(addressesRegistry), address(_systemParams)), SALT); - - return (addressesRegistry, troveManagerAddress); - } - - function _deployAndConnectCollateralContractsMainnet( - DeploymentParamsMainnet memory _params, - ExternalAddresses memory _externalAddresses, - OracleParams memory _oracleParams, - ISystemParams _systemParams - ) internal returns (LiquityContracts memory contracts) { - LiquityContractAddresses memory addresses; - contracts.collToken = _params.collToken; - contracts.systemParams = _systemParams; - contracts.interestRouter = new MockInterestRouter(); - - contracts.addressesRegistry = _params.addressesRegistry; - - // Deploy Metadata - MetadataNFT metadataNFT = deployMetadata(SALT); - addresses.metadataNFT = getAddress( - address(this), getBytecode(type(MetadataNFT).creationCode, address(initializedFixedAssetReader)), SALT - ); - assert(address(metadataNFT) == addresses.metadataNFT); - - // Pre-calc addresses - bytes32 stabilityPoolSalt = keccak256(abi.encodePacked(address(contracts.addressesRegistry))); - addresses.borrowerOperations = getAddress( - address(this), - getBytecode(type(BorrowerOperationsTester).creationCode, address(contracts.addressesRegistry), address(_systemParams)), - SALT - ); - addresses.troveManager = _params.troveManagerAddress; - addresses.troveNFT = getAddress( - address(this), getBytecode(type(TroveNFT).creationCode, address(contracts.addressesRegistry)), SALT - ); - addresses.stabilityPool = - getAddress(address(this), getBytecode(type(StabilityPool).creationCode, false, address(_systemParams)), stabilityPoolSalt); - addresses.activePool = getAddress( - address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry), address(_systemParams)), SALT - ); - addresses.defaultPool = getAddress( - address(this), getBytecode(type(DefaultPool).creationCode, address(contracts.addressesRegistry)), SALT - ); - addresses.gasPool = getAddress( - address(this), getBytecode(type(GasPool).creationCode, address(contracts.addressesRegistry)), SALT - ); - addresses.collSurplusPool = getAddress( - address(this), getBytecode(type(CollSurplusPool).creationCode, address(contracts.addressesRegistry)), SALT - ); - addresses.sortedTroves = getAddress( - address(this), getBytecode(type(SortedTroves).creationCode, address(contracts.addressesRegistry)), SALT - ); - - contracts.priceFeed = - _deployPriceFeed(_params.branch, _externalAddresses, _oracleParams, addresses.borrowerOperations); - - // Deploy contracts - IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ - borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), - troveManager: ITroveManager(addresses.troveManager), - troveNFT: ITroveNFT(addresses.troveNFT), - metadataNFT: IMetadataNFT(addresses.metadataNFT), - stabilityPool: IStabilityPool(addresses.stabilityPool), - priceFeed: contracts.priceFeed, - activePool: IActivePool(addresses.activePool), - defaultPool: IDefaultPool(addresses.defaultPool), - gasPoolAddress: addresses.gasPool, - collSurplusPool: ICollSurplusPool(addresses.collSurplusPool), - sortedTroves: ISortedTroves(addresses.sortedTroves), - interestRouter: contracts.interestRouter, - hintHelpers: _params.hintHelpers, - multiTroveGetter: _params.multiTroveGetter, - collateralRegistry: _params.collateralRegistry, - boldToken: _params.boldToken, - collToken: _params.collToken, - gasToken: _params.gasToken, - // TODO: add liquidity strategy - liquidityStrategy: makeAddr("liquidityStrategy"), - // TODO: add watchdog address - watchdogAddress: makeAddr("watchdog") - }); - contracts.addressesRegistry.setAddresses(addressVars); - - contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry, _systemParams); - contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry, _systemParams); - contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); - contracts.stabilityPool = new StabilityPool{salt: stabilityPoolSalt}(false, _systemParams); - contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, _systemParams); - contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); - contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); - contracts.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); - contracts.sortedTroves = new SortedTroves{salt: SALT}(contracts.addressesRegistry); - - assert(address(contracts.borrowerOperations) == addresses.borrowerOperations); - assert(address(contracts.troveManager) == addresses.troveManager); - assert(address(contracts.troveNFT) == addresses.troveNFT); - assert(address(contracts.stabilityPool) == addresses.stabilityPool); - assert(address(contracts.activePool) == addresses.activePool); - assert(address(contracts.defaultPool) == addresses.defaultPool); - assert(address(contracts.gasPool) == addresses.gasPool); - assert(address(contracts.collSurplusPool) == addresses.collSurplusPool); - assert(address(contracts.sortedTroves) == addresses.sortedTroves); - - contracts.stabilityPool.initialize(contracts.addressesRegistry); - - // Connect contracts - _params.boldToken.setBranchAddresses( - address(contracts.troveManager), - address(contracts.stabilityPool), - address(contracts.borrowerOperations), - address(contracts.activePool) - ); - } - - function _deployPriceFeed( - uint256 _branch, - ExternalAddresses memory _externalAddresses, - OracleParams memory _oracleParams, - address _borrowerOperationsAddress - ) internal returns (IPriceFeed) { - //assert(_branch < vars.numCollaterals); - // Price feeds - // ETH - if (_branch == 0) { - return new WETHPriceFeed( - _externalAddresses.ETHOracle, _oracleParams.ethUsdStalenessThreshold, _borrowerOperationsAddress - ); - } else if (_branch == 1) { - // RETH - return new RETHPriceFeed( - _externalAddresses.ETHOracle, - _externalAddresses.RETHOracle, - _externalAddresses.RETHToken, - _oracleParams.ethUsdStalenessThreshold, - _oracleParams.rEthEthStalenessThreshold, - _borrowerOperationsAddress - ); - } - - // wstETH - return new WSTETHPriceFeed( - _externalAddresses.ETHOracle, - _externalAddresses.STETHOracle, - _externalAddresses.WSTETHToken, - _oracleParams.ethUsdStalenessThreshold, - _oracleParams.stEthUsdStalenessThreshold, - _borrowerOperationsAddress - ); - } - - function _deploySystemParamsMainnet() internal returns (ISystemParams) { - return ISystemParams(address(0)); - } -} +} \ No newline at end of file diff --git a/contracts/test/TestContracts/GasGuzzlerOracle.sol b/contracts/test/TestContracts/GasGuzzlerOracle.sol deleted file mode 100644 index 016ce94b1..000000000 --- a/contracts/test/TestContracts/GasGuzzlerOracle.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity 0.8.24; - -import "src/Dependencies/AggregatorV3Interface.sol"; - -// Mock oracle that consumes all gas in the price getter. -// this contract code is etched over mainnet oracle addresses in mainnet fork tests. -contract GasGuzzlerOracle is AggregatorV3Interface { - uint8 decimal; - - int256 price; - - uint256 lastUpdateTime; - - uint256 pointlessStorageVar = 42; - - // We use 8 decimals unless set to 18 - function decimals() external view returns (uint8) { - return decimal; - } - - function latestRoundData() - external - view - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) - { - // Expensive SLOAD loop that hits the block gas limit before completing - for (uint256 i = 0; i < 1000000; i++) { - uint256 unusedVar = pointlessStorageVar + i; - } - - return (0, price, 0, lastUpdateTime, 0); - } - - function setDecimals(uint8 _decimals) external { - decimal = _decimals; - } - - function setPrice(int256 _price) external { - price = _price; - } - - function setUpdatedAt(uint256 _updatedAt) external { - lastUpdateTime = _updatedAt; - } -} diff --git a/contracts/test/TestContracts/GasGuzzlerToken.sol b/contracts/test/TestContracts/GasGuzzlerToken.sol deleted file mode 100644 index ef1cbd60f..000000000 --- a/contracts/test/TestContracts/GasGuzzlerToken.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity 0.8.24; - -// Mock token that uses all available gas on exchange rate calls. -// This contract code is etched over LST token addresses in mainnet fork tests. -// Has exchange rate functions for WSTETH and RETH. -contract GasGuzzlerToken { - uint256 pointlessStorageVar = 42; - - // RETH exchange rate getter - function getExchangeRate() external view returns (uint256) { - // Expensive SLOAD loop that hits the block gas limit before completing - for (uint256 i = 0; i < 1000000; i++) { - uint256 unusedVar = pointlessStorageVar + i; - } - return 11e17; - } - - // WSTETH exchange rate getter - function stEthPerToken() external view returns (uint256) { - // Expensive SLOAD loop that hits the block gas limit before completing - for (uint256 i = 0; i < 1000000; i++) { - uint256 unusedVar = pointlessStorageVar + i; - } - return 11e17; - } -} diff --git a/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol b/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol new file mode 100644 index 000000000..f7e0be41e --- /dev/null +++ b/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "src/Interfaces/IPriceFeed.sol"; + +interface IMockFXPriceFeed is IPriceFeed { + function REVERT_MSG() external view returns (string memory); + function setPrice(uint256 _price) external; + function getPrice() external view returns (uint256); + function setValidPrice(bool valid) external; +} diff --git a/contracts/test/TestContracts/Interfaces/IPriceFeedMock.sol b/contracts/test/TestContracts/Interfaces/IPriceFeedMock.sol deleted file mode 100644 index bf2d65cac..000000000 --- a/contracts/test/TestContracts/Interfaces/IPriceFeedMock.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.18; - -import "src/Interfaces/IPriceFeed.sol"; - -interface IPriceFeedMock is IPriceFeed { - function setPrice(uint256 _price) external; -} diff --git a/contracts/test/TestContracts/Interfaces/IPriceFeedTestnet.sol b/contracts/test/TestContracts/Interfaces/IPriceFeedTestnet.sol deleted file mode 100644 index 53732c373..000000000 --- a/contracts/test/TestContracts/Interfaces/IPriceFeedTestnet.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "src/Interfaces/IPriceFeed.sol"; - -interface IPriceFeedTestnet is IPriceFeed { - function setPrice(uint256 _price) external returns (bool); - function getPrice() external view returns (uint256); -} diff --git a/contracts/test/TestContracts/MockFXPriceFeed.sol b/contracts/test/TestContracts/MockFXPriceFeed.sol new file mode 100644 index 000000000..6d2d0e753 --- /dev/null +++ b/contracts/test/TestContracts/MockFXPriceFeed.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./Interfaces/IMockFXPriceFeed.sol"; + +/* +* Mock FXPriceFeed contract for testing purposes. +* The price is simply set manually and saved in a state variable. +*/ +contract MockFXPriceFeed is IMockFXPriceFeed { + + string private _revertMsg = "MockFXPriceFeed: no valid price"; + uint256 private _price = 200 * 1e18; + bool private _hasValidPrice = true; + + function getPrice() external view override returns (uint256) { + return _price; + } + + function setValidPrice(bool valid) external { + _hasValidPrice = valid; + } + + function setPrice(uint256 price) external { + _price = price; + } + + function fetchPrice() external view override returns (uint256) { + require(_hasValidPrice, _revertMsg); + + return _price; + } + + function REVERT_MSG() external view override returns (string memory) { + return _revertMsg; + } +} diff --git a/contracts/test/TestContracts/PriceFeedMock.sol b/contracts/test/TestContracts/PriceFeedMock.sol deleted file mode 100644 index 1c8e46fa1..000000000 --- a/contracts/test/TestContracts/PriceFeedMock.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./Interfaces/IPriceFeedMock.sol"; - -contract PriceFeedMock is IPriceFeedMock { - uint256 private PRICE; - - function setPrice(uint256 _price) external { - PRICE = _price; - } - - function getPrice() external view returns (uint256 _price) { - return PRICE; - } - - function fetchPrice() external view returns (uint256, bool) { - return (PRICE, false); - } - - function fetchRedemptionPrice() external view returns (uint256, bool) { - return (PRICE, false); - } - - function lastGoodPrice() external view returns (uint256) { - return PRICE; - } - - function getEthUsdStalenessThreshold() external pure returns (uint256) { - return 0; - } -} diff --git a/contracts/test/TestContracts/PriceFeedTestnet.sol b/contracts/test/TestContracts/PriceFeedTestnet.sol deleted file mode 100644 index 1b5bd89e0..000000000 --- a/contracts/test/TestContracts/PriceFeedTestnet.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./Interfaces/IPriceFeedTestnet.sol"; - -/* -* PriceFeed placeholder for testnet and development. The price is simply set manually and saved in a state -* variable. The contract does not connect to a live Chainlink price feed. -*/ -contract PriceFeedTestnet is IPriceFeedTestnet { - event LastGoodPriceUpdated(uint256 _lastGoodPrice); - - uint256 private _price = 200 * 1e18; - - // --- Functions --- - - // View price getter for simplicity in tests - function getPrice() external view override returns (uint256) { - return _price; - } - - function lastGoodPrice() external view returns (uint256) { - return _price; - } - - function fetchPrice() external override returns (uint256, bool) { - // Fire an event just like the mainnet version would. - // This lets the subgraph rely on events to get the latest price even when developing locally. - emit LastGoodPriceUpdated(_price); - return (_price, false); - } - - function fetchRedemptionPrice() external override returns (uint256, bool) { - // Fire an event just like the mainnet version would. - // This lets the subgraph rely on events to get the latest price even when developing locally. - emit LastGoodPriceUpdated(_price); - return (_price, false); - } - - // Manual external price setter. - function setPrice(uint256 price) external returns (bool) { - _price = price; - return true; - } -} diff --git a/contracts/test/TestContracts/RETHTokenMock.sol b/contracts/test/TestContracts/RETHTokenMock.sol deleted file mode 100644 index 074c8ca59..000000000 --- a/contracts/test/TestContracts/RETHTokenMock.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "src/Interfaces/IRETHToken.sol"; -import "lib/forge-std/src/console2.sol"; - -contract RETHTokenMock is IRETHToken { - uint256 ethPerReth; - - function getExchangeRate() external view returns (uint256) { - return ethPerReth; - } - - function setExchangeRate(uint256 _ethPerReth) external { - ethPerReth = _ethPerReth; - } -} diff --git a/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol b/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol index 6c5dd2d26..3304fe2ac 100644 --- a/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol +++ b/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol @@ -8,7 +8,7 @@ import {IStabilityPool} from "src/Interfaces/IStabilityPool.sol"; import {ITroveManager} from "src/Interfaces/ITroveManager.sol"; import {ICollSurplusPool} from "src/Interfaces/ICollSurplusPool.sol"; import {HintHelpers} from "src/HintHelpers.sol"; -import {IPriceFeedTestnet} from "./Interfaces/IPriceFeedTestnet.sol"; +import {IMockFXPriceFeed} from "./Interfaces/IMockFXPriceFeed.sol"; import {ITroveManagerTester} from "./Interfaces/ITroveManagerTester.sol"; import {LiquityMath} from "src/Dependencies/LiquityMath.sol"; import {ISystemParams} from "src/Interfaces/ISystemParams.sol"; @@ -41,7 +41,7 @@ contract SPInvariantsTestHandler is BaseHandler, TroveId { IBoldToken boldToken; IBorrowerOperations borrowerOperations; IERC20 collateralToken; - IPriceFeedTestnet priceFeed; + IMockFXPriceFeed priceFeed; IStabilityPool stabilityPool; ITroveManagerTester troveManager; ICollSurplusPool collSurplusPool; @@ -51,7 +51,7 @@ contract SPInvariantsTestHandler is BaseHandler, TroveId { IBoldToken immutable boldToken; IBorrowerOperations immutable borrowerOperations; IERC20 collateralToken; - IPriceFeedTestnet immutable priceFeed; + IMockFXPriceFeed immutable priceFeed; IStabilityPool immutable stabilityPool; ITroveManagerTester immutable troveManager; ICollSurplusPool immutable collSurplusPool; diff --git a/contracts/test/TestContracts/WSTETHTokenMock.sol b/contracts/test/TestContracts/WSTETHTokenMock.sol deleted file mode 100644 index 073f37eb7..000000000 --- a/contracts/test/TestContracts/WSTETHTokenMock.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "src/Interfaces/IWSTETH.sol"; - -contract WSTETHTokenMock is IWSTETH { - function stEthPerToken() external pure returns (uint256) { - return 0; - } - - function wrap(uint256 _stETHAmount) external pure returns (uint256) { - return _stETHAmount; - } - - function unwrap(uint256 _wstETHAmount) external pure returns (uint256) { - return _wstETHAmount; - } - - function getWstETHByStETH(uint256 _stETHAmount) external pure returns (uint256) { - return _stETHAmount; - } - - function getStETHByWstETH(uint256 _wstETHAmount) external pure returns (uint256) { - return _wstETHAmount; - } - - function tokensPerStEth() external pure returns (uint256) { - return 0; - } -} diff --git a/contracts/test/basicOps.t.sol b/contracts/test/basicOps.t.sol index 3c154f4c0..1e68064eb 100644 --- a/contracts/test/basicOps.t.sol +++ b/contracts/test/basicOps.t.sol @@ -42,6 +42,17 @@ contract BasicOps is DevTestSetup { assertEq(trovesCount, 1); } + function testOpenTrove_whenNoValidPrice_shouldRevert() public { + priceFeed.setValidPrice(false); + + vm.startPrank(A); + vm.expectRevert(bytes(priceFeed.REVERT_MSG())); + borrowerOperations.openTrove( + A, 0, 2e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + vm.stopPrank(); + } + function testCloseTrove() public { priceFeed.setPrice(2000e18); vm.startPrank(A); @@ -69,6 +80,34 @@ contract BasicOps is DevTestSetup { assertEq(trovesCount, 1); } + function testCloseTrove_whenNoValidPrice_shouldRevert() public { + priceFeed.setPrice(2000e18); + vm.startPrank(A); + borrowerOperations.openTrove( + A, 0, 2e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + // Transfer some Bold to B so that B can close Trove accounting for interest and upfront fee + boldToken.transfer(B, 100e18); + vm.stopPrank(); + + vm.startPrank(B); + uint256 B_Id = borrowerOperations.openTrove( + B, 0, 2e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + vm.stopPrank(); + + assertEq(troveManager.getTroveIdsCount(), 2); + + priceFeed.setValidPrice(false); + + vm.startPrank(B); + vm.expectRevert(bytes(priceFeed.REVERT_MSG())); + borrowerOperations.closeTrove(B_Id); + vm.stopPrank(); + + assertEq(troveManager.getTroveIdsCount(), 2); + } + function testAdjustTrove() public { priceFeed.setPrice(2000e18); vm.startPrank(A); @@ -92,6 +131,37 @@ contract BasicOps is DevTestSetup { assertGt(coll_2, coll_1); } + function testAdjustTrove_whenNoValidPrice_shouldRevert() public { + priceFeed.setPrice(2000e18); + vm.startPrank(A); + uint256 A_Id = borrowerOperations.openTrove( + A, 0, 2e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + + uint256 initialDebt = troveManager.getTroveDebt(A_Id); + assertGt(initialDebt, 0); + uint256 initialColl = troveManager.getTroveColl(A_Id); + assertGt(initialColl, 0); + + priceFeed.setValidPrice(false); + + vm.startPrank(A); + uint256 upfrontFee = predictAdjustTroveUpfrontFee(A_Id, 500e18); + vm.expectRevert(bytes(priceFeed.REVERT_MSG())); + borrowerOperations.adjustTrove( + A_Id, + 1e18, + true, + 500e18, + true, + upfrontFee + ); + vm.stopPrank(); + + assertEq(troveManager.getTroveDebt(A_Id), initialDebt); + assertEq(troveManager.getTroveColl(A_Id), initialColl); + } + function testRedeem() public { priceFeed.setPrice(2000e18); @@ -130,6 +200,38 @@ contract BasicOps is DevTestSetup { assertLt(coll_2, coll_1, "Coll mismatch after"); } + function testRedeem_whenNoValidPrice_shouldRevert() public { + priceFeed.setPrice(2000e18); + + vm.startPrank(A); + borrowerOperations.openTrove( + A, 0, 5e18, 5_000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + vm.stopPrank(); + + vm.startPrank(B); + uint256 B_Id = borrowerOperations.openTrove( + B, 0, 5e18, 4_000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + uint256 debt_1 = troveManager.getTroveDebt(B_Id); + assertGt(debt_1, 0, "Debt cannot be zero"); + uint256 coll_1 = troveManager.getTroveColl(B_Id); + assertGt(coll_1, 0, "Coll cannot be zero"); + vm.stopPrank(); + + vm.warp(block.timestamp + 7 days); + + priceFeed.setValidPrice(false); + + vm.startPrank(A); + vm.expectRevert(bytes(priceFeed.REVERT_MSG())); + collateralRegistry.redeemCollateral(1000e18, 10, 1e18); + vm.stopPrank(); + + assertEq(troveManager.getTroveDebt(B_Id), debt_1, "Debt mismatch after"); + assertEq(troveManager.getTroveColl(B_Id), coll_1, "Coll mismatch after"); + } + function testLiquidation() public { priceFeed.setPrice(2000e18); vm.startPrank(A); @@ -145,7 +247,7 @@ contract BasicOps is DevTestSetup { // Price drops priceFeed.setPrice(1200e18); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); // Check CR_A < MCR and TCR > CCR assertLt(troveManager.getCurrentICR(A_Id, price), MCR); @@ -161,6 +263,39 @@ contract BasicOps is DevTestSetup { assertEq(trovesCount, 1); } + function testLiquidation_whenNoValidPrice_shouldRevert() public { + priceFeed.setPrice(2000e18); + + vm.startPrank(A); + uint256 A_Id = borrowerOperations.openTrove( + A, 0, 2e18, 2200e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + vm.stopPrank(); + + vm.startPrank(B); + borrowerOperations.openTrove( + B, 0, 10e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + vm.stopPrank(); + + assertEq(troveManager.getTroveIdsCount(), 2); + + priceFeed.setPrice(1200e18); + uint256 price = priceFeed.fetchPrice(); + + assertLt(troveManager.getCurrentICR(A_Id, price), MCR); + assertGt(troveManager.getTCR(price), CCR); + + priceFeed.setValidPrice(false); + + vm.startPrank(B); + vm.expectRevert(bytes(priceFeed.REVERT_MSG())); + troveManager.liquidate(A_Id); + vm.stopPrank(); + + assertEq(troveManager.getTroveIdsCount(), 2); + } + function testSPDeposit() public { priceFeed.setPrice(2000e18); vm.startPrank(A); @@ -168,6 +303,9 @@ contract BasicOps is DevTestSetup { A, 0, 2e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) ); + // Simulate a revert on the feed which shouldn't matter, as the price is not used for SP deposits + priceFeed.setValidPrice(false); + // A makes an SP deposit makeSPDepositAndClaim(A, 100e18); @@ -190,6 +328,9 @@ contract BasicOps is DevTestSetup { A, 0, 2e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) ); + // Simulate a revert on the feed which shouldn't matter, as the price is not used for SP withdrawals + priceFeed.setValidPrice(false); + // A makes an SP deposit makeSPDepositAndClaim(A, 100e18); diff --git a/contracts/test/interestRateAggregate.t.sol b/contracts/test/interestRateAggregate.t.sol index 3d6227e33..78d25d875 100644 --- a/contracts/test/interestRateAggregate.t.sol +++ b/contracts/test/interestRateAggregate.t.sol @@ -1833,14 +1833,14 @@ contract InterestRateAggregate is DevTestSetup { // --- TCR tests --- function testGetTCRReturnsMaxUint256ForEmptySystem() public { - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 TCR = troveManager.getTCR(price); assertEq(TCR, MAX_UINT256); } function testGetTCRReturnsICRofTroveForSystemWithOneTrove() public { - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 troveDebtRequest = 2000e18; uint256 coll = 20 ether; uint256 interestRate = 25e16; @@ -1887,13 +1887,13 @@ contract InterestRateAggregate is DevTestSetup { rd.B = r.B * debt.B; debt.C += calcUpfrontFee(debt.C, (rd.A + rd.B + r.C * debt.C) / (debt.A + debt.B + debt.C)); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 sizeWeightedCR = (coll.A + coll.B + coll.C) * price / (debt.A + debt.B + debt.C); assertEq(sizeWeightedCR, troveManager.getTCR(price)); } function testGetTCRIncorporatesTroveInterestForSystemWithSingleTrove() public { - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 troveDebtRequest = 2000e18; uint256 coll = 20 ether; uint256 interestRate = 25e16; @@ -1967,7 +1967,7 @@ contract InterestRateAggregate is DevTestSetup { debt.B += calcInterest(rd.B, interval); debt.C += calcInterest(rd.C, interval); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 expectedTCR = (coll.A + coll.B + coll.C) * price / (debt.A + debt.B + debt.C); assertEq(expectedTCR, troveManager.getTCR(price)); } @@ -1977,14 +1977,14 @@ contract InterestRateAggregate is DevTestSetup { // - 0 for non-existent Trove function testGetCurrentICRReturnsInfinityForNonExistentTrove() public { - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 ICR = troveManager.getCurrentICR(addressToTroveId(A), price); assertEq(ICR, MAX_UINT256); } function testGetCurrentICRReturnsCorrectValueForNoInterest() public { - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 troveDebtRequest = 2000e18; uint256 coll = 20 ether; uint256 interestRate = 25e16; @@ -1999,7 +1999,7 @@ contract InterestRateAggregate is DevTestSetup { } function testGetCurrentICRReturnsCorrectValueWithAccruedInterest() public { - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 troveDebtRequest = 2000e18; uint256 coll = 20 ether; uint256 interestRate = 25e16; diff --git a/contracts/test/liquidationCosts.t.sol b/contracts/test/liquidationCosts.t.sol index 467db6035..a5ba8eec5 100644 --- a/contracts/test/liquidationCosts.t.sol +++ b/contracts/test/liquidationCosts.t.sol @@ -26,7 +26,7 @@ contract LiquidationCostsTest is DevTestSetup { // Price drops priceFeed.setPrice(1000e18); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); // Check not RM assertEq(troveManager.checkBelowCriticalThreshold(price), false, "System should not be below CT"); @@ -60,7 +60,7 @@ contract LiquidationCostsTest is DevTestSetup { // Price drops priceFeed.setPrice(1000e18); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); // Check not RM assertEq(troveManager.checkBelowCriticalThreshold(price), false, "System should not be below CT"); diff --git a/contracts/test/liquidations.t.sol b/contracts/test/liquidations.t.sol index 96c085342..4e8e39236 100644 --- a/contracts/test/liquidations.t.sol +++ b/contracts/test/liquidations.t.sol @@ -61,7 +61,7 @@ contract LiquidationsTest is DevTestSetup { // Price drops priceFeed.setPrice(1100e18 - 1); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); LiquidationsTestVars memory initialValues; initialValues.spBoldBalance = stabilityPool.getTotalBoldDeposits(); @@ -166,7 +166,7 @@ contract LiquidationsTest is DevTestSetup { // Price drops priceFeed.setPrice(1030e18); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 initialSPBoldBalance = stabilityPool.getTotalBoldDeposits(); uint256 initialSPCollBalance = stabilityPool.getCollBalance(); @@ -250,7 +250,7 @@ contract LiquidationsTest is DevTestSetup { // Price drops priceFeed.setPrice(1100e18 - 1); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 BInitialDebt = troveManager.getTroveEntireDebt(BTroveId); uint256 BInitialColl = troveManager.getTroveEntireColl(BTroveId); @@ -345,7 +345,7 @@ contract LiquidationsTest is DevTestSetup { // Price drops priceFeed.setPrice(1100e18 - 1); - (vars.price,) = priceFeed.fetchPrice(); + vars.price = priceFeed.fetchPrice(); vars.spBoldBalance = stabilityPool.getTotalBoldDeposits(); vars.spCollBalance = stabilityPool.getCollBalance(); diff --git a/contracts/test/liquidationsLST.t.sol b/contracts/test/liquidationsLST.t.sol index c9651f52a..a81e42f5c 100644 --- a/contracts/test/liquidationsLST.t.sol +++ b/contracts/test/liquidationsLST.t.sol @@ -99,7 +99,7 @@ contract LiquidationsLSTTest is DevTestSetup { // Price drops priceFeed.setPrice(1200e18 - 1); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); InitialValues memory initialValues; initialValues.BDebt = troveManager.getTroveEntireDebt(BTroveId); From fdb426221e2b23331bb251894ef5706ec5b70220 Mon Sep 17 00:00:00 2001 From: bowd Date: Sun, 19 Oct 2025 14:36:01 +0300 Subject: [PATCH 57/79] chore: get ci to build From 04a21bca24fe8e4fea17d2eca28c7333f5fd1ae5 Mon Sep 17 00:00:00 2001 From: Mourad Kejji Date: Sun, 19 Oct 2025 16:38:38 +0200 Subject: [PATCH 58/79] Separate contract for BatchManager operations (#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add batch management contract * chore: use delegate call * feat: yey it deploys 🥹 * refactor: rename BatchManagerOperations * chore: unify handling of delegate call results * nit: add comment about the batchmanaterops deployment * fix: remove dependency on LiquityBase from BatchManagerOperations * fix: applyUpfrontFee on the memory param * fix: remove unused parameter in _requireBatchInterestRateChangePeriodPassed * fix: remove e2e test * fix: update comments * chore: simplify callback between broken up contract --------- Co-authored-by: Bayological <6872903+bayological@users.noreply.github.com> Co-authored-by: Mourad Co-authored-by: bowd --- .github/workflows/contracts-tests.yml | 46 -- contracts/src/BatchManagerOperations.sol | 612 ++++++++++++++++++ contracts/src/BorrowerOperations.sol | 449 +++---------- .../Interfaces/IBatchManagerOperations.sol | 87 +++ 4 files changed, 772 insertions(+), 422 deletions(-) create mode 100644 contracts/src/BatchManagerOperations.sol create mode 100644 contracts/src/Interfaces/IBatchManagerOperations.sol diff --git a/.github/workflows/contracts-tests.yml b/.github/workflows/contracts-tests.yml index 867683647..dd981a531 100644 --- a/.github/workflows/contracts-tests.yml +++ b/.github/workflows/contracts-tests.yml @@ -55,52 +55,6 @@ jobs: run: | forge test -vvv --match-contract Mainnet - test-e2e: - name: Foundry "E2E" tests - runs-on: ubuntu-latest - env: - SALT: Liquity2-E2E - FORK_URL: ${{ secrets.MAINNET_RPC_URL }} - FORK_CHAIN_ID: 1 - FORK_BLOCK_NUMBER: 21571000 - ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install pnpm - uses: pnpm/action-setup@v3.0.0 - with: - version: 8 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version-file: ".node-version" - cache: "pnpm" - cache-dependency-path: "pnpm-lock.yaml" - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: ${{ env.FOUNDRY_VERSION }} - - - name: Fork mainnet - run: ./fork start && sleep 5 - - - name: Deploy BOLD - run: ./fork deploy --mode bold-only - - - name: Deploy everything else - run: ./fork deploy --mode use-existing-bold - - - name: Run E2E tests - run: ./fork e2e -vvv - console-logs: name: Console imports check runs-on: ubuntu-latest diff --git a/contracts/src/BatchManagerOperations.sol b/contracts/src/BatchManagerOperations.sol new file mode 100644 index 000000000..7e4a15a5c --- /dev/null +++ b/contracts/src/BatchManagerOperations.sol @@ -0,0 +1,612 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.24; + +import "./Interfaces/IBorrowerOperations.sol"; +import "./Interfaces/IAddressesRegistry.sol"; +import "./Interfaces/ITroveManager.sol"; +import "./Interfaces/IActivePool.sol"; +import "./Interfaces/ISortedTroves.sol"; +import "./Interfaces/ITroveNFT.sol"; +import "./Interfaces/ISystemParams.sol"; +import "./Interfaces/IPriceFeed.sol"; +import "./Interfaces/IBatchManagerOperations.sol"; +import "./Dependencies/LiquityBase.sol"; +import "./Dependencies/LiquityMath.sol"; +import "./Dependencies/Constants.sol"; +import "./Types/LatestTroveData.sol"; +import "./Types/LatestBatchData.sol"; + +/** + * @title BatchManagerOperations + * @notice Handles batch manager operations for BorrowerOperations + * @dev This contract is extracted to reduce the size of the main BorrowerOperations contract. + * It contains the batch management functions. BorrowerOperations delegates calls here. + */ +contract BatchManagerOperations is IBatchManagerOperations { + IActivePool private immutable activePool; + IDefaultPool private immutable defaultPool; + IPriceFeed private immutable priceFeed; + ITroveManager private immutable troveManager; + ISortedTroves private immutable sortedTroves; + ITroveNFT private immutable troveNFT; + ISystemParams private immutable systemParams; + + constructor( + IAddressesRegistry _addressesRegistry, + ISystemParams _systemParams + ) { + activePool = _addressesRegistry.activePool(); + defaultPool = _addressesRegistry.defaultPool(); + priceFeed = _addressesRegistry.priceFeed(); + troveManager = _addressesRegistry.troveManager(); + sortedTroves = _addressesRegistry.sortedTroves(); + troveNFT = _addressesRegistry.troveNFT(); + systemParams = _systemParams; + } + + // --- Batch Manager Operations --- + + function registerBatchManager( + uint128 _minInterestRate, + uint128 _maxInterestRate, + uint128 _currentInterestRate, + uint128 _annualManagementFee, + uint128 _minInterestRateChangePeriod + ) external { + _requireValidAnnualInterestRate(_minInterestRate); + _requireValidAnnualInterestRate(_maxInterestRate); + // With the check below, it could only be == + _requireOrderedRange(_minInterestRate, _maxInterestRate); + _requireInterestRateInRange( + _currentInterestRate, + _minInterestRate, + _maxInterestRate + ); + if (_annualManagementFee > MAX_ANNUAL_BATCH_MANAGEMENT_FEE) { + revert AnnualManagementFeeTooHigh(); + } + if (_minInterestRateChangePeriod < MIN_INTEREST_RATE_CHANGE_PERIOD) { + revert MinInterestRateChangePeriodTooLow(); + } + } + + function lowerBatchManagementFee(uint256 _newAnnualManagementFee) external { + LatestBatchData memory batch = troveManager.getLatestBatchData( + msg.sender + ); + if (_newAnnualManagementFee >= batch.annualManagementFee) { + revert NewFeeNotLower(); + } + + // Lower batch fee on TM + troveManager.onLowerBatchManagerAnnualFee( + msg.sender, + batch.entireCollWithoutRedistribution, + batch.entireDebtWithoutRedistribution, + _newAnnualManagementFee + ); + + // active pool mint + TroveChange memory batchChange; + batchChange.batchAccruedManagementFee = batch.accruedManagementFee; + batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; + batchChange.newWeightedRecordedDebt = + batch.entireDebtWithoutRedistribution * + batch.annualInterestRate; + batchChange.oldWeightedRecordedBatchManagementFee = batch + .weightedRecordedBatchManagementFee; + batchChange.newWeightedRecordedBatchManagementFee = + batch.entireDebtWithoutRedistribution * + _newAnnualManagementFee; + + activePool.mintAggInterestAndAccountForTroveChange( + batchChange, + msg.sender + ); + } + + function setBatchManagerAnnualInterestRate( + uint128 _newAnnualInterestRate, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee, + uint256 _minInterestRateChangePeriod + ) external { + + LatestBatchData memory batch = troveManager.getLatestBatchData( + msg.sender + ); + _requireBatchInterestRateChangePeriodPassed( + uint256(batch.lastInterestRateAdjTime), + _minInterestRateChangePeriod + ); + + uint256 newDebt = batch.entireDebtWithoutRedistribution; + + TroveChange memory batchChange; + batchChange.batchAccruedManagementFee = batch.accruedManagementFee; + batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; + batchChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate; + batchChange.oldWeightedRecordedBatchManagementFee = batch + .weightedRecordedBatchManagementFee; + batchChange.newWeightedRecordedBatchManagementFee = + newDebt * + batch.annualManagementFee; + + // Apply upfront fee on premature adjustments + if ( + batch.annualInterestRate != _newAnnualInterestRate && + block.timestamp < + batch.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN + ) { + uint256 price = priceFeed.fetchPrice(); + + uint256 avgInterestRate = activePool + .getNewApproxAvgInterestRateFromTroveChange(batchChange); + batchChange.upfrontFee = _calcUpfrontFee(newDebt, avgInterestRate); + _requireUserAcceptsUpfrontFee( + batchChange.upfrontFee, + _maxUpfrontFee + ); + + newDebt += batchChange.upfrontFee; + + // Recalculate the batch's weighted terms, now taking into account the upfront fee + batchChange.newWeightedRecordedDebt = + newDebt * + _newAnnualInterestRate; + batchChange.newWeightedRecordedBatchManagementFee = + newDebt * + batch.annualManagementFee; + + // Disallow a premature adjustment if it would result in TCR < CCR + // (which includes the case when TCR is already below CCR before the adjustment). + uint256 newTCR = _getNewTCRFromTroveChange(batchChange, price); + _requireNewTCRisAboveCCR(newTCR); + } + + activePool.mintAggInterestAndAccountForTroveChange( + batchChange, + msg.sender + ); + + // Check batch is not empty, and then reinsert in sorted list + if (!sortedTroves.isEmptyBatch(BatchId.wrap(msg.sender))) { + sortedTroves.reInsertBatch( + BatchId.wrap(msg.sender), + _newAnnualInterestRate, + _upperHint, + _lowerHint + ); + } + + troveManager.onSetBatchManagerAnnualInterestRate( + msg.sender, + batch.entireCollWithoutRedistribution, + newDebt, + _newAnnualInterestRate, + batchChange.upfrontFee + ); + } + + function setInterestBatchManager( + uint256 _troveId, + address _newBatchManager, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee + ) external { + LocalVariables_setInterestBatchManager memory vars; + vars.troveManager = troveManager; + vars.activePool = activePool; + vars.sortedTroves = sortedTroves; + + vars.trove = vars.troveManager.getLatestTroveData(_troveId); + vars.newBatch = vars.troveManager.getLatestBatchData(_newBatchManager); + + TroveChange memory newBatchTroveChange; + newBatchTroveChange.appliedRedistBoldDebtGain = vars + .trove + .redistBoldDebtGain; + newBatchTroveChange.appliedRedistCollGain = vars.trove.redistCollGain; + newBatchTroveChange.batchAccruedManagementFee = vars + .newBatch + .accruedManagementFee; + newBatchTroveChange.oldWeightedRecordedDebt = + vars.newBatch.weightedRecordedDebt + + vars.trove.weightedRecordedDebt; + newBatchTroveChange.newWeightedRecordedDebt = + (vars.newBatch.entireDebtWithoutRedistribution + + vars.trove.entireDebt) * + vars.newBatch.annualInterestRate; + + // An upfront fee is always charged upon joining a batch to ensure that borrowers can not game the fee logic + // and gain free interest rate updates (e.g. if they also manage the batch they joined) + // It checks the resulting ICR + vars.trove.entireDebt = _applyUpfrontFee( + vars.trove.entireColl, + vars.trove.entireDebt, + newBatchTroveChange, + _maxUpfrontFee, + true + ); + + // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee + newBatchTroveChange.newWeightedRecordedDebt = + (vars.newBatch.entireDebtWithoutRedistribution + + vars.trove.entireDebt) * + vars.newBatch.annualInterestRate; + + // Add batch fees + newBatchTroveChange.oldWeightedRecordedBatchManagementFee = vars + .newBatch + .weightedRecordedBatchManagementFee; + newBatchTroveChange.newWeightedRecordedBatchManagementFee = + (vars.newBatch.entireDebtWithoutRedistribution + + vars.trove.entireDebt) * + vars.newBatch.annualManagementFee; + vars.activePool.mintAggInterestAndAccountForTroveChange( + newBatchTroveChange, + _newBatchManager + ); + + vars.troveManager.onSetInterestBatchManager( + ITroveManager.OnSetInterestBatchManagerParams({ + troveId: _troveId, + troveColl: vars.trove.entireColl, + troveDebt: vars.trove.entireDebt, + troveChange: newBatchTroveChange, + newBatchAddress: _newBatchManager, + newBatchColl: vars.newBatch.entireCollWithoutRedistribution, + newBatchDebt: vars.newBatch.entireDebtWithoutRedistribution + }) + ); + + vars.sortedTroves.remove(_troveId); + vars.sortedTroves.insertIntoBatch( + _troveId, + BatchId.wrap(_newBatchManager), + vars.newBatch.annualInterestRate, + _upperHint, + _lowerHint + ); + } + + function kickFromBatch( + uint256 _troveId, + uint256 _upperHint, + uint256 _lowerHint + ) external { + _removeFromBatchInternal( + _troveId, + 0, // ignored when kicking + _upperHint, + _lowerHint, + 0, // will use the batch's existing interest rate, so no fee + true + ); + } + + function removeFromBatch( + uint256 _troveId, + uint256 _newAnnualInterestRate, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee + ) external { + _removeFromBatchInternal( + _troveId, + _newAnnualInterestRate, + _upperHint, + _lowerHint, + _maxUpfrontFee, + false + ); + } + + function _removeFromBatchInternal( + uint256 _troveId, + uint256 _newAnnualInterestRate, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee, + bool _kick + ) internal { + LocalVariables_removeFromBatch memory vars; + vars.troveManager = troveManager; + vars.sortedTroves = sortedTroves; + + if (_kick) { + _requireTroveIsOpen(vars.troveManager, _troveId); + } else { + _requireTroveIsActive(vars.troveManager, _troveId); + _requireCallerIsBorrower(_troveId); + _requireValidAnnualInterestRate(_newAnnualInterestRate); + } + + vars.batchManager = _requireIsInBatch(_troveId); + vars.trove = vars.troveManager.getLatestTroveData(_troveId); + vars.batch = vars.troveManager.getLatestBatchData(vars.batchManager); + + if (_kick) { + if ( + vars.batch.totalDebtShares * MAX_BATCH_SHARES_RATIO >= + vars.batch.entireDebtWithoutRedistribution + ) { + revert BatchSharesRatioTooLow(); + } + _newAnnualInterestRate = vars.batch.annualInterestRate; + } + + if (!_checkTroveIsZombie(vars.troveManager, _troveId)) { + // Remove trove from Batch in SortedTroves + vars.sortedTroves.removeFromBatch(_troveId); + // Reinsert as single trove + vars.sortedTroves.insert( + _troveId, + _newAnnualInterestRate, + _upperHint, + _lowerHint + ); + } + + vars.batchFutureDebt = + vars.batch.entireDebtWithoutRedistribution - + (vars.trove.entireDebt - vars.trove.redistBoldDebtGain); + + vars.batchChange.appliedRedistBoldDebtGain = vars + .trove + .redistBoldDebtGain; + vars.batchChange.appliedRedistCollGain = vars.trove.redistCollGain; + vars.batchChange.batchAccruedManagementFee = vars + .batch + .accruedManagementFee; + vars.batchChange.oldWeightedRecordedDebt = vars + .batch + .weightedRecordedDebt; + vars.batchChange.newWeightedRecordedDebt = + vars.batchFutureDebt * + vars.batch.annualInterestRate + + vars.trove.entireDebt * + _newAnnualInterestRate; + + // Apply upfront fee on premature adjustments. It checks the resulting ICR + if ( + vars.batch.annualInterestRate != _newAnnualInterestRate && + block.timestamp < + vars.trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN + ) { + vars.trove.entireDebt = _applyUpfrontFee( + vars.trove.entireColl, + vars.trove.entireDebt, + vars.batchChange, + _maxUpfrontFee, + false + ); + } + + // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee + vars.batchChange.newWeightedRecordedDebt = + vars.batchFutureDebt * + vars.batch.annualInterestRate + + vars.trove.entireDebt * + _newAnnualInterestRate; + // Add batch fees + vars.batchChange.oldWeightedRecordedBatchManagementFee = vars + .batch + .weightedRecordedBatchManagementFee; + vars.batchChange.newWeightedRecordedBatchManagementFee = + vars.batchFutureDebt * + vars.batch.annualManagementFee; + + activePool.mintAggInterestAndAccountForTroveChange( + vars.batchChange, + vars.batchManager + ); + + vars.troveManager.onRemoveFromBatch( + _troveId, + vars.trove.entireColl, + vars.trove.entireDebt, + vars.batchChange, + vars.batchManager, + vars.batch.entireCollWithoutRedistribution, + vars.batch.entireDebtWithoutRedistribution, + _newAnnualInterestRate + ); + } + + // --- Helper Functions --- + + function _calcUpfrontFee( + uint256 _debt, + uint256 _avgInterestRate + ) internal pure returns (uint256) { + return _calcInterest(_debt * _avgInterestRate, UPFRONT_INTEREST_PERIOD); + } + + // Duplicated internal function from BorrowerOperations but calls to getEntireBranchColl and getEntireBranchDebt + // are replaced with their implementations + function _getNewTCRFromTroveChange( + TroveChange memory _troveChange, + uint256 _price + ) internal view returns (uint256 newTCR) { + uint256 activeColl = activePool.getCollBalance(); + uint256 liquidatedColl = defaultPool.getCollBalance(); + uint256 totalColl = activeColl + liquidatedColl + _troveChange.collIncrease - _troveChange.collDecrease; + + uint256 activeDebt = activePool.getBoldDebt(); + uint256 closedDebt = defaultPool.getBoldDebt(); + uint256 totalDebt = + activeDebt + + closedDebt + + _troveChange.debtIncrease + + _troveChange.upfrontFee - + _troveChange.debtDecrease; + + newTCR = LiquityMath._computeCR(totalColl, totalDebt, _price); + } + + function _applyUpfrontFee( + uint256 _troveEntireColl, + uint256 _troveEntireDebt, + TroveChange memory _troveChange, + uint256 _maxUpfrontFee, + bool _isTroveInBatch + ) internal returns (uint256) { + uint256 price = priceFeed.fetchPrice(); + + uint256 avgInterestRate = activePool + .getNewApproxAvgInterestRateFromTroveChange(_troveChange); + _troveChange.upfrontFee = _calcUpfrontFee( + _troveEntireDebt, + avgInterestRate + ); + _requireUserAcceptsUpfrontFee(_troveChange.upfrontFee, _maxUpfrontFee); + + _troveEntireDebt += _troveChange.upfrontFee; + + // ICR is based on the requested Bold amount + upfront fee. + uint256 newICR = LiquityMath._computeCR( + _troveEntireColl, + _troveEntireDebt, + price + ); + if (_isTroveInBatch) { + _requireICRisAboveMCRPlusBCR(newICR); + } else { + _requireICRisAboveMCR(newICR); + } + + // Disallow a premature adjustment if it would result in TCR < CCR + // (which includes the case when TCR is already below CCR before the adjustment). + uint256 newTCR = _getNewTCRFromTroveChange(_troveChange, price); + _requireNewTCRisAboveCCR(newTCR); + + return _troveEntireDebt; + } + + function _checkTroveIsZombie( + ITroveManager _troveManager, + uint256 _troveId + ) internal view returns (bool) { + ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); + return status == ITroveManager.Status.zombie; + } + + function _calcInterest(uint256 _weightedDebt, uint256 _period) internal pure returns (uint256) { + return _weightedDebt * _period / ONE_YEAR / DECIMAL_PRECISION; + } + + // --- Validation Functions --- + + function _requireValidAnnualInterestRate( + uint256 _annualInterestRate + ) internal view { + if (_annualInterestRate < systemParams.MIN_ANNUAL_INTEREST_RATE()) { + revert InterestRateTooLow(); + } + if (_annualInterestRate > MAX_ANNUAL_INTEREST_RATE) { + revert InterestRateTooHigh(); + } + } + + function _requireOrderedRange( + uint256 _minInterestRate, + uint256 _maxInterestRate + ) internal pure { + if (_minInterestRate >= _maxInterestRate) revert MinGeMax(); + } + + function _requireInterestRateInRange( + uint256 _annualInterestRate, + uint256 _minInterestRate, + uint256 _maxInterestRate + ) internal pure { + if ( + _minInterestRate > _annualInterestRate || + _annualInterestRate > _maxInterestRate + ) { + revert InterestNotInRange(); + } + } + + function _requireBatchInterestRateChangePeriodPassed( + uint256 _lastInterestRateAdjTime, + uint256 _minInterestRateChangePeriod + ) internal view { + if (block.timestamp < _lastInterestRateAdjTime + _minInterestRateChangePeriod) { + revert BatchInterestRateChangePeriodNotPassed(); + } + } + + function _requireUserAcceptsUpfrontFee( + uint256 _fee, + uint256 _maxFee + ) internal pure { + if (_fee > _maxFee) { + revert UpfrontFeeTooHigh(); + } + } + + function _requireNewTCRisAboveCCR(uint256 _newTCR) internal view { + if (_newTCR < systemParams.CCR()) { + revert TCRBelowCCR(); + } + } + + function _requireTroveIsActive( + ITroveManager _troveManager, + uint256 _troveId + ) internal view { + ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); + if (status != ITroveManager.Status.active) { + revert TroveNotActive(); + } + } + + function _requireTroveIsOpen( + ITroveManager _troveManager, + uint256 _troveId + ) internal view { + ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); + if ( + status != ITroveManager.Status.active && + status != ITroveManager.Status.zombie + ) { + revert TroveNotOpen(); + } + } + + function _requireCallerIsBorrower(uint256 _troveId) internal view { + if (msg.sender != troveNFT.ownerOf(_troveId)) { + revert NotBorrower(); + } + } + + function _requireIsInBatch( + uint256 _troveId + ) internal view returns (address) { + address batchManager = IBorrowerOperations(address(this)).interestBatchManagerOf( + _troveId + ); + if (batchManager == address(0)) { + revert TroveNotInBatch(); + } + return batchManager; + } + + function _requireICRisAboveMCR(uint256 _newICR) internal view { + if (_newICR < systemParams.MCR()) { + revert ICRBelowMCR(); + } + } + + function _requireICRisAboveMCRPlusBCR(uint256 _newICR) internal view { + if (_newICR < systemParams.MCR() + systemParams.BCR()) { + revert ICRBelowMCRPlusBCR(); + } + } + +} diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index d6224efb5..6b3eea84e 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -15,13 +15,8 @@ import "./Dependencies/LiquityBase.sol"; import "./Dependencies/AddRemoveManagers.sol"; import "./Types/LatestTroveData.sol"; import "./Types/LatestBatchData.sol"; +import "./BatchManagerOperations.sol"; -/** - * @dev System parameters pattern: - * Most system parameters are copied from SystemParams to immutable variables at construction for gas optimization. - * However, to reduce contract size, the following parameter is read directly from SystemParams when needed: - * - SCR: Only used in shutdown() function - */ contract BorrowerOperations is LiquityBase, AddRemoveManagers, @@ -41,6 +36,8 @@ contract BorrowerOperations is // Wrapped ETH for liquidation reserve (gas compensation) IERC20Metadata internal immutable gasToken; ISystemParams public immutable systemParams; + // Helper contract for batch management operations + address public batchManagerOperations; bool public hasBeenShutDown; @@ -100,26 +97,6 @@ contract BorrowerOperations is bool newOracleFailureDetected; } - struct LocalVariables_setInterestBatchManager { - ITroveManager troveManager; - IActivePool activePool; - ISortedTroves sortedTroves; - address oldBatchManager; - LatestTroveData trove; - LatestBatchData oldBatch; - LatestBatchData newBatch; - } - - struct LocalVariables_removeFromBatch { - ITroveManager troveManager; - ISortedTroves sortedTroves; - address batchManager; - LatestTroveData trove; - LatestBatchData batch; - uint256 batchFutureDebt; - TroveChange batchChange; - } - error IsShutDown(); error TCRNotBelowSCR(); error ZeroAdjustment(); @@ -151,6 +128,7 @@ contract BorrowerOperations is error NewFeeNotLower(); error CallerNotTroveManager(); error CallerNotPriceFeed(); + error CallerNotSelf(); error MinGeMax(); error AnnualManagementFeeTooHigh(); error MinInterestRateChangePeriodTooLow(); @@ -183,6 +161,11 @@ contract BorrowerOperations is collSurplusPool = _addressesRegistry.collSurplusPool(); sortedTroves = _addressesRegistry.sortedTroves(); boldToken = _addressesRegistry.boldToken(); + // We can leave the deployment script as-is by just having BorrowerOperations deploy its + // own batchManagerOperations contract + // /!\ If we have to redeploy a BorrowerOps that could need the same batchManagerOps then we + // would replace this line with some extra param, but that seems unlikely + batchManagerOperations = address(new BatchManagerOperations(_addressesRegistry, _systemParams)); emit TroveManagerAddressChanged(address(troveManager)); emit GasPoolAddressChanged(gasPoolAddress); @@ -1070,30 +1053,22 @@ contract BorrowerOperations is ) external { _requireIsNotShutDown(); _requireNonExistentInterestBatchManager(msg.sender); - _requireValidAnnualInterestRate(_minInterestRate); - _requireValidAnnualInterestRate(_maxInterestRate); - // With the check below, it could only be == - _requireOrderedRange(_minInterestRate, _maxInterestRate); - _requireInterestRateInRange( - _currentInterestRate, - _minInterestRate, - _maxInterestRate + (bool success, bytes memory data) = batchManagerOperations.delegatecall( + abi.encodeWithSignature( + "registerBatchManager(uint128,uint128,uint128,uint128,uint128)", + _minInterestRate, + _maxInterestRate, + _currentInterestRate, + _annualManagementFee, + _minInterestRateChangePeriod + ) ); - // Not needed, implicitly checked in the condition above: - //_requireValidAnnualInterestRate(_currentInterestRate); - if (_annualManagementFee > MAX_ANNUAL_BATCH_MANAGEMENT_FEE) { - revert AnnualManagementFeeTooHigh(); - } - if (_minInterestRateChangePeriod < MIN_INTEREST_RATE_CHANGE_PERIOD) { - revert MinInterestRateChangePeriodTooLow(); - } - + _requireDelegateCallSucceeded(success, data); interestBatchManagers[msg.sender] = InterestBatchManager( _minInterestRate, _maxInterestRate, _minInterestRateChangePeriod ); - troveManager.onRegisterBatchManager( msg.sender, _currentInterestRate, @@ -1104,41 +1079,13 @@ contract BorrowerOperations is function lowerBatchManagementFee(uint256 _newAnnualManagementFee) external { _requireIsNotShutDown(); _requireValidInterestBatchManager(msg.sender); - - ITroveManager troveManagerCached = troveManager; - - LatestBatchData memory batch = troveManagerCached.getLatestBatchData( - msg.sender - ); - if (_newAnnualManagementFee >= batch.annualManagementFee) { - revert NewFeeNotLower(); - } - - // Lower batch fee on TM - troveManagerCached.onLowerBatchManagerAnnualFee( - msg.sender, - batch.entireCollWithoutRedistribution, - batch.entireDebtWithoutRedistribution, - _newAnnualManagementFee - ); - - // active pool mint - TroveChange memory batchChange; - batchChange.batchAccruedManagementFee = batch.accruedManagementFee; - batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; - batchChange.newWeightedRecordedDebt = - batch.entireDebtWithoutRedistribution * - batch.annualInterestRate; - batchChange.oldWeightedRecordedBatchManagementFee = batch - .weightedRecordedBatchManagementFee; - batchChange.newWeightedRecordedBatchManagementFee = - batch.entireDebtWithoutRedistribution * - _newAnnualManagementFee; - - activePool.mintAggInterestAndAccountForTroveChange( - batchChange, - msg.sender + (bool success, bytes memory data) = batchManagerOperations.delegatecall( + abi.encodeWithSignature( + "lowerBatchManagementFee(uint256)", + _newAnnualManagementFee + ) ); + _requireDelegateCallSucceeded(success, data); } function setBatchManagerAnnualInterestRate( @@ -1153,86 +1100,18 @@ contract BorrowerOperations is msg.sender, _newAnnualInterestRate ); - // Not needed, implicitly checked in the condition above: - //_requireValidAnnualInterestRate(_newAnnualInterestRate); - - ITroveManager troveManagerCached = troveManager; - IActivePool activePoolCached = activePool; - - LatestBatchData memory batch = troveManagerCached.getLatestBatchData( - msg.sender - ); - _requireBatchInterestRateChangePeriodPassed( - msg.sender, - uint256(batch.lastInterestRateAdjTime) - ); - - uint256 newDebt = batch.entireDebtWithoutRedistribution; - - TroveChange memory batchChange; - batchChange.batchAccruedManagementFee = batch.accruedManagementFee; - batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; - batchChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate; - batchChange.oldWeightedRecordedBatchManagementFee = batch - .weightedRecordedBatchManagementFee; - batchChange.newWeightedRecordedBatchManagementFee = - newDebt * - batch.annualManagementFee; - - // Apply upfront fee on premature adjustments - if ( - batch.annualInterestRate != _newAnnualInterestRate && - block.timestamp < - batch.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN - ) { - uint256 price = priceFeed.fetchPrice(); - - uint256 avgInterestRate = activePoolCached - .getNewApproxAvgInterestRateFromTroveChange(batchChange); - batchChange.upfrontFee = _calcUpfrontFee(newDebt, avgInterestRate); - _requireUserAcceptsUpfrontFee( - batchChange.upfrontFee, - _maxUpfrontFee - ); - - newDebt += batchChange.upfrontFee; - - // Recalculate the batch's weighted terms, now taking into account the upfront fee - batchChange.newWeightedRecordedDebt = - newDebt * - _newAnnualInterestRate; - batchChange.newWeightedRecordedBatchManagementFee = - newDebt * - batch.annualManagementFee; - - // Disallow a premature adjustment if it would result in TCR < CCR - // (which includes the case when TCR is already below CCR before the adjustment). - uint256 newTCR = _getNewTCRFromTroveChange(batchChange, price); - _requireNewTCRisAboveCCR(newTCR); - } - - activePoolCached.mintAggInterestAndAccountForTroveChange( - batchChange, - msg.sender - ); - - // Check batch is not empty, and then reinsert in sorted list - if (!sortedTroves.isEmptyBatch(BatchId.wrap(msg.sender))) { - sortedTroves.reInsertBatch( - BatchId.wrap(msg.sender), + InterestBatchManager memory interestBatchManager = interestBatchManagers[msg.sender]; + (bool success, bytes memory data) = batchManagerOperations.delegatecall( + abi.encodeWithSignature( + "setBatchManagerAnnualInterestRate(uint128,uint256,uint256,uint256,uint256)", _newAnnualInterestRate, _upperHint, - _lowerHint - ); - } - - troveManagerCached.onSetBatchManagerAnnualInterestRate( - msg.sender, - batch.entireCollWithoutRedistribution, - newDebt, - _newAnnualInterestRate, - batchChange.upfrontFee + _lowerHint, + _maxUpfrontFee, + interestBatchManager.minInterestRateChangePeriod + ) ); + _requireDelegateCallSucceeded(success, data); } function setInterestBatchManager( @@ -1243,90 +1122,26 @@ contract BorrowerOperations is uint256 _maxUpfrontFee ) public override { _requireIsNotShutDown(); - LocalVariables_setInterestBatchManager memory vars; - vars.troveManager = troveManager; - vars.activePool = activePool; - vars.sortedTroves = sortedTroves; - - _requireTroveIsActive(vars.troveManager, _troveId); + _requireTroveIsActive(troveManager, _troveId); _requireCallerIsBorrower(_troveId); _requireValidInterestBatchManager(_newBatchManager); _requireIsNotInBatch(_troveId); - interestBatchManagerOf[_troveId] = _newBatchManager; - // Can’t have both individual delegation and batch manager + // Can't have both individual delegation and batch manager if (interestIndividualDelegateOf[_troveId].account != address(0)) delete interestIndividualDelegateOf[_troveId]; - vars.trove = vars.troveManager.getLatestTroveData(_troveId); - vars.newBatch = vars.troveManager.getLatestBatchData(_newBatchManager); - - TroveChange memory newBatchTroveChange; - newBatchTroveChange.appliedRedistBoldDebtGain = vars - .trove - .redistBoldDebtGain; - newBatchTroveChange.appliedRedistCollGain = vars.trove.redistCollGain; - newBatchTroveChange.batchAccruedManagementFee = vars - .newBatch - .accruedManagementFee; - newBatchTroveChange.oldWeightedRecordedDebt = - vars.newBatch.weightedRecordedDebt + - vars.trove.weightedRecordedDebt; - newBatchTroveChange.newWeightedRecordedDebt = - (vars.newBatch.entireDebtWithoutRedistribution + - vars.trove.entireDebt) * - vars.newBatch.annualInterestRate; - - // An upfront fee is always charged upon joining a batch to ensure that borrowers can not game the fee logic - // and gain free interest rate updates (e.g. if they also manage the batch they joined) - // It checks the resulting ICR - vars.trove.entireDebt = _applyUpfrontFee( - vars.trove.entireColl, - vars.trove.entireDebt, - newBatchTroveChange, - _maxUpfrontFee, - true - ); - - // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee - newBatchTroveChange.newWeightedRecordedDebt = - (vars.newBatch.entireDebtWithoutRedistribution + - vars.trove.entireDebt) * - vars.newBatch.annualInterestRate; - - // Add batch fees - newBatchTroveChange.oldWeightedRecordedBatchManagementFee = vars - .newBatch - .weightedRecordedBatchManagementFee; - newBatchTroveChange.newWeightedRecordedBatchManagementFee = - (vars.newBatch.entireDebtWithoutRedistribution + - vars.trove.entireDebt) * - vars.newBatch.annualManagementFee; - vars.activePool.mintAggInterestAndAccountForTroveChange( - newBatchTroveChange, - _newBatchManager - ); - - vars.troveManager.onSetInterestBatchManager( - ITroveManager.OnSetInterestBatchManagerParams({ - troveId: _troveId, - troveColl: vars.trove.entireColl, - troveDebt: vars.trove.entireDebt, - troveChange: newBatchTroveChange, - newBatchAddress: _newBatchManager, - newBatchColl: vars.newBatch.entireCollWithoutRedistribution, - newBatchDebt: vars.newBatch.entireDebtWithoutRedistribution - }) - ); - - vars.sortedTroves.remove(_troveId); - vars.sortedTroves.insertIntoBatch( - _troveId, - BatchId.wrap(_newBatchManager), - vars.newBatch.annualInterestRate, - _upperHint, - _lowerHint + (bool success, bytes memory data) = batchManagerOperations.delegatecall( + abi.encodeWithSignature( + "setInterestBatchManager(uint256,address,uint256,uint256,uint256)", + _troveId, + _newBatchManager, + _upperHint, + _lowerHint, + _maxUpfrontFee + ) ); + _requireDelegateCallSucceeded(success, data); } function kickFromBatch( @@ -1334,14 +1149,16 @@ contract BorrowerOperations is uint256 _upperHint, uint256 _lowerHint ) external override { - _removeFromBatch({ - _troveId: _troveId, - _newAnnualInterestRate: 0, // ignored when kicking - _upperHint: _upperHint, - _lowerHint: _lowerHint, - _maxUpfrontFee: 0, // will use the batch's existing interest rate, so no fee - _kick: true - }); + _requireIsNotShutDown(); + (bool success, bytes memory data) = batchManagerOperations.delegatecall( + abi.encodeWithSignature( + "kickFromBatch(uint256,uint256,uint256)", + _troveId, + _upperHint, + _lowerHint + ) + ); + _requireDelegateCallSucceeded(success, data); } function removeFromBatch( @@ -1351,130 +1168,19 @@ contract BorrowerOperations is uint256 _lowerHint, uint256 _maxUpfrontFee ) public override { - _removeFromBatch({ - _troveId: _troveId, - _newAnnualInterestRate: _newAnnualInterestRate, - _upperHint: _upperHint, - _lowerHint: _lowerHint, - _maxUpfrontFee: _maxUpfrontFee, - _kick: false - }); - } - - function _removeFromBatch( - uint256 _troveId, - uint256 _newAnnualInterestRate, - uint256 _upperHint, - uint256 _lowerHint, - uint256 _maxUpfrontFee, - bool _kick - ) internal { _requireIsNotShutDown(); - - LocalVariables_removeFromBatch memory vars; - vars.troveManager = troveManager; - vars.sortedTroves = sortedTroves; - - if (_kick) { - _requireTroveIsOpen(vars.troveManager, _troveId); - } else { - _requireTroveIsActive(vars.troveManager, _troveId); - _requireCallerIsBorrower(_troveId); - _requireValidAnnualInterestRate(_newAnnualInterestRate); - } - - vars.batchManager = _requireIsInBatch(_troveId); - vars.trove = vars.troveManager.getLatestTroveData(_troveId); - vars.batch = vars.troveManager.getLatestBatchData(vars.batchManager); - - if (_kick) { - if ( - vars.batch.totalDebtShares * MAX_BATCH_SHARES_RATIO >= - vars.batch.entireDebtWithoutRedistribution - ) { - revert BatchSharesRatioTooLow(); - } - _newAnnualInterestRate = vars.batch.annualInterestRate; - } - - delete interestBatchManagerOf[_troveId]; - - if (!_checkTroveIsZombie(vars.troveManager, _troveId)) { - // Remove trove from Batch in SortedTroves - vars.sortedTroves.removeFromBatch(_troveId); - // Reinsert as single trove - vars.sortedTroves.insert( + (bool success, bytes memory data) = batchManagerOperations.delegatecall( + abi.encodeWithSignature( + "removeFromBatch(uint256,uint256,uint256,uint256,uint256)", _troveId, _newAnnualInterestRate, _upperHint, - _lowerHint - ); - } - - vars.batchFutureDebt = - vars.batch.entireDebtWithoutRedistribution - - (vars.trove.entireDebt - vars.trove.redistBoldDebtGain); - - vars.batchChange.appliedRedistBoldDebtGain = vars - .trove - .redistBoldDebtGain; - vars.batchChange.appliedRedistCollGain = vars.trove.redistCollGain; - vars.batchChange.batchAccruedManagementFee = vars - .batch - .accruedManagementFee; - vars.batchChange.oldWeightedRecordedDebt = vars - .batch - .weightedRecordedDebt; - vars.batchChange.newWeightedRecordedDebt = - vars.batchFutureDebt * - vars.batch.annualInterestRate + - vars.trove.entireDebt * - _newAnnualInterestRate; - - // Apply upfront fee on premature adjustments. It checks the resulting ICR - if ( - vars.batch.annualInterestRate != _newAnnualInterestRate && - block.timestamp < - vars.trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN - ) { - vars.trove.entireDebt = _applyUpfrontFee( - vars.trove.entireColl, - vars.trove.entireDebt, - vars.batchChange, - _maxUpfrontFee, - false - ); - } - - // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee - vars.batchChange.newWeightedRecordedDebt = - vars.batchFutureDebt * - vars.batch.annualInterestRate + - vars.trove.entireDebt * - _newAnnualInterestRate; - // Add batch fees - vars.batchChange.oldWeightedRecordedBatchManagementFee = vars - .batch - .weightedRecordedBatchManagementFee; - vars.batchChange.newWeightedRecordedBatchManagementFee = - vars.batchFutureDebt * - vars.batch.annualManagementFee; - - activePool.mintAggInterestAndAccountForTroveChange( - vars.batchChange, - vars.batchManager - ); - - vars.troveManager.onRemoveFromBatch( - _troveId, - vars.trove.entireColl, - vars.trove.entireDebt, - vars.batchChange, - vars.batchManager, - vars.batch.entireCollWithoutRedistribution, - vars.batch.entireDebtWithoutRedistribution, - _newAnnualInterestRate + _lowerHint, + _maxUpfrontFee + ) ); + _requireDelegateCallSucceeded(success, data); + delete interestBatchManagerOf[_troveId]; } function switchBatchManager( @@ -1551,7 +1257,7 @@ contract BorrowerOperations is function _calcUpfrontFee( uint256 _debt, uint256 _avgInterestRate - ) internal view returns (uint256) { + ) internal pure returns (uint256) { return _calcInterest(_debt * _avgInterestRate, UPFRONT_INTEREST_PERIOD); } @@ -1974,23 +1680,6 @@ contract BorrowerOperations is } } - function _requireBatchInterestRateChangePeriodPassed( - address _interestBatchManagerAddress, - uint256 _lastInterestRateAdjTime - ) internal view { - InterestBatchManager - memory interestBatchManager = interestBatchManagers[ - _interestBatchManagerAddress - ]; - if ( - block.timestamp < - _lastInterestRateAdjTime + - uint256(interestBatchManager.minInterestRateChangePeriod) - ) { - revert BatchInterestRateChangePeriodNotPassed(); - } - } - function _requireDelegateInterestRateChangePeriodPassed( uint256 _lastInterestRateAdjTime, uint256 _minInterestRateChangePeriod @@ -2046,6 +1735,14 @@ contract BorrowerOperations is } } + function _requireDelegateCallSucceeded(bool success, bytes memory data) internal pure { + if (!success) { + assembly { + revert(add(0x20, data), mload(data)) + } + } + } + // --- ICR and TCR getters --- function _getNewTCRFromTroveChange( diff --git a/contracts/src/Interfaces/IBatchManagerOperations.sol b/contracts/src/Interfaces/IBatchManagerOperations.sol new file mode 100644 index 000000000..027481827 --- /dev/null +++ b/contracts/src/Interfaces/IBatchManagerOperations.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./ISortedTroves.sol"; +import "./ITroveManager.sol"; + +interface IBatchManagerOperations { + error IsShutDown(); + error InterestNotInRange(); + error BatchInterestRateChangePeriodNotPassed(); + error InvalidInterestBatchManager(); + error BatchManagerExists(); + error NewFeeNotLower(); + error AnnualManagementFeeTooHigh(); + error MinInterestRateChangePeriodTooLow(); + error MinGeMax(); + error NotBorrower(); + error TroveNotActive(); + error TroveNotInBatch(); + error TroveNotOpen(); + error ICRBelowMCRPlusBCR(); + error TCRBelowCCR(); + error ICRBelowMCR(); + error UpfrontFeeTooHigh(); + error InterestRateTooLow(); + error InterestRateTooHigh(); + error BatchSharesRatioTooLow(); + + struct LocalVariables_setInterestBatchManager { + ITroveManager troveManager; + IActivePool activePool; + ISortedTroves sortedTroves; + LatestTroveData trove; + LatestBatchData newBatch; + } + + struct LocalVariables_removeFromBatch { + ITroveManager troveManager; + ISortedTroves sortedTroves; + address batchManager; + LatestTroveData trove; + LatestBatchData batch; + uint256 batchFutureDebt; + TroveChange batchChange; + } + + function registerBatchManager( + uint128 _minInterestRate, + uint128 _maxInterestRate, + uint128 _currentInterestRate, + uint128 _annualManagementFee, + uint128 _minInterestRateChangePeriod + ) external; + + function lowerBatchManagementFee(uint256 _newAnnualManagementFee) external; + + function setBatchManagerAnnualInterestRate( + uint128 _newAnnualInterestRate, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee, + uint256 _minInterestRateChangePeriod + ) external; + + function setInterestBatchManager( + uint256 _troveId, + address _newBatchManager, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee + ) external; + + function kickFromBatch( + uint256 _troveId, + uint256 _upperHint, + uint256 _lowerHint + ) external; + + function removeFromBatch( + uint256 _troveId, + uint256 _newAnnualInterestRate, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee + ) external; +} From 4e1cf936d79c08a56bf0411a5004fcc8d7fd7017 Mon Sep 17 00:00:00 2001 From: baroooo Date: Mon, 20 Oct 2025 12:30:46 +0200 Subject: [PATCH 59/79] chore: format changes --- contracts/script/DeployLiquity2.s.sol | 10 +- contracts/src/AddressesRegistry.sol | 10 +- contracts/src/BatchManagerOperations.sol | 294 ++------ contracts/src/BorrowerOperations.sol | 687 +++++------------- contracts/src/CollateralRegistry.sol | 7 +- .../Interfaces/IBatchManagerOperations.sol | 6 +- contracts/src/Interfaces/IOracleAdapter.sol | 20 +- contracts/src/Interfaces/IStableTokenV3.sol | 2 +- contracts/src/Interfaces/ISystemParams.sol | 5 +- contracts/src/PriceFeeds/FXPriceFeed.sol | 42 +- contracts/src/StabilityPool.sol | 12 +- contracts/src/SystemParams.sol | 61 +- contracts/src/TroveManager.sol | 4 +- contracts/src/tokens/StableTokenV3.sol | 537 +++++++------- 14 files changed, 596 insertions(+), 1101 deletions(-) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index bd406d0af..728ab6c5d 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -258,9 +258,8 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ccr: 150 * 1e16, scr: 110 * 1e16, mcr: 110 * 1e16, bcr: 10 * 1e16}); - ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ - minAnnualInterestRate: 1e18 / 200 - }); + ISystemParams.InterestParams memory interestParams = + ISystemParams.InterestParams({minAnnualInterestRate: 1e18 / 200}); ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ redemptionFeeFloor: 1e18 / 200, @@ -287,9 +286,8 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { } function _deploySystemParams(DeploymentResult memory r) internal { - address systemParamsProxy = address( - new TransparentUpgradeableProxy(address(r.systemParamsImpl), address(r.proxyAdmin), "") - ); + address systemParamsProxy = + address(new TransparentUpgradeableProxy(address(r.systemParamsImpl), address(r.proxyAdmin), "")); r.systemParams = ISystemParams(systemParamsProxy); r.systemParams.initialize(); diff --git a/contracts/src/AddressesRegistry.sol b/contracts/src/AddressesRegistry.sol index 275003668..dfee30760 100644 --- a/contracts/src/AddressesRegistry.sol +++ b/contracts/src/AddressesRegistry.sol @@ -70,9 +70,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { liquidityStrategy = _vars.liquidityStrategy; emit CollTokenAddressChanged(address(_vars.collToken)); - emit BorrowerOperationsAddressChanged( - address(_vars.borrowerOperations) - ); + emit BorrowerOperationsAddressChanged(address(_vars.borrowerOperations)); emit TroveManagerAddressChanged(address(_vars.troveManager)); emit TroveNFTAddressChanged(address(_vars.troveNFT)); emit MetadataNFTAddressChanged(address(_vars.metadataNFT)); @@ -86,13 +84,11 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { emit InterestRouterAddressChanged(address(_vars.interestRouter)); emit HintHelpersAddressChanged(address(_vars.hintHelpers)); emit MultiTroveGetterAddressChanged(address(_vars.multiTroveGetter)); - emit CollateralRegistryAddressChanged( - address(_vars.collateralRegistry) - ); + emit CollateralRegistryAddressChanged(address(_vars.collateralRegistry)); emit BoldTokenAddressChanged(address(_vars.boldToken)); emit GasTokenAddressChanged(address(_vars.gasToken)); emit LiquidityStrategyAddressChanged(address(_vars.liquidityStrategy)); _renounceOwnership(); } -} \ No newline at end of file +} diff --git a/contracts/src/BatchManagerOperations.sol b/contracts/src/BatchManagerOperations.sol index 7e4a15a5c..e0a689908 100644 --- a/contracts/src/BatchManagerOperations.sol +++ b/contracts/src/BatchManagerOperations.sol @@ -32,10 +32,7 @@ contract BatchManagerOperations is IBatchManagerOperations { ITroveNFT private immutable troveNFT; ISystemParams private immutable systemParams; - constructor( - IAddressesRegistry _addressesRegistry, - ISystemParams _systemParams - ) { + constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) { activePool = _addressesRegistry.activePool(); defaultPool = _addressesRegistry.defaultPool(); priceFeed = _addressesRegistry.priceFeed(); @@ -58,11 +55,7 @@ contract BatchManagerOperations is IBatchManagerOperations { _requireValidAnnualInterestRate(_maxInterestRate); // With the check below, it could only be == _requireOrderedRange(_minInterestRate, _maxInterestRate); - _requireInterestRateInRange( - _currentInterestRate, - _minInterestRate, - _maxInterestRate - ); + _requireInterestRateInRange(_currentInterestRate, _minInterestRate, _maxInterestRate); if (_annualManagementFee > MAX_ANNUAL_BATCH_MANAGEMENT_FEE) { revert AnnualManagementFeeTooHigh(); } @@ -72,9 +65,7 @@ contract BatchManagerOperations is IBatchManagerOperations { } function lowerBatchManagementFee(uint256 _newAnnualManagementFee) external { - LatestBatchData memory batch = troveManager.getLatestBatchData( - msg.sender - ); + LatestBatchData memory batch = troveManager.getLatestBatchData(msg.sender); if (_newAnnualManagementFee >= batch.annualManagementFee) { revert NewFeeNotLower(); } @@ -91,19 +82,12 @@ contract BatchManagerOperations is IBatchManagerOperations { TroveChange memory batchChange; batchChange.batchAccruedManagementFee = batch.accruedManagementFee; batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; - batchChange.newWeightedRecordedDebt = - batch.entireDebtWithoutRedistribution * - batch.annualInterestRate; - batchChange.oldWeightedRecordedBatchManagementFee = batch - .weightedRecordedBatchManagementFee; + batchChange.newWeightedRecordedDebt = batch.entireDebtWithoutRedistribution * batch.annualInterestRate; + batchChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; batchChange.newWeightedRecordedBatchManagementFee = - batch.entireDebtWithoutRedistribution * - _newAnnualManagementFee; + batch.entireDebtWithoutRedistribution * _newAnnualManagementFee; - activePool.mintAggInterestAndAccountForTroveChange( - batchChange, - msg.sender - ); + activePool.mintAggInterestAndAccountForTroveChange(batchChange, msg.sender); } function setBatchManagerAnnualInterestRate( @@ -113,13 +97,9 @@ contract BatchManagerOperations is IBatchManagerOperations { uint256 _maxUpfrontFee, uint256 _minInterestRateChangePeriod ) external { - - LatestBatchData memory batch = troveManager.getLatestBatchData( - msg.sender - ); + LatestBatchData memory batch = troveManager.getLatestBatchData(msg.sender); _requireBatchInterestRateChangePeriodPassed( - uint256(batch.lastInterestRateAdjTime), - _minInterestRateChangePeriod + uint256(batch.lastInterestRateAdjTime), _minInterestRateChangePeriod ); uint256 newDebt = batch.entireDebtWithoutRedistribution; @@ -128,37 +108,25 @@ contract BatchManagerOperations is IBatchManagerOperations { batchChange.batchAccruedManagementFee = batch.accruedManagementFee; batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; batchChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate; - batchChange.oldWeightedRecordedBatchManagementFee = batch - .weightedRecordedBatchManagementFee; - batchChange.newWeightedRecordedBatchManagementFee = - newDebt * - batch.annualManagementFee; + batchChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; + batchChange.newWeightedRecordedBatchManagementFee = newDebt * batch.annualManagementFee; // Apply upfront fee on premature adjustments if ( - batch.annualInterestRate != _newAnnualInterestRate && - block.timestamp < - batch.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN + batch.annualInterestRate != _newAnnualInterestRate + && block.timestamp < batch.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN ) { uint256 price = priceFeed.fetchPrice(); - uint256 avgInterestRate = activePool - .getNewApproxAvgInterestRateFromTroveChange(batchChange); + uint256 avgInterestRate = activePool.getNewApproxAvgInterestRateFromTroveChange(batchChange); batchChange.upfrontFee = _calcUpfrontFee(newDebt, avgInterestRate); - _requireUserAcceptsUpfrontFee( - batchChange.upfrontFee, - _maxUpfrontFee - ); + _requireUserAcceptsUpfrontFee(batchChange.upfrontFee, _maxUpfrontFee); newDebt += batchChange.upfrontFee; // Recalculate the batch's weighted terms, now taking into account the upfront fee - batchChange.newWeightedRecordedDebt = - newDebt * - _newAnnualInterestRate; - batchChange.newWeightedRecordedBatchManagementFee = - newDebt * - batch.annualManagementFee; + batchChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate; + batchChange.newWeightedRecordedBatchManagementFee = newDebt * batch.annualManagementFee; // Disallow a premature adjustment if it would result in TCR < CCR // (which includes the case when TCR is already below CCR before the adjustment). @@ -166,27 +134,15 @@ contract BatchManagerOperations is IBatchManagerOperations { _requireNewTCRisAboveCCR(newTCR); } - activePool.mintAggInterestAndAccountForTroveChange( - batchChange, - msg.sender - ); + activePool.mintAggInterestAndAccountForTroveChange(batchChange, msg.sender); // Check batch is not empty, and then reinsert in sorted list if (!sortedTroves.isEmptyBatch(BatchId.wrap(msg.sender))) { - sortedTroves.reInsertBatch( - BatchId.wrap(msg.sender), - _newAnnualInterestRate, - _upperHint, - _lowerHint - ); + sortedTroves.reInsertBatch(BatchId.wrap(msg.sender), _newAnnualInterestRate, _upperHint, _lowerHint); } troveManager.onSetBatchManagerAnnualInterestRate( - msg.sender, - batch.entireCollWithoutRedistribution, - newDebt, - _newAnnualInterestRate, - batchChange.upfrontFee + msg.sender, batch.entireCollWithoutRedistribution, newDebt, _newAnnualInterestRate, batchChange.upfrontFee ); } @@ -206,50 +162,29 @@ contract BatchManagerOperations is IBatchManagerOperations { vars.newBatch = vars.troveManager.getLatestBatchData(_newBatchManager); TroveChange memory newBatchTroveChange; - newBatchTroveChange.appliedRedistBoldDebtGain = vars - .trove - .redistBoldDebtGain; + newBatchTroveChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain; newBatchTroveChange.appliedRedistCollGain = vars.trove.redistCollGain; - newBatchTroveChange.batchAccruedManagementFee = vars - .newBatch - .accruedManagementFee; + newBatchTroveChange.batchAccruedManagementFee = vars.newBatch.accruedManagementFee; newBatchTroveChange.oldWeightedRecordedDebt = - vars.newBatch.weightedRecordedDebt + - vars.trove.weightedRecordedDebt; + vars.newBatch.weightedRecordedDebt + vars.trove.weightedRecordedDebt; newBatchTroveChange.newWeightedRecordedDebt = - (vars.newBatch.entireDebtWithoutRedistribution + - vars.trove.entireDebt) * - vars.newBatch.annualInterestRate; + (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualInterestRate; // An upfront fee is always charged upon joining a batch to ensure that borrowers can not game the fee logic // and gain free interest rate updates (e.g. if they also manage the batch they joined) // It checks the resulting ICR - vars.trove.entireDebt = _applyUpfrontFee( - vars.trove.entireColl, - vars.trove.entireDebt, - newBatchTroveChange, - _maxUpfrontFee, - true - ); + vars.trove.entireDebt = + _applyUpfrontFee(vars.trove.entireColl, vars.trove.entireDebt, newBatchTroveChange, _maxUpfrontFee, true); // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee newBatchTroveChange.newWeightedRecordedDebt = - (vars.newBatch.entireDebtWithoutRedistribution + - vars.trove.entireDebt) * - vars.newBatch.annualInterestRate; + (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualInterestRate; // Add batch fees - newBatchTroveChange.oldWeightedRecordedBatchManagementFee = vars - .newBatch - .weightedRecordedBatchManagementFee; + newBatchTroveChange.oldWeightedRecordedBatchManagementFee = vars.newBatch.weightedRecordedBatchManagementFee; newBatchTroveChange.newWeightedRecordedBatchManagementFee = - (vars.newBatch.entireDebtWithoutRedistribution + - vars.trove.entireDebt) * - vars.newBatch.annualManagementFee; - vars.activePool.mintAggInterestAndAccountForTroveChange( - newBatchTroveChange, - _newBatchManager - ); + (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualManagementFee; + vars.activePool.mintAggInterestAndAccountForTroveChange(newBatchTroveChange, _newBatchManager); vars.troveManager.onSetInterestBatchManager( ITroveManager.OnSetInterestBatchManagerParams({ @@ -265,19 +200,11 @@ contract BatchManagerOperations is IBatchManagerOperations { vars.sortedTroves.remove(_troveId); vars.sortedTroves.insertIntoBatch( - _troveId, - BatchId.wrap(_newBatchManager), - vars.newBatch.annualInterestRate, - _upperHint, - _lowerHint + _troveId, BatchId.wrap(_newBatchManager), vars.newBatch.annualInterestRate, _upperHint, _lowerHint ); } - function kickFromBatch( - uint256 _troveId, - uint256 _upperHint, - uint256 _lowerHint - ) external { + function kickFromBatch(uint256 _troveId, uint256 _upperHint, uint256 _lowerHint) external { _removeFromBatchInternal( _troveId, 0, // ignored when kicking @@ -295,14 +222,7 @@ contract BatchManagerOperations is IBatchManagerOperations { uint256 _lowerHint, uint256 _maxUpfrontFee ) external { - _removeFromBatchInternal( - _troveId, - _newAnnualInterestRate, - _upperHint, - _lowerHint, - _maxUpfrontFee, - false - ); + _removeFromBatchInternal(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee, false); } function _removeFromBatchInternal( @@ -330,10 +250,7 @@ contract BatchManagerOperations is IBatchManagerOperations { vars.batch = vars.troveManager.getLatestBatchData(vars.batchManager); if (_kick) { - if ( - vars.batch.totalDebtShares * MAX_BATCH_SHARES_RATIO >= - vars.batch.entireDebtWithoutRedistribution - ) { + if (vars.batch.totalDebtShares * MAX_BATCH_SHARES_RATIO >= vars.batch.entireDebtWithoutRedistribution) { revert BatchSharesRatioTooLow(); } _newAnnualInterestRate = vars.batch.annualInterestRate; @@ -343,67 +260,36 @@ contract BatchManagerOperations is IBatchManagerOperations { // Remove trove from Batch in SortedTroves vars.sortedTroves.removeFromBatch(_troveId); // Reinsert as single trove - vars.sortedTroves.insert( - _troveId, - _newAnnualInterestRate, - _upperHint, - _lowerHint - ); + vars.sortedTroves.insert(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint); } vars.batchFutureDebt = - vars.batch.entireDebtWithoutRedistribution - - (vars.trove.entireDebt - vars.trove.redistBoldDebtGain); + vars.batch.entireDebtWithoutRedistribution - (vars.trove.entireDebt - vars.trove.redistBoldDebtGain); - vars.batchChange.appliedRedistBoldDebtGain = vars - .trove - .redistBoldDebtGain; + vars.batchChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain; vars.batchChange.appliedRedistCollGain = vars.trove.redistCollGain; - vars.batchChange.batchAccruedManagementFee = vars - .batch - .accruedManagementFee; - vars.batchChange.oldWeightedRecordedDebt = vars - .batch - .weightedRecordedDebt; + vars.batchChange.batchAccruedManagementFee = vars.batch.accruedManagementFee; + vars.batchChange.oldWeightedRecordedDebt = vars.batch.weightedRecordedDebt; vars.batchChange.newWeightedRecordedDebt = - vars.batchFutureDebt * - vars.batch.annualInterestRate + - vars.trove.entireDebt * - _newAnnualInterestRate; + vars.batchFutureDebt * vars.batch.annualInterestRate + vars.trove.entireDebt * _newAnnualInterestRate; // Apply upfront fee on premature adjustments. It checks the resulting ICR if ( - vars.batch.annualInterestRate != _newAnnualInterestRate && - block.timestamp < - vars.trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN + vars.batch.annualInterestRate != _newAnnualInterestRate + && block.timestamp < vars.trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN ) { - vars.trove.entireDebt = _applyUpfrontFee( - vars.trove.entireColl, - vars.trove.entireDebt, - vars.batchChange, - _maxUpfrontFee, - false - ); + vars.trove.entireDebt = + _applyUpfrontFee(vars.trove.entireColl, vars.trove.entireDebt, vars.batchChange, _maxUpfrontFee, false); } // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee vars.batchChange.newWeightedRecordedDebt = - vars.batchFutureDebt * - vars.batch.annualInterestRate + - vars.trove.entireDebt * - _newAnnualInterestRate; + vars.batchFutureDebt * vars.batch.annualInterestRate + vars.trove.entireDebt * _newAnnualInterestRate; // Add batch fees - vars.batchChange.oldWeightedRecordedBatchManagementFee = vars - .batch - .weightedRecordedBatchManagementFee; - vars.batchChange.newWeightedRecordedBatchManagementFee = - vars.batchFutureDebt * - vars.batch.annualManagementFee; - - activePool.mintAggInterestAndAccountForTroveChange( - vars.batchChange, - vars.batchManager - ); + vars.batchChange.oldWeightedRecordedBatchManagementFee = vars.batch.weightedRecordedBatchManagementFee; + vars.batchChange.newWeightedRecordedBatchManagementFee = vars.batchFutureDebt * vars.batch.annualManagementFee; + + activePool.mintAggInterestAndAccountForTroveChange(vars.batchChange, vars.batchManager); vars.troveManager.onRemoveFromBatch( _troveId, @@ -419,19 +305,17 @@ contract BatchManagerOperations is IBatchManagerOperations { // --- Helper Functions --- - function _calcUpfrontFee( - uint256 _debt, - uint256 _avgInterestRate - ) internal pure returns (uint256) { + function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal pure returns (uint256) { return _calcInterest(_debt * _avgInterestRate, UPFRONT_INTEREST_PERIOD); } // Duplicated internal function from BorrowerOperations but calls to getEntireBranchColl and getEntireBranchDebt // are replaced with their implementations - function _getNewTCRFromTroveChange( - TroveChange memory _troveChange, - uint256 _price - ) internal view returns (uint256 newTCR) { + function _getNewTCRFromTroveChange(TroveChange memory _troveChange, uint256 _price) + internal + view + returns (uint256 newTCR) + { uint256 activeColl = activePool.getCollBalance(); uint256 liquidatedColl = defaultPool.getCollBalance(); uint256 totalColl = activeColl + liquidatedColl + _troveChange.collIncrease - _troveChange.collDecrease; @@ -439,11 +323,7 @@ contract BatchManagerOperations is IBatchManagerOperations { uint256 activeDebt = activePool.getBoldDebt(); uint256 closedDebt = defaultPool.getBoldDebt(); uint256 totalDebt = - activeDebt + - closedDebt + - _troveChange.debtIncrease + - _troveChange.upfrontFee - - _troveChange.debtDecrease; + activeDebt + closedDebt + _troveChange.debtIncrease + _troveChange.upfrontFee - _troveChange.debtDecrease; newTCR = LiquityMath._computeCR(totalColl, totalDebt, _price); } @@ -457,22 +337,14 @@ contract BatchManagerOperations is IBatchManagerOperations { ) internal returns (uint256) { uint256 price = priceFeed.fetchPrice(); - uint256 avgInterestRate = activePool - .getNewApproxAvgInterestRateFromTroveChange(_troveChange); - _troveChange.upfrontFee = _calcUpfrontFee( - _troveEntireDebt, - avgInterestRate - ); + uint256 avgInterestRate = activePool.getNewApproxAvgInterestRateFromTroveChange(_troveChange); + _troveChange.upfrontFee = _calcUpfrontFee(_troveEntireDebt, avgInterestRate); _requireUserAcceptsUpfrontFee(_troveChange.upfrontFee, _maxUpfrontFee); _troveEntireDebt += _troveChange.upfrontFee; // ICR is based on the requested Bold amount + upfront fee. - uint256 newICR = LiquityMath._computeCR( - _troveEntireColl, - _troveEntireDebt, - price - ); + uint256 newICR = LiquityMath._computeCR(_troveEntireColl, _troveEntireDebt, price); if (_isTroveInBatch) { _requireICRisAboveMCRPlusBCR(newICR); } else { @@ -487,10 +359,7 @@ contract BatchManagerOperations is IBatchManagerOperations { return _troveEntireDebt; } - function _checkTroveIsZombie( - ITroveManager _troveManager, - uint256 _troveId - ) internal view returns (bool) { + function _checkTroveIsZombie(ITroveManager _troveManager, uint256 _troveId) internal view returns (bool) { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); return status == ITroveManager.Status.zombie; } @@ -501,9 +370,7 @@ contract BatchManagerOperations is IBatchManagerOperations { // --- Validation Functions --- - function _requireValidAnnualInterestRate( - uint256 _annualInterestRate - ) internal view { + function _requireValidAnnualInterestRate(uint256 _annualInterestRate) internal view { if (_annualInterestRate < systemParams.MIN_ANNUAL_INTEREST_RATE()) { revert InterestRateTooLow(); } @@ -512,10 +379,7 @@ contract BatchManagerOperations is IBatchManagerOperations { } } - function _requireOrderedRange( - uint256 _minInterestRate, - uint256 _maxInterestRate - ) internal pure { + function _requireOrderedRange(uint256 _minInterestRate, uint256 _maxInterestRate) internal pure { if (_minInterestRate >= _maxInterestRate) revert MinGeMax(); } @@ -524,10 +388,7 @@ contract BatchManagerOperations is IBatchManagerOperations { uint256 _minInterestRate, uint256 _maxInterestRate ) internal pure { - if ( - _minInterestRate > _annualInterestRate || - _annualInterestRate > _maxInterestRate - ) { + if (_minInterestRate > _annualInterestRate || _annualInterestRate > _maxInterestRate) { revert InterestNotInRange(); } } @@ -541,10 +402,7 @@ contract BatchManagerOperations is IBatchManagerOperations { } } - function _requireUserAcceptsUpfrontFee( - uint256 _fee, - uint256 _maxFee - ) internal pure { + function _requireUserAcceptsUpfrontFee(uint256 _fee, uint256 _maxFee) internal pure { if (_fee > _maxFee) { revert UpfrontFeeTooHigh(); } @@ -556,25 +414,16 @@ contract BatchManagerOperations is IBatchManagerOperations { } } - function _requireTroveIsActive( - ITroveManager _troveManager, - uint256 _troveId - ) internal view { + function _requireTroveIsActive(ITroveManager _troveManager, uint256 _troveId) internal view { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); if (status != ITroveManager.Status.active) { revert TroveNotActive(); } } - function _requireTroveIsOpen( - ITroveManager _troveManager, - uint256 _troveId - ) internal view { + function _requireTroveIsOpen(ITroveManager _troveManager, uint256 _troveId) internal view { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); - if ( - status != ITroveManager.Status.active && - status != ITroveManager.Status.zombie - ) { + if (status != ITroveManager.Status.active && status != ITroveManager.Status.zombie) { revert TroveNotOpen(); } } @@ -585,12 +434,8 @@ contract BatchManagerOperations is IBatchManagerOperations { } } - function _requireIsInBatch( - uint256 _troveId - ) internal view returns (address) { - address batchManager = IBorrowerOperations(address(this)).interestBatchManagerOf( - _troveId - ); + function _requireIsInBatch(uint256 _troveId) internal view returns (address) { + address batchManager = IBorrowerOperations(address(this)).interestBatchManagerOf(_troveId); if (batchManager == address(0)) { revert TroveNotInBatch(); } @@ -608,5 +453,4 @@ contract BatchManagerOperations is IBatchManagerOperations { revert ICRBelowMCRPlusBCR(); } } - } diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 6b3eea84e..c15070d61 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -17,11 +17,7 @@ import "./Types/LatestTroveData.sol"; import "./Types/LatestBatchData.sol"; import "./BatchManagerOperations.sol"; -contract BorrowerOperations is - LiquityBase, - AddRemoveManagers, - IBorrowerOperations -{ +contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperations { using SafeERC20 for IERC20; // --- Connected contract declarations --- @@ -47,8 +43,7 @@ contract BorrowerOperations is * This address then has the ability to update the borrower’s interest rate, but not change its debt or collateral. * Useful for instance for cold/hot wallet setups. */ - mapping(uint256 => InterestIndividualDelegate) - private interestIndividualDelegateOf; + mapping(uint256 => InterestIndividualDelegate) private interestIndividualDelegateOf; /* * Mapping from TroveId to granted address for interest rate setting (batch manager). @@ -143,10 +138,10 @@ contract BorrowerOperations is event ShutDown(uint256 _tcr); - constructor( - IAddressesRegistry _addressesRegistry, - ISystemParams _systemParams - ) AddRemoveManagers(_addressesRegistry) LiquityBase(_addressesRegistry) { + constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) + AddRemoveManagers(_addressesRegistry) + LiquityBase(_addressesRegistry) + { // This makes impossible to open a trove with zero withdrawn Bold assert(_systemParams.MIN_DEBT() > 0); @@ -221,41 +216,29 @@ contract BorrowerOperations is ); // Set the stored Trove properties and mint the NFT - troveManager.onOpenTrove( - _owner, - vars.troveId, - vars.change, - _annualInterestRate - ); + troveManager.onOpenTrove(_owner, vars.troveId, vars.change, _annualInterestRate); - sortedTroves.insert( - vars.troveId, - _annualInterestRate, - _upperHint, - _lowerHint - ); + sortedTroves.insert(vars.troveId, _annualInterestRate, _upperHint, _lowerHint); return vars.troveId; } - function openTroveAndJoinInterestBatchManager( - OpenTroveAndJoinInterestBatchManagerParams calldata _params - ) external override returns (uint256) { + function openTroveAndJoinInterestBatchManager(OpenTroveAndJoinInterestBatchManagerParams calldata _params) + external + override + returns (uint256) + { _requireValidInterestBatchManager(_params.interestBatchManager); OpenTroveVars memory vars; vars.troveManager = troveManager; - vars.batch = vars.troveManager.getLatestBatchData( - _params.interestBatchManager - ); + vars.batch = vars.troveManager.getLatestBatchData(_params.interestBatchManager); // We set old weighted values here, as it’s only necessary for batches, so we don’t need to pass them to _openTrove func vars.change.batchAccruedManagementFee = vars.batch.accruedManagementFee; vars.change.oldWeightedRecordedDebt = vars.batch.weightedRecordedDebt; - vars.change.oldWeightedRecordedBatchManagementFee = vars - .batch - .weightedRecordedBatchManagementFee; + vars.change.oldWeightedRecordedBatchManagementFee = vars.batch.weightedRecordedBatchManagementFee; vars.troveId = _openTrove( _params.owner, _params.ownerIndex, @@ -323,53 +306,35 @@ contract BorrowerOperations is // --- Checks --- - vars.troveId = uint256( - keccak256(abi.encode(msg.sender, _owner, _ownerIndex)) - ); + vars.troveId = uint256(keccak256(abi.encode(msg.sender, _owner, _ownerIndex))); _requireTroveDoesNotExist(vars.troveManager, vars.troveId); _change.collIncrease = _collAmount; _change.debtIncrease = _boldAmount; // For simplicity, we ignore the fee when calculating the approx. interest rate - _change.newWeightedRecordedDebt = - (_batchEntireDebt + _change.debtIncrease) * - _annualInterestRate; - - vars.avgInterestRate = vars - .activePool - .getNewApproxAvgInterestRateFromTroveChange(_change); - _change.upfrontFee = _calcUpfrontFee( - _change.debtIncrease, - vars.avgInterestRate - ); + _change.newWeightedRecordedDebt = (_batchEntireDebt + _change.debtIncrease) * _annualInterestRate; + + vars.avgInterestRate = vars.activePool.getNewApproxAvgInterestRateFromTroveChange(_change); + _change.upfrontFee = _calcUpfrontFee(_change.debtIncrease, vars.avgInterestRate); _requireUserAcceptsUpfrontFee(_change.upfrontFee, _maxUpfrontFee); vars.entireDebt = _change.debtIncrease + _change.upfrontFee; _requireAtLeastMinDebt(vars.entireDebt); - vars.ICR = LiquityMath._computeCR( - _collAmount, - vars.entireDebt, - vars.price - ); + vars.ICR = LiquityMath._computeCR(_collAmount, vars.entireDebt, vars.price); // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee, and the batch fee if needed if (_interestBatchManager == address(0)) { - _change.newWeightedRecordedDebt = - vars.entireDebt * - _annualInterestRate; + _change.newWeightedRecordedDebt = vars.entireDebt * _annualInterestRate; // ICR is based on the requested Bold amount + upfront fee. _requireICRisAboveMCR(vars.ICR); } else { // old values have been set outside, before calling this function - _change.newWeightedRecordedDebt = - (_batchEntireDebt + vars.entireDebt) * - _annualInterestRate; + _change.newWeightedRecordedDebt = (_batchEntireDebt + vars.entireDebt) * _annualInterestRate; _change.newWeightedRecordedBatchManagementFee = - (_batchEntireDebt + vars.entireDebt) * - _batchManagementAnnualFee; + (_batchEntireDebt + vars.entireDebt) * _batchManagementAnnualFee; // ICR is based on the requested Bold amount + upfront fee. // Troves in a batch have a stronger requirement (MCR+BCR) @@ -385,10 +350,7 @@ contract BorrowerOperations is _setAddManager(vars.troveId, _addManager); _setRemoveManagerAndReceiver(vars.troveId, _removeManager, _receiver); - vars.activePool.mintAggInterestAndAccountForTroveChange( - _change, - _interestBatchManager - ); + vars.activePool.mintAggInterestAndAccountForTroveChange(_change, _interestBatchManager); // Pull coll tokens from sender and move them to the Active Pool _pullCollAndSendToActivePool(vars.activePool, _collAmount); @@ -417,10 +379,7 @@ contract BorrowerOperations is } // Withdraw collateral from a trove - function withdrawColl( - uint256 _troveId, - uint256 _collWithdrawal - ) external override { + function withdrawColl(uint256 _troveId, uint256 _collWithdrawal) external override { ITroveManager troveManagerCached = troveManager; _requireTroveIsActive(troveManagerCached, _troveId); @@ -436,11 +395,7 @@ contract BorrowerOperations is } // Withdraw Bold tokens from a trove: mint new Bold tokens to the owner, and increase the trove's debt accordingly - function withdrawBold( - uint256 _troveId, - uint256 _boldAmount, - uint256 _maxUpfrontFee - ) external override { + function withdrawBold(uint256 _troveId, uint256 _boldAmount, uint256 _maxUpfrontFee) external override { ITroveManager troveManagerCached = troveManager; _requireTroveIsActive(troveManagerCached, _troveId); @@ -450,10 +405,7 @@ contract BorrowerOperations is } // Repay Bold tokens to a Trove: Burn the repaid Bold tokens, and reduce the trove's debt accordingly - function repayBold( - uint256 _troveId, - uint256 _boldAmount - ) external override { + function repayBold(uint256 _troveId, uint256 _boldAmount) external override { ITroveManager troveManagerCached = troveManager; _requireTroveIsActive(troveManagerCached, _troveId); @@ -500,13 +452,7 @@ contract BorrowerOperations is _requireTroveIsActive(troveManagerCached, _troveId); TroveChange memory troveChange; - _initTroveChange( - troveChange, - _collChange, - _isCollIncrease, - _boldChange, - _isDebtIncrease - ); + _initTroveChange(troveChange, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease); _adjustTrove(troveManagerCached, _troveId, troveChange, _maxUpfrontFee); } @@ -524,13 +470,7 @@ contract BorrowerOperations is _requireTroveIsZombie(troveManagerCached, _troveId); TroveChange memory troveChange; - _initTroveChange( - troveChange, - _collChange, - _isCollIncrease, - _boldChange, - _isDebtIncrease - ); + _initTroveChange(troveChange, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease); _adjustTrove(troveManagerCached, _troveId, troveChange, _maxUpfrontFee); troveManagerCached.setTroveStatusToActive(_troveId); @@ -538,8 +478,7 @@ contract BorrowerOperations is address batchManager = interestBatchManagerOf[_troveId]; uint256 batchAnnualInterestRate; if (batchManager != address(0)) { - LatestBatchData memory batch = troveManagerCached - .getLatestBatchData(batchManager); + LatestBatchData memory batch = troveManagerCached.getLatestBatchData(batchManager); batchAnnualInterestRate = batch.annualInterestRate; } _reInsertIntoSortedTroves( @@ -568,18 +507,9 @@ contract BorrowerOperations is _requireSenderIsOwnerOrInterestManager(_troveId); _requireTroveIsActive(troveManagerCached, _troveId); - LatestTroveData memory trove = troveManagerCached.getLatestTroveData( - _troveId - ); - _requireValidDelegateAdjustment( - _troveId, - trove.lastInterestRateAdjTime, - _newAnnualInterestRate - ); - _requireAnnualInterestRateIsNew( - trove.annualInterestRate, - _newAnnualInterestRate - ); + LatestTroveData memory trove = troveManagerCached.getLatestTroveData(_troveId); + _requireValidDelegateAdjustment(_troveId, trove.lastInterestRateAdjTime, _newAnnualInterestRate); + _requireAnnualInterestRateIsNew(trove.annualInterestRate, _newAnnualInterestRate); uint256 newDebt = trove.entireDebt; @@ -590,39 +520,18 @@ contract BorrowerOperations is troveChange.oldWeightedRecordedDebt = trove.weightedRecordedDebt; // Apply upfront fee on premature adjustments. It checks the resulting ICR - if ( - block.timestamp < - trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN - ) { - newDebt = _applyUpfrontFee( - trove.entireColl, - newDebt, - troveChange, - _maxUpfrontFee, - false - ); + if (block.timestamp < trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN) { + newDebt = _applyUpfrontFee(trove.entireColl, newDebt, troveChange, _maxUpfrontFee, false); } // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee troveChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate; - activePool.mintAggInterestAndAccountForTroveChange( - troveChange, - address(0) - ); + activePool.mintAggInterestAndAccountForTroveChange(troveChange, address(0)); - sortedTroves.reInsert( - _troveId, - _newAnnualInterestRate, - _upperHint, - _lowerHint - ); + sortedTroves.reInsert(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint); troveManagerCached.onAdjustTroveInterestRate( - _troveId, - trove.entireColl, - newDebt, - _newAnnualInterestRate, - troveChange + _troveId, trove.entireColl, newDebt, _newAnnualInterestRate, troveChange ); } @@ -642,10 +551,7 @@ contract BorrowerOperations is vars.boldToken = boldToken; vars.price = priceFeed.fetchPrice(); - vars.isBelowCriticalThreshold = _checkBelowCriticalThreshold( - vars.price, - systemParams.CCR() - ); + vars.isBelowCriticalThreshold = _checkBelowCriticalThreshold(vars.price, systemParams.CCR()); // --- Checks --- @@ -655,10 +561,7 @@ contract BorrowerOperations is address receiver = owner; // If it’s a withdrawal, and remove manager privilege is set, a different receiver can be defined if (_troveChange.collDecrease > 0 || _troveChange.debtIncrease > 0) { - receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver( - _troveId, - owner - ); + receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner); } else { // RemoveManager assumes AddManager, so if the former is set, there's no need to check the latter _requireSenderIsOwnerOrAddManager(_troveId, owner); @@ -672,37 +575,23 @@ contract BorrowerOperations is // When the adjustment is a debt repayment, check it's a valid amount and that the caller has enough Bold if (_troveChange.debtDecrease > 0) { - uint256 maxRepayment = vars.trove.entireDebt > systemParams.MIN_DEBT() - ? vars.trove.entireDebt - systemParams.MIN_DEBT() - : 0; + uint256 maxRepayment = + vars.trove.entireDebt > systemParams.MIN_DEBT() ? vars.trove.entireDebt - systemParams.MIN_DEBT() : 0; if (_troveChange.debtDecrease > maxRepayment) { _troveChange.debtDecrease = maxRepayment; } - _requireSufficientBoldBalance( - vars.boldToken, - msg.sender, - _troveChange.debtDecrease - ); + _requireSufficientBoldBalance(vars.boldToken, msg.sender, _troveChange.debtDecrease); } _requireNonZeroAdjustment(_troveChange); // When the adjustment is a collateral withdrawal, check that it's no more than the Trove's entire collateral if (_troveChange.collDecrease > 0) { - _requireValidCollWithdrawal( - vars.trove.entireColl, - _troveChange.collDecrease - ); + _requireValidCollWithdrawal(vars.trove.entireColl, _troveChange.collDecrease); } - vars.newColl = - vars.trove.entireColl + - _troveChange.collIncrease - - _troveChange.collDecrease; - vars.newDebt = - vars.trove.entireDebt + - _troveChange.debtIncrease - - _troveChange.debtDecrease; + vars.newColl = vars.trove.entireColl + _troveChange.collIncrease - _troveChange.collDecrease; + vars.newDebt = vars.trove.entireDebt + _troveChange.debtIncrease - _troveChange.debtDecrease; address batchManager = interestBatchManagerOf[_troveId]; bool isTroveInBatch = batchManager != address(0); @@ -711,68 +600,38 @@ contract BorrowerOperations is if (isTroveInBatch) { batch = _troveManager.getLatestBatchData(batchManager); - batchFutureDebt = - batch.entireDebtWithoutRedistribution + - vars.trove.redistBoldDebtGain + - _troveChange.debtIncrease - - _troveChange.debtDecrease; + batchFutureDebt = batch.entireDebtWithoutRedistribution + vars.trove.redistBoldDebtGain + + _troveChange.debtIncrease - _troveChange.debtDecrease; - _troveChange.appliedRedistBoldDebtGain = vars - .trove - .redistBoldDebtGain; + _troveChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain; _troveChange.appliedRedistCollGain = vars.trove.redistCollGain; _troveChange.batchAccruedManagementFee = batch.accruedManagementFee; _troveChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; - _troveChange.newWeightedRecordedDebt = - batchFutureDebt * - batch.annualInterestRate; - _troveChange.oldWeightedRecordedBatchManagementFee = batch - .weightedRecordedBatchManagementFee; - _troveChange.newWeightedRecordedBatchManagementFee = - batchFutureDebt * - batch.annualManagementFee; + _troveChange.newWeightedRecordedDebt = batchFutureDebt * batch.annualInterestRate; + _troveChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; + _troveChange.newWeightedRecordedBatchManagementFee = batchFutureDebt * batch.annualManagementFee; } else { - _troveChange.appliedRedistBoldDebtGain = vars - .trove - .redistBoldDebtGain; + _troveChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain; _troveChange.appliedRedistCollGain = vars.trove.redistCollGain; - _troveChange.oldWeightedRecordedDebt = vars - .trove - .weightedRecordedDebt; - _troveChange.newWeightedRecordedDebt = - vars.newDebt * - vars.trove.annualInterestRate; + _troveChange.oldWeightedRecordedDebt = vars.trove.weightedRecordedDebt; + _troveChange.newWeightedRecordedDebt = vars.newDebt * vars.trove.annualInterestRate; } // Pay an upfront fee on debt increases if (_troveChange.debtIncrease > 0) { - uint256 avgInterestRate = vars - .activePool - .getNewApproxAvgInterestRateFromTroveChange(_troveChange); - _troveChange.upfrontFee = _calcUpfrontFee( - _troveChange.debtIncrease, - avgInterestRate - ); - _requireUserAcceptsUpfrontFee( - _troveChange.upfrontFee, - _maxUpfrontFee - ); + uint256 avgInterestRate = vars.activePool.getNewApproxAvgInterestRateFromTroveChange(_troveChange); + _troveChange.upfrontFee = _calcUpfrontFee(_troveChange.debtIncrease, avgInterestRate); + _requireUserAcceptsUpfrontFee(_troveChange.upfrontFee, _maxUpfrontFee); vars.newDebt += _troveChange.upfrontFee; if (isTroveInBatch) { batchFutureDebt += _troveChange.upfrontFee; // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee - _troveChange.newWeightedRecordedDebt = - batchFutureDebt * - batch.annualInterestRate; - _troveChange.newWeightedRecordedBatchManagementFee = - batchFutureDebt * - batch.annualManagementFee; + _troveChange.newWeightedRecordedDebt = batchFutureDebt * batch.annualInterestRate; + _troveChange.newWeightedRecordedBatchManagementFee = batchFutureDebt * batch.annualManagementFee; } else { // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee - _troveChange.newWeightedRecordedDebt = - vars.newDebt * - vars.trove.annualInterestRate; + _troveChange.newWeightedRecordedDebt = vars.newDebt * vars.trove.annualInterestRate; } } @@ -780,18 +639,10 @@ contract BorrowerOperations is // Now the max repayment is capped to stay above MIN_DEBT, so this only applies to adjustZombieTrove _requireAtLeastMinDebt(vars.newDebt); - vars.newICR = LiquityMath._computeCR( - vars.newColl, - vars.newDebt, - vars.price - ); + vars.newICR = LiquityMath._computeCR(vars.newColl, vars.newDebt, vars.price); // Check the adjustment satisfies all conditions for the current system mode - _requireValidAdjustmentInCurrentMode( - _troveChange, - vars, - isTroveInBatch - ); + _requireValidAdjustmentInCurrentMode(_troveChange, vars, isTroveInBatch); // --- Effects and interactions --- @@ -806,24 +657,11 @@ contract BorrowerOperations is batch.entireDebtWithoutRedistribution ); } else { - _troveManager.onAdjustTrove( - _troveId, - vars.newColl, - vars.newDebt, - _troveChange - ); + _troveManager.onAdjustTrove(_troveId, vars.newColl, vars.newDebt, _troveChange); } - vars.activePool.mintAggInterestAndAccountForTroveChange( - _troveChange, - batchManager - ); - _moveTokensFromAdjustment( - receiver, - _troveChange, - vars.boldToken, - vars.activePool - ); + vars.activePool.mintAggInterestAndAccountForTroveChange(_troveChange, batchManager); + _moveTokensFromAdjustment(receiver, _troveChange, vars.boldToken, vars.activePool); } function closeTrove(uint256 _troveId) external override { @@ -834,22 +672,13 @@ contract BorrowerOperations is // --- Checks --- address owner = troveNFT.ownerOf(_troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver( - _troveId, - owner - ); + address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner); _requireTroveIsOpen(troveManagerCached, _troveId); - LatestTroveData memory trove = troveManagerCached.getLatestTroveData( - _troveId - ); + LatestTroveData memory trove = troveManagerCached.getLatestTroveData(_troveId); // The borrower must repay their entire debt including accrued interest, batch fee and redist. gains - _requireSufficientBoldBalance( - boldTokenCached, - msg.sender, - trove.entireDebt - ); + _requireSufficientBoldBalance(boldTokenCached, msg.sender, trove.entireDebt); TroveChange memory troveChange; troveChange.appliedRedistBoldDebtGain = trove.redistBoldDebtGain; @@ -861,18 +690,13 @@ contract BorrowerOperations is LatestBatchData memory batch; if (batchManager != address(0)) { batch = troveManagerCached.getLatestBatchData(batchManager); - uint256 batchFutureDebt = batch.entireDebtWithoutRedistribution - - (trove.entireDebt - trove.redistBoldDebtGain); + uint256 batchFutureDebt = + batch.entireDebtWithoutRedistribution - (trove.entireDebt - trove.redistBoldDebtGain); troveChange.batchAccruedManagementFee = batch.accruedManagementFee; troveChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; - troveChange.newWeightedRecordedDebt = - batchFutureDebt * - batch.annualInterestRate; - troveChange.oldWeightedRecordedBatchManagementFee = batch - .weightedRecordedBatchManagementFee; - troveChange.newWeightedRecordedBatchManagementFee = - batchFutureDebt * - batch.annualManagementFee; + troveChange.newWeightedRecordedDebt = batchFutureDebt * batch.annualInterestRate; + troveChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; + troveChange.newWeightedRecordedBatchManagementFee = batchFutureDebt * batch.annualManagementFee; } else { troveChange.oldWeightedRecordedDebt = trove.weightedRecordedDebt; // troveChange.newWeightedRecordedDebt = 0; @@ -896,10 +720,7 @@ contract BorrowerOperations is interestBatchManagerOf[_troveId] = address(0); } - activePoolCached.mintAggInterestAndAccountForTroveChange( - troveChange, - batchManager - ); + activePoolCached.mintAggInterestAndAccountForTroveChange(troveChange, batchManager); // Return ETH gas compensation gasToken.transferFrom(gasPoolAddress, receiver, systemParams.ETH_GAS_COMPENSATION()); @@ -912,20 +733,14 @@ contract BorrowerOperations is _wipeTroveMappings(_troveId); } - function applyPendingDebt( - uint256 _troveId, - uint256 _lowerHint, - uint256 _upperHint - ) public { + function applyPendingDebt(uint256 _troveId, uint256 _lowerHint, uint256 _upperHint) public { _requireIsNotShutDown(); ITroveManager troveManagerCached = troveManager; _requireTroveIsOpen(troveManagerCached, _troveId); - LatestTroveData memory trove = troveManagerCached.getLatestTroveData( - _troveId - ); + LatestTroveData memory trove = troveManagerCached.getLatestTroveData(_troveId); _requireNonZeroDebt(trove.entireDebt); TroveChange memory change; @@ -937,23 +752,16 @@ contract BorrowerOperations is if (batchManager == address(0)) { change.oldWeightedRecordedDebt = trove.weightedRecordedDebt; - change.newWeightedRecordedDebt = - trove.entireDebt * - trove.annualInterestRate; + change.newWeightedRecordedDebt = trove.entireDebt * trove.annualInterestRate; } else { batch = troveManagerCached.getLatestBatchData(batchManager); change.batchAccruedManagementFee = batch.accruedManagementFee; change.oldWeightedRecordedDebt = batch.weightedRecordedDebt; change.newWeightedRecordedDebt = - (batch.entireDebtWithoutRedistribution + - trove.redistBoldDebtGain) * - batch.annualInterestRate; - change.oldWeightedRecordedBatchManagementFee = batch - .weightedRecordedBatchManagementFee; + (batch.entireDebtWithoutRedistribution + trove.redistBoldDebtGain) * batch.annualInterestRate; + change.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; change.newWeightedRecordedBatchManagementFee = - (batch.entireDebtWithoutRedistribution + - trove.redistBoldDebtGain) * - batch.annualManagementFee; + (batch.entireDebtWithoutRedistribution + trove.redistBoldDebtGain) * batch.annualManagementFee; } troveManagerCached.onApplyTroveInterest( @@ -965,31 +773,22 @@ contract BorrowerOperations is batch.entireDebtWithoutRedistribution, change ); - activePool.mintAggInterestAndAccountForTroveChange( - change, - batchManager - ); + activePool.mintAggInterestAndAccountForTroveChange(change, batchManager); // If the trove was zombie, and now it's not anymore, put it back in the list - if ( - _checkTroveIsZombie(troveManagerCached, _troveId) && - trove.entireDebt >= systemParams.MIN_DEBT() - ) { + if (_checkTroveIsZombie(troveManagerCached, _troveId) && trove.entireDebt >= systemParams.MIN_DEBT()) { troveManagerCached.setTroveStatusToActive(_troveId); _reInsertIntoSortedTroves( - _troveId, - trove.annualInterestRate, - _upperHint, - _lowerHint, - batchManager, - batch.annualInterestRate + _troveId, trove.annualInterestRate, _upperHint, _lowerHint, batchManager, batch.annualInterestRate ); } } - function getInterestIndividualDelegateOf( - uint256 _troveId - ) external view returns (InterestIndividualDelegate memory) { + function getInterestIndividualDelegateOf(uint256 _troveId) + external + view + returns (InterestIndividualDelegate memory) + { return interestIndividualDelegateOf[_troveId]; } @@ -1013,23 +812,13 @@ contract BorrowerOperations is // With the check below, it could only be == _requireOrderedRange(_minInterestRate, _maxInterestRate); - interestIndividualDelegateOf[_troveId] = InterestIndividualDelegate( - _delegate, - _minInterestRate, - _maxInterestRate, - _minInterestRateChangePeriod - ); + interestIndividualDelegateOf[_troveId] = + InterestIndividualDelegate(_delegate, _minInterestRate, _maxInterestRate, _minInterestRateChangePeriod); // Can’t have both individual delegation and batch manager if (interestBatchManagerOf[_troveId] != address(0)) { // Not needed, implicitly checked in removeFromBatch //_requireValidAnnualInterestRate(_newAnnualInterestRate); - removeFromBatch( - _troveId, - _newAnnualInterestRate, - _upperHint, - _lowerHint, - _maxUpfrontFee - ); + removeFromBatch(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee); } } @@ -1038,9 +827,7 @@ contract BorrowerOperations is delete interestIndividualDelegateOf[_troveId]; } - function getInterestBatchManager( - address _account - ) external view returns (InterestBatchManager memory) { + function getInterestBatchManager(address _account) external view returns (InterestBatchManager memory) { return interestBatchManagers[_account]; } @@ -1064,26 +851,16 @@ contract BorrowerOperations is ) ); _requireDelegateCallSucceeded(success, data); - interestBatchManagers[msg.sender] = InterestBatchManager( - _minInterestRate, - _maxInterestRate, - _minInterestRateChangePeriod - ); - troveManager.onRegisterBatchManager( - msg.sender, - _currentInterestRate, - _annualManagementFee - ); + interestBatchManagers[msg.sender] = + InterestBatchManager(_minInterestRate, _maxInterestRate, _minInterestRateChangePeriod); + troveManager.onRegisterBatchManager(msg.sender, _currentInterestRate, _annualManagementFee); } function lowerBatchManagementFee(uint256 _newAnnualManagementFee) external { _requireIsNotShutDown(); _requireValidInterestBatchManager(msg.sender); (bool success, bytes memory data) = batchManagerOperations.delegatecall( - abi.encodeWithSignature( - "lowerBatchManagementFee(uint256)", - _newAnnualManagementFee - ) + abi.encodeWithSignature("lowerBatchManagementFee(uint256)", _newAnnualManagementFee) ); _requireDelegateCallSucceeded(success, data); } @@ -1096,10 +873,7 @@ contract BorrowerOperations is ) external { _requireIsNotShutDown(); _requireValidInterestBatchManager(msg.sender); - _requireInterestRateInBatchManagerRange( - msg.sender, - _newAnnualInterestRate - ); + _requireInterestRateInBatchManagerRange(msg.sender, _newAnnualInterestRate); InterestBatchManager memory interestBatchManager = interestBatchManagers[msg.sender]; (bool success, bytes memory data) = batchManagerOperations.delegatecall( abi.encodeWithSignature( @@ -1128,8 +902,9 @@ contract BorrowerOperations is _requireIsNotInBatch(_troveId); interestBatchManagerOf[_troveId] = _newBatchManager; // Can't have both individual delegation and batch manager - if (interestIndividualDelegateOf[_troveId].account != address(0)) + if (interestIndividualDelegateOf[_troveId].account != address(0)) { delete interestIndividualDelegateOf[_troveId]; + } (bool success, bytes memory data) = batchManagerOperations.delegatecall( abi.encodeWithSignature( @@ -1144,19 +919,10 @@ contract BorrowerOperations is _requireDelegateCallSucceeded(success, data); } - function kickFromBatch( - uint256 _troveId, - uint256 _upperHint, - uint256 _lowerHint - ) external override { + function kickFromBatch(uint256 _troveId, uint256 _upperHint, uint256 _lowerHint) external override { _requireIsNotShutDown(); (bool success, bytes memory data) = batchManagerOperations.delegatecall( - abi.encodeWithSignature( - "kickFromBatch(uint256,uint256,uint256)", - _troveId, - _upperHint, - _lowerHint - ) + abi.encodeWithSignature("kickFromBatch(uint256,uint256,uint256)", _troveId, _upperHint, _lowerHint) ); _requireDelegateCallSucceeded(success, data); } @@ -1195,24 +961,10 @@ contract BorrowerOperations is address oldBatchManager = _requireIsInBatch(_troveId); _requireNewInterestBatchManager(oldBatchManager, _newBatchManager); - LatestBatchData memory oldBatch = troveManager.getLatestBatchData( - oldBatchManager - ); + LatestBatchData memory oldBatch = troveManager.getLatestBatchData(oldBatchManager); - removeFromBatch( - _troveId, - oldBatch.annualInterestRate, - _removeUpperHint, - _removeLowerHint, - 0 - ); - setInterestBatchManager( - _troveId, - _newBatchManager, - _addUpperHint, - _addLowerHint, - _maxUpfrontFee - ); + removeFromBatch(_troveId, oldBatch.annualInterestRate, _removeUpperHint, _removeLowerHint, 0); + setInterestBatchManager(_troveId, _newBatchManager, _addUpperHint, _addLowerHint, _maxUpfrontFee); } function _applyUpfrontFee( @@ -1224,22 +976,14 @@ contract BorrowerOperations is ) internal returns (uint256) { uint256 price = priceFeed.fetchPrice(); - uint256 avgInterestRate = activePool - .getNewApproxAvgInterestRateFromTroveChange(_troveChange); - _troveChange.upfrontFee = _calcUpfrontFee( - _troveEntireDebt, - avgInterestRate - ); + uint256 avgInterestRate = activePool.getNewApproxAvgInterestRateFromTroveChange(_troveChange); + _troveChange.upfrontFee = _calcUpfrontFee(_troveEntireDebt, avgInterestRate); _requireUserAcceptsUpfrontFee(_troveChange.upfrontFee, _maxUpfrontFee); _troveEntireDebt += _troveChange.upfrontFee; // ICR is based on the requested Bold amount + upfront fee. - uint256 newICR = LiquityMath._computeCR( - _troveEntireColl, - _troveEntireDebt, - price - ); + uint256 newICR = LiquityMath._computeCR(_troveEntireColl, _troveEntireDebt, price); if (_isTroveInBatch) { _requireICRisAboveMCRPlusBCR(newICR); } else { @@ -1254,10 +998,7 @@ contract BorrowerOperations is return _troveEntireDebt; } - function _calcUpfrontFee( - uint256 _debt, - uint256 _avgInterestRate - ) internal pure returns (uint256) { + function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal pure returns (uint256) { return _calcInterest(_debt * _avgInterestRate, UPFRONT_INTEREST_PERIOD); } @@ -1327,19 +1068,10 @@ contract BorrowerOperations is ) internal { // If it was in a batch, we need to put it back, otherwise we insert it normally if (_batchManager == address(0)) { - sortedTroves.insert( - _troveId, - _troveAnnualInterestRate, - _upperHint, - _lowerHint - ); + sortedTroves.insert(_troveId, _troveAnnualInterestRate, _upperHint, _lowerHint); } else { sortedTroves.insertIntoBatch( - _troveId, - BatchId.wrap(_batchManager), - _batchAnnualInterestRate, - _upperHint, - _lowerHint + _troveId, BatchId.wrap(_batchManager), _batchAnnualInterestRate, _upperHint, _lowerHint ); } } @@ -1360,29 +1092,21 @@ contract BorrowerOperations is if (_troveChange.collIncrease > 0) { // Pull coll tokens from sender and move them to the Active Pool - _pullCollAndSendToActivePool( - _activePool, - _troveChange.collIncrease - ); + _pullCollAndSendToActivePool(_activePool, _troveChange.collIncrease); } else if (_troveChange.collDecrease > 0) { // Pull Coll from Active Pool and decrease its recorded Coll balance _activePool.sendColl(withdrawalReceiver, _troveChange.collDecrease); } } - function _pullCollAndSendToActivePool( - IActivePool _activePool, - uint256 _amount - ) internal { + function _pullCollAndSendToActivePool(IActivePool _activePool, uint256 _amount) internal { // Send Coll tokens from sender to active pool collToken.safeTransferFrom(msg.sender, address(_activePool), _amount); // Make sure Active Pool accountancy is right _activePool.accountForReceivedColl(_amount); } - function checkBatchManagerExists( - address _batchManager - ) external view returns (bool) { + function checkBatchManagerExists(address _batchManager) external view returns (bool) { return interestBatchManagers[_batchManager].maxInterestRate > 0; } @@ -1394,27 +1118,18 @@ contract BorrowerOperations is } } - function _requireNonZeroAdjustment( - TroveChange memory _troveChange - ) internal pure { + function _requireNonZeroAdjustment(TroveChange memory _troveChange) internal pure { if ( - _troveChange.collIncrease == 0 && - _troveChange.collDecrease == 0 && - _troveChange.debtIncrease == 0 && - _troveChange.debtDecrease == 0 + _troveChange.collIncrease == 0 && _troveChange.collDecrease == 0 && _troveChange.debtIncrease == 0 + && _troveChange.debtDecrease == 0 ) { revert ZeroAdjustment(); } } - function _requireSenderIsOwnerOrInterestManager( - uint256 _troveId - ) internal view { + function _requireSenderIsOwnerOrInterestManager(uint256 _troveId) internal view { address owner = troveNFT.ownerOf(_troveId); - if ( - msg.sender != owner && - msg.sender != interestIndividualDelegateOf[_troveId].account - ) { + if (msg.sender != owner && msg.sender != interestIndividualDelegateOf[_troveId].account) { revert NotOwnerNorInterestManager(); } } @@ -1424,19 +1139,15 @@ contract BorrowerOperations is uint256 _lastInterestRateAdjTime, uint256 _annualInterestRate ) internal view { - InterestIndividualDelegate - memory individualDelegate = interestIndividualDelegateOf[_troveId]; + InterestIndividualDelegate memory individualDelegate = interestIndividualDelegateOf[_troveId]; // We have previously checked that sender is either owner or delegate // If it’s owner, this restriction doesn’t apply if (individualDelegate.account == msg.sender) { _requireInterestRateInRange( - _annualInterestRate, - individualDelegate.minInterestRate, - individualDelegate.maxInterestRate + _annualInterestRate, individualDelegate.minInterestRate, individualDelegate.maxInterestRate ); _requireDelegateInterestRateChangePeriodPassed( - _lastInterestRateAdjTime, - individualDelegate.minInterestRateChangePeriod + _lastInterestRateAdjTime, individualDelegate.minInterestRateChangePeriod ); } } @@ -1447,9 +1158,7 @@ contract BorrowerOperations is } } - function _requireIsInBatch( - uint256 _troveId - ) internal view returns (address) { + function _requireIsInBatch(uint256 _troveId) internal view returns (address) { address batchManager = interestBatchManagerOf[_troveId]; if (batchManager == address(0)) { revert TroveNotInBatch(); @@ -1458,52 +1167,34 @@ contract BorrowerOperations is return batchManager; } - function _requireTroveDoesNotExist( - ITroveManager _troveManager, - uint256 _troveId - ) internal view { + function _requireTroveDoesNotExist(ITroveManager _troveManager, uint256 _troveId) internal view { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); if (status != ITroveManager.Status.nonExistent) { revert TroveExists(); } } - function _requireTroveIsOpen( - ITroveManager _troveManager, - uint256 _troveId - ) internal view { + function _requireTroveIsOpen(ITroveManager _troveManager, uint256 _troveId) internal view { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); - if ( - status != ITroveManager.Status.active && - status != ITroveManager.Status.zombie - ) { + if (status != ITroveManager.Status.active && status != ITroveManager.Status.zombie) { revert TroveNotOpen(); } } - function _requireTroveIsActive( - ITroveManager _troveManager, - uint256 _troveId - ) internal view { + function _requireTroveIsActive(ITroveManager _troveManager, uint256 _troveId) internal view { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); if (status != ITroveManager.Status.active) { revert TroveNotActive(); } } - function _requireTroveIsZombie( - ITroveManager _troveManager, - uint256 _troveId - ) internal view { + function _requireTroveIsZombie(ITroveManager _troveManager, uint256 _troveId) internal view { if (!_checkTroveIsZombie(_troveManager, _troveId)) { revert TroveNotZombie(); } } - function _checkTroveIsZombie( - ITroveManager _troveManager, - uint256 _troveId - ) internal view returns (bool) { + function _checkTroveIsZombie(ITroveManager _troveManager, uint256 _troveId) internal view returns (bool) { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); return status == ITroveManager.Status.zombie; } @@ -1514,10 +1205,7 @@ contract BorrowerOperations is } } - function _requireUserAcceptsUpfrontFee( - uint256 _fee, - uint256 _maxFee - ) internal pure { + function _requireUserAcceptsUpfrontFee(uint256 _fee, uint256 _maxFee) internal pure { if (_fee > _maxFee) { revert UpfrontFeeTooHigh(); } @@ -1550,10 +1238,7 @@ contract BorrowerOperations is uint256 newTCR = _getNewTCRFromTroveChange(_troveChange, _vars.price); if (_vars.isBelowCriticalThreshold) { - _requireNoBorrowingUnlessNewTCRisAboveCCR( - _troveChange.debtIncrease, - newTCR - ); + _requireNoBorrowingUnlessNewTCRisAboveCCR(_troveChange.debtIncrease, newTCR); _requireDebtRepaymentGeCollWithdrawal(_troveChange, _vars.price); } else { // if Normal Mode @@ -1573,23 +1258,14 @@ contract BorrowerOperations is } } - function _requireNoBorrowingUnlessNewTCRisAboveCCR( - uint256 _debtIncrease, - uint256 _newTCR - ) internal view { + function _requireNoBorrowingUnlessNewTCRisAboveCCR(uint256 _debtIncrease, uint256 _newTCR) internal view { if (_debtIncrease > 0 && _newTCR < systemParams.CCR()) { revert TCRBelowCCR(); } } - function _requireDebtRepaymentGeCollWithdrawal( - TroveChange memory _troveChange, - uint256 _price - ) internal pure { - if ( - (_troveChange.debtDecrease * DECIMAL_PRECISION < - _troveChange.collDecrease * _price) - ) { + function _requireDebtRepaymentGeCollWithdrawal(TroveChange memory _troveChange, uint256 _price) internal pure { + if ((_troveChange.debtDecrease * DECIMAL_PRECISION < _troveChange.collDecrease * _price)) { revert RepaymentNotMatchingCollWithdrawal(); } } @@ -1606,28 +1282,22 @@ contract BorrowerOperations is } } - function _requireValidCollWithdrawal( - uint256 _currentColl, - uint256 _collWithdrawal - ) internal pure { + function _requireValidCollWithdrawal(uint256 _currentColl, uint256 _collWithdrawal) internal pure { if (_collWithdrawal > _currentColl) { revert CollWithdrawalTooHigh(); } } - function _requireSufficientBoldBalance( - IBoldToken _boldToken, - address _borrower, - uint256 _debtRepayment - ) internal view { + function _requireSufficientBoldBalance(IBoldToken _boldToken, address _borrower, uint256 _debtRepayment) + internal + view + { if (_boldToken.balanceOf(_borrower) < _debtRepayment) { revert NotEnoughBoldBalance(); } } - function _requireValidAnnualInterestRate( - uint256 _annualInterestRate - ) internal view { + function _requireValidAnnualInterestRate(uint256 _annualInterestRate) internal view { if (_annualInterestRate < systemParams.MIN_ANNUAL_INTEREST_RATE()) { revert InterestRateTooLow(); } @@ -1636,34 +1306,26 @@ contract BorrowerOperations is } } - function _requireAnnualInterestRateIsNew( - uint256 _oldAnnualInterestRate, - uint256 _newAnnualInterestRate - ) internal pure { + function _requireAnnualInterestRateIsNew(uint256 _oldAnnualInterestRate, uint256 _newAnnualInterestRate) + internal + pure + { if (_oldAnnualInterestRate == _newAnnualInterestRate) { revert InterestRateNotNew(); } } - function _requireOrderedRange( - uint256 _minInterestRate, - uint256 _maxInterestRate - ) internal pure { + function _requireOrderedRange(uint256 _minInterestRate, uint256 _maxInterestRate) internal pure { if (_minInterestRate >= _maxInterestRate) revert MinGeMax(); } - function _requireInterestRateInBatchManagerRange( - address _interestBatchManagerAddress, - uint256 _annualInterestRate - ) internal view { - InterestBatchManager - memory interestBatchManager = interestBatchManagers[ - _interestBatchManagerAddress - ]; + function _requireInterestRateInBatchManagerRange(address _interestBatchManagerAddress, uint256 _annualInterestRate) + internal + view + { + InterestBatchManager memory interestBatchManager = interestBatchManagers[_interestBatchManagerAddress]; _requireInterestRateInRange( - _annualInterestRate, - interestBatchManager.minInterestRate, - interestBatchManager.maxInterestRate + _annualInterestRate, interestBatchManager.minInterestRate, interestBatchManager.maxInterestRate ); } @@ -1672,10 +1334,7 @@ contract BorrowerOperations is uint256 _minInterestRate, uint256 _maxInterestRate ) internal pure { - if ( - _minInterestRate > _annualInterestRate || - _annualInterestRate > _maxInterestRate - ) { + if (_minInterestRate > _annualInterestRate || _annualInterestRate > _maxInterestRate) { revert InterestNotInRange(); } } @@ -1684,40 +1343,27 @@ contract BorrowerOperations is uint256 _lastInterestRateAdjTime, uint256 _minInterestRateChangePeriod ) internal view { - if ( - block.timestamp < - _lastInterestRateAdjTime + _minInterestRateChangePeriod - ) { + if (block.timestamp < _lastInterestRateAdjTime + _minInterestRateChangePeriod) { revert DelegateInterestRateChangePeriodNotPassed(); } } - function _requireValidInterestBatchManager( - address _interestBatchManagerAddress - ) internal view { - if ( - interestBatchManagers[_interestBatchManagerAddress] - .maxInterestRate == 0 - ) { + function _requireValidInterestBatchManager(address _interestBatchManagerAddress) internal view { + if (interestBatchManagers[_interestBatchManagerAddress].maxInterestRate == 0) { revert InvalidInterestBatchManager(); } } - function _requireNonExistentInterestBatchManager( - address _interestBatchManagerAddress - ) internal view { - if ( - interestBatchManagers[_interestBatchManagerAddress] - .maxInterestRate > 0 - ) { + function _requireNonExistentInterestBatchManager(address _interestBatchManagerAddress) internal view { + if (interestBatchManagers[_interestBatchManagerAddress].maxInterestRate > 0) { revert BatchManagerExists(); } } - function _requireNewInterestBatchManager( - address _oldBatchManagerAddress, - address _newBatchManagerAddress - ) internal pure { + function _requireNewInterestBatchManager(address _oldBatchManagerAddress, address _newBatchManagerAddress) + internal + pure + { if (_oldBatchManagerAddress == _newBatchManagerAddress) { revert BatchManagerNotNew(); } @@ -1745,10 +1391,11 @@ contract BorrowerOperations is // --- ICR and TCR getters --- - function _getNewTCRFromTroveChange( - TroveChange memory _troveChange, - uint256 _price - ) internal view returns (uint256 newTCR) { + function _getNewTCRFromTroveChange(TroveChange memory _troveChange, uint256 _price) + internal + view + returns (uint256 newTCR) + { uint256 totalColl = getEntireBranchColl(); totalColl += _troveChange.collIncrease; totalColl -= _troveChange.collDecrease; diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index b62a3afa9..f9537feec 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -49,7 +49,12 @@ contract CollateralRegistry is ICollateralRegistry { event BaseRateUpdated(uint256 _baseRate); event LastFeeOpTimeUpdated(uint256 _lastFeeOpTime); - constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers, ISystemParams _systemParams) { + constructor( + IBoldToken _boldToken, + IERC20Metadata[] memory _tokens, + ITroveManager[] memory _troveManagers, + ISystemParams _systemParams + ) { uint256 numTokens = _tokens.length; require(numTokens > 0, "Collateral list cannot be empty"); require(numTokens <= 10, "Collateral list too long"); diff --git a/contracts/src/Interfaces/IBatchManagerOperations.sol b/contracts/src/Interfaces/IBatchManagerOperations.sol index 027481827..8dce41b07 100644 --- a/contracts/src/Interfaces/IBatchManagerOperations.sol +++ b/contracts/src/Interfaces/IBatchManagerOperations.sol @@ -71,11 +71,7 @@ interface IBatchManagerOperations { uint256 _maxUpfrontFee ) external; - function kickFromBatch( - uint256 _troveId, - uint256 _upperHint, - uint256 _lowerHint - ) external; + function kickFromBatch(uint256 _troveId, uint256 _upperHint, uint256 _lowerHint) external; function removeFromBatch( uint256 _troveId, diff --git a/contracts/src/Interfaces/IOracleAdapter.sol b/contracts/src/Interfaces/IOracleAdapter.sol index 0347012a6..20fab15d4 100644 --- a/contracts/src/Interfaces/IOracleAdapter.sol +++ b/contracts/src/Interfaces/IOracleAdapter.sol @@ -7,13 +7,13 @@ pragma solidity 0.8.24; * @notice Interface for the Oracle Adapter contract that provides FX rate data */ interface IOracleAdapter { - /** - * @notice Returns the exchange rate for a given rate feed ID - * with 18 decimals of precision if considered valid, based on - * FX market hours, trading mode, and recent rate, otherwise reverts - * @param rateFeedID The address of the rate feed - * @return numerator The numerator of the rate - * @return denominator The denominator of the rate - */ - function getFXRateIfValid(address rateFeedID) external view returns (uint256 numerator, uint256 denominator); -} \ No newline at end of file + /** + * @notice Returns the exchange rate for a given rate feed ID + * with 18 decimals of precision if considered valid, based on + * FX market hours, trading mode, and recent rate, otherwise reverts + * @param rateFeedID The address of the rate feed + * @return numerator The numerator of the rate + * @return denominator The denominator of the rate + */ + function getFXRateIfValid(address rateFeedID) external view returns (uint256 numerator, uint256 denominator); +} diff --git a/contracts/src/Interfaces/IStableTokenV3.sol b/contracts/src/Interfaces/IStableTokenV3.sol index 5db40bbfe..a0b7c1e37 100644 --- a/contracts/src/Interfaces/IStableTokenV3.sol +++ b/contracts/src/Interfaces/IStableTokenV3.sol @@ -209,4 +209,4 @@ interface IStableTokenV3 { uint256 gatewayFee, uint256 baseTxFee ) external; -} \ No newline at end of file +} diff --git a/contracts/src/Interfaces/ISystemParams.sol b/contracts/src/Interfaces/ISystemParams.sol index cfa7173e6..97ffa78da 100644 --- a/contracts/src/Interfaces/ISystemParams.sol +++ b/contracts/src/Interfaces/ISystemParams.sol @@ -70,10 +70,7 @@ interface ISystemParams { function LIQUIDATION_PENALTY_SP() external view returns (uint256); /// @notice Liquidation penalty for troves redistributed. - function LIQUIDATION_PENALTY_REDISTRIBUTION() - external - view - returns (uint256); + function LIQUIDATION_PENALTY_REDISTRIBUTION() external view returns (uint256); /* ========== GAS COMPENSATION PARAMETERS ========== */ diff --git a/contracts/src/PriceFeeds/FXPriceFeed.sol b/contracts/src/PriceFeeds/FXPriceFeed.sol index dbbe13637..ed623150f 100644 --- a/contracts/src/PriceFeeds/FXPriceFeed.sol +++ b/contracts/src/PriceFeeds/FXPriceFeed.sol @@ -6,15 +6,15 @@ import "../Interfaces/IOracleAdapter.sol"; import "../Interfaces/IPriceFeed.sol"; import "../Interfaces/IBorrowerOperations.sol"; -import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; /** * @title FXPriceFeed * @author Mento Labs * @notice A contract that fetches the price of an FX rate from an OracleAdapter. * Implements emergency shutdown functionality to handle oracle failures. */ -contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { +contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { /* ==================== State Variables ==================== */ /// @notice The OracleAdapter contract that provides FX rate data @@ -55,19 +55,19 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { * @param disableInitializers Boolean to disable initializers for implementation contract */ constructor(bool disableInitializers) { - if (disableInitializers) { - _disableInitializers(); - } + if (disableInitializers) { + _disableInitializers(); + } } /** - * @notice Initializes the FXPriceFeed contract - * @param _oracleAdapterAddress The address of the OracleAdapter contract - * @param _rateFeedID The address of the rate feed ID - * @param _borrowerOperationsAddress The address of the BorrowerOperations contract - * @param _watchdogAddress The address of the watchdog contract - * @param _initialOwner The address of the initial owner - */ + * @notice Initializes the FXPriceFeed contract + * @param _oracleAdapterAddress The address of the OracleAdapter contract + * @param _rateFeedID The address of the rate feed ID + * @param _borrowerOperationsAddress The address of the BorrowerOperations contract + * @param _watchdogAddress The address of the watchdog contract + * @param _initialOwner The address of the initial owner + */ function initialize( address _oracleAdapterAddress, address _rateFeedID, @@ -92,9 +92,9 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { } /** - * @notice Sets the watchdog address - * @param _newWatchdogAddress The address of the new watchdog contract - */ + * @notice Sets the watchdog address + * @param _newWatchdogAddress The address of the new watchdog contract + */ function setWatchdogAddress(address _newWatchdogAddress) external onlyOwner { if (_newWatchdogAddress == address(0)) revert ZeroAddress(); @@ -105,17 +105,17 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { } /** - * @notice Fetches the price of the FX rate, if valid - * @dev If the contract is shutdown due to oracle failure, the last valid price is returned - * @return The price of the FX rate - */ + * @notice Fetches the price of the FX rate, if valid + * @dev If the contract is shutdown due to oracle failure, the last valid price is returned + * @return The price of the FX rate + */ function fetchPrice() public returns (uint256) { if (isShutdown) { return lastValidPrice; } // Denominator is always 1e18, so we only use the numerator as the price - (uint256 price, ) = oracleAdapter.getFXRateIfValid(rateFeedID); + (uint256 price,) = oracleAdapter.getFXRateIfValid(rateFeedID); lastValidPrice = price; @@ -139,4 +139,4 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { emit FXPriceFeedShutdown(); } -} \ No newline at end of file +} diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 563ee89d0..009d0e88d 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -340,7 +340,10 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi _sendBoldtoDepositor(msg.sender, boldToWithdraw + yieldGainToSend); _sendCollGainToDepositor(collToSend); - require(newTotalBoldDeposits >= systemParams.MIN_BOLD_IN_SP(), "Withdrawal must leave totalBoldDeposits >= MIN_BOLD_IN_SP"); + require( + newTotalBoldDeposits >= systemParams.MIN_BOLD_IN_SP(), + "Withdrawal must leave totalBoldDeposits >= MIN_BOLD_IN_SP" + ); } function _getNewStashedCollAndCollToSend(address _depositor, uint256 _currentCollGain, bool _doClaim) @@ -406,14 +409,16 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi * Removed stable tokens will be factored out from LPs' positions. * Added collateral will be added to LPs collateral gain which can be later claimed by the depositor. */ - function swapCollateralForStable(uint256 amountCollIn, uint256 amountStableOut ) external { + function swapCollateralForStable(uint256 amountCollIn, uint256 amountStableOut) external { _requireCallerIsLiquidityStrategy(); _updateTrackingVariables(amountStableOut, amountCollIn); _swapCollateralForStable(amountCollIn, amountStableOut); - require(totalBoldDeposits >= MIN_BOLD_AFTER_REBALANCE, "Total Bold deposits must be >= MIN_BOLD_AFTER_REBALANCE"); + require( + totalBoldDeposits >= MIN_BOLD_AFTER_REBALANCE, "Total Bold deposits must be >= MIN_BOLD_AFTER_REBALANCE" + ); } // --- Liquidation functions --- @@ -471,7 +476,6 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi collToken.safeTransferFrom(msg.sender, address(this), _amountCollIn); emit StabilityPoolCollBalanceUpdated(collBalance); - } function _moveOffsetCollAndDebt(uint256 _collToAdd, uint256 _debtToOffset) internal { diff --git a/contracts/src/SystemParams.sol b/contracts/src/SystemParams.sol index 6602a964e..b4640d98c 100644 --- a/contracts/src/SystemParams.sol +++ b/contracts/src/SystemParams.sol @@ -19,41 +19,41 @@ import "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; contract SystemParams is ISystemParams, Initializable { /* ========== DEBT PARAMETERS ========== */ - uint256 immutable public MIN_DEBT; + uint256 public immutable MIN_DEBT; /* ========== LIQUIDATION PARAMETERS ========== */ - uint256 immutable public LIQUIDATION_PENALTY_SP; - uint256 immutable public LIQUIDATION_PENALTY_REDISTRIBUTION; + uint256 public immutable LIQUIDATION_PENALTY_SP; + uint256 public immutable LIQUIDATION_PENALTY_REDISTRIBUTION; /* ========== GAS COMPENSATION PARAMETERS ========== */ - uint256 immutable public COLL_GAS_COMPENSATION_DIVISOR; - uint256 immutable public COLL_GAS_COMPENSATION_CAP; - uint256 immutable public ETH_GAS_COMPENSATION; + uint256 public immutable COLL_GAS_COMPENSATION_DIVISOR; + uint256 public immutable COLL_GAS_COMPENSATION_CAP; + uint256 public immutable ETH_GAS_COMPENSATION; /* ========== COLLATERAL PARAMETERS ========== */ - uint256 immutable public CCR; - uint256 immutable public SCR; - uint256 immutable public MCR; - uint256 immutable public BCR; + uint256 public immutable CCR; + uint256 public immutable SCR; + uint256 public immutable MCR; + uint256 public immutable BCR; /* ========== INTEREST PARAMETERS ========== */ - uint256 immutable public MIN_ANNUAL_INTEREST_RATE; + uint256 public immutable MIN_ANNUAL_INTEREST_RATE; /* ========== REDEMPTION PARAMETERS ========== */ - uint256 immutable public REDEMPTION_FEE_FLOOR; - uint256 immutable public INITIAL_BASE_RATE; - uint256 immutable public REDEMPTION_MINUTE_DECAY_FACTOR; - uint256 immutable public REDEMPTION_BETA; + uint256 public immutable REDEMPTION_FEE_FLOOR; + uint256 public immutable INITIAL_BASE_RATE; + uint256 public immutable REDEMPTION_MINUTE_DECAY_FACTOR; + uint256 public immutable REDEMPTION_BETA; /* ========== STABILITY POOL PARAMETERS ========== */ - uint256 immutable public SP_YIELD_SPLIT; - uint256 immutable public MIN_BOLD_IN_SP; + uint256 public immutable SP_YIELD_SPLIT; + uint256 public immutable MIN_BOLD_IN_SP; /* ========== CONSTRUCTOR ========== */ @@ -76,22 +76,26 @@ contract SystemParams is ISystemParams, Initializable { // Validate liquidation parameters // Hardcoded validation bounds: MIN_LIQUIDATION_PENALTY_SP = 5% - if (_liquidationParams.liquidationPenaltySP < 5 * _1pct) + if (_liquidationParams.liquidationPenaltySP < 5 * _1pct) { revert SPPenaltyTooLow(); - if (_liquidationParams.liquidationPenaltySP > _liquidationParams.liquidationPenaltyRedistribution) + } + if (_liquidationParams.liquidationPenaltySP > _liquidationParams.liquidationPenaltyRedistribution) { revert SPPenaltyGtRedist(); - if (_liquidationParams.liquidationPenaltyRedistribution > MAX_LIQUIDATION_PENALTY_REDISTRIBUTION) + } + if (_liquidationParams.liquidationPenaltyRedistribution > MAX_LIQUIDATION_PENALTY_REDISTRIBUTION) { revert RedistPenaltyTooHigh(); + } // Validate gas compensation parameters - if ( - _gasCompParams.collGasCompensationDivisor == 0 || - _gasCompParams.collGasCompensationDivisor > 1000 - ) revert InvalidGasCompensation(); - if (_gasCompParams.collGasCompensationCap == 0 || _gasCompParams.collGasCompensationCap > 10 ether) + if (_gasCompParams.collGasCompensationDivisor == 0 || _gasCompParams.collGasCompensationDivisor > 1000) { + revert InvalidGasCompensation(); + } + if (_gasCompParams.collGasCompensationCap == 0 || _gasCompParams.collGasCompensationCap > 10 ether) { revert InvalidGasCompensation(); - if (_gasCompParams.ethGasCompensation == 0 || _gasCompParams.ethGasCompensation > 1 ether) + } + if (_gasCompParams.ethGasCompensation == 0 || _gasCompParams.ethGasCompensation > 1 ether) { revert InvalidGasCompensation(); + } // Validate collateral parameters if (_collateralParams.ccr <= _100pct || _collateralParams.ccr >= 2 * _100pct) revert InvalidCCR(); @@ -100,8 +104,9 @@ contract SystemParams is ISystemParams, Initializable { if (_collateralParams.scr <= _100pct || _collateralParams.scr >= 2 * _100pct) revert InvalidSCR(); // Validate interest parameters - if (_interestParams.minAnnualInterestRate > MAX_ANNUAL_INTEREST_RATE) + if (_interestParams.minAnnualInterestRate > MAX_ANNUAL_INTEREST_RATE) { revert MinInterestRateGtMax(); + } // Validate redemption parameters if (_redemptionParams.redemptionFeeFloor > _100pct) revert InvalidFeeValue(); @@ -143,7 +148,7 @@ contract SystemParams is ISystemParams, Initializable { MIN_BOLD_IN_SP = _poolParams.minBoldInSP; } - /* + /* * Initializes proxy storage * All parameters are immutable from constructor. This function * only marks initialization complete for proxy pattern. diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index e269a7c62..7349299c0 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -319,7 +319,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // Return the amount of Coll to be drawn from a trove's collateral and sent as gas compensation. function _getCollGasCompensation(uint256 _coll) internal view returns (uint256) { // _entireDebt should never be zero, but we add the condition defensively to avoid an unexpected revert - return LiquityMath._min(_coll / systemParams.COLL_GAS_COMPENSATION_DIVISOR(), systemParams.COLL_GAS_COMPENSATION_CAP()); + return LiquityMath._min( + _coll / systemParams.COLL_GAS_COMPENSATION_DIVISOR(), systemParams.COLL_GAS_COMPENSATION_CAP() + ); } /* In a full liquidation, returns the values for a trove's coll and debt to be offset, and coll and debt to be diff --git a/contracts/src/tokens/StableTokenV3.sol b/contracts/src/tokens/StableTokenV3.sol index 50da08e69..14130e1c2 100644 --- a/contracts/src/tokens/StableTokenV3.sol +++ b/contracts/src/tokens/StableTokenV3.sol @@ -2,293 +2,294 @@ // // solhint-disable gas-custom-errors pragma solidity 0.8.24; -import { ERC20PermitUpgradeable } from "./patched/ERC20PermitUpgradeable.sol"; -import { ERC20Upgradeable } from "./patched/ERC20Upgradeable.sol"; +import {ERC20PermitUpgradeable} from "./patched/ERC20PermitUpgradeable.sol"; +import {ERC20Upgradeable} from "./patched/ERC20Upgradeable.sol"; -import { IStableTokenV3 } from "../Interfaces/IStableTokenV3.sol"; +import {IStableTokenV3} from "../Interfaces/IStableTokenV3.sol"; /** * @title ERC20 token with minting and burning permissiones to a minter and burner roles. * Direct transfers between the protocol and the user are done by the operator role. */ contract StableTokenV3 is ERC20PermitUpgradeable, IStableTokenV3 { - /* ========================================================= */ - /* ==================== State Variables ==================== */ - /* ========================================================= */ - - // Deprecated storage slots for backwards compatibility with StableTokenV2 - // solhint-disable-next-line var-name-mixedcase - address public deprecated_validators_storage_slot__; - // solhint-disable-next-line var-name-mixedcase - address public deprecated_broker_storage_slot__; - // solhint-disable-next-line var-name-mixedcase - address public deprecated_exchange_storage_slot__; - - // Mapping of allowed addresses that can mint - mapping(address => bool) public isMinter; - // Mapping of allowed addresses that can burn - mapping(address => bool) public isBurner; - // Mapping of allowed addresses that can call the operator functions - // These functions are used to do direct transfers between the protocol and the user - // This will be the StabilityPools - mapping(address => bool) public isOperator; - - /* ========================================================= */ - /* ======================== Events ========================= */ - /* ========================================================= */ - - event MinterUpdated(address indexed minter, bool isMinter); - event BurnerUpdated(address indexed burner, bool isBurner); - event OperatorUpdated(address indexed operator, bool isOperator); - - /* ========================================================= */ - /* ====================== Modifiers ======================== */ - /* ========================================================= */ - - /// @dev Celo contracts legacy helper to ensure only the vm can call this function - modifier onlyVm() { - require(msg.sender == address(0), "Only VM can call"); - _; - } - - /// @dev Restricts a function so it can only be executed by an address that's allowed to mint. - modifier onlyMinter() { - address sender = _msgSender(); - require(isMinter[sender], "StableTokenV3: not allowed to mint"); - _; - } - - /// @dev Restricts a function so it can only be executed by an address that's allowed to burn. - modifier onlyBurner() { - address sender = _msgSender(); - require(isBurner[sender], "StableTokenV3: not allowed to burn"); - _; - } - - /// @dev Restricts a function so it can only be executed by the operator role. - modifier onlyOperator() { - address sender = _msgSender(); - require(isOperator[sender], "StableTokenV3: not allowed to call only by operator"); - _; - } - - /* ========================================================= */ - /* ====================== Constructor ====================== */ - /* ========================================================= */ - - /** - * @notice The constructor for the StableTokenV3 contract. - * @dev Should be called with disable=true in deployments when - * it's accessed through a Proxy. - * Call this with disable=false during testing, when used - * without a proxy. - * @param disable Set to true to run `_disableInitializers()` inherited from - * openzeppelin-contracts-upgradeable/Initializable.sol - */ - constructor(bool disable) { - if (disable) { - _disableInitializers(); + /* ========================================================= */ + /* ==================== State Variables ==================== */ + /* ========================================================= */ + + // Deprecated storage slots for backwards compatibility with StableTokenV2 + // solhint-disable-next-line var-name-mixedcase + address public deprecated_validators_storage_slot__; + // solhint-disable-next-line var-name-mixedcase + address public deprecated_broker_storage_slot__; + // solhint-disable-next-line var-name-mixedcase + address public deprecated_exchange_storage_slot__; + + // Mapping of allowed addresses that can mint + mapping(address => bool) public isMinter; + // Mapping of allowed addresses that can burn + mapping(address => bool) public isBurner; + // Mapping of allowed addresses that can call the operator functions + // These functions are used to do direct transfers between the protocol and the user + // This will be the StabilityPools + mapping(address => bool) public isOperator; + + /* ========================================================= */ + /* ======================== Events ========================= */ + /* ========================================================= */ + + event MinterUpdated(address indexed minter, bool isMinter); + event BurnerUpdated(address indexed burner, bool isBurner); + event OperatorUpdated(address indexed operator, bool isOperator); + + /* ========================================================= */ + /* ====================== Modifiers ======================== */ + /* ========================================================= */ + + /// @dev Celo contracts legacy helper to ensure only the vm can call this function + modifier onlyVm() { + require(msg.sender == address(0), "Only VM can call"); + _; } - } - - /// @inheritdoc IStableTokenV3 - function initialize( - // slither-disable-start shadowing-local - string memory _name, - string memory _symbol, - // slither-disable-end shadowing-local - address[] memory initialBalanceAddresses, - uint256[] memory initialBalanceValues, - address[] memory _minters, - address[] memory _burners, - address[] memory _operators - ) external reinitializer(3) { - __ERC20_init_unchained(_name, _symbol); - __ERC20Permit_init(_symbol); - _transferOwnership(_msgSender()); - - require(initialBalanceAddresses.length == initialBalanceValues.length, "Array length mismatch"); - for (uint256 i = 0; i < initialBalanceAddresses.length; i += 1) { - _mint(initialBalanceAddresses[i], initialBalanceValues[i]); + + /// @dev Restricts a function so it can only be executed by an address that's allowed to mint. + modifier onlyMinter() { + address sender = _msgSender(); + require(isMinter[sender], "StableTokenV3: not allowed to mint"); + _; + } + + /// @dev Restricts a function so it can only be executed by an address that's allowed to burn. + modifier onlyBurner() { + address sender = _msgSender(); + require(isBurner[sender], "StableTokenV3: not allowed to burn"); + _; + } + + /// @dev Restricts a function so it can only be executed by the operator role. + modifier onlyOperator() { + address sender = _msgSender(); + require(isOperator[sender], "StableTokenV3: not allowed to call only by operator"); + _; + } + + /* ========================================================= */ + /* ====================== Constructor ====================== */ + /* ========================================================= */ + + /** + * @notice The constructor for the StableTokenV3 contract. + * @dev Should be called with disable=true in deployments when + * it's accessed through a Proxy. + * Call this with disable=false during testing, when used + * without a proxy. + * @param disable Set to true to run `_disableInitializers()` inherited from + * openzeppelin-contracts-upgradeable/Initializable.sol + */ + constructor(bool disable) { + if (disable) { + _disableInitializers(); + } + } + + /// @inheritdoc IStableTokenV3 + function initialize( + // slither-disable-start shadowing-local + string memory _name, + string memory _symbol, + // slither-disable-end shadowing-local + address[] memory initialBalanceAddresses, + uint256[] memory initialBalanceValues, + address[] memory _minters, + address[] memory _burners, + address[] memory _operators + ) external reinitializer(3) { + __ERC20_init_unchained(_name, _symbol); + __ERC20Permit_init(_symbol); + _transferOwnership(_msgSender()); + + require(initialBalanceAddresses.length == initialBalanceValues.length, "Array length mismatch"); + for (uint256 i = 0; i < initialBalanceAddresses.length; i += 1) { + _mint(initialBalanceAddresses[i], initialBalanceValues[i]); + } + for (uint256 i = 0; i < _minters.length; i += 1) { + _setMinter(_minters[i], true); + } + for (uint256 i = 0; i < _burners.length; i += 1) { + _setBurner(_burners[i], true); + } + for (uint256 i = 0; i < _operators.length; i += 1) { + _setOperator(_operators[i], true); + } + } + + /// @inheritdoc IStableTokenV3 + function initializeV3(address[] memory _minters, address[] memory _burners, address[] memory _operators) + public + reinitializer(3) + onlyOwner + { + for (uint256 i = 0; i < _minters.length; i += 1) { + _setMinter(_minters[i], true); + } + for (uint256 i = 0; i < _burners.length; i += 1) { + _setBurner(_burners[i], true); + } + for (uint256 i = 0; i < _operators.length; i += 1) { + _setOperator(_operators[i], true); + } + } + + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ + + /// @inheritdoc IStableTokenV3 + function setOperator(address _operator, bool _isOperator) external onlyOwner { + _setOperator(_operator, _isOperator); + } + + /// @inheritdoc IStableTokenV3 + function setMinter(address _minter, bool _isMinter) external onlyOwner { + _setMinter(_minter, _isMinter); + } + + /// @inheritdoc IStableTokenV3 + function setBurner(address _burner, bool _isBurner) external onlyOwner { + _setBurner(_burner, _isBurner); + } + + /// @inheritdoc IStableTokenV3 + function mint(address to, uint256 value) external onlyMinter returns (bool) { + _mint(to, value); + return true; } - for (uint256 i = 0; i < _minters.length; i += 1) { - _setMinter(_minters[i], true); + + /// @inheritdoc IStableTokenV3 + function burn(uint256 value) external onlyBurner returns (bool) { + _burn(msg.sender, value); + return true; } - for (uint256 i = 0; i < _burners.length; i += 1) { - _setBurner(_burners[i], true); + + /// @inheritdoc IStableTokenV3 + function sendToPool(address _sender, address _poolAddress, uint256 _amount) external onlyOperator { + _transfer(_sender, _poolAddress, _amount); + } + + /// @inheritdoc IStableTokenV3 + function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external onlyOperator { + _transfer(_poolAddress, _receiver, _amount); } - for (uint256 i = 0; i < _operators.length; i += 1) { - _setOperator(_operators[i], true); + + /// @inheritdoc IStableTokenV3 + function transferFrom(address from, address to, uint256 amount) + public + override(ERC20Upgradeable, IStableTokenV3) + returns (bool) + { + return ERC20Upgradeable.transferFrom(from, to, amount); } - } - - /// @inheritdoc IStableTokenV3 - function initializeV3( - address[] memory _minters, - address[] memory _burners, - address[] memory _operators - ) public reinitializer(3) onlyOwner { - for (uint256 i = 0; i < _minters.length; i += 1) { - _setMinter(_minters[i], true); + + /// @inheritdoc IStableTokenV3 + function transfer(address to, uint256 amount) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { + return ERC20Upgradeable.transfer(to, amount); } - for (uint256 i = 0; i < _burners.length; i += 1) { - _setBurner(_burners[i], true); + + /// @inheritdoc IStableTokenV3 + function balanceOf(address account) public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { + return ERC20Upgradeable.balanceOf(account); } - for (uint256 i = 0; i < _operators.length; i += 1) { - _setOperator(_operators[i], true); + + /// @inheritdoc IStableTokenV3 + function approve(address spender, uint256 amount) + public + override(ERC20Upgradeable, IStableTokenV3) + returns (bool) + { + return ERC20Upgradeable.approve(spender, amount); } - } - - /* ============================================================ */ - /* ==================== Mutative Functions ==================== */ - /* ============================================================ */ - - /// @inheritdoc IStableTokenV3 - function setOperator(address _operator, bool _isOperator) external onlyOwner { - _setOperator(_operator, _isOperator); - } - - /// @inheritdoc IStableTokenV3 - function setMinter(address _minter, bool _isMinter) external onlyOwner { - _setMinter(_minter, _isMinter); - } - - /// @inheritdoc IStableTokenV3 - function setBurner(address _burner, bool _isBurner) external onlyOwner { - _setBurner(_burner, _isBurner); - } - - /// @inheritdoc IStableTokenV3 - function mint(address to, uint256 value) external onlyMinter returns (bool) { - _mint(to, value); - return true; - } - - /// @inheritdoc IStableTokenV3 - function burn(uint256 value) external onlyBurner returns (bool) { - _burn(msg.sender, value); - return true; - } - - /// @inheritdoc IStableTokenV3 - function sendToPool(address _sender, address _poolAddress, uint256 _amount) external onlyOperator { - _transfer(_sender, _poolAddress, _amount); - } - - /// @inheritdoc IStableTokenV3 - function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external onlyOperator { - _transfer(_poolAddress, _receiver, _amount); - } - - /// @inheritdoc IStableTokenV3 - function transferFrom( - address from, - address to, - uint256 amount - ) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { - return ERC20Upgradeable.transferFrom(from, to, amount); - } - - /// @inheritdoc IStableTokenV3 - function transfer(address to, uint256 amount) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { - return ERC20Upgradeable.transfer(to, amount); - } - - /// @inheritdoc IStableTokenV3 - function balanceOf(address account) public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { - return ERC20Upgradeable.balanceOf(account); - } - - /// @inheritdoc IStableTokenV3 - function approve(address spender, uint256 amount) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { - return ERC20Upgradeable.approve(spender, amount); - } - - /// @inheritdoc IStableTokenV3 - function allowance( - address owner, - address spender - ) public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { - return ERC20Upgradeable.allowance(owner, spender); - } - - /// @inheritdoc IStableTokenV3 - function totalSupply() public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { - return ERC20Upgradeable.totalSupply(); - } - - /// @inheritdoc IStableTokenV3 - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public override(ERC20PermitUpgradeable, IStableTokenV3) { - ERC20PermitUpgradeable.permit(owner, spender, value, deadline, v, r, s); - } - - /// @inheritdoc IStableTokenV3 - function debitGasFees(address from, uint256 value) external onlyVm { - _burn(from, value); - } - - /// @inheritdoc IStableTokenV3 - function creditGasFees( - address from, - address feeRecipient, - address gatewayFeeRecipient, - address communityFund, - uint256 refund, - uint256 tipTxFee, - uint256 gatewayFee, - uint256 baseTxFee - ) external onlyVm { - // slither-disable-next-line uninitialized-local - uint256 amountToBurn; - _mint(from, refund + tipTxFee + gatewayFee + baseTxFee); - - if (feeRecipient != address(0)) { - _transfer(from, feeRecipient, tipTxFee); - } else if (tipTxFee > 0) { - amountToBurn += tipTxFee; + + /// @inheritdoc IStableTokenV3 + function allowance(address owner, address spender) + public + view + override(ERC20Upgradeable, IStableTokenV3) + returns (uint256) + { + return ERC20Upgradeable.allowance(owner, spender); } - if (gatewayFeeRecipient != address(0)) { - _transfer(from, gatewayFeeRecipient, gatewayFee); - } else if (gatewayFee > 0) { - amountToBurn += gatewayFee; + /// @inheritdoc IStableTokenV3 + function totalSupply() public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { + return ERC20Upgradeable.totalSupply(); + } + + /// @inheritdoc IStableTokenV3 + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + public + override(ERC20PermitUpgradeable, IStableTokenV3) + { + ERC20PermitUpgradeable.permit(owner, spender, value, deadline, v, r, s); + } + + /// @inheritdoc IStableTokenV3 + function debitGasFees(address from, uint256 value) external onlyVm { + _burn(from, value); + } + + /// @inheritdoc IStableTokenV3 + function creditGasFees( + address from, + address feeRecipient, + address gatewayFeeRecipient, + address communityFund, + uint256 refund, + uint256 tipTxFee, + uint256 gatewayFee, + uint256 baseTxFee + ) external onlyVm { + // slither-disable-next-line uninitialized-local + uint256 amountToBurn; + _mint(from, refund + tipTxFee + gatewayFee + baseTxFee); + + if (feeRecipient != address(0)) { + _transfer(from, feeRecipient, tipTxFee); + } else if (tipTxFee > 0) { + amountToBurn += tipTxFee; + } + + if (gatewayFeeRecipient != address(0)) { + _transfer(from, gatewayFeeRecipient, gatewayFee); + } else if (gatewayFee > 0) { + amountToBurn += gatewayFee; + } + + if (communityFund != address(0)) { + _transfer(from, communityFund, baseTxFee); + } else if (baseTxFee > 0) { + amountToBurn += baseTxFee; + } + + if (amountToBurn > 0) { + _burn(from, amountToBurn); + } + } + + /* =========================================================== */ + /* ==================== Private Functions ==================== */ + /* =========================================================== */ + + function _setOperator(address _operator, bool _isOperator) internal { + isOperator[_operator] = _isOperator; + emit OperatorUpdated(_operator, _isOperator); } - if (communityFund != address(0)) { - _transfer(from, communityFund, baseTxFee); - } else if (baseTxFee > 0) { - amountToBurn += baseTxFee; + function _setMinter(address _minter, bool _isMinter) internal { + isMinter[_minter] = _isMinter; + emit MinterUpdated(_minter, _isMinter); } - if (amountToBurn > 0) { - _burn(from, amountToBurn); + function _setBurner(address _burner, bool _isBurner) internal { + isBurner[_burner] = _isBurner; + emit BurnerUpdated(_burner, _isBurner); } - } - - /* =========================================================== */ - /* ==================== Private Functions ==================== */ - /* =========================================================== */ - - function _setOperator(address _operator, bool _isOperator) internal { - isOperator[_operator] = _isOperator; - emit OperatorUpdated(_operator, _isOperator); - } - - function _setMinter(address _minter, bool _isMinter) internal { - isMinter[_minter] = _isMinter; - emit MinterUpdated(_minter, _isMinter); - } - - function _setBurner(address _burner, bool _isBurner) internal { - isBurner[_burner] = _isBurner; - emit BurnerUpdated(_burner, _isBurner); - } -} \ No newline at end of file +} From a7614e7306ee64e4abfb687aaf185e4ca9b5b79e Mon Sep 17 00:00:00 2001 From: baroooo Date: Tue, 4 Nov 2025 12:32:39 +0100 Subject: [PATCH 60/79] test: make liquity tests use stableTokenv3 --- contracts/script/DeployLiquity2.s.sol | 5 +- contracts/src/BoldToken.sol | 131 ----- contracts/src/Interfaces/IStableTokenV3.sol | 446 ++++++++++-------- contracts/src/tokens/StableTokenV3.sol | 295 ------------ .../test/TestContracts/BoldTokenTester.sol | 13 - contracts/test/TestContracts/Deployment.t.sol | 96 ++-- .../test/TestContracts/StableTokenV3.sol | 305 ++++++++++++ .../patched/ERC20PermitUpgradeable.sol | 0 .../patched/ERC20Upgradeable.sol | 0 9 files changed, 611 insertions(+), 680 deletions(-) delete mode 100644 contracts/src/BoldToken.sol delete mode 100644 contracts/src/tokens/StableTokenV3.sol delete mode 100644 contracts/test/TestContracts/BoldTokenTester.sol create mode 100644 contracts/test/TestContracts/StableTokenV3.sol rename contracts/{src/tokens => test/TestContracts}/patched/ERC20PermitUpgradeable.sol (100%) rename contracts/{src/tokens => test/TestContracts}/patched/ERC20Upgradeable.sol (100%) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 728ab6c5d..f5773447c 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -24,7 +24,6 @@ import {ERC20Faucet} from "test/TestContracts/ERC20Faucet.sol"; import {WETHTester} from "test/TestContracts/WETHTester.sol"; import "src/AddressesRegistry.sol"; import "src/ActivePool.sol"; -import "src/BoldToken.sol"; import "src/BorrowerOperations.sol"; import "src/TroveManager.sol"; import "src/TroveNFT.sol"; @@ -37,8 +36,7 @@ import "src/SortedTroves.sol"; import "src/StabilityPool.sol"; import "src/CollateralRegistry.sol"; -import "src/tokens/StableTokenV3.sol"; -import "src/Interfaces/IStableTokenV3.sol"; +import "test/TestContracts/StableTokenV3.sol"; import "test/TestContracts/MockFXPriceFeed.sol"; import "test/TestContracts/MetadataDeployment.sol"; import "test/Utils/Logging.sol"; @@ -360,6 +358,7 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { r.stableToken.initialize( CONFIG.stableTokenName, CONFIG.stableTokenSymbol, + deployer, new address[](0), new uint256[](0), minters, diff --git a/contracts/src/BoldToken.sol b/contracts/src/BoldToken.sol deleted file mode 100644 index f3496ddb6..000000000 --- a/contracts/src/BoldToken.sol +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol"; -import "./Dependencies/Ownable.sol"; -import "./Interfaces/IBoldToken.sol"; - -/* - * --- Functionality added specific to the BoldToken --- - * - * 1) Transfer protection: blacklist of addresses that are invalid recipients (i.e. core Liquity contracts) in external - * transfer() and transferFrom() calls. The purpose is to protect users from losing tokens by mistakenly sending BOLD directly to a Liquity - * core contract, when they should rather call the right function. - * - * 2) sendToPool() and returnFromPool(): functions callable only Liquity core contracts, which move BOLD tokens between Liquity <-> user. - */ - -contract BoldToken is Ownable, IBoldToken, ERC20Permit { - string internal constant _NAME = "BOLD Stablecoin"; - string internal constant _SYMBOL = "BOLD"; - - // --- Addresses --- - - address public collateralRegistryAddress; - mapping(address => bool) troveManagerAddresses; - mapping(address => bool) stabilityPoolAddresses; - mapping(address => bool) borrowerOperationsAddresses; - mapping(address => bool) activePoolAddresses; - - // --- Events --- - event CollateralRegistryAddressChanged(address _newCollateralRegistryAddress); - event TroveManagerAddressAdded(address _newTroveManagerAddress); - event StabilityPoolAddressAdded(address _newStabilityPoolAddress); - event BorrowerOperationsAddressAdded(address _newBorrowerOperationsAddress); - event ActivePoolAddressAdded(address _newActivePoolAddress); - - constructor(address _owner) Ownable(_owner) ERC20(_NAME, _SYMBOL) ERC20Permit(_NAME) {} - - function setBranchAddresses( - address _troveManagerAddress, - address _stabilityPoolAddress, - address _borrowerOperationsAddress, - address _activePoolAddress - ) external override onlyOwner { - troveManagerAddresses[_troveManagerAddress] = true; - emit TroveManagerAddressAdded(_troveManagerAddress); - - stabilityPoolAddresses[_stabilityPoolAddress] = true; - emit StabilityPoolAddressAdded(_stabilityPoolAddress); - - borrowerOperationsAddresses[_borrowerOperationsAddress] = true; - emit BorrowerOperationsAddressAdded(_borrowerOperationsAddress); - - activePoolAddresses[_activePoolAddress] = true; - emit ActivePoolAddressAdded(_activePoolAddress); - } - - function setCollateralRegistry(address _collateralRegistryAddress) external override onlyOwner { - collateralRegistryAddress = _collateralRegistryAddress; - emit CollateralRegistryAddressChanged(_collateralRegistryAddress); - - _renounceOwnership(); - } - - // --- Functions for intra-Liquity calls --- - - function mint(address _account, uint256 _amount) external override { - _requireCallerIsBOorAP(); - _mint(_account, _amount); - } - - function burn(address _account, uint256 _amount) external override { - _requireCallerIsCRorBOorTMorSP(); - _burn(_account, _amount); - } - - function sendToPool(address _sender, address _poolAddress, uint256 _amount) external override { - _requireCallerIsStabilityPool(); - _transfer(_sender, _poolAddress, _amount); - } - - function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external override { - _requireCallerIsStabilityPool(); - _transfer(_poolAddress, _receiver, _amount); - } - - // --- External functions --- - - function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) { - _requireValidRecipient(recipient); - return super.transfer(recipient, amount); - } - - function transferFrom(address sender, address recipient, uint256 amount) - public - override(ERC20, IERC20) - returns (bool) - { - _requireValidRecipient(recipient); - return super.transferFrom(sender, recipient, amount); - } - - // --- 'require' functions --- - - function _requireValidRecipient(address _recipient) internal view { - require( - _recipient != address(0) && _recipient != address(this), - "BoldToken: Cannot transfer tokens directly to the Bold token contract or the zero address" - ); - } - - function _requireCallerIsBOorAP() internal view { - require( - borrowerOperationsAddresses[msg.sender] || activePoolAddresses[msg.sender], - "BoldToken: Caller is not BO or AP" - ); - } - - function _requireCallerIsCRorBOorTMorSP() internal view { - require( - msg.sender == collateralRegistryAddress || borrowerOperationsAddresses[msg.sender] - || troveManagerAddresses[msg.sender] || stabilityPoolAddresses[msg.sender], - "BoldToken: Caller is neither CR nor BorrowerOperations nor TroveManager nor StabilityPool" - ); - } - - function _requireCallerIsStabilityPool() internal view { - require(stabilityPoolAddresses[msg.sender], "BoldToken: Caller is not the StabilityPool"); - } -} diff --git a/contracts/src/Interfaces/IStableTokenV3.sol b/contracts/src/Interfaces/IStableTokenV3.sol index a0b7c1e37..296d55ffe 100644 --- a/contracts/src/Interfaces/IStableTokenV3.sol +++ b/contracts/src/Interfaces/IStableTokenV3.sol @@ -6,207 +6,245 @@ pragma solidity 0.8.24; * @notice Interface for the StableTokenV3 contract. */ interface IStableTokenV3 { - /** - * @notice Initializes a StableTokenV3. - * @param _name The name of the stable token (English) - * @param _symbol A short symbol identifying the token (e.g. "cUSD") - * @param initialBalanceAddresses Array of addresses with an initial balance. - * @param initialBalanceValues Array of balance values corresponding to initialBalanceAddresses. - * @param _minters The addresses that are allowed to mint. - * @param _burners The addresses that are allowed to burn. - * @param _operators The addresses that are allowed to call the operator functions. - */ - function initialize( - string calldata _name, - string calldata _symbol, - address[] calldata initialBalanceAddresses, - uint256[] calldata initialBalanceValues, - address[] calldata _minters, - address[] calldata _burners, - address[] calldata _operators - ) external; - - /** - * @notice Initializes a StableTokenV3 contract - * when upgrading from StableTokenV2.sol. - * It sets the addresses of the minters, burners, and operators. - * @dev This function is only callable once. - * @param _minters The addresses that are allowed to mint. - * @param _burners The addresses that are allowed to burn. - * @param _operators The addresses that are allowed to call the operator functions. - */ - function initializeV3(address[] calldata _minters, address[] calldata _burners, address[] calldata _operators) - external; - - /** - * @notice Sets the operator role for an address. - * @param _operator The address of the operator. - * @param _isOperator The boolean value indicating if the address is an operator. - */ - function setOperator(address _operator, bool _isOperator) external; - - /** - * @notice Sets the minter role for an address. - * @param _minter The address of the minter. - * @param _isMinter The boolean value indicating if the address is a minter. - */ - function setMinter(address _minter, bool _isMinter) external; - - /** - * @notice Sets the burner role for an address. - * @param _burner The address of the burner. - * @param _isBurner The boolean value indicating if the address is a burner. - */ - function setBurner(address _burner, bool _isBurner) external; - - /** - * From openzeppelin's IERC20.sol - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * From openzeppelin's IERC20.sol - * @dev Returns the amount of tokens owned by `account`. - */ - function balanceOf(address account) external view returns (uint256); - - /** - * From openzeppelin's IERC20.sol - * @dev Moves `amount` tokens from the caller's account to `to`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 amount) external returns (bool); - - /** - * From openzeppelin's IERC20.sol - * @dev Returns the remaining number of tokens that `spender` will be - * allowed to spend on behalf of `owner` through {transferFrom}. This is - * zero by default. - * - * This value changes when {approve} or {transferFrom} are called. - */ - function allowance(address owner, address spender) external view returns (uint256); - - /** - * From openzeppelin's IERC20.sol - * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - */ - function approve(address spender, uint256 amount) external returns (bool); - - /** - * From openzeppelin's IERC20.sol - * @dev Moves `amount` tokens from `from` to `to` using the - * allowance mechanism. `amount` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); - - /** - * @notice Mints new StableToken and gives it to 'to'. - * @param to The account for which to mint tokens. - * @param value The amount of StableToken to mint. - */ - function mint(address to, uint256 value) external returns (bool); - - /** - * @notice Burns StableToken from the balance of msg.sender. - * @param value The amount of StableToken to burn. - */ - function burn(uint256 value) external returns (bool); - - /** - * From openzeppelin's IERC20PermitUpgradeable.sol - * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, - * given ``owner``'s signed approval. - * - * IMPORTANT: The same issues {IERC20-approve} has related to transaction - * ordering also apply here. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `deadline` must be a timestamp in the future. - * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` - * over the EIP712-formatted function arguments. - * - the signature must use ``owner``'s current nonce (see {nonces}). - * - * For more information on the signature format, see the - * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP - * section]. - */ - function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - external; - - /** - * @notice Transfer token from a specified address to the stability pool. - * @param _sender The address to transfer from. - * @param _poolAddress The address of the pool to transfer to. - * @param _amount The amount to be transferred. - */ - function sendToPool(address _sender, address _poolAddress, uint256 _amount) external; - - /** - * @notice Transfer token to a specified address from the stability pool. - * @param _poolAddress The address of the pool to transfer from - * @param _receiver The address to transfer to. - * @param _amount The amount to be transferred. - */ - function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external; - - /** - * @notice Reserve balance for making payments for gas in this StableToken currency. - * @param from The account to reserve balance from - * @param value The amount of balance to reserve - * @dev Note that this function is called by the protocol when paying for tx fees in this - * currency. After the tx is executed, gas is refunded to the sender and credited to the - * various tx fee recipients via a call to `creditGasFees`. - */ - function debitGasFees(address from, uint256 value) external; - - /** - * @notice Alternative function to credit balance after making payments - * for gas in this StableToken currency. - * @param from The account to debit balance from - * @param feeRecipient Coinbase address - * @param gatewayFeeRecipient Gateway address - * @param communityFund Community fund address - * @param refund amount to be refunded by the VM - * @param tipTxFee Coinbase fee - * @param baseTxFee Community fund fee - * @param gatewayFee Gateway fee - * @dev Note that this function is called by the protocol when paying for tx fees in this - * currency. Before the tx is executed, gas is debited from the sender via a call to - * `debitGasFees`. - */ - function creditGasFees( - address from, - address feeRecipient, - address gatewayFeeRecipient, - address communityFund, - uint256 refund, - uint256 tipTxFee, - uint256 gatewayFee, - uint256 baseTxFee - ) external; -} + /** + * @notice Checks if an address is a minter. + * @param account The address to check. + * @return bool True if the address is a minter, false otherwise. + */ + function isMinter(address account) external view returns (bool); + /** + * @notice Checks if an address is a burner. + * @param account The address to check. + * @return bool True if the address is a burner, false otherwise. + */ + function isBurner(address account) external view returns (bool); + /** + * @notice Checks if an address is an operator. + * @param account The address to check. + * @return bool True if the address is an operator, false otherwise. + */ + function isOperator(address account) external view returns (bool); + + /** + * @notice Initializes a StableTokenV3. + * @param _name The name of the stable token (English) + * @param _symbol A short symbol identifying the token (e.g. "cUSD") + * @param _initialOwner The address that will be the owner of the contract. + * @param initialBalanceAddresses Array of addresses with an initial balance. + * @param initialBalanceValues Array of balance values corresponding to initialBalanceAddresses. + * @param _minters The addresses that are allowed to mint. + * @param _burners The addresses that are allowed to burn. + * @param _operators The addresses that are allowed to call the operator functions. + */ + function initialize( + string calldata _name, + string calldata _symbol, + address _initialOwner, + address[] calldata initialBalanceAddresses, + uint256[] calldata initialBalanceValues, + address[] calldata _minters, + address[] calldata _burners, + address[] calldata _operators + ) external; + + /** + * @notice Initializes a StableTokenV3 contract + * when upgrading from StableTokenV2.sol. + * It sets the addresses of the minters, burners, and operators. + * @dev This function is only callable once. + * @param _minters The addresses that are allowed to mint. + * @param _burners The addresses that are allowed to burn. + * @param _operators The addresses that are allowed to call the operator functions. + */ + function initializeV3( + address[] calldata _minters, + address[] calldata _burners, + address[] calldata _operators + ) external; + + /** + * @notice Sets the operator role for an address. + * @param _operator The address of the operator. + * @param _isOperator The boolean value indicating if the address is an operator. + */ + function setOperator(address _operator, bool _isOperator) external; + + /** + * @notice Sets the minter role for an address. + * @param _minter The address of the minter. + * @param _isMinter The boolean value indicating if the address is a minter. + */ + function setMinter(address _minter, bool _isMinter) external; + + /** + * @notice Sets the burner role for an address. + * @param _burner The address of the burner. + * @param _isBurner The boolean value indicating if the address is a burner. + */ + function setBurner(address _burner, bool _isBurner) external; + + /** + * From openzeppelin's IERC20.sol + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * From openzeppelin's IERC20.sol + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * From openzeppelin's IERC20.sol + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * From openzeppelin's IERC20.sol + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * From openzeppelin's IERC20.sol + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * From openzeppelin's IERC20.sol + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + /** + * @notice Mints new StableToken and gives it to 'to'. + * @param to The account for which to mint tokens. + * @param value The amount of StableToken to mint. + */ + function mint(address to, uint256 value) external returns (bool); + + /** + * @notice Burns StableToken from the balance of msg.sender. + * @param value The amount of StableToken to burn. + */ + function burn(uint256 value) external returns (bool); + + /** + * @notice Burns StableToken from the balance of an account. + * @param account The account to burn from. + * @param value The amount of StableToken to burn. + */ + function burn(address account, uint256 value) external returns (bool); + + /** + * From openzeppelin's IERC20PermitUpgradeable.sol + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @notice Transfer token from a specified address to the stability pool. + * @param _sender The address to transfer from. + * @param _poolAddress The address of the pool to transfer to. + * @param _amount The amount to be transferred. + */ + function sendToPool(address _sender, address _poolAddress, uint256 _amount) external; + + /** + * @notice Transfer token to a specified address from the stability pool. + * @param _poolAddress The address of the pool to transfer from + * @param _receiver The address to transfer to. + * @param _amount The amount to be transferred. + */ + function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external; + + /** + * @notice Reserve balance for making payments for gas in this StableToken currency. + * @param from The account to reserve balance from + * @param value The amount of balance to reserve + * @dev Note that this function is called by the protocol when paying for tx fees in this + * currency. After the tx is executed, gas is refunded to the sender and credited to the + * various tx fee recipients via a call to `creditGasFees`. + */ + function debitGasFees(address from, uint256 value) external; + + /** + * @notice Alternative function to credit balance after making payments + * for gas in this StableToken currency. + * @param from The account to debit balance from + * @param feeRecipient Coinbase address + * @param gatewayFeeRecipient Gateway address + * @param communityFund Community fund address + * @param refund amount to be refunded by the VM + * @param tipTxFee Coinbase fee + * @param baseTxFee Community fund fee + * @param gatewayFee Gateway fee + * @dev Note that this function is called by the protocol when paying for tx fees in this + * currency. Before the tx is executed, gas is debited from the sender via a call to + * `debitGasFees`. + */ + function creditGasFees( + address from, + address feeRecipient, + address gatewayFeeRecipient, + address communityFund, + uint256 refund, + uint256 tipTxFee, + uint256 gatewayFee, + uint256 baseTxFee + ) external; +} \ No newline at end of file diff --git a/contracts/src/tokens/StableTokenV3.sol b/contracts/src/tokens/StableTokenV3.sol deleted file mode 100644 index 14130e1c2..000000000 --- a/contracts/src/tokens/StableTokenV3.sol +++ /dev/null @@ -1,295 +0,0 @@ -// // SPDX-License-Identifier: GPL-3.0-or-later -// // solhint-disable gas-custom-errors -pragma solidity 0.8.24; - -import {ERC20PermitUpgradeable} from "./patched/ERC20PermitUpgradeable.sol"; -import {ERC20Upgradeable} from "./patched/ERC20Upgradeable.sol"; - -import {IStableTokenV3} from "../Interfaces/IStableTokenV3.sol"; - -/** - * @title ERC20 token with minting and burning permissiones to a minter and burner roles. - * Direct transfers between the protocol and the user are done by the operator role. - */ -contract StableTokenV3 is ERC20PermitUpgradeable, IStableTokenV3 { - /* ========================================================= */ - /* ==================== State Variables ==================== */ - /* ========================================================= */ - - // Deprecated storage slots for backwards compatibility with StableTokenV2 - // solhint-disable-next-line var-name-mixedcase - address public deprecated_validators_storage_slot__; - // solhint-disable-next-line var-name-mixedcase - address public deprecated_broker_storage_slot__; - // solhint-disable-next-line var-name-mixedcase - address public deprecated_exchange_storage_slot__; - - // Mapping of allowed addresses that can mint - mapping(address => bool) public isMinter; - // Mapping of allowed addresses that can burn - mapping(address => bool) public isBurner; - // Mapping of allowed addresses that can call the operator functions - // These functions are used to do direct transfers between the protocol and the user - // This will be the StabilityPools - mapping(address => bool) public isOperator; - - /* ========================================================= */ - /* ======================== Events ========================= */ - /* ========================================================= */ - - event MinterUpdated(address indexed minter, bool isMinter); - event BurnerUpdated(address indexed burner, bool isBurner); - event OperatorUpdated(address indexed operator, bool isOperator); - - /* ========================================================= */ - /* ====================== Modifiers ======================== */ - /* ========================================================= */ - - /// @dev Celo contracts legacy helper to ensure only the vm can call this function - modifier onlyVm() { - require(msg.sender == address(0), "Only VM can call"); - _; - } - - /// @dev Restricts a function so it can only be executed by an address that's allowed to mint. - modifier onlyMinter() { - address sender = _msgSender(); - require(isMinter[sender], "StableTokenV3: not allowed to mint"); - _; - } - - /// @dev Restricts a function so it can only be executed by an address that's allowed to burn. - modifier onlyBurner() { - address sender = _msgSender(); - require(isBurner[sender], "StableTokenV3: not allowed to burn"); - _; - } - - /// @dev Restricts a function so it can only be executed by the operator role. - modifier onlyOperator() { - address sender = _msgSender(); - require(isOperator[sender], "StableTokenV3: not allowed to call only by operator"); - _; - } - - /* ========================================================= */ - /* ====================== Constructor ====================== */ - /* ========================================================= */ - - /** - * @notice The constructor for the StableTokenV3 contract. - * @dev Should be called with disable=true in deployments when - * it's accessed through a Proxy. - * Call this with disable=false during testing, when used - * without a proxy. - * @param disable Set to true to run `_disableInitializers()` inherited from - * openzeppelin-contracts-upgradeable/Initializable.sol - */ - constructor(bool disable) { - if (disable) { - _disableInitializers(); - } - } - - /// @inheritdoc IStableTokenV3 - function initialize( - // slither-disable-start shadowing-local - string memory _name, - string memory _symbol, - // slither-disable-end shadowing-local - address[] memory initialBalanceAddresses, - uint256[] memory initialBalanceValues, - address[] memory _minters, - address[] memory _burners, - address[] memory _operators - ) external reinitializer(3) { - __ERC20_init_unchained(_name, _symbol); - __ERC20Permit_init(_symbol); - _transferOwnership(_msgSender()); - - require(initialBalanceAddresses.length == initialBalanceValues.length, "Array length mismatch"); - for (uint256 i = 0; i < initialBalanceAddresses.length; i += 1) { - _mint(initialBalanceAddresses[i], initialBalanceValues[i]); - } - for (uint256 i = 0; i < _minters.length; i += 1) { - _setMinter(_minters[i], true); - } - for (uint256 i = 0; i < _burners.length; i += 1) { - _setBurner(_burners[i], true); - } - for (uint256 i = 0; i < _operators.length; i += 1) { - _setOperator(_operators[i], true); - } - } - - /// @inheritdoc IStableTokenV3 - function initializeV3(address[] memory _minters, address[] memory _burners, address[] memory _operators) - public - reinitializer(3) - onlyOwner - { - for (uint256 i = 0; i < _minters.length; i += 1) { - _setMinter(_minters[i], true); - } - for (uint256 i = 0; i < _burners.length; i += 1) { - _setBurner(_burners[i], true); - } - for (uint256 i = 0; i < _operators.length; i += 1) { - _setOperator(_operators[i], true); - } - } - - /* ============================================================ */ - /* ==================== Mutative Functions ==================== */ - /* ============================================================ */ - - /// @inheritdoc IStableTokenV3 - function setOperator(address _operator, bool _isOperator) external onlyOwner { - _setOperator(_operator, _isOperator); - } - - /// @inheritdoc IStableTokenV3 - function setMinter(address _minter, bool _isMinter) external onlyOwner { - _setMinter(_minter, _isMinter); - } - - /// @inheritdoc IStableTokenV3 - function setBurner(address _burner, bool _isBurner) external onlyOwner { - _setBurner(_burner, _isBurner); - } - - /// @inheritdoc IStableTokenV3 - function mint(address to, uint256 value) external onlyMinter returns (bool) { - _mint(to, value); - return true; - } - - /// @inheritdoc IStableTokenV3 - function burn(uint256 value) external onlyBurner returns (bool) { - _burn(msg.sender, value); - return true; - } - - /// @inheritdoc IStableTokenV3 - function sendToPool(address _sender, address _poolAddress, uint256 _amount) external onlyOperator { - _transfer(_sender, _poolAddress, _amount); - } - - /// @inheritdoc IStableTokenV3 - function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external onlyOperator { - _transfer(_poolAddress, _receiver, _amount); - } - - /// @inheritdoc IStableTokenV3 - function transferFrom(address from, address to, uint256 amount) - public - override(ERC20Upgradeable, IStableTokenV3) - returns (bool) - { - return ERC20Upgradeable.transferFrom(from, to, amount); - } - - /// @inheritdoc IStableTokenV3 - function transfer(address to, uint256 amount) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { - return ERC20Upgradeable.transfer(to, amount); - } - - /// @inheritdoc IStableTokenV3 - function balanceOf(address account) public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { - return ERC20Upgradeable.balanceOf(account); - } - - /// @inheritdoc IStableTokenV3 - function approve(address spender, uint256 amount) - public - override(ERC20Upgradeable, IStableTokenV3) - returns (bool) - { - return ERC20Upgradeable.approve(spender, amount); - } - - /// @inheritdoc IStableTokenV3 - function allowance(address owner, address spender) - public - view - override(ERC20Upgradeable, IStableTokenV3) - returns (uint256) - { - return ERC20Upgradeable.allowance(owner, spender); - } - - /// @inheritdoc IStableTokenV3 - function totalSupply() public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { - return ERC20Upgradeable.totalSupply(); - } - - /// @inheritdoc IStableTokenV3 - function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - public - override(ERC20PermitUpgradeable, IStableTokenV3) - { - ERC20PermitUpgradeable.permit(owner, spender, value, deadline, v, r, s); - } - - /// @inheritdoc IStableTokenV3 - function debitGasFees(address from, uint256 value) external onlyVm { - _burn(from, value); - } - - /// @inheritdoc IStableTokenV3 - function creditGasFees( - address from, - address feeRecipient, - address gatewayFeeRecipient, - address communityFund, - uint256 refund, - uint256 tipTxFee, - uint256 gatewayFee, - uint256 baseTxFee - ) external onlyVm { - // slither-disable-next-line uninitialized-local - uint256 amountToBurn; - _mint(from, refund + tipTxFee + gatewayFee + baseTxFee); - - if (feeRecipient != address(0)) { - _transfer(from, feeRecipient, tipTxFee); - } else if (tipTxFee > 0) { - amountToBurn += tipTxFee; - } - - if (gatewayFeeRecipient != address(0)) { - _transfer(from, gatewayFeeRecipient, gatewayFee); - } else if (gatewayFee > 0) { - amountToBurn += gatewayFee; - } - - if (communityFund != address(0)) { - _transfer(from, communityFund, baseTxFee); - } else if (baseTxFee > 0) { - amountToBurn += baseTxFee; - } - - if (amountToBurn > 0) { - _burn(from, amountToBurn); - } - } - - /* =========================================================== */ - /* ==================== Private Functions ==================== */ - /* =========================================================== */ - - function _setOperator(address _operator, bool _isOperator) internal { - isOperator[_operator] = _isOperator; - emit OperatorUpdated(_operator, _isOperator); - } - - function _setMinter(address _minter, bool _isMinter) internal { - isMinter[_minter] = _isMinter; - emit MinterUpdated(_minter, _isMinter); - } - - function _setBurner(address _burner, bool _isBurner) internal { - isBurner[_burner] = _isBurner; - emit BurnerUpdated(_burner, _isBurner); - } -} diff --git a/contracts/test/TestContracts/BoldTokenTester.sol b/contracts/test/TestContracts/BoldTokenTester.sol deleted file mode 100644 index eaa912470..000000000 --- a/contracts/test/TestContracts/BoldTokenTester.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "src/BoldToken.sol"; - -contract BoldTokenTester is BoldToken { - constructor(address _owner) BoldToken(_owner) {} - - function unprotectedMint(address _account, uint256 _amount) external { - _mint(_account, _amount); - } -} diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index f6aa000d5..a3da37f72 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.24; import "src/AddressesRegistry.sol"; import "src/ActivePool.sol"; -import "src/BoldToken.sol"; +import "./StableTokenV3.sol"; import "src/BorrowerOperations.sol"; import "src/CollSurplusPool.sol"; import "src/DefaultPool.sol"; @@ -128,7 +128,11 @@ contract TestDeployer is MetadataDeployment { return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry)); } - function getBytecode(bytes memory _creationCode, address _addressesRegistry, address _systemParams) public pure returns (bytes memory) { + function getBytecode(bytes memory _creationCode, address _addressesRegistry, address _systemParams) + public + pure + returns (bytes memory) + { return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry, _systemParams)); } @@ -136,7 +140,11 @@ contract TestDeployer is MetadataDeployment { return abi.encodePacked(_creationCode, abi.encode(_disable)); } - function getBytecode(bytes memory _creationCode, bool _disable, address _systemParams) public pure returns (bytes memory) { + function getBytecode(bytes memory _creationCode, bool _disable, address _systemParams) + public + pure + returns (bytes memory) + { return abi.encodePacked(_creationCode, abi.encode(_disable, _systemParams)); } @@ -226,11 +234,12 @@ contract TestDeployer is MetadataDeployment { { DeploymentVarsDev memory vars; vars.numCollaterals = troveManagerParamsArray.length; - - // Deploy Bold - vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(address(this))); + + // Deploy Bold (StableTokenV3) + vars.bytecode = abi.encodePacked(type(StableTokenV3).creationCode, abi.encode(false)); vars.boldTokenAddress = getAddress(address(this), vars.bytecode, SALT); - boldToken = new BoldToken{salt: SALT}(address(this)); + StableTokenV3 stableTokenV3 = new StableTokenV3{salt: SALT}(false); + boldToken = IBoldToken(address(stableTokenV3)); assert(address(boldToken) == vars.boldTokenAddress); contractsArray = new LiquityContractsDev[](vars.numCollaterals); @@ -239,7 +248,7 @@ contract TestDeployer is MetadataDeployment { vars.troveManagers = new ITroveManager[](vars.numCollaterals); ISystemParams[] memory systemParamsArray = new ISystemParams[](vars.numCollaterals); - + for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { systemParamsArray[vars.i] = deploySystemParamsDev(troveManagerParamsArray[vars.i], vars.i); } @@ -264,7 +273,8 @@ contract TestDeployer is MetadataDeployment { vars.troveManagers[vars.i] = ITroveManager(troveManagerAddress); } - collateralRegistry = new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers, systemParamsArray[0]); + collateralRegistry = + new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers, systemParamsArray[0]); hintHelpers = new HintHelpers(collateralRegistry, systemParamsArray[0]); multiTroveGetter = new MultiTroveGetter(collateralRegistry); @@ -295,16 +305,33 @@ contract TestDeployer is MetadataDeployment { ); } - boldToken.setCollateralRegistry(address(collateralRegistry)); + // Initialize StableTokenV3 with all minters, burners, and operators + // This should handle both cases with multiple collaterals and single collateral + address[] memory minters = new address[](vars.numCollaterals * 2); + address[] memory burners = new address[](vars.numCollaterals * 3 + 1); + address[] memory operators = new address[](vars.numCollaterals); + + for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { + minters[vars.i * 2] = address(contractsArray[vars.i].borrowerOperations); + minters[vars.i * 2 + 1] = address(contractsArray[vars.i].activePool); + + burners[vars.i * 3] = address(contractsArray[vars.i].troveManager); + burners[vars.i * 3 + 1] = address(contractsArray[vars.i].borrowerOperations); + burners[vars.i * 3 + 2] = address(contractsArray[vars.i].stabilityPool); + + operators[vars.i] = address(contractsArray[vars.i].stabilityPool); + } + burners[vars.numCollaterals * 3] = address(collateralRegistry); + + stableTokenV3.initialize("Bold Token", "BOLD", address(this), new address[](0), new uint256[](0), minters, burners, operators); } - function _deployAddressesRegistryDev(ISystemParams _systemParams) - internal - returns (IAddressesRegistry, address) - { + function _deployAddressesRegistryDev(ISystemParams _systemParams) internal returns (IAddressesRegistry, address) { IAddressesRegistry addressesRegistry = new AddressesRegistry(address(this)); address troveManagerAddress = getAddress( - address(this), getBytecode(type(TroveManagerTester).creationCode, address(addressesRegistry), address(_systemParams)), SALT + address(this), + getBytecode(type(TroveManagerTester).creationCode, address(addressesRegistry), address(_systemParams)), + SALT ); return (addressesRegistry, troveManagerAddress); @@ -329,12 +356,8 @@ contract TestDeployer is MetadataDeployment { ethGasCompensation: 0.0375 ether // ETH_GAS_COMPENSATION }); - ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ - ccr: params.CCR, - scr: params.SCR, - mcr: params.MCR, - bcr: params.BCR - }); + ISystemParams.CollateralParams memory collateralParams = + ISystemParams.CollateralParams({ccr: params.CCR, scr: params.SCR, mcr: params.MCR, bcr: params.BCR}); ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ minAnnualInterestRate: DECIMAL_PRECISION / 200 // MIN_ANNUAL_INTEREST_RATE (0.5%) @@ -398,7 +421,11 @@ contract TestDeployer is MetadataDeployment { // Pre-calc addresses addresses.borrowerOperations = getAddress( address(this), - getBytecode(type(BorrowerOperationsTester).creationCode, address(contracts.addressesRegistry), address(_systemParams)), + getBytecode( + type(BorrowerOperationsTester).creationCode, + address(contracts.addressesRegistry), + address(_systemParams) + ), SALT ); addresses.troveManager = _troveManagerAddress; @@ -406,10 +433,15 @@ contract TestDeployer is MetadataDeployment { address(this), getBytecode(type(TroveNFT).creationCode, address(contracts.addressesRegistry)), SALT ); bytes32 stabilityPoolSalt = keccak256(abi.encodePacked(address(contracts.addressesRegistry))); - addresses.stabilityPool = - getAddress(address(this), getBytecode(type(StabilityPool).creationCode, bool(false), address(_systemParams)), stabilityPoolSalt); + addresses.stabilityPool = getAddress( + address(this), + getBytecode(type(StabilityPool).creationCode, bool(false), address(_systemParams)), + stabilityPoolSalt + ); addresses.activePool = getAddress( - address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry), address(_systemParams)), SALT + address(this), + getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry), address(_systemParams)), + SALT ); addresses.defaultPool = getAddress( address(this), getBytecode(type(DefaultPool).creationCode, address(contracts.addressesRegistry)), SALT @@ -449,7 +481,8 @@ contract TestDeployer is MetadataDeployment { }); contracts.addressesRegistry.setAddresses(addressVars); - contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry, _systemParams); + contracts.borrowerOperations = + new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.troveManager = new TroveManagerTester{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); contracts.stabilityPool = new StabilityPool{salt: stabilityPoolSalt}(false, _systemParams); @@ -471,12 +504,7 @@ contract TestDeployer is MetadataDeployment { contracts.stabilityPool.initialize(contracts.addressesRegistry); - // Connect contracts - _boldToken.setBranchAddresses( - address(contracts.troveManager), - address(contracts.stabilityPool), - address(contracts.borrowerOperations), - address(contracts.activePool) - ); + // Note: StableTokenV3 initialization is done after all branches are deployed + // in the deployAndConnectContracts function } -} \ No newline at end of file +} diff --git a/contracts/test/TestContracts/StableTokenV3.sol b/contracts/test/TestContracts/StableTokenV3.sol new file mode 100644 index 000000000..04d0ad9a1 --- /dev/null +++ b/contracts/test/TestContracts/StableTokenV3.sol @@ -0,0 +1,305 @@ +// // SPDX-License-Identifier: GPL-3.0-or-later +// // solhint-disable gas-custom-errors +pragma solidity 0.8.24; + +import { ERC20PermitUpgradeable } from "./patched/ERC20PermitUpgradeable.sol"; +import { ERC20Upgradeable } from "./patched/ERC20Upgradeable.sol"; + +import { IStableTokenV3 } from "src/interfaces/IStableTokenV3.sol"; + + +contract CalledByVm { + modifier onlyVm() { + require(msg.sender == address(0), "Only VM can call"); + _; + } +} + +/** + * @title ERC20 token with minting and burning permissiones to a minter and burner roles. + * Direct transfers between the protocol and the user are done by the operator role. + */ +contract StableTokenV3 is ERC20PermitUpgradeable, IStableTokenV3, CalledByVm { + /* ========================================================= */ + /* ==================== State Variables ==================== */ + /* ========================================================= */ + + // Deprecated storage slots for backwards compatibility with StableTokenV2 + // slither-disable-start constable-states + // solhint-disable-next-line var-name-mixedcase + address public deprecated_validators_storage_slot__; + // solhint-disable-next-line var-name-mixedcase + address public deprecated_broker_storage_slot__; + // solhint-disable-next-line var-name-mixedcase + address public deprecated_exchange_storage_slot__; + // slither-disable-end constable-states + + // Mapping of allowed addresses that can mint + mapping(address => bool) public isMinter; + // Mapping of allowed addresses that can burn + mapping(address => bool) public isBurner; + // Mapping of allowed addresses that can call the operator functions + // These functions are used to do direct transfers between the protocol and the user + // This will be the StabilityPools + mapping(address => bool) public isOperator; + + /* ========================================================= */ + /* ======================== Events ========================= */ + /* ========================================================= */ + + event MinterUpdated(address indexed minter, bool isMinter); + event BurnerUpdated(address indexed burner, bool isBurner); + event OperatorUpdated(address indexed operator, bool isOperator); + + /* ========================================================= */ + /* ====================== Modifiers ======================== */ + /* ========================================================= */ + + /// @dev Restricts a function so it can only be executed by an address that's allowed to mint. + modifier onlyMinter() { + address sender = _msgSender(); + require(isMinter[sender], "StableTokenV3: not allowed to mint"); + _; + } + + /// @dev Restricts a function so it can only be executed by an address that's allowed to burn. + modifier onlyBurner() { + address sender = _msgSender(); + require(isBurner[sender], "StableTokenV3: not allowed to burn"); + _; + } + + /// @dev Restricts a function so it can only be executed by the operator role. + modifier onlyOperator() { + address sender = _msgSender(); + require(isOperator[sender], "StableTokenV3: not allowed to call only by operator"); + _; + } + + /* ========================================================= */ + /* ====================== Constructor ====================== */ + /* ========================================================= */ + + /** + * @notice The constructor for the StableTokenV3 contract. + * @dev Should be called with disable=true in deployments when + * it's accessed through a Proxy. + * Call this with disable=false during testing, when used + * without a proxy. + * @param disable Set to true to run `_disableInitializers()` inherited from + * openzeppelin-contracts-upgradeable/Initializable.sol + */ + constructor(bool disable) { + if (disable) { + _disableInitializers(); + } + } + + /// @inheritdoc IStableTokenV3 + function initialize( + // slither-disable-start shadowing-local + string memory _name, + string memory _symbol, + address _initialOwner, + // slither-disable-end shadowing-local + address[] memory initialBalanceAddresses, + uint256[] memory initialBalanceValues, + address[] memory _minters, + address[] memory _burners, + address[] memory _operators + ) external reinitializer(3) { + __ERC20_init_unchained(_name, _symbol); + __ERC20Permit_init(_symbol); + _transferOwnership(_initialOwner); + + require(initialBalanceAddresses.length == initialBalanceValues.length, "Array length mismatch"); + for (uint256 i = 0; i < initialBalanceAddresses.length; i += 1) { + _mint(initialBalanceAddresses[i], initialBalanceValues[i]); + } + for (uint256 i = 0; i < _minters.length; i += 1) { + _setMinter(_minters[i], true); + } + for (uint256 i = 0; i < _burners.length; i += 1) { + _setBurner(_burners[i], true); + } + for (uint256 i = 0; i < _operators.length; i += 1) { + _setOperator(_operators[i], true); + } + } + + /// @inheritdoc IStableTokenV3 + function initializeV3( + address[] memory _minters, + address[] memory _burners, + address[] memory _operators + ) public reinitializer(3) onlyOwner { + for (uint256 i = 0; i < _minters.length; i += 1) { + _setMinter(_minters[i], true); + } + for (uint256 i = 0; i < _burners.length; i += 1) { + _setBurner(_burners[i], true); + } + for (uint256 i = 0; i < _operators.length; i += 1) { + _setOperator(_operators[i], true); + } + } + + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ + + /// @inheritdoc IStableTokenV3 + function setOperator(address _operator, bool _isOperator) external onlyOwner { + _setOperator(_operator, _isOperator); + } + + /// @inheritdoc IStableTokenV3 + function setMinter(address _minter, bool _isMinter) external onlyOwner { + _setMinter(_minter, _isMinter); + } + + /// @inheritdoc IStableTokenV3 + function setBurner(address _burner, bool _isBurner) external onlyOwner { + _setBurner(_burner, _isBurner); + } + + /// @inheritdoc IStableTokenV3 + function mint(address to, uint256 value) external onlyMinter returns (bool) { + _mint(to, value); + return true; + } + + /// @inheritdoc IStableTokenV3 + function burn(uint256 value) external onlyBurner returns (bool) { + _burn(msg.sender, value); + return true; + } + + /// @inheritdoc IStableTokenV3 + function burn(address account, uint256 value) external onlyBurner returns (bool) { + _burn(account, value); + return true; + } + + /// @inheritdoc IStableTokenV3 + function sendToPool(address _sender, address _poolAddress, uint256 _amount) external onlyOperator { + _transfer(_sender, _poolAddress, _amount); + } + + /// @inheritdoc IStableTokenV3 + function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external onlyOperator { + _transfer(_poolAddress, _receiver, _amount); + } + + /// @inheritdoc IStableTokenV3 + function transferFrom( + address from, + address to, + uint256 amount + ) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { + return ERC20Upgradeable.transferFrom(from, to, amount); + } + + /// @inheritdoc IStableTokenV3 + function transfer(address to, uint256 amount) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { + return ERC20Upgradeable.transfer(to, amount); + } + + /// @inheritdoc IStableTokenV3 + function balanceOf(address account) public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { + return ERC20Upgradeable.balanceOf(account); + } + + /// @inheritdoc IStableTokenV3 + function approve(address spender, uint256 amount) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { + return ERC20Upgradeable.approve(spender, amount); + } + + /// @inheritdoc IStableTokenV3 + function allowance( + address owner, + address spender + ) public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { + return ERC20Upgradeable.allowance(owner, spender); + } + + /// @inheritdoc IStableTokenV3 + function totalSupply() public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { + return ERC20Upgradeable.totalSupply(); + } + + /// @inheritdoc IStableTokenV3 + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public override(ERC20PermitUpgradeable, IStableTokenV3) { + ERC20PermitUpgradeable.permit(owner, spender, value, deadline, v, r, s); + } + + /// @inheritdoc IStableTokenV3 + function debitGasFees(address from, uint256 value) external onlyVm { + _burn(from, value); + } + + /// @inheritdoc IStableTokenV3 + function creditGasFees( + address from, + address feeRecipient, + address gatewayFeeRecipient, + address communityFund, + uint256 refund, + uint256 tipTxFee, + uint256 gatewayFee, + uint256 baseTxFee + ) external onlyVm { + // slither-disable-next-line uninitialized-local + uint256 amountToBurn; + _mint(from, refund + tipTxFee + gatewayFee + baseTxFee); + + if (feeRecipient != address(0)) { + _transfer(from, feeRecipient, tipTxFee); + } else if (tipTxFee > 0) { + amountToBurn += tipTxFee; + } + + if (gatewayFeeRecipient != address(0)) { + _transfer(from, gatewayFeeRecipient, gatewayFee); + } else if (gatewayFee > 0) { + amountToBurn += gatewayFee; + } + + if (communityFund != address(0)) { + _transfer(from, communityFund, baseTxFee); + } else if (baseTxFee > 0) { + amountToBurn += baseTxFee; + } + + if (amountToBurn > 0) { + _burn(from, amountToBurn); + } + } + + /* =========================================================== */ + /* ==================== Private Functions ==================== */ + /* =========================================================== */ + + function _setOperator(address _operator, bool _isOperator) internal { + isOperator[_operator] = _isOperator; + emit OperatorUpdated(_operator, _isOperator); + } + + function _setMinter(address _minter, bool _isMinter) internal { + isMinter[_minter] = _isMinter; + emit MinterUpdated(_minter, _isMinter); + } + + function _setBurner(address _burner, bool _isBurner) internal { + isBurner[_burner] = _isBurner; + emit BurnerUpdated(_burner, _isBurner); + } +} \ No newline at end of file diff --git a/contracts/src/tokens/patched/ERC20PermitUpgradeable.sol b/contracts/test/TestContracts/patched/ERC20PermitUpgradeable.sol similarity index 100% rename from contracts/src/tokens/patched/ERC20PermitUpgradeable.sol rename to contracts/test/TestContracts/patched/ERC20PermitUpgradeable.sol diff --git a/contracts/src/tokens/patched/ERC20Upgradeable.sol b/contracts/test/TestContracts/patched/ERC20Upgradeable.sol similarity index 100% rename from contracts/src/tokens/patched/ERC20Upgradeable.sol rename to contracts/test/TestContracts/patched/ERC20Upgradeable.sol From bcd695c8edf3a0dd4495b6d36db1501144e095e3 Mon Sep 17 00:00:00 2001 From: baroooo Date: Tue, 4 Nov 2025 12:38:27 +0100 Subject: [PATCH 61/79] chore: format interface --- contracts/src/Interfaces/IStableTokenV3.sol | 474 ++++++++++---------- 1 file changed, 232 insertions(+), 242 deletions(-) diff --git a/contracts/src/Interfaces/IStableTokenV3.sol b/contracts/src/Interfaces/IStableTokenV3.sol index 296d55ffe..42b8064ab 100644 --- a/contracts/src/Interfaces/IStableTokenV3.sol +++ b/contracts/src/Interfaces/IStableTokenV3.sol @@ -6,245 +6,235 @@ pragma solidity 0.8.24; * @notice Interface for the StableTokenV3 contract. */ interface IStableTokenV3 { - /** - * @notice Checks if an address is a minter. - * @param account The address to check. - * @return bool True if the address is a minter, false otherwise. - */ - function isMinter(address account) external view returns (bool); - /** - * @notice Checks if an address is a burner. - * @param account The address to check. - * @return bool True if the address is a burner, false otherwise. - */ - function isBurner(address account) external view returns (bool); - /** - * @notice Checks if an address is an operator. - * @param account The address to check. - * @return bool True if the address is an operator, false otherwise. - */ - function isOperator(address account) external view returns (bool); - - /** - * @notice Initializes a StableTokenV3. - * @param _name The name of the stable token (English) - * @param _symbol A short symbol identifying the token (e.g. "cUSD") - * @param _initialOwner The address that will be the owner of the contract. - * @param initialBalanceAddresses Array of addresses with an initial balance. - * @param initialBalanceValues Array of balance values corresponding to initialBalanceAddresses. - * @param _minters The addresses that are allowed to mint. - * @param _burners The addresses that are allowed to burn. - * @param _operators The addresses that are allowed to call the operator functions. - */ - function initialize( - string calldata _name, - string calldata _symbol, - address _initialOwner, - address[] calldata initialBalanceAddresses, - uint256[] calldata initialBalanceValues, - address[] calldata _minters, - address[] calldata _burners, - address[] calldata _operators - ) external; - - /** - * @notice Initializes a StableTokenV3 contract - * when upgrading from StableTokenV2.sol. - * It sets the addresses of the minters, burners, and operators. - * @dev This function is only callable once. - * @param _minters The addresses that are allowed to mint. - * @param _burners The addresses that are allowed to burn. - * @param _operators The addresses that are allowed to call the operator functions. - */ - function initializeV3( - address[] calldata _minters, - address[] calldata _burners, - address[] calldata _operators - ) external; - - /** - * @notice Sets the operator role for an address. - * @param _operator The address of the operator. - * @param _isOperator The boolean value indicating if the address is an operator. - */ - function setOperator(address _operator, bool _isOperator) external; - - /** - * @notice Sets the minter role for an address. - * @param _minter The address of the minter. - * @param _isMinter The boolean value indicating if the address is a minter. - */ - function setMinter(address _minter, bool _isMinter) external; - - /** - * @notice Sets the burner role for an address. - * @param _burner The address of the burner. - * @param _isBurner The boolean value indicating if the address is a burner. - */ - function setBurner(address _burner, bool _isBurner) external; - - /** - * From openzeppelin's IERC20.sol - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * From openzeppelin's IERC20.sol - * @dev Returns the amount of tokens owned by `account`. - */ - function balanceOf(address account) external view returns (uint256); - - /** - * From openzeppelin's IERC20.sol - * @dev Moves `amount` tokens from the caller's account to `to`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 amount) external returns (bool); - - /** - * From openzeppelin's IERC20.sol - * @dev Returns the remaining number of tokens that `spender` will be - * allowed to spend on behalf of `owner` through {transferFrom}. This is - * zero by default. - * - * This value changes when {approve} or {transferFrom} are called. - */ - function allowance(address owner, address spender) external view returns (uint256); - - /** - * From openzeppelin's IERC20.sol - * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - */ - function approve(address spender, uint256 amount) external returns (bool); - - /** - * From openzeppelin's IERC20.sol - * @dev Moves `amount` tokens from `from` to `to` using the - * allowance mechanism. `amount` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); - - /** - * @notice Mints new StableToken and gives it to 'to'. - * @param to The account for which to mint tokens. - * @param value The amount of StableToken to mint. - */ - function mint(address to, uint256 value) external returns (bool); - - /** - * @notice Burns StableToken from the balance of msg.sender. - * @param value The amount of StableToken to burn. - */ - function burn(uint256 value) external returns (bool); - - /** - * @notice Burns StableToken from the balance of an account. - * @param account The account to burn from. - * @param value The amount of StableToken to burn. - */ - function burn(address account, uint256 value) external returns (bool); - - /** - * From openzeppelin's IERC20PermitUpgradeable.sol - * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, - * given ``owner``'s signed approval. - * - * IMPORTANT: The same issues {IERC20-approve} has related to transaction - * ordering also apply here. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `deadline` must be a timestamp in the future. - * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` - * over the EIP712-formatted function arguments. - * - the signature must use ``owner``'s current nonce (see {nonces}). - * - * For more information on the signature format, see the - * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP - * section]. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /** - * @notice Transfer token from a specified address to the stability pool. - * @param _sender The address to transfer from. - * @param _poolAddress The address of the pool to transfer to. - * @param _amount The amount to be transferred. - */ - function sendToPool(address _sender, address _poolAddress, uint256 _amount) external; - - /** - * @notice Transfer token to a specified address from the stability pool. - * @param _poolAddress The address of the pool to transfer from - * @param _receiver The address to transfer to. - * @param _amount The amount to be transferred. - */ - function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external; - - /** - * @notice Reserve balance for making payments for gas in this StableToken currency. - * @param from The account to reserve balance from - * @param value The amount of balance to reserve - * @dev Note that this function is called by the protocol when paying for tx fees in this - * currency. After the tx is executed, gas is refunded to the sender and credited to the - * various tx fee recipients via a call to `creditGasFees`. - */ - function debitGasFees(address from, uint256 value) external; - - /** - * @notice Alternative function to credit balance after making payments - * for gas in this StableToken currency. - * @param from The account to debit balance from - * @param feeRecipient Coinbase address - * @param gatewayFeeRecipient Gateway address - * @param communityFund Community fund address - * @param refund amount to be refunded by the VM - * @param tipTxFee Coinbase fee - * @param baseTxFee Community fund fee - * @param gatewayFee Gateway fee - * @dev Note that this function is called by the protocol when paying for tx fees in this - * currency. Before the tx is executed, gas is debited from the sender via a call to - * `debitGasFees`. - */ - function creditGasFees( - address from, - address feeRecipient, - address gatewayFeeRecipient, - address communityFund, - uint256 refund, - uint256 tipTxFee, - uint256 gatewayFee, - uint256 baseTxFee - ) external; -} \ No newline at end of file + /** + * @notice Checks if an address is a minter. + * @param account The address to check. + * @return bool True if the address is a minter, false otherwise. + */ + function isMinter(address account) external view returns (bool); + /** + * @notice Checks if an address is a burner. + * @param account The address to check. + * @return bool True if the address is a burner, false otherwise. + */ + function isBurner(address account) external view returns (bool); + /** + * @notice Checks if an address is an operator. + * @param account The address to check. + * @return bool True if the address is an operator, false otherwise. + */ + function isOperator(address account) external view returns (bool); + + /** + * @notice Initializes a StableTokenV3. + * @param _name The name of the stable token (English) + * @param _symbol A short symbol identifying the token (e.g. "cUSD") + * @param _initialOwner The address that will be the owner of the contract. + * @param initialBalanceAddresses Array of addresses with an initial balance. + * @param initialBalanceValues Array of balance values corresponding to initialBalanceAddresses. + * @param _minters The addresses that are allowed to mint. + * @param _burners The addresses that are allowed to burn. + * @param _operators The addresses that are allowed to call the operator functions. + */ + function initialize( + string calldata _name, + string calldata _symbol, + address _initialOwner, + address[] calldata initialBalanceAddresses, + uint256[] calldata initialBalanceValues, + address[] calldata _minters, + address[] calldata _burners, + address[] calldata _operators + ) external; + + /** + * @notice Initializes a StableTokenV3 contract + * when upgrading from StableTokenV2.sol. + * It sets the addresses of the minters, burners, and operators. + * @dev This function is only callable once. + * @param _minters The addresses that are allowed to mint. + * @param _burners The addresses that are allowed to burn. + * @param _operators The addresses that are allowed to call the operator functions. + */ + function initializeV3(address[] calldata _minters, address[] calldata _burners, address[] calldata _operators) + external; + + /** + * @notice Sets the operator role for an address. + * @param _operator The address of the operator. + * @param _isOperator The boolean value indicating if the address is an operator. + */ + function setOperator(address _operator, bool _isOperator) external; + + /** + * @notice Sets the minter role for an address. + * @param _minter The address of the minter. + * @param _isMinter The boolean value indicating if the address is a minter. + */ + function setMinter(address _minter, bool _isMinter) external; + + /** + * @notice Sets the burner role for an address. + * @param _burner The address of the burner. + * @param _isBurner The boolean value indicating if the address is a burner. + */ + function setBurner(address _burner, bool _isBurner) external; + + /** + * From openzeppelin's IERC20.sol + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * From openzeppelin's IERC20.sol + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * From openzeppelin's IERC20.sol + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * From openzeppelin's IERC20.sol + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * From openzeppelin's IERC20.sol + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * From openzeppelin's IERC20.sol + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + /** + * @notice Mints new StableToken and gives it to 'to'. + * @param to The account for which to mint tokens. + * @param value The amount of StableToken to mint. + */ + function mint(address to, uint256 value) external returns (bool); + + /** + * @notice Burns StableToken from the balance of msg.sender. + * @param value The amount of StableToken to burn. + */ + function burn(uint256 value) external returns (bool); + + /** + * @notice Burns StableToken from the balance of an account. + * @param account The account to burn from. + * @param value The amount of StableToken to burn. + */ + function burn(address account, uint256 value) external returns (bool); + + /** + * From openzeppelin's IERC20PermitUpgradeable.sol + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; + + /** + * @notice Transfer token from a specified address to the stability pool. + * @param _sender The address to transfer from. + * @param _poolAddress The address of the pool to transfer to. + * @param _amount The amount to be transferred. + */ + function sendToPool(address _sender, address _poolAddress, uint256 _amount) external; + + /** + * @notice Transfer token to a specified address from the stability pool. + * @param _poolAddress The address of the pool to transfer from + * @param _receiver The address to transfer to. + * @param _amount The amount to be transferred. + */ + function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external; + + /** + * @notice Reserve balance for making payments for gas in this StableToken currency. + * @param from The account to reserve balance from + * @param value The amount of balance to reserve + * @dev Note that this function is called by the protocol when paying for tx fees in this + * currency. After the tx is executed, gas is refunded to the sender and credited to the + * various tx fee recipients via a call to `creditGasFees`. + */ + function debitGasFees(address from, uint256 value) external; + + /** + * @notice Alternative function to credit balance after making payments + * for gas in this StableToken currency. + * @param from The account to debit balance from + * @param feeRecipient Coinbase address + * @param gatewayFeeRecipient Gateway address + * @param communityFund Community fund address + * @param refund amount to be refunded by the VM + * @param tipTxFee Coinbase fee + * @param baseTxFee Community fund fee + * @param gatewayFee Gateway fee + * @dev Note that this function is called by the protocol when paying for tx fees in this + * currency. Before the tx is executed, gas is debited from the sender via a call to + * `debitGasFees`. + */ + function creditGasFees( + address from, + address feeRecipient, + address gatewayFeeRecipient, + address communityFund, + uint256 refund, + uint256 tipTxFee, + uint256 gatewayFee, + uint256 baseTxFee + ) external; +} From ff84b1a1746ff4f1e078522d289aa69362d009b1 Mon Sep 17 00:00:00 2001 From: baroooo Date: Tue, 4 Nov 2025 12:44:59 +0100 Subject: [PATCH 62/79] chore: fix typo --- contracts/test/TestContracts/StableTokenV3.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/TestContracts/StableTokenV3.sol b/contracts/test/TestContracts/StableTokenV3.sol index 04d0ad9a1..9385b4aba 100644 --- a/contracts/test/TestContracts/StableTokenV3.sol +++ b/contracts/test/TestContracts/StableTokenV3.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.24; import { ERC20PermitUpgradeable } from "./patched/ERC20PermitUpgradeable.sol"; import { ERC20Upgradeable } from "./patched/ERC20Upgradeable.sol"; -import { IStableTokenV3 } from "src/interfaces/IStableTokenV3.sol"; +import { IStableTokenV3 } from "src/Interfaces/IStableTokenV3.sol"; contract CalledByVm { From de641e770b835fba9ffc45dd98bf80b7df921f6e Mon Sep 17 00:00:00 2001 From: baroooo Date: Tue, 4 Nov 2025 13:12:49 +0100 Subject: [PATCH 63/79] chore: remove audit tag to make linter happy --- contracts/test/rebasingBatchShares.t.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/test/rebasingBatchShares.t.sol b/contracts/test/rebasingBatchShares.t.sol index e936821b5..417d5d03e 100644 --- a/contracts/test/rebasingBatchShares.t.sol +++ b/contracts/test/rebasingBatchShares.t.sol @@ -36,7 +36,6 @@ contract RebasingBatchShares is DevTestSetup { // TODO: Open A, Mint 1 extra (forgiven to A) _addOneDebtAndEnsureItDoesntMintShares(ATroveId, A); - /// @audit MED impact LatestBatchData memory afterBatch = troveManager.getLatestBatchData(address(B)); From 11a276d151b3f5d606e3deb191084ad17d9c7f85 Mon Sep 17 00:00:00 2001 From: baroooo Date: Tue, 18 Nov 2025 11:01:27 +0100 Subject: [PATCH 64/79] fix: disable rebalances during shutdown --- contracts/src/StabilityPool.sol | 7 ++++++- contracts/test/swapCollateralForStable.t.sol | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 009d0e88d..6d2bcd8c3 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -411,7 +411,8 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi */ function swapCollateralForStable(uint256 amountCollIn, uint256 amountStableOut) external { _requireCallerIsLiquidityStrategy(); - + _requireNoShutdown(); + _updateTrackingVariables(amountStableOut, amountCollIn); _swapCollateralForStable(amountCollIn, amountStableOut); @@ -666,4 +667,8 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi function _requireNonZeroAmount(uint256 _amount) internal pure { require(_amount > 0, "StabilityPool: Amount must be non-zero"); } + + function _requireNoShutdown() internal view { + require(troveManager.shutdownTime() == 0, "StabilityPool: System is shut down"); + } } diff --git a/contracts/test/swapCollateralForStable.t.sol b/contracts/test/swapCollateralForStable.t.sol index 2abf9ea8a..d86da45c5 100644 --- a/contracts/test/swapCollateralForStable.t.sol +++ b/contracts/test/swapCollateralForStable.t.sol @@ -48,6 +48,20 @@ contract SwapCollateralForStableTest is DevTestSetup { stabilityPool.swapCollateralForStable(1e18, 1e18); } + function testSwapCollateralForStableRevertsWhenSystemIsShutDown() public { + vm.mockCall( + address(troveManager), + abi.encodeWithSelector(ITroveManager.shutdownTime.selector), + abi.encode(block.timestamp - 1) + ); + + + vm.expectRevert("StabilityPool: System is shut down"); + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(1e18, 1e18); + vm.stopPrank(); + } + function testSwapCollateralForStableRevertsWithInsufficientStableLiquidity() public { uint256 stableAmount = 1000e18; From c4030fc1367e0d6f99041f928075d15e0efb4cfb Mon Sep 17 00:00:00 2001 From: baroooo Date: Tue, 18 Nov 2025 13:37:54 +0100 Subject: [PATCH 65/79] fix: missing checks for min_bold_after_rebalance --- contracts/script/DeployLiquity2.s.sol | 7 +++-- contracts/src/Interfaces/IStabilityPool.sol | 1 - contracts/src/Interfaces/ISystemParams.sol | 4 +++ contracts/src/StabilityPool.sol | 7 ++--- contracts/src/SystemParams.sol | 3 ++ contracts/test/TestContracts/Deployment.t.sol | 3 +- contracts/test/systemParams.t.sol | 31 ++++++++++++++++--- 7 files changed, 43 insertions(+), 13 deletions(-) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 728ab6c5d..a187edd4e 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -268,8 +268,11 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { redemptionBeta: 1 }); - ISystemParams.StabilityPoolParams memory poolParams = - ISystemParams.StabilityPoolParams({spYieldSplit: 75 * (1e18 / 100), minBoldInSP: 1e18}); + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ + spYieldSplit: 75 * (1e18 / 100), + minBoldInSP: 1e18, + minBoldAfterRebalance: 1_000e18 + }); r.systemParamsImpl = address( new SystemParams{salt: SALT}( diff --git a/contracts/src/Interfaces/IStabilityPool.sol b/contracts/src/Interfaces/IStabilityPool.sol index 8723c7188..b55984f3c 100644 --- a/contracts/src/Interfaces/IStabilityPool.sol +++ b/contracts/src/Interfaces/IStabilityPool.sol @@ -119,5 +119,4 @@ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { function liquidityStrategy() external view returns (address); function P_PRECISION() external view returns (uint256); - function MIN_BOLD_AFTER_REBALANCE() external view returns (uint256); } diff --git a/contracts/src/Interfaces/ISystemParams.sol b/contracts/src/Interfaces/ISystemParams.sol index 97ffa78da..f0a4ff2f5 100644 --- a/contracts/src/Interfaces/ISystemParams.sol +++ b/contracts/src/Interfaces/ISystemParams.sol @@ -41,6 +41,7 @@ interface ISystemParams { struct StabilityPoolParams { uint256 spYieldSplit; uint256 minBoldInSP; + uint256 minBoldAfterRebalance; } /* ========== ERRORS ========== */ @@ -138,5 +139,8 @@ interface ISystemParams { /// @notice Minimum BOLD that must remain in Stability Pool to prevent complete drainage. function MIN_BOLD_IN_SP() external view returns (uint256); + /// @notice Minimum BOLD that must remain in Stability Pool after a rebalance operation. + function MIN_BOLD_AFTER_REBALANCE() external view returns (uint256); + function initialize() external; } diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 009d0e88d..1be031c99 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -173,10 +173,6 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi // The number of scale changes after which an untouched deposit stops receiving yield / coll gains uint256 public constant SCALE_SPAN = 2; - // The minimum amount of Bold in the SP after a rebalance - // Introduced to avoid higher rate of scale changes - uint256 public constant MIN_BOLD_AFTER_REBALANCE = 1_000e18; - // Each time the scale of P shifts by SCALE_FACTOR, the scale is incremented by 1 uint256 public currentScale; @@ -417,7 +413,8 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi _swapCollateralForStable(amountCollIn, amountStableOut); require( - totalBoldDeposits >= MIN_BOLD_AFTER_REBALANCE, "Total Bold deposits must be >= MIN_BOLD_AFTER_REBALANCE" + totalBoldDeposits >= systemParams.MIN_BOLD_AFTER_REBALANCE(), + "Total Bold deposits must be >= MIN_BOLD_AFTER_REBALANCE" ); } diff --git a/contracts/src/SystemParams.sol b/contracts/src/SystemParams.sol index b4640d98c..4e097f348 100644 --- a/contracts/src/SystemParams.sol +++ b/contracts/src/SystemParams.sol @@ -54,6 +54,7 @@ contract SystemParams is ISystemParams, Initializable { uint256 public immutable SP_YIELD_SPLIT; uint256 public immutable MIN_BOLD_IN_SP; + uint256 public immutable MIN_BOLD_AFTER_REBALANCE; /* ========== CONSTRUCTOR ========== */ @@ -115,6 +116,7 @@ contract SystemParams is ISystemParams, Initializable { // Validate stability pool parameters if (_poolParams.spYieldSplit > _100pct) revert InvalidFeeValue(); if (_poolParams.minBoldInSP == 0) revert InvalidMinDebt(); + if (_poolParams.minBoldAfterRebalance < _poolParams.minBoldInSP) revert InvalidMinDebt(); // Set debt parameters MIN_DEBT = _debtParams.minDebt; @@ -146,6 +148,7 @@ contract SystemParams is ISystemParams, Initializable { // Set stability pool parameters SP_YIELD_SPLIT = _poolParams.spYieldSplit; MIN_BOLD_IN_SP = _poolParams.minBoldInSP; + MIN_BOLD_AFTER_REBALANCE = _poolParams.minBoldAfterRebalance; } /* diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index f6aa000d5..ffbc9c7f7 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -349,7 +349,8 @@ contract TestDeployer is MetadataDeployment { ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ spYieldSplit: 75 * (DECIMAL_PRECISION / 100), // SP_YIELD_SPLIT (75%) - minBoldInSP: 1e18 // MIN_BOLD_IN_SP + minBoldInSP: 1e18, // MIN_BOLD_IN_SP + minBoldAfterRebalance: 1_000e18 // MIN_BOLD_AFTER_REBALANCE }); SystemParams systemParams = new SystemParams{salt: uniqueSalt}( diff --git a/contracts/test/systemParams.t.sol b/contracts/test/systemParams.t.sol index fa187120a..63eea4d10 100644 --- a/contracts/test/systemParams.t.sol +++ b/contracts/test/systemParams.t.sol @@ -43,7 +43,8 @@ contract SystemParamsTest is DevTestSetup { ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ spYieldSplit: 75 * _1pct, - minBoldInSP: 1e18 + minBoldInSP: 1e18, + minBoldAfterRebalance: 1_000e18 }); SystemParams params = new SystemParams(false, @@ -509,7 +510,8 @@ contract SystemParamsTest is DevTestSetup { function testConstructorRevertsWhenSPYieldSplitTooHigh() public { ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ spYieldSplit: _100pct + 1, - minBoldInSP: 1e18 + minBoldInSP: 1e18, + minBoldAfterRebalance: 1_000e18 }); vm.expectRevert(ISystemParams.InvalidFeeValue.selector); @@ -527,7 +529,27 @@ contract SystemParamsTest is DevTestSetup { function testConstructorRevertsWhenMinBoldInSPZero() public { ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ spYieldSplit: 75 * _1pct, - minBoldInSP: 0 + minBoldInSP: 0, + minBoldAfterRebalance: 1_000e18 + }); + + vm.expectRevert(ISystemParams.InvalidMinDebt.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + poolParams + ); + } + + function testConstructorRevertsWhenMinBoldAfterRebalanceIsLessThanMinBoldInSP() public { + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ + spYieldSplit: 75 * _1pct, + minBoldInSP: 1e18, + minBoldAfterRebalance: 1e18 - 1 // < 1e18 }); vm.expectRevert(ISystemParams.InvalidMinDebt.selector); @@ -590,7 +612,8 @@ contract SystemParamsTest is DevTestSetup { function _getValidPoolParams() internal pure returns (ISystemParams.StabilityPoolParams memory) { return ISystemParams.StabilityPoolParams({ spYieldSplit: 75 * _1pct, - minBoldInSP: 1e18 + minBoldInSP: 1e18, + minBoldAfterRebalance: 1_000e18 }); } } \ No newline at end of file From dc7cac44e39754a396a5d307c8c1e201d1059cfa Mon Sep 17 00:00:00 2001 From: nvtaveras <4562733+nvtaveras@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:33:33 +0100 Subject: [PATCH 66/79] fix: add setRateFeedID to FXPriceFeed (#22) --- contracts/src/PriceFeeds/FXPriceFeed.sol | 14 ++++++++++++ contracts/test/FXPriceFeed.t.sol | 27 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/contracts/src/PriceFeeds/FXPriceFeed.sol b/contracts/src/PriceFeeds/FXPriceFeed.sol index ed623150f..af615a8ae 100644 --- a/contracts/src/PriceFeeds/FXPriceFeed.sol +++ b/contracts/src/PriceFeeds/FXPriceFeed.sol @@ -42,6 +42,11 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { /// @notice Thrown when a zero address is provided as a parameter error ZeroAddress(); + /// @notice Emitted when the rate feed ID is updated + /// @param _oldRateFeedID The previous rate feed ID + /// @param _newRateFeedID The new rate feed ID + event RateFeedIDUpdated(address indexed _oldRateFeedID, address indexed _newRateFeedID); + /// @notice Emitted when the watchdog address is updated /// @param _oldWatchdogAddress The previous watchdog address /// @param _newWatchdogAddress The new watchdog address @@ -91,6 +96,15 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { _transferOwnership(_initialOwner); } + function setRateFeedID(address _newRateFeedID) external onlyOwner { + if (_newRateFeedID == address(0)) revert ZeroAddress(); + + address oldRateFeedID = rateFeedID; + rateFeedID = _newRateFeedID; + + emit RateFeedIDUpdated(oldRateFeedID, _newRateFeedID); + } + /** * @notice Sets the watchdog address * @param _newWatchdogAddress The address of the new watchdog contract diff --git a/contracts/test/FXPriceFeed.t.sol b/contracts/test/FXPriceFeed.t.sol index 086095556..e3b0a0a11 100644 --- a/contracts/test/FXPriceFeed.t.sol +++ b/contracts/test/FXPriceFeed.t.sol @@ -191,6 +191,33 @@ contract FXPriceFeedTest is Test { ); } + function test_setRateFeedID_whenCalledByNonOwner_shouldRevert() initialized public { + address notOwner = makeAddr("notOwner"); + address newRateFeedID = makeAddr("newRateFeedID"); + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + fxPriceFeed.setRateFeedID(newRateFeedID); + vm.stopPrank(); + } + + function test_setRateFeedID_whenNewAddressIsZero_shouldRevert() initialized public { + vm.prank(owner); + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + fxPriceFeed.setRateFeedID(address(0)); + vm.stopPrank(); + } + + function test_setRateFeedID_whenCalledByOwner_shouldSucceed() initialized public { + address newRateFeedID = makeAddr("newRateFeedID"); + + vm.prank(owner); + fxPriceFeed.setRateFeedID(newRateFeedID); + vm.stopPrank(); + + assertEq(fxPriceFeed.rateFeedID(), newRateFeedID); + } + function test_setWatchdogAddress_whenCalledByNonOwner_shouldRevert() initialized public { address notOwner = makeAddr("notOwner"); address newWatchdog = makeAddr("newWatchdog"); From 6ab6d8b708e6d202c5b8112b5d98d9aa7c07dc36 Mon Sep 17 00:00:00 2001 From: baroooo Date: Tue, 18 Nov 2025 14:34:25 +0100 Subject: [PATCH 67/79] fix: delete BM for kicked troves (#20) --- contracts/src/BorrowerOperations.sol | 1 + contracts/test/interestBatchManagement.t.sol | 105 +++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index c15070d61..959dbbbc8 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -925,6 +925,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio abi.encodeWithSignature("kickFromBatch(uint256,uint256,uint256)", _troveId, _upperHint, _lowerHint) ); _requireDelegateCallSucceeded(success, data); + delete interestBatchManagerOf[_troveId]; } function removeFromBatch( diff --git a/contracts/test/interestBatchManagement.t.sol b/contracts/test/interestBatchManagement.t.sol index 710ba81c8..9ab7736dc 100644 --- a/contracts/test/interestBatchManagement.t.sol +++ b/contracts/test/interestBatchManagement.t.sol @@ -1513,4 +1513,109 @@ contract InterestBatchManagementTest is DevTestSetup { borrowerOperations.adjustTrove(troveId, 1 ether, true, 100e18, false, 1000e18); vm.stopPrank(); } + + function testRemoveFromBatchDeletesInterestBatchManagerOf() public { + uint256 troveId = openTroveAndJoinBatchManager(); + + // Verify trove is in batch + address batchManagerAddress = borrowerOperations.interestBatchManagerOf(troveId); + assertEq(batchManagerAddress, B, "Trove should be in batch B"); + + // Remove from batch + vm.startPrank(A); + borrowerOperations.removeFromBatch(troveId, 4e16, 0, 0, 1e24); + vm.stopPrank(); + + // Verify mapping is deleted + assertEq(borrowerOperations.interestBatchManagerOf(troveId), address(0), "interestBatchManagerOf should be deleted"); + (,,,,,,,, address tmBatchManagerAddress,) = troveManager.Troves(troveId); + assertEq(tmBatchManagerAddress, address(0), "TM batch manager should be address(0)"); + } + + function testKickFromBatchDeletesInterestBatchManagerOf() public { + registerBatchManager({ + _account: B, + _minInterestRate: uint128(MIN_ANNUAL_INTEREST_RATE), + _maxInterestRate: uint128(MAX_ANNUAL_INTEREST_RATE), + _currentInterestRate: uint128(MAX_ANNUAL_INTEREST_RATE), + _fee: MAX_ANNUAL_BATCH_MANAGEMENT_FEE, + _minInterestRateChangePeriod: MIN_INTEREST_RATE_CHANGE_PERIOD + }); + + // Placeholder Trove so that the batch isn't wiped out fully when we redeem the target Trove later + uint256 placeholderTrove = openTroveAndJoinBatchManager({ + _troveOwner: C, + _coll: 1_000_000 ether, + _debt: MIN_DEBT, + _batchAddress: B, + _annualInterestRate: 0 // ignored + }); + + // Open the target Trove, the one we will make irredeemable + uint256 targetTrove = openTroveAndJoinBatchManager({ + _troveOwner: A, + _coll: 1_000_000 ether, + _debt: MIN_DEBT, + _batchAddress: B, + _annualInterestRate: 0 // ignored + }); + + // Verify trove is in batch + address batchManagerAddress = borrowerOperations.interestBatchManagerOf(targetTrove); + assertEq(batchManagerAddress, B, "Trove should be in batch B"); + + // Another Trove to provide funds and keep the average interest rate high, + // which speeds up our manipulation of the batch:shares ratio + openTroveHelper({ + _account: A, + _index: 1, + _coll: 1_000_000 ether, + _boldAmount: 10_000_000 ether, + _annualInterestRate: MAX_ANNUAL_INTEREST_RATE + }); + + // Increase the batch:shares ratio past the limit + for (uint256 i = 1;; ++i) { + skip(MIN_INTEREST_RATE_CHANGE_PERIOD); + setBatchInterestRate(B, MAX_ANNUAL_INTEREST_RATE - i % 2); + + (uint256 debt,,,,,,, uint256 shares) = troveManager.getBatch(B); + if (shares * MAX_BATCH_SHARES_RATIO < debt) break; + + // Keep debt low to minimize interest and maintain healthy TCR + repayBold(A, targetTrove, troveManager.getTroveEntireDebt(targetTrove) - MIN_DEBT); + repayBold(A, placeholderTrove, troveManager.getTroveEntireDebt(placeholderTrove) - MIN_DEBT); + } + + // Make a zombie out of the target Trove + skip(MIN_INTEREST_RATE_CHANGE_PERIOD); + setBatchInterestRate(B, MIN_ANNUAL_INTEREST_RATE); + redeem(A, troveManager.getTroveEntireDebt(targetTrove)); + assertTrue(troveManager.checkTroveIsZombie(targetTrove), "not a zombie"); + + // Open a Trove to be liquidated + (uint256 liquidatedTrove,) = openTroveWithExactICRAndDebt({ + _account: D, + _index: 0, + _ICR: MCR, + _debt: 100_000 ether, + _interestRate: MIN_ANNUAL_INTEREST_RATE + }); + + // Liquidate by redistribution + priceFeed.setPrice(priceFeed.getPrice() * 99 / 100); + liquidate(A, liquidatedTrove); + + // Verify trove is still in batch before kicking + batchManagerAddress = borrowerOperations.interestBatchManagerOf(targetTrove); + assertEq(batchManagerAddress, B, "Trove should still be in batch B before kick"); + + // Kick the trove from batch + borrowerOperations.kickFromBatch(targetTrove, 0, 0); + + // Verify mapping is deleted after kick + assertEq(borrowerOperations.interestBatchManagerOf(targetTrove), address(0), "interestBatchManagerOf should be deleted after kick"); + (,,,,,,,, address tmBatchManagerAddress,) = troveManager.Troves(targetTrove); + assertEq(tmBatchManagerAddress, address(0), "TM batch manager should be address(0) after kick"); + } } From d41844f67f7df9fb9a6ca518e448019aa34bcfa6 Mon Sep 17 00:00:00 2001 From: baroooo Date: Tue, 18 Nov 2025 14:35:04 +0100 Subject: [PATCH 68/79] fix: mintAggInterest before rebalance (#19) --- contracts/src/StabilityPool.sol | 2 + contracts/test/swapCollateralForStable.t.sol | 51 ++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 009d0e88d..73542d151 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -412,6 +412,8 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi function swapCollateralForStable(uint256 amountCollIn, uint256 amountStableOut) external { _requireCallerIsLiquidityStrategy(); + activePool.mintAggInterest(); + _updateTrackingVariables(amountStableOut, amountCollIn); _swapCollateralForStable(amountCollIn, amountStableOut); diff --git a/contracts/test/swapCollateralForStable.t.sol b/contracts/test/swapCollateralForStable.t.sol index 2abf9ea8a..30e5116ef 100644 --- a/contracts/test/swapCollateralForStable.t.sol +++ b/contracts/test/swapCollateralForStable.t.sol @@ -461,4 +461,55 @@ contract SwapCollateralForStableTest is DevTestSetup { assertEq(stabilityPool.getTotalBoldDeposits(), 1_000e18); assertEq(stabilityPool.getCollBalance(), collSwapAmount); } + + function testSwapCollateralForStableWithYieldGains() public { + uint256 stableAmount = 2000e18; + uint256 yieldAmount = 100e18; + uint256 collSwapAmount = 1e18; + uint256 stableSwapAmount = 1000e18; + + // Setup initial deposits + deal(address(boldToken), A, stableAmount); + makeSPDepositAndClaim(A, stableAmount); + + deal(address(boldToken), B, stableAmount); + makeSPDepositAndClaim(B, stableAmount); + + // Generate some yield gains + vm.prank(address(activePool)); + stabilityPool.triggerBoldRewards(yieldAmount); + + uint256 initialYieldGainA = stabilityPool.getDepositorYieldGain(A); + assertEq(initialYieldGainA, yieldAmount / 2); + + uint256 initialYieldGainB = stabilityPool.getDepositorYieldGain(B); + assertEq(initialYieldGainB, yieldAmount / 2); + + // Perform rebalance + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + + // Verify yield gain is preserved after swap + uint256 finalYieldGainA = stabilityPool.getDepositorYieldGain(A); + assertEq(finalYieldGainA, initialYieldGainA); + + uint256 finalYieldGainB = stabilityPool.getDepositorYieldGain(B); + assertEq(finalYieldGainB, initialYieldGainB); + + // Verify depositors can still claim yield gains + uint256 preBoldBalance = boldToken.balanceOf(A); + vm.prank(A); + stabilityPool.withdrawFromSP(0, true); + + uint256 postBoldBalance = boldToken.balanceOf(A); + assertEq(postBoldBalance - preBoldBalance, initialYieldGainA); + + + preBoldBalance = boldToken.balanceOf(B); + vm.prank(B); + stabilityPool.withdrawFromSP(0, true); + postBoldBalance = boldToken.balanceOf(B); + + assertEq(postBoldBalance - preBoldBalance, initialYieldGainB); + } } From 874f61d284c43c5e1ab97055c91f8cc658d5dc84 Mon Sep 17 00:00:00 2001 From: baroooo Date: Tue, 18 Nov 2025 14:43:13 +0100 Subject: [PATCH 69/79] fix: Incorrect IStabilityPool interface --- contracts/src/Interfaces/IStabilityPool.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/Interfaces/IStabilityPool.sol b/contracts/src/Interfaces/IStabilityPool.sol index 8723c7188..dfb999b2e 100644 --- a/contracts/src/Interfaces/IStabilityPool.sol +++ b/contracts/src/Interfaces/IStabilityPool.sol @@ -61,7 +61,7 @@ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { * Removed stable tokens will be factored out from LPs' positions. * Added collateral will be added to LPs collateral gain which can be later claimed by the depositor. */ - function swapCollateralForStable(uint256 amountStableOut, uint256 amountCollIn) external; + function swapCollateralForStable(uint256 amountCollIn, uint256 amountStableOut) external; /* * Initial checks: From c8dea1330e56b5efc6dfb0df51194c28b48fd32b Mon Sep 17 00:00:00 2001 From: baroooo Date: Tue, 18 Nov 2025 16:19:53 +0100 Subject: [PATCH 70/79] chore: add event for rebalance --- contracts/src/StabilityPool.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 73542d151..3ec5c665b 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -197,6 +197,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi event TroveManagerAddressChanged(address _newTroveManagerAddress); event BoldTokenAddressChanged(address _newBoldTokenAddress); + event RebalanceExecuted(uint256 amountCollIn, uint256 amountStableOut); /** * @dev Should be called with disable=true in deployments when it's accessed through a Proxy. @@ -421,6 +422,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi require( totalBoldDeposits >= MIN_BOLD_AFTER_REBALANCE, "Total Bold deposits must be >= MIN_BOLD_AFTER_REBALANCE" ); + emit RebalanceExecuted(amountCollIn, amountStableOut); } // --- Liquidation functions --- From a01b4a83cce6071529abdfdbcd9b5149d596c877 Mon Sep 17 00:00:00 2001 From: baroooo Date: Tue, 18 Nov 2025 16:48:12 +0100 Subject: [PATCH 71/79] test: update stabletokenv3 --- contracts/src/Interfaces/IStableTokenV3.sol | 7 +++ .../test/TestContracts/StableTokenV3.sol | 47 +++++++------------ 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/contracts/src/Interfaces/IStableTokenV3.sol b/contracts/src/Interfaces/IStableTokenV3.sol index 42b8064ab..7b55a9e93 100644 --- a/contracts/src/Interfaces/IStableTokenV3.sol +++ b/contracts/src/Interfaces/IStableTokenV3.sol @@ -237,4 +237,11 @@ interface IStableTokenV3 { uint256 gatewayFee, uint256 baseTxFee ) external; + + /** + * @notice Credit gas fees to multiple addresses. + * @param recipients The addresses to credit the fees to. + * @param amounts The amounts of fees to credit to each address. + */ + function creditGasFees(address[] calldata recipients, uint256[] calldata amounts) external; } diff --git a/contracts/test/TestContracts/StableTokenV3.sol b/contracts/test/TestContracts/StableTokenV3.sol index 9385b4aba..bfe0af954 100644 --- a/contracts/test/TestContracts/StableTokenV3.sol +++ b/contracts/test/TestContracts/StableTokenV3.sol @@ -248,39 +248,26 @@ contract StableTokenV3 is ERC20PermitUpgradeable, IStableTokenV3, CalledByVm { /// @inheritdoc IStableTokenV3 function creditGasFees( - address from, - address feeRecipient, - address gatewayFeeRecipient, - address communityFund, - uint256 refund, - uint256 tipTxFee, - uint256 gatewayFee, - uint256 baseTxFee + address refundRecipient, + address tipRecipient, + address, // _gatewayFeeRecipient, unused + address baseFeeRecipient, + uint256 refundAmount, + uint256 tipAmount, + uint256, // _gatewayFeeAmount, unused + uint256 baseFeeAmount ) external onlyVm { - // slither-disable-next-line uninitialized-local - uint256 amountToBurn; - _mint(from, refund + tipTxFee + gatewayFee + baseTxFee); - - if (feeRecipient != address(0)) { - _transfer(from, feeRecipient, tipTxFee); - } else if (tipTxFee > 0) { - amountToBurn += tipTxFee; - } - - if (gatewayFeeRecipient != address(0)) { - _transfer(from, gatewayFeeRecipient, gatewayFee); - } else if (gatewayFee > 0) { - amountToBurn += gatewayFee; - } + _mint(refundRecipient, refundAmount); + _mint(tipRecipient, tipAmount); + _mint(baseFeeRecipient, baseFeeAmount); + } - if (communityFund != address(0)) { - _transfer(from, communityFund, baseTxFee); - } else if (baseTxFee > 0) { - amountToBurn += baseTxFee; - } + /// @inheritdoc IStableTokenV3 + function creditGasFees(address[] calldata recipients, uint256[] calldata amounts) external onlyVm { + require(recipients.length == amounts.length, "StableTokenV3: recipients and amounts must be the same length."); - if (amountToBurn > 0) { - _burn(from, amountToBurn); + for (uint256 i = 0; i < recipients.length; i++) { + _mint(recipients[i], amounts[i]); } } From 19e5c6dfb87b4423d308166a5bd450f0b507df18 Mon Sep 17 00:00:00 2001 From: baroooo Date: Wed, 19 Nov 2025 19:05:08 +0100 Subject: [PATCH 72/79] fix: min bold in stability pool can not be lower than 1e18 (#25) --- contracts/src/Interfaces/ISystemParams.sol | 1 + contracts/src/SystemParams.sol | 2 +- contracts/test/systemParams.t.sol | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/src/Interfaces/ISystemParams.sol b/contracts/src/Interfaces/ISystemParams.sol index 97ffa78da..418cf664d 100644 --- a/contracts/src/Interfaces/ISystemParams.sol +++ b/contracts/src/Interfaces/ISystemParams.sol @@ -58,6 +58,7 @@ interface ISystemParams { error SPPenaltyTooLow(); error SPPenaltyGtRedist(); error RedistPenaltyTooHigh(); + error InvalidMinBoldInSP(); /* ========== DEBT PARAMETERS ========== */ diff --git a/contracts/src/SystemParams.sol b/contracts/src/SystemParams.sol index b4640d98c..5eb955b42 100644 --- a/contracts/src/SystemParams.sol +++ b/contracts/src/SystemParams.sol @@ -114,7 +114,7 @@ contract SystemParams is ISystemParams, Initializable { // Validate stability pool parameters if (_poolParams.spYieldSplit > _100pct) revert InvalidFeeValue(); - if (_poolParams.minBoldInSP == 0) revert InvalidMinDebt(); + if (_poolParams.minBoldInSP < 1e18) revert InvalidMinBoldInSP(); // Set debt parameters MIN_DEBT = _debtParams.minDebt; diff --git a/contracts/test/systemParams.t.sol b/contracts/test/systemParams.t.sol index fa187120a..99e2c9030 100644 --- a/contracts/test/systemParams.t.sol +++ b/contracts/test/systemParams.t.sol @@ -524,13 +524,13 @@ contract SystemParamsTest is DevTestSetup { ); } - function testConstructorRevertsWhenMinBoldInSPZero() public { + function testConstructorRevertsWhenMinBoldInSPLessThan1e18() public { ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ spYieldSplit: 75 * _1pct, - minBoldInSP: 0 + minBoldInSP: 1e18 - 1 // < 1e18 }); - vm.expectRevert(ISystemParams.InvalidMinDebt.selector); + vm.expectRevert(ISystemParams.InvalidMinBoldInSP.selector); new SystemParams(false, _getValidDebtParams(), _getValidLiquidationParams(), From cfaeebd12ba81b0ebbbe5ffd0dc34fc67c535741 Mon Sep 17 00:00:00 2001 From: baroooo Date: Wed, 19 Nov 2025 19:06:17 +0100 Subject: [PATCH 73/79] fix: add checks for liquidationPenaltyRedistribution (#27) --- contracts/src/SystemParams.sol | 11 +++-- contracts/test/systemParams.t.sol | 81 +++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/contracts/src/SystemParams.sol b/contracts/src/SystemParams.sol index 5eb955b42..a8f62c78f 100644 --- a/contracts/src/SystemParams.sol +++ b/contracts/src/SystemParams.sol @@ -82,9 +82,6 @@ contract SystemParams is ISystemParams, Initializable { if (_liquidationParams.liquidationPenaltySP > _liquidationParams.liquidationPenaltyRedistribution) { revert SPPenaltyGtRedist(); } - if (_liquidationParams.liquidationPenaltyRedistribution > MAX_LIQUIDATION_PENALTY_REDISTRIBUTION) { - revert RedistPenaltyTooHigh(); - } // Validate gas compensation parameters if (_gasCompParams.collGasCompensationDivisor == 0 || _gasCompParams.collGasCompensationDivisor > 1000) { @@ -103,6 +100,14 @@ contract SystemParams is ISystemParams, Initializable { if (_collateralParams.bcr < 5 * _1pct || _collateralParams.bcr >= 50 * _1pct) revert InvalidBCR(); if (_collateralParams.scr <= _100pct || _collateralParams.scr >= 2 * _100pct) revert InvalidSCR(); + // The redistribution penalty must not exceed the overcollateralization buffer (MCR - 100%) + if ( + _liquidationParams.liquidationPenaltyRedistribution > MAX_LIQUIDATION_PENALTY_REDISTRIBUTION + || _liquidationParams.liquidationPenaltyRedistribution > _collateralParams.mcr - _100pct + ) { + revert RedistPenaltyTooHigh(); + } + // Validate interest parameters if (_interestParams.minAnnualInterestRate > MAX_ANNUAL_INTEREST_RATE) { revert MinInterestRateGtMax(); diff --git a/contracts/test/systemParams.t.sol b/contracts/test/systemParams.t.sol index 99e2c9030..51d64446b 100644 --- a/contracts/test/systemParams.t.sol +++ b/contracts/test/systemParams.t.sol @@ -164,6 +164,87 @@ contract SystemParamsTest is DevTestSetup { ); } + function testConstructorRevertsWhenRedistPenaltyExceedsMCRBuffer() public { + // MCR = 110%, so buffer = 10%. Set redistribution penalty to 11% (exceeds buffer) + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, + liquidationPenaltyRedistribution: 11 * _1pct // 11% > (110% - 100%) + }); + + vm.expectRevert(ISystemParams.RedistPenaltyTooHigh.selector); + new SystemParams(false, + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorAllowsRedistPenaltyEqualToMCRBuffer() public { + // MCR = 110%, so buffer = 10%. Set redistribution penalty to exactly 10% + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, + liquidationPenaltyRedistribution: 10 * _1pct // 10% == (110% - 100%) + }); + + // Should not revert + SystemParams params = new SystemParams(false, + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + + assertEq(params.LIQUIDATION_PENALTY_REDISTRIBUTION(), 10 * _1pct); + } + + function testConstructorRevertsWhenRedistPenaltyExceedsMCRBufferWithDifferentMCR() public { + // MCR = 115%, so buffer = 15%. Set redistribution penalty to 16% (exceeds buffer) + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 115 * _1pct, + mcr: 115 * _1pct, + bcr: 10 * _1pct + }); + + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, + liquidationPenaltyRedistribution: 16 * _1pct // 16% > (115% - 100%) + }); + + vm.expectRevert(ISystemParams.RedistPenaltyTooHigh.selector); + new SystemParams(false, + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + + liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, + liquidationPenaltyRedistribution: 15 * _1pct // 15% + }); + + new SystemParams(false, + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + // ========== GAS COMPENSATION VALIDATION TESTS ========== function testConstructorRevertsWhenGasCompDivisorZero() public { From b066fc2fec3f404c353db3c12de3018d6c0cdd8d Mon Sep 17 00:00:00 2001 From: baroooo Date: Wed, 19 Nov 2025 19:07:11 +0100 Subject: [PATCH 74/79] fix: dont check for the upper limit of MIN_DEBT (#28) --- contracts/src/SystemParams.sol | 4 ++-- contracts/test/systemParams.t.sol | 15 --------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/contracts/src/SystemParams.sol b/contracts/src/SystemParams.sol index a8f62c78f..fe6a53cc9 100644 --- a/contracts/src/SystemParams.sol +++ b/contracts/src/SystemParams.sol @@ -71,8 +71,8 @@ contract SystemParams is ISystemParams, Initializable { _disableInitializers(); } - // Validate debt parameters - if (_debtParams.minDebt == 0 || _debtParams.minDebt > 10000e18) revert InvalidMinDebt(); + // minDebt should be choosen depending on the debt currency + if (_debtParams.minDebt == 0) revert InvalidMinDebt(); // Validate liquidation parameters // Hardcoded validation bounds: MIN_LIQUIDATION_PENALTY_SP = 5% diff --git a/contracts/test/systemParams.t.sol b/contracts/test/systemParams.t.sol index 51d64446b..5d9523883 100644 --- a/contracts/test/systemParams.t.sol +++ b/contracts/test/systemParams.t.sol @@ -93,21 +93,6 @@ contract SystemParamsTest is DevTestSetup { ); } - function testConstructorRevertsWhenMinDebtTooHigh() public { - ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({minDebt: 10001e18}); - - vm.expectRevert(ISystemParams.InvalidMinDebt.selector); - new SystemParams(false, - debtParams, - _getValidLiquidationParams(), - _getValidGasCompParams(), - _getValidCollateralParams(), - _getValidInterestParams(), - _getValidRedemptionParams(), - _getValidPoolParams() - ); - } - // ========== LIQUIDATION VALIDATION TESTS ========== function testConstructorRevertsWhenSPPenaltyTooLow() public { From fdd14e0e1bf297b62b280fe5b61f6299fbe2d9e3 Mon Sep 17 00:00:00 2001 From: baroooo Date: Thu, 20 Nov 2025 09:02:06 +0100 Subject: [PATCH 75/79] test: fix tests --- contracts/test/systemParams.t.sol | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/contracts/test/systemParams.t.sol b/contracts/test/systemParams.t.sol index 347505dff..326460d40 100644 --- a/contracts/test/systemParams.t.sol +++ b/contracts/test/systemParams.t.sol @@ -595,7 +595,27 @@ contract SystemParamsTest is DevTestSetup { function testConstructorRevertsWhenMinBoldInSPLessThan1e18() public { ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ spYieldSplit: 75 * _1pct, - minBoldInSP: 1e18 - 1 // < 1e18 + minBoldInSP: 1e18 - 1, // < 1e18 + minBoldAfterRebalance: 1_000e18 + }); + + vm.expectRevert(ISystemParams.InvalidMinBoldInSP.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + poolParams + ); + } + + function testConstructorRevertsWhenMinBoldAfterRebalanceLessThanMinBoldInSP() public { + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ + spYieldSplit: 75 * _1pct, + minBoldInSP: 1e18, + minBoldAfterRebalance: 1e18 - 1 // < 1e18 }); vm.expectRevert(ISystemParams.InvalidMinBoldInSP.selector); From 3512e63b4ed6eff7269d8c300ad7dfa1f546ea42 Mon Sep 17 00:00:00 2001 From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:07:53 +0100 Subject: [PATCH 76/79] feat: add redemption with fixed fee --- contracts/script/DeployLiquity2.s.sol | 2 +- contracts/src/CollateralRegistry.sol | 46 +++++++- .../src/Interfaces/ICollateralRegistry.sol | 3 +- contracts/src/Interfaces/IStabilityPool.sol | 3 +- .../CollateralRegistryTester.sol | 4 +- contracts/test/TestContracts/Deployment.t.sol | 2 +- contracts/test/rebalancingRedemptions.t.sol | 108 ++++++++++++++++++ 7 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 contracts/test/rebalancingRedemptions.t.sol diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 0ec4db4c6..e88e0fba6 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -197,7 +197,7 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { troveManagers[0] = ITroveManager(troveManagerAddress); r.collateralRegistry = - new CollateralRegistry(IBoldToken(address(r.stableToken)), collaterals, troveManagers, r.systemParams); + new CollateralRegistry(IBoldToken(address(r.stableToken)), collaterals, troveManagers, r.systemParams, makeAddr("liquidityStrategy")); r.hintHelpers = new HintHelpers(r.collateralRegistry, r.systemParams); r.multiTroveGetter = new MultiTroveGetter(r.collateralRegistry); diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index f9537feec..67eee28c4 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -43,17 +43,21 @@ contract CollateralRegistry is ICollateralRegistry { uint256 public baseRate; + address public immutable liquidityStrategy; + // The timestamp of the latest fee operation (redemption or new Bold issuance) uint256 public lastFeeOperationTime = block.timestamp; event BaseRateUpdated(uint256 _baseRate); event LastFeeOpTimeUpdated(uint256 _lastFeeOpTime); + event LiquidityStrategyUpdated(address indexed _liquidityStrategy); constructor( IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers, - ISystemParams _systemParams + ISystemParams _systemParams, + address _liquidityStrategy ) { uint256 numTokens = _tokens.length; require(numTokens > 0, "Collateral list cannot be empty"); @@ -88,6 +92,10 @@ contract CollateralRegistry is ICollateralRegistry { // Initialize the baseRate state variable baseRate = _systemParams.INITIAL_BASE_RATE(); emit BaseRateUpdated(baseRate); + + // Initialize the liquidityStrategy state variable + liquidityStrategy = _liquidityStrategy; + emit LiquidityStrategyUpdated(liquidityStrategy); } struct RedemptionTotals { @@ -97,6 +105,34 @@ contract CollateralRegistry is ICollateralRegistry { uint256 redeemedAmount; } + /** + * @notice Redeems debt tokens with a fixed fee for the trove owner + * @dev This function is used during the rebalancing of a CDP pool and can only be called by the liquidity strategy + * @param _boldAmount The amount of bold to redeem + * @param _maxIterationsPerCollateral The maximum number of iterations per collateral + * @param _troveOwnerFee The fee to pay to the trove owner + */ + function redeemCollateralRebalancing(uint256 _boldAmount, uint256 _maxIterationsPerCollateral, uint256 _troveOwnerFee) external { + _requireCallerIsLiquidityStrategy(); + _requireAmountGreaterThanZero(_boldAmount); + _requireValidTroveOwnerFee(_troveOwnerFee); + require(totalCollaterals == 1, "CollateralRegistry: Only one collateral supported for rebalancing"); + + ITroveManager troveManager = getTroveManager(0); + (, uint256 price, bool redeemable) = + troveManager.getUnbackedPortionPriceAndRedeemability(); + require(redeemable, "CollateralRegistry: Collateral is not redeemable"); + uint256 redeemedAmount = troveManager.redeemCollateral( + msg.sender, + _boldAmount, + price, + _troveOwnerFee, + _maxIterationsPerCollateral + ); + require(redeemedAmount == _boldAmount, "CollateralRegistry: Redeemed amount does not match requested amount"); + boldToken.burn(msg.sender, redeemedAmount); + } + function redeemCollateral(uint256 _boldAmount, uint256 _maxIterationsPerCollateral, uint256 _maxFeePercentage) external { @@ -321,4 +357,12 @@ contract CollateralRegistry is ICollateralRegistry { function _requireAmountGreaterThanZero(uint256 _amount) internal pure { require(_amount > 0, "CollateralRegistry: Amount must be greater than zero"); } + + function _requireCallerIsLiquidityStrategy() internal view { + require(msg.sender == address(liquidityStrategy), "CollateralRegistry: Caller is not LiquidityStrategy"); + } + + function _requireValidTroveOwnerFee(uint256 _troveOwnerFee) internal pure { + require(_troveOwnerFee <= DECIMAL_PRECISION, "CollateralRegistry: Trove owner fee must be between 0% and 100%"); + } } diff --git a/contracts/src/Interfaces/ICollateralRegistry.sol b/contracts/src/Interfaces/ICollateralRegistry.sol index 418db3f78..e75f8d8ef 100644 --- a/contracts/src/Interfaces/ICollateralRegistry.sol +++ b/contracts/src/Interfaces/ICollateralRegistry.sol @@ -9,7 +9,8 @@ import "./ITroveManager.sol"; interface ICollateralRegistry { function baseRate() external view returns (uint256); function lastFeeOperationTime() external view returns (uint256); - + function liquidityStrategy() external view returns (address); + function redeemCollateralRebalancing(uint256 _boldamount, uint256 _maxIterationsPerCollateral, uint256 _troveOwnerFee) external; function redeemCollateral(uint256 _boldamount, uint256 _maxIterations, uint256 _maxFeePercentage) external; // getters function totalCollaterals() external view returns (uint256); diff --git a/contracts/src/Interfaces/IStabilityPool.sol b/contracts/src/Interfaces/IStabilityPool.sol index 953411d5d..16ced277c 100644 --- a/contracts/src/Interfaces/IStabilityPool.sol +++ b/contracts/src/Interfaces/IStabilityPool.sol @@ -35,7 +35,8 @@ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { function boldToken() external view returns (IBoldToken); function troveManager() external view returns (ITroveManager); - + function systemParams() external view returns (ISystemParams); + /* provideToSP(): * - Calculates depositor's Coll gain * - Calculates the compounded deposit diff --git a/contracts/test/TestContracts/CollateralRegistryTester.sol b/contracts/test/TestContracts/CollateralRegistryTester.sol index 3f4a92918..1101abffd 100644 --- a/contracts/test/TestContracts/CollateralRegistryTester.sol +++ b/contracts/test/TestContracts/CollateralRegistryTester.sol @@ -9,8 +9,8 @@ import "src/Interfaces/ISystemParams.sol"; for testing the parent's internal functions. */ contract CollateralRegistryTester is CollateralRegistry { - constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers, ISystemParams _systemParams) - CollateralRegistry(_boldToken, _tokens, _troveManagers, _systemParams) + constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers, ISystemParams _systemParams, address _liquidityStrategy) + CollateralRegistry(_boldToken, _tokens, _troveManagers, _systemParams, _liquidityStrategy) {} function unprotectedDecayBaseRateFromBorrowing() external returns (uint256) { diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index 9f1969cd6..e4ccc4841 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -274,7 +274,7 @@ contract TestDeployer is MetadataDeployment { } collateralRegistry = - new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers, systemParamsArray[0]); + new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers, systemParamsArray[0], makeAddr("liquidityStrategy")); hintHelpers = new HintHelpers(collateralRegistry, systemParamsArray[0]); multiTroveGetter = new MultiTroveGetter(collateralRegistry); diff --git a/contracts/test/rebalancingRedemptions.t.sol b/contracts/test/rebalancingRedemptions.t.sol new file mode 100644 index 000000000..eabe8d2d7 --- /dev/null +++ b/contracts/test/rebalancingRedemptions.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "./TestContracts/DevTestSetup.sol"; + +contract RebalancingRedemptions is DevTestSetup { + using stdStorage for StdStorage; + + function test_redeemCollateralRebalancing_whenCallerIsNotLiquidityStrategy_shouldRevert() public { + vm.expectRevert("CollateralRegistry: Caller is not LiquidityStrategy"); + collateralRegistry.redeemCollateralRebalancing(100, 10, 1e18); + } + + function test_redeemCollateralRebalancing_whenAmountIsZero_shouldRevert() public { + vm.startPrank(collateralRegistry.liquidityStrategy()); + vm.expectRevert("CollateralRegistry: Amount must be greater than zero"); + collateralRegistry.redeemCollateralRebalancing(0, 10, 1e18); + } + + function test_redeemCollateralRebalancing_whenTroveOwnerFeeIsGreaterThan100_shouldRevert() public { + vm.startPrank(collateralRegistry.liquidityStrategy()); + vm.expectRevert("CollateralRegistry: Trove owner fee must be between 0% and 100%"); + collateralRegistry.redeemCollateralRebalancing(100, 10, 1e18 + 1); + } + + function test_redeemCollateralRebalancing_whenCallerIsLiquidityStrategy_shouldRedeemAmountCorrectly() public { + (,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest(); + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B); + uint256 debt_C = troveManager.getTroveEntireDebt(troveIDs.C); + + uint256 coll_A = troveManager.getTroveEntireColl(troveIDs.A); + uint256 coll_B = troveManager.getTroveEntireColl(troveIDs.B); + uint256 coll_C = troveManager.getTroveEntireColl(troveIDs.C); + + uint256 debtToRedeem = debt_A + debt_B + debt_C/2; + + deal(address(boldToken), address(collateralRegistry.liquidityStrategy()), debtToRedeem); + + vm.startPrank(collateralRegistry.liquidityStrategy()); + // redemption fee is 50 bps scaled to 1e18 + collateralRegistry.redeemCollateralRebalancing(debtToRedeem, 10, 50 * 1e12); + + assertEq(troveManager.getTroveEntireDebt(troveIDs.A), 0); + assertEq(troveManager.getTroveEntireDebt(troveIDs.B), 0); + // plus 1 because of rounding down when calculating the debt to redeem + assertEq(troveManager.getTroveEntireDebt(troveIDs.C), debt_C/2 + 1 ); + } + + function test_redeemCollateralRebalancing_whenCallerIsLiquidityStrategy_shouldLeaveCorrectFeeInTroves() public { + (,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest(); + uint256 price = priceFeed.getPrice(); + + // redemption fee is 50 bps scaled to 1e18 + uint256 fee = 50 * 1e12; + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B); + uint256 debt_C = troveManager.getTroveEntireDebt(troveIDs.C); + + uint256 coll_A = troveManager.getTroveEntireColl(troveIDs.A); + uint256 coll_B = troveManager.getTroveEntireColl(troveIDs.B); + uint256 coll_C = troveManager.getTroveEntireColl(troveIDs.C); + + uint256 debtToRedeem = debt_A + debt_B + debt_C/2; + + uint256 expectedColl_A_After = calculateCorrespondingCollAfterRedemption(debt_A, coll_A, price, 50 * 1e12); + uint256 expectedColl_B_After = calculateCorrespondingCollAfterRedemption(debt_B, coll_B, price, 50 * 1e12); + uint256 expectedColl_C_After = calculateCorrespondingCollAfterRedemption(debt_C/2, coll_C, price, 50 * 1e12); + + deal(address(boldToken), address(collateralRegistry.liquidityStrategy()), debtToRedeem); + + vm.startPrank(collateralRegistry.liquidityStrategy()); + collateralRegistry.redeemCollateralRebalancing(debtToRedeem, 10, fee); + + + assertEq(troveManager.getTroveEntireColl(troveIDs.A), expectedColl_A_After); + assertEq(troveManager.getTroveEntireColl(troveIDs.B), expectedColl_B_After); + assertEq(troveManager.getTroveEntireColl(troveIDs.C), expectedColl_C_After); + } + + function test_redeemCollateralRebalancing_whenCallerIsLiquidityStrategyAndCollateralIsNotRedeemable_shouldRevert() public { + (,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest(); + uint256 price = priceFeed.getPrice(); + + priceFeed.setPrice((price * 5e17)/1e18); // 50% below the initial price + + vm.startPrank(collateralRegistry.liquidityStrategy()); + vm.expectRevert("CollateralRegistry: Collateral is not redeemable"); + collateralRegistry.redeemCollateralRebalancing(100, 10, 50 * 1e12); + } + + function test_redeemCollateralRebalancing_whenCallerIsLiquidityStrategyAndFullAmountIsNotRedeemed_shouldRevert() public { + (,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest(); + + uint256 totalDebtSupply = boldToken.totalSupply(); + + vm.startPrank(collateralRegistry.liquidityStrategy()); + vm.expectRevert("CollateralRegistry: Redeemed amount does not match requested amount"); + collateralRegistry.redeemCollateralRebalancing(totalDebtSupply + 1, 10, 50 * 1e12); + } + + function calculateCorrespondingCollAfterRedemption(uint256 debtRedeemed, uint256 collInitial, uint256 price, uint256 fee) public pure returns (uint256 collateralAfter) { + uint256 correspondingColl = debtRedeemed * DECIMAL_PRECISION / price; + uint256 correspondingCollFee = (debtRedeemed * fee * DECIMAL_PRECISION) / (1e18 * price); + return collInitial - correspondingColl + correspondingCollFee; + } +} From 7a8b810d37bc4c0335ca24b4f7827c281df43b86 Mon Sep 17 00:00:00 2001 From: philbow61 <80156619+philbow61@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:24:00 +0100 Subject: [PATCH 77/79] feat: add invertRateFeed flag to fxPriceFeed (#32) --- contracts/src/PriceFeeds/FXPriceFeed.sol | 38 ++++++++++++++-- contracts/test/FXPriceFeed.t.sol | 56 ++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/contracts/src/PriceFeeds/FXPriceFeed.sol b/contracts/src/PriceFeeds/FXPriceFeed.sol index af615a8ae..7d8eb538e 100644 --- a/contracts/src/PriceFeeds/FXPriceFeed.sol +++ b/contracts/src/PriceFeeds/FXPriceFeed.sol @@ -35,6 +35,9 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { /// @notice Whether the contract has been shutdown due to an oracle failure bool public isShutdown; + // @notice Whether the rate from the OracleAdapter should be inverted + bool public invertRateFeed; + /// @notice Thrown when the attempting to shutdown the contract when it is already shutdown error IsShutDown(); /// @notice Thrown when a non-watchdog address attempts to shutdown the contract @@ -47,6 +50,11 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { /// @param _newRateFeedID The new rate feed ID event RateFeedIDUpdated(address indexed _oldRateFeedID, address indexed _newRateFeedID); + /// @notice Emitted when the invert rate feed flag is updated + /// @param _oldInvertRateFeed The previous invert rate feed flag + /// @param _newInvertRateFeed The new invert rate feed flag + event InvertRateFeedUpdated(bool _oldInvertRateFeed, bool _newInvertRateFeed); + /// @notice Emitted when the watchdog address is updated /// @param _oldWatchdogAddress The previous watchdog address /// @param _newWatchdogAddress The new watchdog address @@ -69,6 +77,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { * @notice Initializes the FXPriceFeed contract * @param _oracleAdapterAddress The address of the OracleAdapter contract * @param _rateFeedID The address of the rate feed ID + * @param _invertRateFeed Whether the rate from the OracleAdapter should be inverted * @param _borrowerOperationsAddress The address of the BorrowerOperations contract * @param _watchdogAddress The address of the watchdog contract * @param _initialOwner The address of the initial owner @@ -76,6 +85,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { function initialize( address _oracleAdapterAddress, address _rateFeedID, + bool _invertRateFeed, address _borrowerOperationsAddress, address _watchdogAddress, address _initialOwner @@ -88,6 +98,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { oracleAdapter = IOracleAdapter(_oracleAdapterAddress); rateFeedID = _rateFeedID; + invertRateFeed = _invertRateFeed; borrowerOperations = IBorrowerOperations(_borrowerOperationsAddress); watchdogAddress = _watchdogAddress; @@ -105,6 +116,17 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { emit RateFeedIDUpdated(oldRateFeedID, _newRateFeedID); } + /** + * @notice Sets the invert rate feed flag + * @param _invertRateFeed Whether the rate from the OracleAdapter should be inverted + */ + function setInvertRateFeed(bool _invertRateFeed) external onlyOwner { + bool oldInvertRateFeed = invertRateFeed; + invertRateFeed = _invertRateFeed; + + emit InvertRateFeedUpdated(oldInvertRateFeed, _invertRateFeed); + } + /** * @notice Sets the watchdog address * @param _newWatchdogAddress The address of the new watchdog contract @@ -121,15 +143,23 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { /** * @notice Fetches the price of the FX rate, if valid * @dev If the contract is shutdown due to oracle failure, the last valid price is returned - * @return The price of the FX rate + * @return price The price of the FX rate */ - function fetchPrice() public returns (uint256) { + function fetchPrice() public returns (uint256 price) { if (isShutdown) { return lastValidPrice; } - // Denominator is always 1e18, so we only use the numerator as the price - (uint256 price,) = oracleAdapter.getFXRateIfValid(rateFeedID); + (uint256 numerator, uint256 denominator) = oracleAdapter.getFXRateIfValid(rateFeedID); + + if (invertRateFeed) { + // Multiply by 1e18 to get the price in 18 decimals + price = (denominator * 1e18) / numerator; + } else { + // Denominator is always 1e18, so we only use the numerator as the price + assert(denominator == 1e18); + price = numerator; + } lastValidPrice = price; diff --git a/contracts/test/FXPriceFeed.t.sol b/contracts/test/FXPriceFeed.t.sol index e3b0a0a11..0d9f95109 100644 --- a/contracts/test/FXPriceFeed.t.sol +++ b/contracts/test/FXPriceFeed.t.sol @@ -36,6 +36,7 @@ contract MockOracleAdapter { contract FXPriceFeedTest is Test { event WatchdogAddressUpdated(address indexed _oldWatchdogAddress, address indexed _newWatchdogAddress); + event InvertRateFeedUpdated(bool _oldInvertRateFeed, bool _newInvertRateFeed); event FXPriceFeedShutdown(); FXPriceFeed public fxPriceFeed; @@ -54,6 +55,7 @@ contract FXPriceFeedTest is Test { fxPriceFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), watchdog, owner @@ -78,6 +80,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), watchdog, owner @@ -91,6 +94,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(0), rateFeedID, + false, address(mockBorrowerOperations), watchdog, owner @@ -104,6 +108,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), address(0), + false, address(mockBorrowerOperations), watchdog, owner @@ -117,6 +122,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(0), watchdog, owner @@ -130,6 +136,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), address(0), owner @@ -143,6 +150,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), watchdog, address(0) @@ -157,6 +165,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), watchdog, owner @@ -176,6 +185,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), watchdog, owner @@ -185,6 +195,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), watchdog, owner @@ -218,6 +229,34 @@ contract FXPriceFeedTest is Test { assertEq(fxPriceFeed.rateFeedID(), newRateFeedID); } + function test_setInvertRateFeed_whenCalledByNonOwner_shouldRevert() initialized public { + address notOwner = makeAddr("notOwner"); + bool newInvertRateFeed = true; + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + fxPriceFeed.setInvertRateFeed(newInvertRateFeed); + vm.stopPrank(); + } + + function test_setInvertRateFeed_whenCalledByOwner_shouldSucceed() initialized public { + vm.startPrank(owner); + vm.expectEmit(); + emit InvertRateFeedUpdated(false, true); + fxPriceFeed.setInvertRateFeed(true); + vm.stopPrank(); + + assertEq(fxPriceFeed.invertRateFeed(), true); + + vm.startPrank(owner); + vm.expectEmit(); + emit InvertRateFeedUpdated(true, false); + fxPriceFeed.setInvertRateFeed(false); + vm.stopPrank(); + + assertEq(fxPriceFeed.invertRateFeed(), false); + } + function test_setWatchdogAddress_whenCalledByNonOwner_shouldRevert() initialized public { address notOwner = makeAddr("notOwner"); address newWatchdog = makeAddr("newWatchdog"); @@ -270,6 +309,23 @@ contract FXPriceFeedTest is Test { assertEq(fxPriceFeed.lastValidPrice(), initialPrice); } + function test_fetchPrice_whenInvertRateFeedIsTrue_shouldReturnInvertedPrice() initialized public { + vm.startPrank(owner); + fxPriceFeed.setInvertRateFeed(true); + vm.stopPrank(); + + uint256 price = fxPriceFeed.fetchPrice(); + + assertEq(price, (mockRateDenominator * 1e18) / mockRateNumerator); + assertEq(fxPriceFeed.lastValidPrice(), (mockRateDenominator * 1e18) / mockRateNumerator); + + uint256 XOFUSDRateNumerator = 1771165426850867; // 0.001771 USD = ~1 XOF + mockOracleAdapter.setFXRate(XOFUSDRateNumerator, 1e18); + + assertEq(fxPriceFeed.fetchPrice(), 564600000000000277670); // 1 USD = ~564 XOF + assertEq(fxPriceFeed.lastValidPrice(), 564600000000000277670); + } + function test_shutdown_whenCalledByNonWatchdog_shouldRevert() initialized public { address notWatchdog = makeAddr("notWatchdog"); From c3041c339c790f7fd26ee6d1c6a0bc1971dc62ac Mon Sep 17 00:00:00 2001 From: nvtaveras <4562733+nvtaveras@users.noreply.github.com> Date: Sun, 18 Jan 2026 17:37:19 -0400 Subject: [PATCH 78/79] fix: disable liquidations when the L2 sequencer is down (#33) --- contracts/src/Interfaces/IOracleAdapter.sol | 7 ++ contracts/src/Interfaces/IPriceFeed.sol | 1 + contracts/src/PriceFeeds/FXPriceFeed.sol | 72 +++++++++++-- contracts/src/TroveManager.sol | 8 ++ contracts/test/FXPriceFeed.t.sol | 100 +++++++++++++++++- .../Interfaces/IMockFXPriceFeed.sol | 1 + .../test/TestContracts/MockFXPriceFeed.sol | 9 ++ contracts/test/troveManager.t.sol | 15 +++ 8 files changed, 204 insertions(+), 9 deletions(-) diff --git a/contracts/src/Interfaces/IOracleAdapter.sol b/contracts/src/Interfaces/IOracleAdapter.sol index 20fab15d4..083d33ca4 100644 --- a/contracts/src/Interfaces/IOracleAdapter.sol +++ b/contracts/src/Interfaces/IOracleAdapter.sol @@ -16,4 +16,11 @@ interface IOracleAdapter { * @return denominator The denominator of the rate */ function getFXRateIfValid(address rateFeedID) external view returns (uint256 numerator, uint256 denominator); + + /** + * @notice Returns true if the L2 sequencer has been up and operational for at least the specified duration. + * @param since The minimum number of seconds the L2 sequencer must have been up (e.g., 1 hours = 3600). + * @return up True if the sequencer has been up for at least `since` seconds, false otherwise + */ + function isL2SequencerUp(uint256 since) external view returns (bool up); } diff --git a/contracts/src/Interfaces/IPriceFeed.sol b/contracts/src/Interfaces/IPriceFeed.sol index c9bb33a1a..63c813fd8 100644 --- a/contracts/src/Interfaces/IPriceFeed.sol +++ b/contracts/src/Interfaces/IPriceFeed.sol @@ -4,4 +4,5 @@ pragma solidity ^0.8.0; interface IPriceFeed { function fetchPrice() external returns (uint256); + function isL2SequencerUp() external view returns (bool); } diff --git a/contracts/src/PriceFeeds/FXPriceFeed.sol b/contracts/src/PriceFeeds/FXPriceFeed.sol index 7d8eb538e..44747c521 100644 --- a/contracts/src/PriceFeeds/FXPriceFeed.sol +++ b/contracts/src/PriceFeeds/FXPriceFeed.sol @@ -23,6 +23,12 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { /// @notice The identifier address for the specific rate feed to query address public rateFeedID; + // @notice Whether the rate from the OracleAdapter should be inverted + bool public invertRateFeed; + + /// @notice The grace period for the L2 sequencer to recover from failure + uint256 public l2SequencerGracePeriod; + /// @notice The watchdog contract address authorized to trigger emergency shutdown address public watchdogAddress; @@ -35,15 +41,20 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { /// @notice Whether the contract has been shutdown due to an oracle failure bool public isShutdown; - // @notice Whether the rate from the OracleAdapter should be inverted - bool public invertRateFeed; + /// @notice Thrown when the attempting to shutdown an already shutdown contract + error AlreadyShutdown(); - /// @notice Thrown when the attempting to shutdown the contract when it is already shutdown - error IsShutDown(); /// @notice Thrown when a non-watchdog address attempts to shutdown the contract - error CallerNotWatchdog(); + error OnlyWatchdog(); /// @notice Thrown when a zero address is provided as a parameter error ZeroAddress(); + /// @notice Thrown when an invalid grace period is provided + error InvalidL2SequencerGracePeriod(); + + /// @notice Emitted when the OracleAdapter contract is updated + /// @param _oldOracleAdapterAddress The previous OracleAdapter contract + /// @param _newOracleAdapterAddress The new OracleAdapter contract + event OracleAdapterUpdated(address indexed _oldOracleAdapterAddress, address indexed _newOracleAdapterAddress); /// @notice Emitted when the rate feed ID is updated /// @param _oldRateFeedID The previous rate feed ID @@ -55,6 +66,11 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { /// @param _newInvertRateFeed The new invert rate feed flag event InvertRateFeedUpdated(bool _oldInvertRateFeed, bool _newInvertRateFeed); + /// @notice Emitted when the L2 sequencer grace period is updated + /// @param _oldL2SequencerGracePeriod The previous L2 sequencer grace period + /// @param _newL2SequencerGracePeriod The new L2 sequencer grace period + event L2SequencerGracePeriodUpdated(uint256 indexed _oldL2SequencerGracePeriod, uint256 indexed _newL2SequencerGracePeriod); + /// @notice Emitted when the watchdog address is updated /// @param _oldWatchdogAddress The previous watchdog address /// @param _newWatchdogAddress The new watchdog address @@ -78,6 +94,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { * @param _oracleAdapterAddress The address of the OracleAdapter contract * @param _rateFeedID The address of the rate feed ID * @param _invertRateFeed Whether the rate from the OracleAdapter should be inverted + * @param _l2SequencerGracePeriod The grace period for the L2 sequencer to recover from failure * @param _borrowerOperationsAddress The address of the BorrowerOperations contract * @param _watchdogAddress The address of the watchdog contract * @param _initialOwner The address of the initial owner @@ -86,6 +103,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { address _oracleAdapterAddress, address _rateFeedID, bool _invertRateFeed, + uint256 _l2SequencerGracePeriod, address _borrowerOperationsAddress, address _watchdogAddress, address _initialOwner @@ -99,6 +117,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { oracleAdapter = IOracleAdapter(_oracleAdapterAddress); rateFeedID = _rateFeedID; invertRateFeed = _invertRateFeed; + l2SequencerGracePeriod = _l2SequencerGracePeriod; borrowerOperations = IBorrowerOperations(_borrowerOperationsAddress); watchdogAddress = _watchdogAddress; @@ -107,6 +126,24 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { _transferOwnership(_initialOwner); } + + /** + * @notice Sets the OracleAdapter contract + * @param _newOracleAdapterAddress The address of the new OracleAdapter contract + */ + function setOracleAdapter(address _newOracleAdapterAddress) external onlyOwner { + if (_newOracleAdapterAddress == address(0)) revert ZeroAddress(); + + address oldOracleAdapter = address(oracleAdapter); + oracleAdapter = IOracleAdapter(_newOracleAdapterAddress); + + emit OracleAdapterUpdated(oldOracleAdapter, _newOracleAdapterAddress); + } + + /** + * @notice Sets the rate feed ID to be queried + * @param _newRateFeedID The address of the new rate feed ID + */ function setRateFeedID(address _newRateFeedID) external onlyOwner { if (_newRateFeedID == address(0)) revert ZeroAddress(); @@ -127,6 +164,19 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { emit InvertRateFeedUpdated(oldInvertRateFeed, _invertRateFeed); } + /** + * @notice Sets the L2 sequencer grace period + * @param _newL2SequencerGracePeriod The new L2 sequencer grace period (in seconds) + */ + function setL2SequencerGracePeriod(uint256 _newL2SequencerGracePeriod) external onlyOwner { + if (_newL2SequencerGracePeriod == 0) revert InvalidL2SequencerGracePeriod(); + + uint256 oldL2SequencerGracePeriod = l2SequencerGracePeriod; + l2SequencerGracePeriod = _newL2SequencerGracePeriod; + + emit L2SequencerGracePeriodUpdated(oldL2SequencerGracePeriod, _newL2SequencerGracePeriod); + } + /** * @notice Sets the watchdog address * @param _newWatchdogAddress The address of the new watchdog contract @@ -140,6 +190,14 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { emit WatchdogAddressUpdated(oldWatchdogAddress, _newWatchdogAddress); } + /** + * @notice Checks if the L2 sequencer is up and the grace period has passed + * @return True if the L2 sequencer is up and the grace period has passed, false otherwise + */ + function isL2SequencerUp() public view returns (bool) { + return oracleAdapter.isL2SequencerUp(l2SequencerGracePeriod); + } + /** * @notice Fetches the price of the FX rate, if valid * @dev If the contract is shutdown due to oracle failure, the last valid price is returned @@ -175,8 +233,8 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { * - The shutdown state is permanent and cannot be reversed */ function shutdown() external { - if (isShutdown) revert IsShutDown(); - if (msg.sender != watchdogAddress) revert CallerNotWatchdog(); + if (isShutdown) revert AlreadyShutdown(); + if (msg.sender != watchdogAddress) revert OnlyWatchdog(); isShutdown = true; borrowerOperations.shutdownFromOracleFailure(); diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 7349299c0..a896cb4f9 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -163,6 +163,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { error NotEnoughBoldBalance(); error MinCollNotReached(uint256 _coll); error BatchSharesRatioTooHigh(); + error L2SequencerDown(); // --- Events --- @@ -402,6 +403,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { * Attempt to liquidate a custom list of troves provided by the caller. */ function batchLiquidateTroves(uint256[] memory _troveArray) public override { + _requireL2SequencerIsUp(); if (_troveArray.length == 0) { revert EmptyData(); } @@ -1198,6 +1200,12 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } } + function _requireL2SequencerIsUp() internal view { + if (!priceFeed.isL2SequencerUp()) { + revert L2SequencerDown(); + } + } + // --- Trove property getters --- function getUnbackedPortionPriceAndRedeemability() external returns (uint256, uint256, bool) { diff --git a/contracts/test/FXPriceFeed.t.sol b/contracts/test/FXPriceFeed.t.sol index 0d9f95109..0dd3c2802 100644 --- a/contracts/test/FXPriceFeed.t.sol +++ b/contracts/test/FXPriceFeed.t.sol @@ -21,6 +21,7 @@ contract MockBorrowerOperations { contract MockOracleAdapter { uint256 numerator; uint256 denominator; + bool public sequencerUp = true; function setFXRate(uint256 _numerator, uint256 _denominator) external { numerator = _numerator; @@ -30,6 +31,14 @@ contract MockOracleAdapter { function getFXRateIfValid(address) external view returns (uint256, uint256) { return (numerator, denominator); } + + function setIsL2SequencerUp(bool _isUp) external { + sequencerUp = _isUp; + } + + function isL2SequencerUp(uint256) external view returns (bool) { + return sequencerUp; + } } @@ -38,11 +47,15 @@ contract FXPriceFeedTest is Test { event WatchdogAddressUpdated(address indexed _oldWatchdogAddress, address indexed _newWatchdogAddress); event InvertRateFeedUpdated(bool _oldInvertRateFeed, bool _newInvertRateFeed); event FXPriceFeedShutdown(); + event OracleAdapterUpdated(address indexed _oldOracleAdapterAddress, address indexed _newOracleAdapterAddress); + event L2SequencerGracePeriodUpdated(uint256 indexed _oldL2SequencerGracePeriod, uint256 indexed _newL2SequencerGracePeriod); FXPriceFeed public fxPriceFeed; MockOracleAdapter public mockOracleAdapter; MockBorrowerOperations public mockBorrowerOperations; + MockFXPriceFeed public mockFXPriceFeed; + uint256 public l2SequencerGracePeriod = 6 hours; address public rateFeedID = makeAddr("rateFeedID"); address public watchdog = makeAddr("watchdog"); address public owner = makeAddr("owner"); @@ -56,6 +69,7 @@ contract FXPriceFeedTest is Test { address(mockOracleAdapter), rateFeedID, false, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -67,6 +81,7 @@ contract FXPriceFeedTest is Test { function setUp() public { mockOracleAdapter = new MockOracleAdapter(); mockOracleAdapter.setFXRate(mockRateNumerator, mockRateDenominator); + mockOracleAdapter.setIsL2SequencerUp(true); mockBorrowerOperations = new MockBorrowerOperations(); @@ -81,6 +96,7 @@ contract FXPriceFeedTest is Test { address(mockOracleAdapter), rateFeedID, false, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -95,6 +111,7 @@ contract FXPriceFeedTest is Test { address(0), rateFeedID, false, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -109,6 +126,7 @@ contract FXPriceFeedTest is Test { address(mockOracleAdapter), address(0), false, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -123,6 +141,7 @@ contract FXPriceFeedTest is Test { address(mockOracleAdapter), rateFeedID, false, + l2SequencerGracePeriod, address(0), watchdog, owner @@ -137,6 +156,7 @@ contract FXPriceFeedTest is Test { address(mockOracleAdapter), rateFeedID, false, + l2SequencerGracePeriod, address(mockBorrowerOperations), address(0), owner @@ -151,6 +171,7 @@ contract FXPriceFeedTest is Test { address(mockOracleAdapter), rateFeedID, false, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, address(0) @@ -166,6 +187,7 @@ contract FXPriceFeedTest is Test { address(mockOracleAdapter), rateFeedID, false, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -173,6 +195,7 @@ contract FXPriceFeedTest is Test { assertEq(address(newFeed.oracleAdapter()), address(mockOracleAdapter)); assertEq(newFeed.rateFeedID(), rateFeedID); + assertEq(newFeed.l2SequencerGracePeriod(), l2SequencerGracePeriod); assertEq(address(newFeed.borrowerOperations()), address(mockBorrowerOperations)); assertEq(newFeed.watchdogAddress(), watchdog); assertEq(newFeed.owner(), owner); @@ -186,6 +209,7 @@ contract FXPriceFeedTest is Test { address(mockOracleAdapter), rateFeedID, false, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -196,6 +220,7 @@ contract FXPriceFeedTest is Test { address(mockOracleAdapter), rateFeedID, false, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -330,7 +355,7 @@ contract FXPriceFeedTest is Test { address notWatchdog = makeAddr("notWatchdog"); vm.prank(notWatchdog); - vm.expectRevert(FXPriceFeed.CallerNotWatchdog.selector); + vm.expectRevert(FXPriceFeed.OnlyWatchdog.selector); fxPriceFeed.shutdown(); vm.stopPrank(); } @@ -352,8 +377,79 @@ contract FXPriceFeedTest is Test { function test_shutdown_whenAlreadyShutdown_shouldRevert() initialized public { vm.prank(watchdog); fxPriceFeed.shutdown(); - vm.expectRevert(FXPriceFeed.IsShutDown.selector); + vm.expectRevert(FXPriceFeed.AlreadyShutdown.selector); fxPriceFeed.shutdown(); vm.stopPrank(); } + + function test_setOracleAdapter_whenCalledByNonOwner_shouldRevert() initialized public { + address notOwner = makeAddr("notOwner"); + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + fxPriceFeed.setOracleAdapter(makeAddr("newOracleAdapter")); + vm.stopPrank(); + } + + function test_setOracleAdapter_whenNewAddressIsZero_shouldRevert() initialized public { + vm.prank(owner); + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + fxPriceFeed.setOracleAdapter(address(0)); + vm.stopPrank(); + } + + function test_setOracleAdapter_whenCalledByOwner_shouldSucceed() initialized public { + address newOracleAdapter = makeAddr("newOracleAdapter"); + + vm.prank(owner); + vm.expectEmit(); + emit OracleAdapterUpdated(address(mockOracleAdapter), newOracleAdapter); + fxPriceFeed.setOracleAdapter(newOracleAdapter); + vm.stopPrank(); + + assertEq(address(fxPriceFeed.oracleAdapter()), newOracleAdapter); + } + + function test_setL2SequencerGracePeriod_whenCalledByNonOwner_shouldRevert() initialized public { + address notOwner = makeAddr("notOwner"); + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + fxPriceFeed.setL2SequencerGracePeriod(12 hours); + vm.stopPrank(); + } + + function test_setL2SequencerGracePeriod_whenNewPeriodIsZero_shouldRevert() initialized public { + vm.prank(owner); + vm.expectRevert(FXPriceFeed.InvalidL2SequencerGracePeriod.selector); + fxPriceFeed.setL2SequencerGracePeriod(0); + vm.stopPrank(); + } + + function test_setL2SequencerGracePeriod_whenCalledByOwner_shouldSucceed() initialized public { + uint256 oldGracePeriod = fxPriceFeed.l2SequencerGracePeriod(); + uint256 newGracePeriod = 12 hours; + + vm.prank(owner); + vm.expectEmit(); + emit L2SequencerGracePeriodUpdated(oldGracePeriod, newGracePeriod); + fxPriceFeed.setL2SequencerGracePeriod(newGracePeriod); + vm.stopPrank(); + + assertEq(fxPriceFeed.l2SequencerGracePeriod(), newGracePeriod); + } + + function test_isL2SequencerUp_whenSequencerIsUp_shouldReturnTrue() initialized public { + mockOracleAdapter.setIsL2SequencerUp(true); + + bool result = fxPriceFeed.isL2SequencerUp(); + assertTrue(result); + } + + function test_isL2SequencerUp_whenSequencerIsDown_shouldReturnFalse() initialized public { + mockOracleAdapter.setIsL2SequencerUp(false); + + bool result = fxPriceFeed.isL2SequencerUp(); + assertFalse(result); + } } diff --git a/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol b/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol index f7e0be41e..d2a05e163 100644 --- a/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol +++ b/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol @@ -9,4 +9,5 @@ interface IMockFXPriceFeed is IPriceFeed { function setPrice(uint256 _price) external; function getPrice() external view returns (uint256); function setValidPrice(bool valid) external; + function setL2SequencerUp(bool up) external; } diff --git a/contracts/test/TestContracts/MockFXPriceFeed.sol b/contracts/test/TestContracts/MockFXPriceFeed.sol index 6d2d0e753..1c4b7a493 100644 --- a/contracts/test/TestContracts/MockFXPriceFeed.sol +++ b/contracts/test/TestContracts/MockFXPriceFeed.sol @@ -13,6 +13,7 @@ contract MockFXPriceFeed is IMockFXPriceFeed { string private _revertMsg = "MockFXPriceFeed: no valid price"; uint256 private _price = 200 * 1e18; bool private _hasValidPrice = true; + bool private _isL2SequencerUp = true; function getPrice() external view override returns (uint256) { return _price; @@ -26,12 +27,20 @@ contract MockFXPriceFeed is IMockFXPriceFeed { _price = price; } + function setL2SequencerUp(bool up) external { + _isL2SequencerUp = up; + } + function fetchPrice() external view override returns (uint256) { require(_hasValidPrice, _revertMsg); return _price; } + function isL2SequencerUp() external view override returns (bool) { + return _isL2SequencerUp; + } + function REVERT_MSG() external view override returns (string memory) { return _revertMsg; } diff --git a/contracts/test/troveManager.t.sol b/contracts/test/troveManager.t.sol index 621383f88..a63b80915 100644 --- a/contracts/test/troveManager.t.sol +++ b/contracts/test/troveManager.t.sol @@ -217,4 +217,19 @@ contract TroveManagerTest is DevTestSetup { liquidatedTroves[1] = troveIDs.B; troveManager.batchLiquidateTroves(liquidatedTroves); } + + function testLiquidationRevertsWhenL2SequencerIsDown() public { + priceFeed.setPrice(2000e18); + uint256 ATroveId = openTroveNoHints100pct(A, 100 ether, 100_000e18, 1e17); + uint256 BTroveId = openTroveNoHints100pct(B, 100 ether, 100_000e18, 1e17); + + + priceFeed.setPrice(1_000e18); + priceFeed.setL2SequencerUp(false); + + vm.startPrank(A); + vm.expectRevert(TroveManager.L2SequencerDown.selector); + troveManager.liquidate(ATroveId); + vm.stopPrank(); + } } From 418b48a2aba25e0ffb3f71f1ba9c619319ad8dce Mon Sep 17 00:00:00 2001 From: Mouradif Date: Sun, 18 Jan 2026 12:09:16 +0100 Subject: [PATCH 79/79] fix: initialize P in SP's initializer --- contracts/src/StabilityPool.sol | 1 + contracts/test/stabilityPoolUpgradeable.t.sol | 299 ++++++++++++++++++ 2 files changed, 300 insertions(+) create mode 100644 contracts/test/stabilityPoolUpgradeable.t.sol diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 3101f4e33..3bc9089c8 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -215,6 +215,7 @@ contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabi troveManager = _addressesRegistry.troveManager(); boldToken = _addressesRegistry.boldToken(); liquidityStrategy = _addressesRegistry.liquidityStrategy(); + P = P_PRECISION; emit TroveManagerAddressChanged(address(troveManager)); emit BoldTokenAddressChanged(address(boldToken)); diff --git a/contracts/test/stabilityPoolUpgradeable.t.sol b/contracts/test/stabilityPoolUpgradeable.t.sol new file mode 100644 index 000000000..ec613c652 --- /dev/null +++ b/contracts/test/stabilityPoolUpgradeable.t.sol @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {SPTest} from "./stabilityPool.t.sol"; +import {StabilityPool} from "../src/StabilityPool.sol"; +import {IStabilityPool} from "../src/Interfaces/IStabilityPool.sol"; +import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ProxyAdmin} from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; + +/* + * Tests for upgradeable StabilityPool with the P initialization fix. + */ +contract SPUpgradeableTest is SPTest { + bytes32 private constant IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + bytes32 private constant ADMIN_SLOT = + 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + address proxyAdmin; + + function setUp() public override { + super.setUp(); + + address SPAddress = address(stabilityPool); + + StabilityPool spImplementation = new StabilityPool(true, systemParams); + + proxyAdmin = address(new ProxyAdmin()); + + TransparentUpgradeableProxy tempProxy = new TransparentUpgradeableProxy( + address(spImplementation), + proxyAdmin, + "" + ); + + bytes memory proxyBytecode = address(tempProxy).code; + + vm.etch(SPAddress, proxyBytecode); + + for (uint256 i = 0; i < 70; i++) { + vm.store(SPAddress, bytes32(i), bytes32(0)); + } + + vm.store( + SPAddress, + IMPLEMENTATION_SLOT, + bytes32(uint256(uint160(address(spImplementation)))) + ); + vm.store(SPAddress, ADMIN_SLOT, bytes32(uint256(uint160(proxyAdmin)))); + + vm.store(SPAddress, bytes32(uint256(0)), bytes32(uint256(0))); + + IStabilityPool(SPAddress).initialize(addressesRegistry); + } + + function testPValue() public view { + assertEq(stabilityPool.P(), stabilityPool.P_PRECISION()); + assertEq(stabilityPool.P(), 1e36); + } + + function testGetDepositorBoldGain_2SPDepositor1LiqFreshDeposit_Upgraded_EarnFairShareOfSPYield() + public + { + ABCDEF memory troveIDs = _setupForSPDepositAdjustments(); + ABCDEF[2] memory expectedShareOfReward; + + vm.warp(block.timestamp + 90 days + 1); + + uint256 pendingAggInterest_0 = activePool.calcPendingAggInterest(); + uint256 expectedSpYield_0 = (SP_YIELD_SPLIT * pendingAggInterest_0) / + 1e18; + + expectedShareOfReward[0].A = getShareofSPReward(A, expectedSpYield_0); + expectedShareOfReward[0].B = getShareofSPReward(B, expectedSpYield_0); + uint256 totalSPDeposits_0 = stabilityPool.getTotalBoldDeposits(); + + makeSPWithdrawalAndClaim(A, 500e18); + + // Upgrade SP to similar impl + address spImplementation = address( + new StabilityPool(true, systemParams) + ); + vm.prank(proxyAdmin); + ITransparentUpgradeableProxy(address(stabilityPool)).upgradeTo( + spImplementation + ); + + // A liquidates D + liquidate(A, troveIDs.D); + // Check SP has only 1e18 BOLD now, and A and B have small remaining deposits + assertEq( + stabilityPool.getTotalBoldDeposits(), + 1e18, + "SP total bold deposits should be 1e18" + ); + assertLt( + stabilityPool.getCompoundedBoldDeposit(A), + 1e18, + "A should have <1e18 deposit" + ); + assertLt( + stabilityPool.getCompoundedBoldDeposit(B), + 1e18, + "B should have <1e18 deposit" + ); + assertLe( + stabilityPool.getCompoundedBoldDeposit(B) + + stabilityPool.getCompoundedBoldDeposit(A), + 1e18, + "A & B deposits should sum to <=1e18" + ); + + // C and D makes fresh deposit + uint256 deposit_C = 1e18; + uint256 deposit_D = 1e18; + makeSPDepositAndClaim(C, deposit_C); + transferBold(C, D, deposit_D); + makeSPDepositAndClaim(D, deposit_D); + + // Check SP still has funds + uint256 totalSPDeposits_1 = stabilityPool.getTotalBoldDeposits(); + assertGt(totalSPDeposits_1, 0); + assertLt(totalSPDeposits_1, totalSPDeposits_0); + + // fast-forward time again and accrue interest + vm.warp(block.timestamp + STALE_TROVE_DURATION + 1); + + uint256 pendingAggInterest_1 = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest_1, 0); + uint256 expectedSpYield_1 = (SP_YIELD_SPLIT * pendingAggInterest_1) / + 1e18; + + // Expected reward round 2 calculated with a different totalSPDeposits denominator. Expect A and B to earn a small share of + // this reward. + expectedShareOfReward[1].A = getShareofSPReward(A, expectedSpYield_1); + expectedShareOfReward[1].B = getShareofSPReward(B, expectedSpYield_1); + expectedShareOfReward[1].C = getShareofSPReward(C, expectedSpYield_1); + expectedShareOfReward[1].D = getShareofSPReward(D, expectedSpYield_1); + assertGt(expectedShareOfReward[1].A, 0); + assertGt(expectedShareOfReward[1].B, 0); + assertGt(expectedShareOfReward[1].C, 0); + assertGt(expectedShareOfReward[1].D, 0); + // A and B should get small rewards from reward 2, as their deposits were reduced to 1e18. + // Confirm A, B are smaller than C, D's rewards + assertLt(expectedShareOfReward[1].A, expectedShareOfReward[1].C); + assertLt(expectedShareOfReward[1].A, expectedShareOfReward[1].D); + assertLt(expectedShareOfReward[1].B, expectedShareOfReward[1].C); + assertLt(expectedShareOfReward[1].B, expectedShareOfReward[1].D); + + // Confirm the expected shares sum up to the total expected yield + assertApproximatelyEqual( + expectedShareOfReward[1].A + + expectedShareOfReward[1].B + + expectedShareOfReward[1].C + + expectedShareOfReward[1].D, + expectedSpYield_1, + 1e4, + "expected shares should sum up to the total expected yield" + ); + + // A trove gets poked again, interst minted and yield paid to SP + applyPendingDebt(B, troveIDs.A); + + // Expect A to receive only their share of 2nd reward, since they already claimed first + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(A), + expectedShareOfReward[1].A, + 1e4, + "A should receive only 2nd reward" + ); + // Expect B to receive their share of 1st and 2nd rewards + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(B), + expectedShareOfReward[0].B + expectedShareOfReward[1].B, + 1e4, + "B should receive only their share of 1st and 2nd" + ); + // Expect C to receive a share of only 2nd reward + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(C), + expectedShareOfReward[1].C, + 1e4, + "C should receive a share of both reward 1 and 2" + ); + } + + function testGetDepositorBoldGain_2SPDepositor1LiqScaleChangeFreshDeposit_Upgraded_EarnFairShareOfSPYield() + public + { + ABCDEF memory troveIDs = _setupForSPDepositAdjustmentsBigTroves(); + ABCDEF[3] memory expectedShareOfReward; + + vm.warp(block.timestamp + 90 days + 1); + + uint256 pendingAggInterest_0 = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest_0, 0); + uint256 expectedSpYield_0 = (SP_YIELD_SPLIT * pendingAggInterest_0) / + 1e18; + + expectedShareOfReward[0].A = getShareofSPReward(A, expectedSpYield_0); + expectedShareOfReward[0].B = getShareofSPReward(B, expectedSpYield_0); + assertGt(expectedShareOfReward[0].A, 0); + assertGt(expectedShareOfReward[0].B, 0); + uint256 totalSPDeposits_0 = stabilityPool.getTotalBoldDeposits(); + + // Confirm the expected shares sum up to the total expected yield + assertApproximatelyEqual( + expectedShareOfReward[0].A + expectedShareOfReward[0].B, + expectedSpYield_0, + 1e3 + ); + + // A withdraws some deposit so that D's liq will trigger a scale change. + // This also mints interest and pays the yield to the SP + uint256 debtSPDelta = totalSPDeposits_0 - + troveManager.getTroveEntireDebt(troveIDs.D); + makeSPWithdrawalAndClaim(A, debtSPDelta - 1e12); + assertEq(stabilityPool.getDepositorYieldGain(A), 0); + assertEq(activePool.calcPendingAggInterest(), 0); + + assertEq(stabilityPool.currentScale(), 0); + + // A liquidates D + liquidate(A, troveIDs.D); + + // Upgrade SP to similar impl + address spImplementation = address( + new StabilityPool(true, systemParams) + ); + vm.prank(proxyAdmin); + ITransparentUpgradeableProxy(address(stabilityPool)).upgradeTo( + spImplementation + ); + + // Check scale increased + assertEq(stabilityPool.currentScale(), 1); + + // C and D makes fresh deposit + uint256 deposit_C = 1e27; + uint256 deposit_D = 1e27; + makeSPDepositAndClaim(C, deposit_C); + transferBold(C, D, deposit_D); + makeSPDepositAndClaim(D, deposit_D); + + // fast-forward time again and accrue interest + vm.warp(block.timestamp + STALE_TROVE_DURATION + 1); + + uint256 pendingAggInterest_1 = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest_1, 0); + uint256 expectedSpYield_1 = (SP_YIELD_SPLIT * pendingAggInterest_1) / + 1e18; + + // Expected reward round 2 calculated with a different totalSPDeposits denominator. + expectedShareOfReward[1].A = getShareofSPReward(A, expectedSpYield_1); + expectedShareOfReward[1].B = getShareofSPReward(B, expectedSpYield_1); + expectedShareOfReward[1].C = getShareofSPReward(C, expectedSpYield_1); + expectedShareOfReward[1].D = getShareofSPReward(D, expectedSpYield_1); + + // Expect A, B, C and D to get reward 2 + assertGt(expectedShareOfReward[1].A, 0); + assertGt(expectedShareOfReward[1].B, 0); + assertGt(expectedShareOfReward[1].C, 0); + assertGt(expectedShareOfReward[1].D, 0); + // ... though A, B's share should be smaller than C, D's share + assertLt(expectedShareOfReward[1].A, expectedShareOfReward[1].C); + assertLt(expectedShareOfReward[1].A, expectedShareOfReward[1].D); + assertLt(expectedShareOfReward[1].B, expectedShareOfReward[1].C); + assertLt(expectedShareOfReward[1].B, expectedShareOfReward[1].D); + + // A trove gets poked again, interst minted and yield paid to SP + applyPendingDebt(B, troveIDs.A); + + // A only gets reward 2 since already claimed reward 1 + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(A), + expectedShareOfReward[1].A, + 1e15 + ); + + // B gets reward 1 + 2 + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(B), + expectedShareOfReward[0].B + expectedShareOfReward[1].B, + 1e14 + ); + // C, D get reward 2 + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(C), + expectedShareOfReward[1].C, + 1e14 + ); + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(D), + expectedShareOfReward[1].D, + 1e14 + ); + } +}