diff --git a/snapshots/Hub.Operations.json b/snapshots/Hub.Operations.json index 54b1b87c4..9e37b3cd9 100644 --- a/snapshots/Hub.Operations.json +++ b/snapshots/Hub.Operations.json @@ -9,7 +9,7 @@ "refreshPremium": "71999", "remove: full": "76993", "remove: partial": "81640", - "reportDeficit": "115225", + "reportDeficit": "115076", "restore: full": "80471", "restore: full - with transfer": "173377", "restore: partial": "89137", diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index 08ab45046..9210ff5b4 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -886,7 +886,6 @@ contract Hub is IHub, AccessManaged { uint256 premiumAmountRay ) internal view { require(spoke.active, SpokeNotActive()); - require(!spoke.paused, SpokePaused()); require(drawnAmount > 0 || premiumAmountRay > 0, InvalidAmount()); uint256 drawn = _getSpokeDrawn(asset, spoke); uint256 premiumRay = _getSpokePremiumRay(asset, spoke); diff --git a/tests/Base.t.sol b/tests/Base.t.sol index 424bc3683..d56fc8744 100644 --- a/tests/Base.t.sol +++ b/tests/Base.t.sol @@ -11,7 +11,10 @@ import {console2 as console} from 'forge-std/console2.sol'; // dependencies import {AggregatorV3Interface} from 'src/dependencies/chainlink/AggregatorV3Interface.sol'; -import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; +import { + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy +} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; import {IERC20Metadata} from 'src/dependencies/openzeppelin/IERC20Metadata.sol'; import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; import {IERC20Errors} from 'src/dependencies/openzeppelin/IERC20Errors.sol'; @@ -44,7 +47,11 @@ import {AccessManagerEnumerable} from 'src/access/AccessManagerEnumerable.sol'; import {HubConfigurator, IHubConfigurator} from 'src/hub/HubConfigurator.sol'; import {Hub, IHub, IHubBase} from 'src/hub/Hub.sol'; import {SharesMath} from 'src/hub/libraries/SharesMath.sol'; -import {AssetInterestRateStrategy, IAssetInterestRateStrategy, IBasicInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; +import { + AssetInterestRateStrategy, + IAssetInterestRateStrategy, + IBasicInterestRateStrategy +} from 'src/hub/AssetInterestRateStrategy.sol'; // spoke import {Spoke, ISpoke, ISpokeBase} from 'src/spoke/Spoke.sol'; @@ -967,7 +974,7 @@ abstract contract Base is Test { assertEq(hub.getAssetConfig(assetId), config); } - function updateReserveFrozenFlag( + function _updateReserveFrozenFlag( ISpoke spoke, uint256 reserveId, bool newFrozenFlag @@ -1097,7 +1104,7 @@ abstract contract Base is Test { assertEq(_getLatestDynamicReserveConfig(spoke, reserveId), config); } - function updateReserveBorrowableFlag( + function _updateReserveBorrowableFlag( ISpoke spoke, uint256 reserveId, bool newBorrowable @@ -1206,7 +1213,7 @@ abstract contract Base is Test { assertEq(hub.getSpokeConfig(assetId, spoke), spokeConfig); } - function updateSpokeActive( + function _updateSpokeActive( IHub hub, uint256 assetId, address spoke, diff --git a/tests/unit/Hub/Hub.Add.t.sol b/tests/unit/Hub/Hub.Add.t.sol index 0a462710b..383bd3ac8 100644 --- a/tests/unit/Hub/Hub.Add.t.sol +++ b/tests/unit/Hub/Hub.Add.t.sol @@ -76,7 +76,7 @@ contract HubAddTest is HubBase { } function test_add_revertsWith_SpokeNotActive() public { - updateSpokeActive(hub1, daiAssetId, address(spoke1), false); + _updateSpokeActive(hub1, daiAssetId, address(spoke1), false); vm.startPrank(address(spoke1)); tokenList.dai.transferFrom(alice, address(hub1), 100e18); diff --git a/tests/unit/Hub/Hub.Config.t.sol b/tests/unit/Hub/Hub.Config.t.sol index 8428a31c7..a0e890f9b 100644 --- a/tests/unit/Hub/Hub.Config.t.sol +++ b/tests/unit/Hub/Hub.Config.t.sol @@ -631,7 +631,7 @@ contract HubConfigTest is HubBase { _drawLiquidity(assetId, amount, true); skip(365 days); - updateSpokeActive(hub1, assetId, _getFeeReceiver(hub1, assetId), false); + _updateSpokeActive(hub1, assetId, _getFeeReceiver(hub1, assetId), false); IHub.AssetConfig memory config = hub1.getAssetConfig(assetId); config.feeReceiver = makeAddr('newFeeReceiver'); @@ -650,7 +650,7 @@ contract HubConfigTest is HubBase { Utils.mintFeeShares(hub1, assetId, ADMIN); - updateSpokeActive(hub1, assetId, _getFeeReceiver(hub1, assetId), false); + _updateSpokeActive(hub1, assetId, _getFeeReceiver(hub1, assetId), false); IHub.AssetConfig memory config = hub1.getAssetConfig(assetId); config.feeReceiver = makeAddr('newFeeReceiver'); diff --git a/tests/unit/Hub/Hub.Draw.t.sol b/tests/unit/Hub/Hub.Draw.t.sol index 3771bef6f..d7b18caf6 100644 --- a/tests/unit/Hub/Hub.Draw.t.sol +++ b/tests/unit/Hub/Hub.Draw.t.sol @@ -165,7 +165,7 @@ contract HubDrawTest is HubBase { } function test_draw_revertsWith_SpokeNotActive() public { - updateSpokeActive(hub1, daiAssetId, address(spoke1), false); + _updateSpokeActive(hub1, daiAssetId, address(spoke1), false); vm.expectRevert(IHub.SpokeNotActive.selector); vm.prank(address(spoke1)); hub1.draw(daiAssetId, 100e18, alice); diff --git a/tests/unit/Hub/Hub.EliminateDeficit.t.sol b/tests/unit/Hub/Hub.EliminateDeficit.t.sol index dfb253b6a..1e32d6990 100644 --- a/tests/unit/Hub/Hub.EliminateDeficit.t.sol +++ b/tests/unit/Hub/Hub.EliminateDeficit.t.sol @@ -60,7 +60,7 @@ contract HubEliminateDeficitTest is HubBase { _createDeficit(_assetId, _coveredSpoke, _deficitAmountRay); Utils.add(hub1, _assetId, _callerSpoke, _deficitAmountRay.fromRayUp() + 1, alice); - updateSpokeActive(hub1, _assetId, _callerSpoke, true); + _updateSpokeActive(hub1, _assetId, _callerSpoke, true); _updateSpokePaused(hub1, _assetId, _callerSpoke, true); vm.prank(_callerSpoke); diff --git a/tests/unit/Hub/Hub.MintFeeShares.t.sol b/tests/unit/Hub/Hub.MintFeeShares.t.sol index 581756ba3..2a8bdb49a 100644 --- a/tests/unit/Hub/Hub.MintFeeShares.t.sol +++ b/tests/unit/Hub/Hub.MintFeeShares.t.sol @@ -26,7 +26,7 @@ contract HubMintFeeSharesTest is HubBase { skipTime: 365 days }); - updateSpokeActive(hub1, daiAssetId, _getFeeReceiver(hub1, daiAssetId), false); + _updateSpokeActive(hub1, daiAssetId, _getFeeReceiver(hub1, daiAssetId), false); vm.expectRevert(IHub.SpokeNotActive.selector, address(hub1)); Utils.mintFeeShares(hub1, daiAssetId, ADMIN); } @@ -99,7 +99,7 @@ contract HubMintFeeSharesTest is HubBase { IHub.Asset memory asset = hub1.getAsset(daiAssetId); // pausing the fee receiver does not revert the action since no shares are minted - updateSpokeActive(hub1, daiAssetId, _getFeeReceiver(hub1, daiAssetId), false); + _updateSpokeActive(hub1, daiAssetId, _getFeeReceiver(hub1, daiAssetId), false); vm.expectEmit(address(hub1)); emit IHub.UpdateAsset(daiAssetId, asset.drawnIndex, asset.drawnRate, 0); diff --git a/tests/unit/Hub/Hub.PayFee.t.sol b/tests/unit/Hub/Hub.PayFee.t.sol index 2a286557f..bf9e93fab 100644 --- a/tests/unit/Hub/Hub.PayFee.t.sol +++ b/tests/unit/Hub/Hub.PayFee.t.sol @@ -12,7 +12,7 @@ contract HubPayFeeTest is HubBase { } function test_payFee_revertsWith_SpokeNotActive() public { - updateSpokeActive(hub1, daiAssetId, address(spoke1), false); + _updateSpokeActive(hub1, daiAssetId, address(spoke1), false); vm.expectRevert(IHub.SpokeNotActive.selector, address(hub1)); vm.prank(address(spoke1)); hub1.payFeeShares(daiAssetId, 1); diff --git a/tests/unit/Hub/Hub.RefreshPremium.t.sol b/tests/unit/Hub/Hub.RefreshPremium.t.sol index 1b9405b21..4f9345183 100644 --- a/tests/unit/Hub/Hub.RefreshPremium.t.sol +++ b/tests/unit/Hub/Hub.RefreshPremium.t.sol @@ -17,7 +17,7 @@ contract HubRefreshPremiumTest is HubBase { function test_refreshPremium_revertsWith_SpokeNotActive() public { IHubBase.PremiumDelta memory premiumDelta; - updateSpokeActive(hub1, daiAssetId, address(spoke1), false); + _updateSpokeActive(hub1, daiAssetId, address(spoke1), false); vm.expectRevert(IHub.SpokeNotActive.selector); vm.prank(address(spoke1)); hub1.refreshPremium(daiAssetId, premiumDelta); @@ -166,7 +166,7 @@ contract HubRefreshPremiumTest is HubBase { /// @dev paused but active spokes are allowed to refresh premium function test_refreshPremium_pausedSpokesAllowed() public { - updateSpokeActive(hub1, daiAssetId, address(spoke1), true); + _updateSpokeActive(hub1, daiAssetId, address(spoke1), true); _updateSpokePaused(hub1, daiAssetId, address(spoke1), true); vm.expectEmit(address(hub1)); diff --git a/tests/unit/Hub/Hub.Remove.t.sol b/tests/unit/Hub/Hub.Remove.t.sol index 328c76f46..f0f794f4c 100644 --- a/tests/unit/Hub/Hub.Remove.t.sol +++ b/tests/unit/Hub/Hub.Remove.t.sol @@ -515,7 +515,7 @@ contract HubRemoveTest is HubBase { } function test_remove_revertsWith_SpokeNotActive() public { - updateSpokeActive(hub1, daiAssetId, address(spoke1), false); + _updateSpokeActive(hub1, daiAssetId, address(spoke1), false); vm.expectRevert(IHub.SpokeNotActive.selector); vm.prank(address(spoke1)); hub1.remove(daiAssetId, 100e18, alice); diff --git a/tests/unit/Hub/Hub.ReportDeficit.t.sol b/tests/unit/Hub/Hub.ReportDeficit.t.sol index e4849e07d..1c8a91d32 100644 --- a/tests/unit/Hub/Hub.ReportDeficit.t.sol +++ b/tests/unit/Hub/Hub.ReportDeficit.t.sol @@ -55,7 +55,13 @@ contract HubReportDeficitTest is HubBase { drawnAmount = bound(drawnAmount, 1, MAX_SUPPLY_AMOUNT); // draw usdx liquidity to be restored - _drawLiquidity(usdxAssetId, drawnAmount, true, true, address(spoke1)); + _drawLiquidity({ + assetId: usdxAssetId, + amount: drawnAmount, + withPremium: true, + skipTime: true, + spoke: address(spoke1) + }); (uint256 drawn, uint256 premium) = hub1.getSpokeOwed(usdxAssetId, address(spoke1)); assertGt(drawn, 0); @@ -130,6 +136,24 @@ contract HubReportDeficitTest is HubBase { ); } + /// @dev paused spoke can still report deficit + function test_reportDeficit_paused() public { + // draw usdx liquidity to be restored + _drawLiquidity({ + assetId: usdxAssetId, + amount: 1, + withPremium: true, + skipTime: true, + spoke: address(spoke1) + }); + + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + + // even if spoke is paused, it can report deficit + vm.prank(address(spoke1)); + hub1.reportDeficit(usdxAssetId, 1, ZERO_PREMIUM_DELTA); + } + function test_reportDeficit_with_premium() public { uint256 drawnAmount = 10_000e6; test_reportDeficit_fuzz_with_premium({ diff --git a/tests/unit/Hub/Hub.SpokeConfig.t.sol b/tests/unit/Hub/Hub.SpokeConfig.t.sol new file mode 100644 index 000000000..def580a6b --- /dev/null +++ b/tests/unit/Hub/Hub.SpokeConfig.t.sol @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/unit/Hub/HubBase.t.sol'; + +contract HubSpokeConfigTest is HubBase { + function setUp() public override { + super.setUp(); + + // deploy borrowable liquidity + _addLiquidity(usdxAssetId, MAX_SUPPLY_AMOUNT); + } + + function test_mintFeeShares_active_paused_scenarios() public { + address feeReceiver = _getFeeReceiver(hub1, usdxAssetId); + + // set spoke to active / paused; reverts + _accrueLiquidityFees(hub1, spoke1, usdxAssetId); + _updateSpokePaused(hub1, usdxAssetId, feeReceiver, true); + _updateSpokeActive(hub1, usdxAssetId, feeReceiver, true); + + vm.prank(HUB_ADMIN); + hub1.mintFeeShares(usdxAssetId); + + // set spoke to inactive / paused; reverts + _accrueLiquidityFees(hub1, spoke1, usdxAssetId); + _updateSpokePaused(hub1, usdxAssetId, feeReceiver, true); + _updateSpokeActive(hub1, usdxAssetId, feeReceiver, false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(HUB_ADMIN); + hub1.mintFeeShares(usdxAssetId); + + // set spoke to active / not paused; succeeds + _accrueLiquidityFees(hub1, spoke1, usdxAssetId); + _updateSpokePaused(hub1, usdxAssetId, feeReceiver, false); + _updateSpokeActive(hub1, usdxAssetId, feeReceiver, true); + + vm.prank(HUB_ADMIN); + hub1.mintFeeShares(usdxAssetId); + + // set spoke to inactive / not paused; reverts + _accrueLiquidityFees(hub1, spoke1, usdxAssetId); + _updateSpokePaused(hub1, usdxAssetId, feeReceiver, false); + _updateSpokeActive(hub1, usdxAssetId, feeReceiver, false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(HUB_ADMIN); + hub1.mintFeeShares(usdxAssetId); + } + + function test_add_active_paused_scenarios() public { + // set spoke to active / paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + vm.expectRevert(IHub.SpokePaused.selector); + vm.prank(address(spoke1)); + hub1.add(usdxAssetId, 1); + + // set spoke to inactive / paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.add(usdxAssetId, 1); + + // set spoke to active / not paused; succeeds + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + Utils.add(hub1, usdxAssetId, address(spoke1), 1, alice); + + // set spoke to inactive / not paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.add(usdxAssetId, 1); + } + + function test_remove_active_paused_scenarios() public { + Utils.add(hub1, usdxAssetId, address(spoke1), 100, alice); + + // set spoke to active / paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + vm.expectRevert(IHub.SpokePaused.selector); + vm.prank(address(spoke1)); + hub1.remove(usdxAssetId, 1, alice); + + // set spoke to inactive / paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.remove(usdxAssetId, 1, alice); + + // set spoke to active / not paused; succeeds + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + Utils.remove(hub1, usdxAssetId, address(spoke1), 1, alice); + + // set spoke to inactive / not paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.remove(usdxAssetId, 1, alice); + } + + function test_draw_active_paused_scenarios() public { + // set spoke to active / paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + vm.expectRevert(IHub.SpokePaused.selector); + vm.prank(address(spoke1)); + hub1.draw(usdxAssetId, 1, alice); + + // set spoke to inactive / paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.draw(usdxAssetId, 1, alice); + + // set spoke to active / not paused; succeeds + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + Utils.draw(hub1, usdxAssetId, address(spoke1), alice, 1); + + // set spoke to inactive / not paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.draw(usdxAssetId, 1, alice); + } + + function test_restore_active_paused_scenarios() public { + Utils.draw(hub1, usdxAssetId, address(spoke1), alice, 100); + + // set spoke to active / paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + vm.expectRevert(IHub.SpokePaused.selector); + vm.prank(address(spoke1)); + hub1.restore(usdxAssetId, 1, ZERO_PREMIUM_DELTA); + + // set spoke to inactive / paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.restore(usdxAssetId, 1, ZERO_PREMIUM_DELTA); + + // set spoke to active / not paused; succeeds + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + Utils.restoreDrawn(hub1, usdxAssetId, address(spoke1), 1, alice); + + // set spoke to inactive / not paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.restore(usdxAssetId, 1, ZERO_PREMIUM_DELTA); + } + + function test_reportDeficit_active_paused_scenarios() public { + // draw usdx liquidity to be restored + _drawLiquidity({ + assetId: usdxAssetId, + amount: 1, + withPremium: true, + skipTime: true, + spoke: address(spoke1) + }); + + // set spoke to active / paused; succeeds + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + vm.prank(address(spoke1)); + hub1.reportDeficit(usdxAssetId, 1, ZERO_PREMIUM_DELTA); + + // set spoke to inactive and paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.reportDeficit(usdxAssetId, 1, ZERO_PREMIUM_DELTA); + + // set spoke to active and not paused; succeeds + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + vm.prank(address(spoke1)); + hub1.reportDeficit(usdxAssetId, 1, ZERO_PREMIUM_DELTA); + + // set spoke to inactive and not paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.reportDeficit(usdxAssetId, 1, ZERO_PREMIUM_DELTA); + } + + function test_eliminateDeficit_active_paused_scenarios() public { + address coveredSpoke = address(spoke1); + address callerSpoke = address(spoke2); + + // create reported deficit on spoke1 + _createReportedDeficit(hub1, coveredSpoke, usdxAssetId); + Utils.add(hub1, usdxAssetId, callerSpoke, 1e18, alice); + + // covered spoke status does not matter + _updateSpokePaused(hub1, usdxAssetId, coveredSpoke, true); + _updateSpokeActive(hub1, usdxAssetId, coveredSpoke, false); + + // set caller spoke to active / not paused; succeeds + _updateSpokePaused(hub1, usdxAssetId, callerSpoke, true); + _updateSpokeActive(hub1, usdxAssetId, callerSpoke, true); + + vm.prank(callerSpoke); + hub1.eliminateDeficit(usdxAssetId, 1, coveredSpoke); + + // set spoke to inactive / paused; succeeds + _updateSpokePaused(hub1, usdxAssetId, callerSpoke, true); + _updateSpokeActive(hub1, usdxAssetId, callerSpoke, false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(callerSpoke); + hub1.eliminateDeficit(usdxAssetId, 1, coveredSpoke); + + // set spoke to active / not paused; succeeds + _updateSpokePaused(hub1, usdxAssetId, callerSpoke, false); + _updateSpokeActive(hub1, usdxAssetId, callerSpoke, true); + + vm.prank(callerSpoke); + hub1.eliminateDeficit(usdxAssetId, 1, coveredSpoke); + + // set spoke to inactive / not paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.eliminateDeficit(usdxAssetId, 1, coveredSpoke); + } + + function test_refreshPremium_active_paused_scenarios() public { + // set spoke to active / paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + vm.prank(address(spoke1)); + hub1.refreshPremium(usdxAssetId, ZERO_PREMIUM_DELTA); + + // set spoke to inactive / paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.refreshPremium(usdxAssetId, ZERO_PREMIUM_DELTA); + + // set spoke to active / not paused; succeeds + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + vm.prank(address(spoke1)); + hub1.refreshPremium(usdxAssetId, ZERO_PREMIUM_DELTA); + + // set spoke to inactive / not paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.refreshPremium(usdxAssetId, ZERO_PREMIUM_DELTA); + } + + function test_payFeeShares_active_paused_scenarios() public { + address feeReceiver = _getFeeReceiver(hub1, usdxAssetId); + Utils.add(hub1, usdxAssetId, address(spoke1), 1e18, alice); + + // set fee receiver to inactive / paused; does not matter + _updateSpokePaused(hub1, usdxAssetId, feeReceiver, true); + _updateSpokeActive(hub1, usdxAssetId, feeReceiver, false); + + // set spoke to active / paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + vm.expectRevert(IHub.SpokePaused.selector); + vm.prank(address(spoke1)); + hub1.payFeeShares(usdxAssetId, 1); + + // set spoke to inactive / paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), true); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.payFeeShares(usdxAssetId, 1); + + // set spoke to active / not paused; succeeds + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), true); + + vm.prank(address(spoke1)); + hub1.payFeeShares(usdxAssetId, 1); + + // set spoke to inactive / not paused; reverts + _updateSpokePaused(hub1, usdxAssetId, address(spoke1), false); + _updateSpokeActive(hub1, usdxAssetId, address(spoke1), false); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(address(spoke1)); + hub1.payFeeShares(usdxAssetId, 1); + } + + function test_transferShares_fuzz_active_paused_scenarios( + bool senderPaused, + bool receiverPaused, + bool senderActive, + bool receiverActive + ) public { + address sender = address(spoke1); + address receiver = address(spoke2); + Utils.add(hub1, usdxAssetId, sender, 1e18, alice); + + // set sender + _updateSpokePaused(hub1, usdxAssetId, sender, senderPaused); + _updateSpokeActive(hub1, usdxAssetId, sender, senderActive); + // set receiver + _updateSpokePaused(hub1, usdxAssetId, receiver, receiverPaused); + _updateSpokeActive(hub1, usdxAssetId, receiver, receiverActive); + + if (!senderActive || !receiverActive) { + vm.expectRevert(IHub.SpokeNotActive.selector); + } else if (senderPaused || receiverPaused) { + vm.expectRevert(IHub.SpokePaused.selector); + } + vm.prank(sender); + hub1.transferShares(usdxAssetId, 1, receiver); + } + + function _accrueLiquidityFees(IHub hub, ISpoke spoke, uint256 assetId) internal { + Utils.add(hub, wbtcAssetId, address(spoke), 1e18, alice); + Utils.draw(hub, assetId, address(spoke), alice, 1e18); + + skip(365 days); + Utils.add(hub, assetId, address(spoke), 1e18, alice); + + assertGt(hub.getAsset(assetId).realizedFees, 0); + } + + function _createReportedDeficit(IHub hub, address spoke, uint256 assetId) internal { + Utils.add(hub, wbtcAssetId, spoke, 1e18, alice); + Utils.draw(hub, assetId, spoke, alice, 1e18); + + skip(365 days); + Utils.add(hub, assetId, spoke, 1e18, alice); + + vm.prank(spoke); + hub.reportDeficit(assetId, 1e18, ZERO_PREMIUM_DELTA); + + assertGt(hub.getAssetDeficitRay(assetId), 0); + } +} diff --git a/tests/unit/Spoke/Liquidations/Spoke.LiquidationCall.Scenarios.t.sol b/tests/unit/Spoke/Liquidations/Spoke.LiquidationCall.Scenarios.t.sol index c3ea80aa3..81a391cc2 100644 --- a/tests/unit/Spoke/Liquidations/Spoke.LiquidationCall.Scenarios.t.sol +++ b/tests/unit/Spoke/Liquidations/Spoke.LiquidationCall.Scenarios.t.sol @@ -331,4 +331,57 @@ contract SpokeLiquidationCallScenariosTest is SpokeLiquidationCallBaseTest { }) ); } + + /// @dev a paused peripheral asset won't block a liquidation + function test_scenario_paused_asset() public { + uint256 collateralReserveId = _wethReserveId(spoke); + uint256 debtReserveId = _daiReserveId(spoke); + + _increaseCollateralSupply(spoke, collateralReserveId, 10e18, user); + // borrow usdx as peripheral debt asset not directly involved in liquidation + _openSupplyPosition(spoke, _usdxReserveId(spoke), 100e6); + Utils.borrow(spoke, _usdxReserveId(spoke), user, 100e6, user); + _makeUserLiquidatable(spoke, user, debtReserveId, 0.95e18); + + // set spoke paused + IHub hub = _hub(spoke, _usdxReserveId(spoke)); + _updateSpokePaused(hub, usdxAssetId, address(spoke), true); + + _openSupplyPosition(spoke, collateralReserveId, MAX_SUPPLY_AMOUNT); + + vm.expectCall( + address(hub), + abi.encodeWithSelector(IHubBase.refreshPremium.selector, usdxAssetId) + ); + + vm.prank(liquidator); + spoke.liquidationCall(collateralReserveId, debtReserveId, user, type(uint256).max, false); + } + + /// @dev a paused peripheral asset won't block a liquidation with deficit + function test_scenario_paused_asset_with_deficit() public { + uint256 collateralReserveId = _wethReserveId(spoke); + uint256 debtReserveId = _daiReserveId(spoke); + + _increaseCollateralSupply(spoke, collateralReserveId, 10e18, user); + // borrow usdx as peripheral debt asset not directly involved in liquidation + _openSupplyPosition(spoke, _usdxReserveId(spoke), 100e6); + Utils.borrow(spoke, _usdxReserveId(spoke), user, 100e6, user); + // make user unhealthy to result in deficit + _makeUserLiquidatable(spoke, user, debtReserveId, 0.5e18); + + // set spoke paused + IHub hub = _hub(spoke, _usdxReserveId(spoke)); + _updateSpokePaused(hub, usdxAssetId, address(spoke), true); + + _openSupplyPosition(spoke, collateralReserveId, MAX_SUPPLY_AMOUNT); + + vm.expectCall( + address(hub), + abi.encodeWithSelector(IHubBase.reportDeficit.selector, usdxAssetId) + ); + + vm.prank(liquidator); + spoke.liquidationCall(collateralReserveId, debtReserveId, user, type(uint256).max, false); + } } diff --git a/tests/unit/Spoke/Spoke.Borrow.Validation.t.sol b/tests/unit/Spoke/Spoke.Borrow.Validation.t.sol index cd37c7940..ddb47d079 100644 --- a/tests/unit/Spoke/Spoke.Borrow.Validation.t.sol +++ b/tests/unit/Spoke/Spoke.Borrow.Validation.t.sol @@ -22,7 +22,7 @@ contract SpokeBorrowValidationTest is SpokeBase { amount = bound(amount, 1, MAX_SUPPLY_AMOUNT); // set reserve not borrowable - updateReserveBorrowableFlag(spoke1, reserveId, false); + _updateReserveBorrowableFlag(spoke1, reserveId, false); assertFalse(spoke1.getReserve(reserveId).flags.borrowable()); // Bob tries to draw @@ -76,7 +76,7 @@ contract SpokeBorrowValidationTest is SpokeBase { reserveId = bound(reserveId, 0, spoke1.getReserveCount() - 1); amount = bound(amount, 1, MAX_SUPPLY_AMOUNT); - updateReserveFrozenFlag(spoke1, reserveId, true); + _updateReserveFrozenFlag(spoke1, reserveId, true); assertTrue(spoke1.getReserve(reserveId).flags.frozen()); // Bob try to draw diff --git a/tests/unit/Spoke/Spoke.ReserveConfig.t.sol b/tests/unit/Spoke/Spoke.ReserveConfig.t.sol new file mode 100644 index 000000000..edcbf5847 --- /dev/null +++ b/tests/unit/Spoke/Spoke.ReserveConfig.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/unit/Spoke/SpokeBase.t.sol'; + +contract SpokeReserveConfigTest is SpokeBase { + function setUp() public override { + super.setUp(); + _openSupplyPosition(spoke1, _daiReserveId(spoke1), 100e18); + } + + function test_supply_paused_frozen_scenarios() public { + uint256 daiReserveId = _daiReserveId(spoke1); + uint256 amount = 100e18; + + // paused / frozen; reverts + _updateReservePausedFlag(spoke1, daiReserveId, true); + _updateReserveFrozenFlag(spoke1, daiReserveId, true); + vm.expectRevert(ISpoke.ReservePaused.selector); + Utils.supply(spoke1, daiReserveId, bob, amount, bob); + + // not paused / frozen; reverts + _updateReservePausedFlag(spoke1, daiReserveId, false); + _updateReserveFrozenFlag(spoke1, daiReserveId, true); + vm.expectRevert(ISpoke.ReserveFrozen.selector); + Utils.supply(spoke1, daiReserveId, bob, amount, bob); + + // paused / not frozen; reverts + _updateReservePausedFlag(spoke1, daiReserveId, true); + _updateReserveFrozenFlag(spoke1, daiReserveId, false); + vm.expectRevert(ISpoke.ReservePaused.selector); + Utils.supply(spoke1, daiReserveId, bob, amount, bob); + + // not paused / not frozen; succeeds + _updateReservePausedFlag(spoke1, daiReserveId, false); + _updateReserveFrozenFlag(spoke1, daiReserveId, false); + deal(spoke1, daiReserveId, bob, amount); + Utils.approve(spoke1, daiReserveId, bob, amount); + Utils.supply(spoke1, daiReserveId, bob, amount, bob); + } + + function test_withdraw_paused_scenarios() public { + uint256 daiReserveId = _daiReserveId(spoke1); + uint256 supplyAmount = 100e18; + uint256 withdrawAmount = 1e18; + + // ensure user can withdraw + deal(spoke1, daiReserveId, bob, supplyAmount); + Utils.approve(spoke1, daiReserveId, bob, supplyAmount); + Utils.supplyCollateral(spoke1, daiReserveId, bob, supplyAmount, bob); + + // frozen does not matter + _updateReserveFrozenFlag(spoke1, daiReserveId, true); + + // paused; reverts + _updateReservePausedFlag(spoke1, daiReserveId, true); + vm.expectRevert(ISpoke.ReservePaused.selector); + Utils.withdraw(spoke1, daiReserveId, bob, withdrawAmount, bob); + + // unpaused; succeeds + _updateReservePausedFlag(spoke1, daiReserveId, false); + Utils.withdraw(spoke1, daiReserveId, bob, withdrawAmount, bob); + } + + function test_borrow_fuzz_borrowable_paused_frozen_scenarios( + bool borrowable, + bool paused, + bool frozen + ) public { + _increaseCollateralSupply(spoke1, _daiReserveId(spoke1), 100e18, bob); + uint256 daiReserveId = _daiReserveId(spoke1); + uint256 amount = 1; + + // paused / borrowable / frozen; reverts + _updateReservePausedFlag(spoke1, daiReserveId, paused); + _updateReserveBorrowableFlag(spoke1, daiReserveId, borrowable); + _updateReserveFrozenFlag(spoke1, daiReserveId, frozen); + if (paused) { + vm.expectRevert(ISpoke.ReservePaused.selector); + } else if (frozen) { + vm.expectRevert(ISpoke.ReserveFrozen.selector); + } else if (!borrowable) { + vm.expectRevert(ISpoke.ReserveNotBorrowable.selector); + } + Utils.borrow(spoke1, daiReserveId, bob, amount, bob); + } + + function test_repay_fuzz_paused_scenarios(bool frozen) public { + uint256 daiReserveId = _daiReserveId(spoke1); + + // create a simple debt position for bob + uint256 wethReserveId = _wethReserveId(spoke1); + uint256 wethCollateral = 10e18; + uint256 daiLiquidity = 1_000e18; + uint256 borrowAmount = 100e18; + + deal(spoke1, wethReserveId, bob, wethCollateral); + Utils.approve(spoke1, wethReserveId, bob, wethCollateral); + Utils.supplyCollateral(spoke1, wethReserveId, bob, wethCollateral, bob); + + deal(spoke1, daiReserveId, alice, daiLiquidity); + Utils.approve(spoke1, daiReserveId, alice, daiLiquidity); + Utils.supply(spoke1, daiReserveId, alice, daiLiquidity, alice); + + Utils.borrow(spoke1, daiReserveId, bob, borrowAmount, bob); + Utils.approve(spoke1, daiReserveId, bob, UINT256_MAX); + + _updateReserveFrozenFlag(spoke1, daiReserveId, frozen); + + // paused; reverts + _updateReservePausedFlag(spoke1, daiReserveId, true); + vm.expectRevert(ISpoke.ReservePaused.selector); + Utils.repay(spoke1, daiReserveId, bob, borrowAmount, bob); + + // unpaused; succeeds + _updateReservePausedFlag(spoke1, daiReserveId, false); + Utils.repay(spoke1, daiReserveId, bob, borrowAmount, bob); + } + + function test_setUsingAsCollateral_fuzz_paused_frozen_scenarios(bool frozen) public { + uint256 daiReserveId = _daiReserveId(spoke1); + + _updateReserveFrozenFlag(spoke1, daiReserveId, frozen); + + // paused; reverts + _updateReservePausedFlag(spoke1, daiReserveId, true); + vm.expectRevert(ISpoke.ReservePaused.selector); + Utils.setUsingAsCollateral(spoke1, daiReserveId, alice, true, alice); + + _updateReserveFrozenFlag(spoke1, daiReserveId, false); + _updateReservePausedFlag(spoke1, daiReserveId, false); + + // alice enables collateral + Utils.setUsingAsCollateral(spoke1, daiReserveId, alice, true, alice); + assertTrue(_isUsingAsCollateral(spoke1, daiReserveId, alice), 'alice using as collateral'); + + // frozen: disallow when enabling, allow when disabling + _updateReserveFrozenFlag(spoke1, daiReserveId, true); + vm.expectRevert(ISpoke.ReserveFrozen.selector); + Utils.setUsingAsCollateral(spoke1, daiReserveId, bob, true, bob); + + Utils.setUsingAsCollateral(spoke1, daiReserveId, alice, false, alice); + assertFalse(_isUsingAsCollateral(spoke1, daiReserveId, alice)); + } +} diff --git a/tests/unit/Spoke/Spoke.SetUsingAsCollateral.t.sol b/tests/unit/Spoke/Spoke.SetUsingAsCollateral.t.sol index f5e440fd7..febae0b8f 100644 --- a/tests/unit/Spoke/Spoke.SetUsingAsCollateral.t.sol +++ b/tests/unit/Spoke/Spoke.SetUsingAsCollateral.t.sol @@ -24,7 +24,7 @@ contract SpokeConfigTest is SpokeBase { assertTrue(_isUsingAsCollateral(spoke1, daiReserveId, alice), 'alice using as collateral'); assertFalse(_isUsingAsCollateral(spoke1, daiReserveId, bob), 'bob not using as collateral'); - updateReserveFrozenFlag(spoke1, daiReserveId, true); + _updateReserveFrozenFlag(spoke1, daiReserveId, true); assertTrue(spoke1.getReserve(daiReserveId).flags.frozen(), 'reserve status frozen'); // disallow when activating diff --git a/tests/unit/Spoke/Spoke.Supply.t.sol b/tests/unit/Spoke/Spoke.Supply.t.sol index 947b8e174..a3b2b6d97 100644 --- a/tests/unit/Spoke/Spoke.Supply.t.sol +++ b/tests/unit/Spoke/Spoke.Supply.t.sol @@ -33,7 +33,7 @@ contract SpokeSupplyTest is SpokeBase { uint256 daiReserveId = _daiReserveId(spoke1); uint256 amount = 100e18; - updateReserveFrozenFlag(spoke1, daiReserveId, true); + _updateReserveFrozenFlag(spoke1, daiReserveId, true); assertTrue(spoke1.getReserve(daiReserveId).flags.frozen()); vm.expectRevert(ISpoke.ReserveFrozen.selector);