diff --git a/snapshots/Hub.Operations.json b/snapshots/Hub.Operations.json index 16573542b..d2aa55a90 100644 --- a/snapshots/Hub.Operations.json +++ b/snapshots/Hub.Operations.json @@ -1,9 +1,9 @@ { "add": "88006", - "add: with transfer": "109613", + "add: with transfer": "109616", "draw": "105931", - "eliminateDeficit: full": "59781", - "eliminateDeficit: partial": "69429", + "eliminateDeficit: full": "73925", + "eliminateDeficit: partial": "83573", "mintFeeShares": "86130", "payFee": "72302", "refreshPremium": "71999", diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index 244965734..0d615372a 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -353,22 +353,21 @@ contract Hub is IHub, AccessManaged { uint256 assetId, uint256 amount, address spoke - ) external returns (uint256) { + ) external restricted returns (uint256) { Asset storage asset = _assets[assetId]; SpokeData storage callerSpoke = _spokes[assetId][msg.sender]; SpokeData storage coveredSpoke = _spokes[assetId][spoke]; asset.accrue(); - _validateEliminateDeficit(callerSpoke, amount); - uint256 deficitRay = coveredSpoke.deficitRay; uint256 deficitAmountRay = (amount < deficitRay.fromRayUp()) ? amount.toRay() : deficitRay; + _validateEliminateDeficit(callerSpoke, deficitAmountRay); uint120 shares = asset.toAddedSharesUp(deficitAmountRay.fromRayUp()).toUint120(); asset.addedShares -= shares; callerSpoke.addedShares -= shares; - asset.deficitRay = asset.deficitRay.uncheckedSub(deficitAmountRay).toUint200(); - coveredSpoke.deficitRay = deficitRay.uncheckedSub(deficitAmountRay).toUint200(); + asset.deficitRay -= deficitAmountRay.toUint200(); + coveredSpoke.deficitRay -= deficitAmountRay.toUint200(); asset.updateDrawnRate(assetId); @@ -895,9 +894,12 @@ contract Hub is IHub, AccessManaged { require(premiumAmountRay <= premiumRay, SurplusPremiumRayDeficitReported(premiumRay)); } - function _validateEliminateDeficit(SpokeData storage spoke, uint256 amount) internal view { - require(spoke.active, SpokeNotActive()); - require(amount > 0, InvalidAmount()); + function _validateEliminateDeficit( + SpokeData storage callerSpoke, + uint256 deficitAmountRay + ) internal view { + require(callerSpoke.active, SpokeNotActive()); + require(deficitAmountRay > 0, InvalidAmount()); } function _validatePayFeeShares(SpokeData storage senderSpoke, uint256 feeShares) internal view { diff --git a/src/libraries/types/Roles.sol b/src/libraries/types/Roles.sol index ed3436165..1681e3f19 100644 --- a/src/libraries/types/Roles.sol +++ b/src/libraries/types/Roles.sol @@ -10,4 +10,5 @@ library Roles { uint64 public constant HUB_ADMIN_ROLE = 1; uint64 public constant SPOKE_ADMIN_ROLE = 2; uint64 public constant USER_POSITION_UPDATER_ROLE = 3; + uint64 public constant HUB_UMBRELLA_ROLE = 4; } diff --git a/tests/Base.t.sol b/tests/Base.t.sol index 424bc3683..3bae2e0f5 100644 --- a/tests/Base.t.sol +++ b/tests/Base.t.sol @@ -147,6 +147,7 @@ abstract contract Base is Test { address internal HUB_ADMIN = makeAddr('HUB_ADMIN'); address internal SPOKE_ADMIN = makeAddr('SPOKE_ADMIN'); address internal USER_POSITION_UPDATER = makeAddr('USER_POSITION_UPDATER'); + address internal HUB_UMBRELLA = makeAddr('HUB_UMBRELLA'); address internal TREASURY_ADMIN = makeAddr('TREASURY_ADMIN'); address internal LIQUIDATOR = makeAddr('LIQUIDATOR'); address internal POSITION_MANAGER = makeAddr('POSITION_MANAGER'); @@ -303,6 +304,9 @@ abstract contract Base is Test { manager.grantRole(Roles.USER_POSITION_UPDATER_ROLE, SPOKE_ADMIN, 0); manager.grantRole(Roles.USER_POSITION_UPDATER_ROLE, USER_POSITION_UPDATER, 0); + manager.grantRole(Roles.HUB_UMBRELLA_ROLE, HUB_ADMIN, 0); + manager.grantRole(Roles.HUB_UMBRELLA_ROLE, HUB_UMBRELLA, 0); + // Grant responsibilities to roles { bytes4[] memory selectors = new bytes4[](7); @@ -333,6 +337,12 @@ abstract contract Base is Test { selectors[5] = IHub.mintFeeShares.selector; manager.setTargetFunctionRole(address(targetHub), selectors, Roles.HUB_ADMIN_ROLE); } + + { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IHub.eliminateDeficit.selector; + manager.setTargetFunctionRole(address(targetHub), selectors, Roles.HUB_UMBRELLA_ROLE); + } vm.stopPrank(); } diff --git a/tests/gas/Hub.Operations.gas.t.sol b/tests/gas/Hub.Operations.gas.t.sol index 6723966cd..e8aada48f 100644 --- a/tests/gas/Hub.Operations.gas.t.sol +++ b/tests/gas/Hub.Operations.gas.t.sol @@ -279,6 +279,9 @@ contract HubOperations_Gas_Tests is Base { type(uint256).max ); + vm.prank(ADMIN); + accessManager.grantRole(Roles.HUB_UMBRELLA_ROLE, address(spoke1), 0); + vm.prank(address(spoke1)); hub1.reportDeficit(daiAssetId, drawnDebt, premiumDelta); vm.snapshotGasLastCall('Hub.Operations', 'reportDeficit'); diff --git a/tests/unit/Hub/Hub.Access.t.sol b/tests/unit/Hub/Hub.Access.t.sol index ca9a7af03..a8dd3d273 100644 --- a/tests/unit/Hub/Hub.Access.t.sol +++ b/tests/unit/Hub/Hub.Access.t.sol @@ -71,6 +71,12 @@ contract HubAccessTest is HubBase { // Hub Admin can update spoke config vm.prank(HUB_ADMIN); hub1.updateSpokeConfig(assetAId, address(spoke1), spokeConfig); + + // Only registered spoke with Hub Umbrella or Hub Admin role can eliminate deficit + vm.expectRevert( + abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, address(this)) + ); + hub1.eliminateDeficit(daiAssetId, 1000, address(spoke1)); } function test_setInterestRateData_access() public { diff --git a/tests/unit/Hub/Hub.EliminateDeficit.t.sol b/tests/unit/Hub/Hub.EliminateDeficit.t.sol index dfb253b6a..d011647e5 100644 --- a/tests/unit/Hub/Hub.EliminateDeficit.t.sol +++ b/tests/unit/Hub/Hub.EliminateDeficit.t.sol @@ -5,8 +5,8 @@ pragma solidity ^0.8.0; import 'tests/unit/Hub/HubBase.t.sol'; contract HubEliminateDeficitTest is HubBase { - using WadRayMath for uint256; - using MathUtils for uint256; + using WadRayMath for *; + using MathUtils for *; using SafeCast for uint256; uint256 internal _assetId; @@ -22,6 +22,9 @@ contract HubEliminateDeficitTest is HubBase { _callerSpoke = address(spoke2); _coveredSpoke = address(spoke1); _otherSpoke = address(spoke3); + + vm.prank(ADMIN); + accessManager.grantRole(Roles.HUB_UMBRELLA_ROLE, address(_callerSpoke), 0); } function test_eliminateDeficit_revertsWith_InvalidAmount_ZeroAmountNoDeficit() public { @@ -38,6 +41,24 @@ contract HubEliminateDeficitTest is HubBase { hub1.eliminateDeficit(_assetId, 0, _coveredSpoke); } + function test_eliminateDeficit_revertsWith_SpokeNotActive_on_UnregisteredAsset() public { + _createDeficit(_assetId, _coveredSpoke, _deficitAmountRay); + assertEq(hub1.getSpokeDeficitRay(_assetId, _coveredSpoke), _deficitAmountRay); + + uint256 invalidAssetId = vm.randomUint(hub1.getAssetCount() + 1, UINT256_MAX); + + vm.expectRevert(IHub.SpokeNotActive.selector); + vm.prank(_callerSpoke); + hub1.eliminateDeficit(invalidAssetId, vm.randomUint(1, UINT256_MAX), vm.randomAddress()); + } + + function test_eliminateDeficit_revertsWith_InvalidAmount_on_UnregisteredCoveredSpoke() public { + // since amount is bounded to covered spoke deficit, deficit to be eliminated bounds to 0 + vm.expectRevert(IHub.InvalidAmount.selector); + vm.prank(_callerSpoke); + hub1.eliminateDeficit(_assetId, vm.randomUint(1, UINT256_MAX), alice); // alice is not a spoke + } + // Caller spoke does not have funds function test_eliminateDeficit_fuzz_revertsWith_ArithmeticUnderflow_CallerSpokeNoFunds( uint256 @@ -48,7 +69,25 @@ contract HubEliminateDeficitTest is HubBase { hub1.eliminateDeficit(_assetId, vm.randomUint(_deficitAmountRay, UINT256_MAX), _coveredSpoke); } + function test_eliminateDeficit_fuzz_revertsWith_AccessManagedUnauthorized(address caller) public { + (bool immediate, uint32 delay) = IAccessManager(hub1.authority()).canCall( + caller, + address(hub1), + IHub.eliminateDeficit.selector + ); + vm.assume(!immediate || delay > 0); + vm.expectRevert( + abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, caller) + ); + vm.prank(caller); + hub1.eliminateDeficit(_assetId, vm.randomUint(), _coveredSpoke); + } + function test_eliminateDeficit_fuzz_revertsWith_callerSpokeNotActive(address caller) public { + vm.assume(caller != address(accessManager)); + vm.prank(ADMIN); + accessManager.grantRole(Roles.HUB_UMBRELLA_ROLE, caller, 0); + vm.assume(!hub1.getSpoke(_assetId, caller).active); vm.expectRevert(IHub.SpokeNotActive.selector); vm.prank(caller);