From c9e04d386c65d0443927e0a916197c2323ff0eff Mon Sep 17 00:00:00 2001 From: green Date: Thu, 12 Sep 2024 11:56:08 +0200 Subject: [PATCH 01/26] feat(pool-monitor): add initial liquidity monitor --- .../dollar/monitors/PoolLiquidityMonitor.sol | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol diff --git a/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol b/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol new file mode 100644 index 000000000..874b06f50 --- /dev/null +++ b/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../facets/UbiquityPoolFacet.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract PoolLiquidityMonitor is Ownable { + UbiquityPoolFacet public ubiquityPoolFacet; + address public defenderRelayer; + + event LiquidityChecked(uint256 currentLiquidity); + + constructor(address _ubiquityPoolFacetAddress, address _defenderRelayer) { + ubiquityPoolFacet = UbiquityPoolFacet(_ubiquityPoolFacetAddress); + defenderRelayer = _defenderRelayer; + } + + modifier onlyAuthorized() { + require( + msg.sender == defenderRelayer, + "Not authorized: Only Defender Relayer allowed" + ); + _; + } + + function setDefenderRelayer( + address _newDefenderRelayer + ) external onlyOwner { + defenderRelayer = _newDefenderRelayer; + } + + function checkLiquidity() external onlyAuthorized { + uint256 currentCollateralLiquidity = ubiquityPoolFacet + .collateralUsdBalance(); + + emit LiquidityChecked(currentCollateralLiquidity); + } +} From 281ca3348de2f1f01c6cbe47c9ab8312c611acb3 Mon Sep 17 00:00:00 2001 From: green Date: Thu, 12 Sep 2024 12:13:25 +0200 Subject: [PATCH 02/26] feat(pool-monitor): add immutable for ubiquityPoolFacet --- packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol b/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol index 874b06f50..52bfed422 100644 --- a/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol +++ b/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol @@ -5,7 +5,7 @@ import "../facets/UbiquityPoolFacet.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract PoolLiquidityMonitor is Ownable { - UbiquityPoolFacet public ubiquityPoolFacet; + UbiquityPoolFacet public immutable ubiquityPoolFacet; address public defenderRelayer; event LiquidityChecked(uint256 currentLiquidity); From 32237f56cb8f03783371b78ef97720dec9fb1df6 Mon Sep 17 00:00:00 2001 From: green Date: Thu, 12 Sep 2024 12:26:50 +0200 Subject: [PATCH 03/26] test(pool-monitor): add pool monitor initial test --- .../monitors/PoolLiquidityMonitorTest.t.sol | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol diff --git a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol new file mode 100644 index 000000000..33196781c --- /dev/null +++ b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../../src/dollar/monitors/PoolLiquidityMonitor.sol"; +import "../../helpers/LocalTestHelper.sol"; + +contract PoolLiquidityMonitorTest is LocalTestHelper { + PoolLiquidityMonitor monitor; + address defenderRelayer = address(0x456); + + function setUp() public override { + super.setUp(); + + monitor = new PoolLiquidityMonitor( + address(ubiquityPoolFacet), + defenderRelayer + ); + } + + function testInitialSetup() public { + assertEq(monitor.defenderRelayer(), defenderRelayer); + } + + function testSetDefenderRelayer() public { + address newRelayer = address(0x789); + + monitor.setDefenderRelayer(newRelayer); + + assertEq(monitor.defenderRelayer(), newRelayer); + } +} From e691a209dcf9c8be4e5b83623fe81261d166a719 Mon Sep 17 00:00:00 2001 From: green Date: Thu, 12 Sep 2024 13:09:45 +0200 Subject: [PATCH 04/26] test(pool-monitor): increase test coverage --- .../monitors/PoolLiquidityMonitorTest.t.sol | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol index 33196781c..64b801ce7 100644 --- a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol @@ -8,6 +8,7 @@ import "../../helpers/LocalTestHelper.sol"; contract PoolLiquidityMonitorTest is LocalTestHelper { PoolLiquidityMonitor monitor; address defenderRelayer = address(0x456); + address unauthorized = address(0x123); function setUp() public override { super.setUp(); @@ -25,8 +26,54 @@ contract PoolLiquidityMonitorTest is LocalTestHelper { function testSetDefenderRelayer() public { address newRelayer = address(0x789); + vm.prank(monitor.owner()); monitor.setDefenderRelayer(newRelayer); assertEq(monitor.defenderRelayer(), newRelayer); } + + function testUnauthorizedCheckLiquidity() public { + vm.prank(unauthorized); + vm.expectRevert("Not authorized: Only Defender Relayer allowed"); + + monitor.checkLiquidity(); + } + + function testCheckLiquidity() public { + vm.prank(defenderRelayer); + monitor.checkLiquidity(); + } + + function testSetDefenderRelayerToZeroAddress() public { + vm.prank(owner); + vm.expectRevert(); + monitor.setDefenderRelayer(address(0)); + } + + function testCheckLiquidityWithDifferentValues() public { + uint256 mockedLiquidityHigh = 10000; + uint256 mockedLiquidityLow = 100; + + vm.mockCall( + address(ubiquityPoolFacet), + abi.encodeWithSelector( + UbiquityPoolFacet.collateralUsdBalance.selector + ), + abi.encode(mockedLiquidityHigh) + ); + + vm.prank(defenderRelayer); + monitor.checkLiquidity(); + + vm.mockCall( + address(ubiquityPoolFacet), + abi.encodeWithSelector( + UbiquityPoolFacet.collateralUsdBalance.selector + ), + abi.encode(mockedLiquidityLow) + ); + + vm.prank(defenderRelayer); + monitor.checkLiquidity(); + } } From 5aa078ee687c8e803ddeb441f6a9ba928e0c5236 Mon Sep 17 00:00:00 2001 From: green Date: Thu, 12 Sep 2024 22:27:51 +0200 Subject: [PATCH 05/26] feat(pool-monitor): add pool's initial monitor --- .../dollar/monitors/PoolLiquidityMonitor.sol | 74 +++++++++++++++++-- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol b/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol index 52bfed422..6ed8fee24 100644 --- a/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol +++ b/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol @@ -2,17 +2,31 @@ pragma solidity ^0.8.19; import "../facets/UbiquityPoolFacet.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; +import {Modifiers} from "../libraries/LibAppStorage.sol"; +import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; + +contract PoolLiquidityMonitor is Modifiers { + using SafeMath for uint256; -contract PoolLiquidityMonitor is Ownable { UbiquityPoolFacet public immutable ubiquityPoolFacet; address public defenderRelayer; + uint256 public liquidityVertex; + bool public paused; + uint256 public thresholdPercentage; - event LiquidityChecked(uint256 currentLiquidity); + event LiquidityVertexUpdated(uint256 collateralLiquidity); + event MonitorPaused(uint256 collateralLiquidity, uint256 diffPercentage); + event VertexDropped(); + event PausedToggled(bool paused); - constructor(address _ubiquityPoolFacetAddress, address _defenderRelayer) { + constructor( + address _ubiquityPoolFacetAddress, + address _defenderRelayer, + uint256 _thresholdPercentage + ) { ubiquityPoolFacet = UbiquityPoolFacet(_ubiquityPoolFacetAddress); defenderRelayer = _defenderRelayer; + thresholdPercentage = _thresholdPercentage; } modifier onlyAuthorized() { @@ -23,16 +37,62 @@ contract PoolLiquidityMonitor is Ownable { _; } + function setThresholdPercentage( + uint256 _newThresholdPercentage + ) external onlyAdmin { + thresholdPercentage = _newThresholdPercentage; + } + function setDefenderRelayer( address _newDefenderRelayer - ) external onlyOwner { + ) external onlyAdmin { defenderRelayer = _newDefenderRelayer; } - function checkLiquidity() external onlyAuthorized { + function togglePaused() external onlyAdmin { + paused = !paused; + emit PausedToggled(paused); + } + + function dropLiquidityVertex() external onlyAdmin { uint256 currentCollateralLiquidity = ubiquityPoolFacet .collateralUsdBalance(); - emit LiquidityChecked(currentCollateralLiquidity); + require(currentCollateralLiquidity > 0, "Insufficient liquidity"); + + liquidityVertex = currentCollateralLiquidity; + + emit VertexDropped(); + } + + function checkLiquidityVertex() external onlyAuthorized { + uint256 currentCollateralLiquidity = ubiquityPoolFacet + .collateralUsdBalance(); + + require(currentCollateralLiquidity > 0, "Insufficient liquidity"); + require(!paused, "Monitor paused"); + + if (currentCollateralLiquidity > liquidityVertex) { + liquidityVertex = currentCollateralLiquidity; + + emit LiquidityVertexUpdated(liquidityVertex); + } else { + uint256 liquidityDiffPercentage = liquidityVertex + .sub(currentCollateralLiquidity) + .mul(100) + .div(liquidityVertex); + + if (liquidityDiffPercentage >= thresholdPercentage) { + paused = true; + + // Pause the UbiquityDollarToken + // Pause LibUbiquityPool by disabling collateral + + emit MonitorPaused( + currentCollateralLiquidity, + liquidityDiffPercentage + ); + } + } } } From 89505a911a9b42d0050c9684a7eb1481c0defd83 Mon Sep 17 00:00:00 2001 From: green Date: Fri, 13 Sep 2024 16:18:19 +0200 Subject: [PATCH 06/26] test(pool-monitor): fix monitor parameters --- .../monitors/PoolLiquidityMonitorTest.t.sol | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol index 64b801ce7..6e7f3d66c 100644 --- a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol @@ -4,8 +4,9 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; import "../../../src/dollar/monitors/PoolLiquidityMonitor.sol"; import "../../helpers/LocalTestHelper.sol"; +import {DiamondTestSetup} from "../../../test/diamond/DiamondTestSetup.sol"; -contract PoolLiquidityMonitorTest is LocalTestHelper { +contract PoolLiquidityMonitorTest is DiamondTestSetup { PoolLiquidityMonitor monitor; address defenderRelayer = address(0x456); address unauthorized = address(0x123); @@ -15,7 +16,8 @@ contract PoolLiquidityMonitorTest is LocalTestHelper { monitor = new PoolLiquidityMonitor( address(ubiquityPoolFacet), - defenderRelayer + defenderRelayer, + 30 ); } @@ -23,31 +25,33 @@ contract PoolLiquidityMonitorTest is LocalTestHelper { assertEq(monitor.defenderRelayer(), defenderRelayer); } - function testSetDefenderRelayer() public { - address newRelayer = address(0x789); - - vm.prank(monitor.owner()); - monitor.setDefenderRelayer(newRelayer); - - assertEq(monitor.defenderRelayer(), newRelayer); - } - function testUnauthorizedCheckLiquidity() public { vm.prank(unauthorized); vm.expectRevert("Not authorized: Only Defender Relayer allowed"); - monitor.checkLiquidity(); + monitor.checkLiquidityVertex(); } function testCheckLiquidity() public { + uint256 mockedLiquidity = 10000; + + vm.mockCall( + address(ubiquityPoolFacet), + abi.encodeWithSelector( + UbiquityPoolFacet.collateralUsdBalance.selector + ), + abi.encode(mockedLiquidity) + ); + vm.prank(defenderRelayer); - monitor.checkLiquidity(); + monitor.checkLiquidityVertex(); } - function testSetDefenderRelayerToZeroAddress() public { - vm.prank(owner); - vm.expectRevert(); - monitor.setDefenderRelayer(address(0)); + function testSetDefenderRelayer() public { + address newRelayer = address(0x789); + + vm.expectRevert("Manager: Caller is not admin"); + monitor.setDefenderRelayer(newRelayer); } function testCheckLiquidityWithDifferentValues() public { @@ -63,7 +67,7 @@ contract PoolLiquidityMonitorTest is LocalTestHelper { ); vm.prank(defenderRelayer); - monitor.checkLiquidity(); + monitor.checkLiquidityVertex(); vm.mockCall( address(ubiquityPoolFacet), @@ -74,6 +78,6 @@ contract PoolLiquidityMonitorTest is LocalTestHelper { ); vm.prank(defenderRelayer); - monitor.checkLiquidity(); + monitor.checkLiquidityVertex(); } } From c3d3fc6b52c1a6647e283b4aa9c5326e26a1d117 Mon Sep 17 00:00:00 2001 From: green Date: Sat, 14 Sep 2024 23:23:06 +0200 Subject: [PATCH 07/26] test(pool-monitor): add pool-monitor to the diamond-test-setup --- .../dollar/monitors/PoolLiquidityMonitor.sol | 29 ++++----- .../test/diamond/DiamondTestSetup.sol | 25 +++++++- .../monitors/PoolLiquidityMonitorTest.t.sol | 64 +++++-------------- 3 files changed, 51 insertions(+), 67 deletions(-) diff --git a/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol b/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol index 6ed8fee24..6b56a56a3 100644 --- a/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol +++ b/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol @@ -4,14 +4,17 @@ pragma solidity ^0.8.19; import "../facets/UbiquityPoolFacet.sol"; import {Modifiers} from "../libraries/LibAppStorage.sol"; import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import {DEFAULT_ADMIN_ROLE} from "../libraries/Constants.sol"; +import {LibUbiquityPool} from "../libraries/LibUbiquityPool.sol"; +import "forge-std/console.sol"; contract PoolLiquidityMonitor is Modifiers { using SafeMath for uint256; - UbiquityPoolFacet public immutable ubiquityPoolFacet; + UbiquityPoolFacet public ubiquityPoolFacet; address public defenderRelayer; uint256 public liquidityVertex; - bool public paused; + bool public monitorPaused; uint256 public thresholdPercentage; event LiquidityVertexUpdated(uint256 collateralLiquidity); @@ -19,16 +22,6 @@ contract PoolLiquidityMonitor is Modifiers { event VertexDropped(); event PausedToggled(bool paused); - constructor( - address _ubiquityPoolFacetAddress, - address _defenderRelayer, - uint256 _thresholdPercentage - ) { - ubiquityPoolFacet = UbiquityPoolFacet(_ubiquityPoolFacetAddress); - defenderRelayer = _defenderRelayer; - thresholdPercentage = _thresholdPercentage; - } - modifier onlyAuthorized() { require( msg.sender == defenderRelayer, @@ -50,12 +43,12 @@ contract PoolLiquidityMonitor is Modifiers { } function togglePaused() external onlyAdmin { - paused = !paused; - emit PausedToggled(paused); + monitorPaused = !monitorPaused; + emit PausedToggled(monitorPaused); } function dropLiquidityVertex() external onlyAdmin { - uint256 currentCollateralLiquidity = ubiquityPoolFacet + uint256 currentCollateralLiquidity = LibUbiquityPool .collateralUsdBalance(); require(currentCollateralLiquidity > 0, "Insufficient liquidity"); @@ -66,11 +59,11 @@ contract PoolLiquidityMonitor is Modifiers { } function checkLiquidityVertex() external onlyAuthorized { - uint256 currentCollateralLiquidity = ubiquityPoolFacet + uint256 currentCollateralLiquidity = LibUbiquityPool .collateralUsdBalance(); require(currentCollateralLiquidity > 0, "Insufficient liquidity"); - require(!paused, "Monitor paused"); + require(!monitorPaused, "Monitor paused"); if (currentCollateralLiquidity > liquidityVertex) { liquidityVertex = currentCollateralLiquidity; @@ -83,7 +76,7 @@ contract PoolLiquidityMonitor is Modifiers { .div(liquidityVertex); if (liquidityDiffPercentage >= thresholdPercentage) { - paused = true; + monitorPaused = true; // Pause the UbiquityDollarToken // Pause LibUbiquityPool by disabling collateral diff --git a/packages/contracts/test/diamond/DiamondTestSetup.sol b/packages/contracts/test/diamond/DiamondTestSetup.sol index c15c8edfa..3eaab57be 100644 --- a/packages/contracts/test/diamond/DiamondTestSetup.sol +++ b/packages/contracts/test/diamond/DiamondTestSetup.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.19; +import "forge-std/Test.sol"; + import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol"; import {Diamond, DiamondArgs} from "../../src/dollar/Diamond.sol"; import {ERC1155Ubiquity} from "../../src/dollar/core/ERC1155Ubiquity.sol"; @@ -31,6 +33,7 @@ import {MockERC20} from "../../src/dollar/mocks/MockERC20.sol"; import {DiamondInit} from "../../src/dollar/upgradeInitializers/DiamondInit.sol"; import {DiamondTestHelper} from "../helpers/DiamondTestHelper.sol"; import {UUPSTestHelper} from "../helpers/UUPSTestHelper.sol"; +import {PoolLiquidityMonitor} from "../../src/dollar/monitors/PoolLiquidityMonitor.sol"; import {CREDIT_NFT_MANAGER_ROLE, CREDIT_TOKEN_BURNER_ROLE, CREDIT_TOKEN_MINTER_ROLE, CURVE_DOLLAR_MANAGER_ROLE, DOLLAR_TOKEN_BURNER_ROLE, DOLLAR_TOKEN_MINTER_ROLE, GOVERNANCE_TOKEN_BURNER_ROLE, GOVERNANCE_TOKEN_MANAGER_ROLE, GOVERNANCE_TOKEN_MINTER_ROLE, STAKING_SHARE_MINTER_ROLE} from "../../src/dollar/libraries/Constants.sol"; /** @@ -61,6 +64,7 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { StakingFacet stakingFacet; StakingFormulasFacet stakingFormulasFacet; UbiquityPoolFacet ubiquityPoolFacet; + PoolLiquidityMonitor poolLiquidityMonitor; // diamond facet implementation instances (should not be used in tests, use only on upgrades) AccessControlFacet accessControlFacetImplementation; @@ -82,6 +86,7 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { StakingFacet stakingFacetImplementation; StakingFormulasFacet stakingFormulasFacetImplementation; UbiquityPoolFacet ubiquityPoolFacetImplementation; + PoolLiquidityMonitor poolLiquidityMonitorImplementation; // facet names with addresses string[] facetNames; @@ -114,6 +119,7 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { bytes4[] selectorsOfStakingFacet; bytes4[] selectorsOfStakingFormulasFacet; bytes4[] selectorsOfUbiquityPoolFacet; + bytes4[] selectorsOfPoolLiquidityMonitor; /// @notice Deploys diamond and connects facets function setUp() public virtual { @@ -183,6 +189,10 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { "/out/UbiquityPoolFacet.sol/UbiquityPoolFacet.json" ); + selectorsOfPoolLiquidityMonitor = getSelectorsFromAbi( + "/out/PoolLiquidityMonitor.sol/PoolLiquidityMonitor.json" + ); + // deploy facet implementation instances accessControlFacetImplementation = new AccessControlFacet(); bondingCurveFacetImplementation = new BondingCurveFacet(); @@ -203,6 +213,7 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { stakingFacetImplementation = new StakingFacet(); stakingFormulasFacetImplementation = new StakingFormulasFacet(); ubiquityPoolFacetImplementation = new UbiquityPoolFacet(); + poolLiquidityMonitorImplementation = new PoolLiquidityMonitor(); // prepare diamond init args diamondInit = new DiamondInit(); @@ -225,7 +236,8 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { "OwnershipFacet", "StakingFacet", "StakingFormulasFacet", - "UbiquityPoolFacet" + "UbiquityPoolFacet", + "PoolLiquidityMonitor" ]; DiamondInit.Args memory initArgs = DiamondInit.Args({ admin: admin, @@ -245,7 +257,7 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { ) }); - FacetCut[] memory cuts = new FacetCut[](19); + FacetCut[] memory cuts = new FacetCut[](20); cuts[0] = ( FacetCut({ @@ -387,6 +399,14 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { }) ); + cuts[19] = ( + FacetCut({ + facetAddress: address(poolLiquidityMonitorImplementation), + action: FacetCutAction.Add, + functionSelectors: selectorsOfPoolLiquidityMonitor + }) + ); + // deploy diamond vm.prank(owner); diamond = new Diamond(_args, cuts); @@ -417,6 +437,7 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { stakingFacet = StakingFacet(address(diamond)); stakingFormulasFacet = StakingFormulasFacet(address(diamond)); ubiquityPoolFacet = UbiquityPoolFacet(address(diamond)); + poolLiquidityMonitor = PoolLiquidityMonitor(address(diamond)); // get all addresses facetAddressList = diamondLoupeFacet.facetAddresses(); diff --git a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol index 6e7f3d66c..2025a23a0 100644 --- a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import "../../../src/dollar/monitors/PoolLiquidityMonitor.sol"; import "../../helpers/LocalTestHelper.sol"; import {DiamondTestSetup} from "../../../test/diamond/DiamondTestSetup.sol"; +import {DEFAULT_ADMIN_ROLE} from "../../../src/dollar/libraries/Constants.sol"; contract PoolLiquidityMonitorTest is DiamondTestSetup { PoolLiquidityMonitor monitor; @@ -13,71 +14,40 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { function setUp() public override { super.setUp(); - - monitor = new PoolLiquidityMonitor( - address(ubiquityPoolFacet), - defenderRelayer, - 30 - ); - } - - function testInitialSetup() public { - assertEq(monitor.defenderRelayer(), defenderRelayer); } function testUnauthorizedCheckLiquidity() public { vm.prank(unauthorized); vm.expectRevert("Not authorized: Only Defender Relayer allowed"); - monitor.checkLiquidityVertex(); + poolLiquidityMonitor.checkLiquidityVertex(); } - function testCheckLiquidity() public { - uint256 mockedLiquidity = 10000; - - vm.mockCall( - address(ubiquityPoolFacet), - abi.encodeWithSelector( - UbiquityPoolFacet.collateralUsdBalance.selector - ), - abi.encode(mockedLiquidity) - ); + function testUnauthorizedSetDefenderRelayer() public { + address newRelayer = address(0x789); - vm.prank(defenderRelayer); - monitor.checkLiquidityVertex(); + vm.expectRevert("Manager: Caller is not admin"); + poolLiquidityMonitor.setDefenderRelayer(newRelayer); } function testSetDefenderRelayer() public { address newRelayer = address(0x789); - vm.expectRevert("Manager: Caller is not admin"); - monitor.setDefenderRelayer(newRelayer); + vm.prank(admin); + poolLiquidityMonitor.setDefenderRelayer(newRelayer); } - function testCheckLiquidityWithDifferentValues() public { - uint256 mockedLiquidityHigh = 10000; - uint256 mockedLiquidityLow = 100; + function testSetThresholdPercentage() public { + uint256 newThresholdPercentage = 30; - vm.mockCall( - address(ubiquityPoolFacet), - abi.encodeWithSelector( - UbiquityPoolFacet.collateralUsdBalance.selector - ), - abi.encode(mockedLiquidityHigh) - ); - - vm.prank(defenderRelayer); - monitor.checkLiquidityVertex(); + vm.prank(admin); + poolLiquidityMonitor.setThresholdPercentage(newThresholdPercentage); + } - vm.mockCall( - address(ubiquityPoolFacet), - abi.encodeWithSelector( - UbiquityPoolFacet.collateralUsdBalance.selector - ), - abi.encode(mockedLiquidityLow) - ); + function testDropLiquidityVertex() public { + vm.expectRevert("Insufficient liquidity"); - vm.prank(defenderRelayer); - monitor.checkLiquidityVertex(); + vm.prank(admin); + poolLiquidityMonitor.dropLiquidityVertex(); } } From e032f0cdcdbd8aa9941e17a78bbb55b67fe5a628 Mon Sep 17 00:00:00 2001 From: green Date: Sun, 15 Sep 2024 14:13:25 +0200 Subject: [PATCH 08/26] test(pool-monitor): increase pool-monitor test coverage --- .../contracts/test/diamond/DiamondTest.t.sol | 2 +- .../monitors/PoolLiquidityMonitorTest.t.sol | 62 ++++++++++++++----- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/packages/contracts/test/diamond/DiamondTest.t.sol b/packages/contracts/test/diamond/DiamondTest.t.sol index f4b530683..2ad0a8790 100644 --- a/packages/contracts/test/diamond/DiamondTest.t.sol +++ b/packages/contracts/test/diamond/DiamondTest.t.sol @@ -19,7 +19,7 @@ contract TestDiamond is DiamondTestSetup { } function testHasMultipleFacets() public { - assertEq(facetAddressList.length, 19); + assertEq(facetAddressList.length, 20); } function testFacetsHaveCorrectSelectors() public { diff --git a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol index 2025a23a0..724c0ec8c 100644 --- a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol @@ -14,40 +14,72 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { function setUp() public override { super.setUp(); + + vm.prank(admin); + poolLiquidityMonitor.setDefenderRelayer(defenderRelayer); } - function testUnauthorizedCheckLiquidity() public { - vm.prank(unauthorized); - vm.expectRevert("Not authorized: Only Defender Relayer allowed"); + function testSetThresholdPercentage() public { + uint256 newThresholdPercentage = 30; - poolLiquidityMonitor.checkLiquidityVertex(); + vm.prank(admin); + poolLiquidityMonitor.setThresholdPercentage(newThresholdPercentage); } - function testUnauthorizedSetDefenderRelayer() public { - address newRelayer = address(0x789); + function testUnauthorizedSetThresholdPercentage() public { + uint256 newThresholdPercentage = 30; vm.expectRevert("Manager: Caller is not admin"); - poolLiquidityMonitor.setDefenderRelayer(newRelayer); + poolLiquidityMonitor.setThresholdPercentage(newThresholdPercentage); } - function testSetDefenderRelayer() public { - address newRelayer = address(0x789); + function testDropLiquidityVertex() public { + vm.expectRevert("Insufficient liquidity"); vm.prank(admin); - poolLiquidityMonitor.setDefenderRelayer(newRelayer); + poolLiquidityMonitor.dropLiquidityVertex(); } - function testSetThresholdPercentage() public { - uint256 newThresholdPercentage = 30; + function testUnauthorizedDropLiquidityVertex() public { + vm.expectRevert("Manager: Caller is not admin"); + poolLiquidityMonitor.dropLiquidityVertex(); + } + function testTogglePaused() public { vm.prank(admin); - poolLiquidityMonitor.setThresholdPercentage(newThresholdPercentage); + poolLiquidityMonitor.togglePaused(); } - function testDropLiquidityVertex() public { + function testUnauthorizedTogglePaused() public { + vm.expectRevert("Manager: Caller is not admin"); + poolLiquidityMonitor.togglePaused(); + } + + function testUnauthorizedCheckLiquidity() public { + vm.prank(unauthorized); + vm.expectRevert("Not authorized: Only Defender Relayer allowed"); + + poolLiquidityMonitor.checkLiquidityVertex(); + } + + function testCheckLiquidity() public { vm.expectRevert("Insufficient liquidity"); + vm.prank(defenderRelayer); + poolLiquidityMonitor.checkLiquidityVertex(); + } + + function testSetDefenderRelayer() public { + address newRelayer = address(0x789); + vm.prank(admin); - poolLiquidityMonitor.dropLiquidityVertex(); + poolLiquidityMonitor.setDefenderRelayer(newRelayer); + } + + function testUnauthorizedSetDefenderRelayer() public { + address newRelayer = address(0x789); + + vm.expectRevert("Manager: Caller is not admin"); + poolLiquidityMonitor.setDefenderRelayer(newRelayer); } } From 1df5a772d665b975c71666cfd68826bbd4aca3bc Mon Sep 17 00:00:00 2001 From: green Date: Sun, 15 Sep 2024 15:16:40 +0200 Subject: [PATCH 09/26] test(pool-monitor): add liquidity drop test --- .../dollar/monitors/PoolLiquidityMonitor.sol | 2 - .../monitors/PoolLiquidityMonitorTest.t.sol | 184 ++++++++++++++++-- 2 files changed, 168 insertions(+), 18 deletions(-) diff --git a/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol b/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol index 6b56a56a3..438c88212 100644 --- a/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol +++ b/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import "../facets/UbiquityPoolFacet.sol"; import {Modifiers} from "../libraries/LibAppStorage.sol"; import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; import {DEFAULT_ADMIN_ROLE} from "../libraries/Constants.sol"; @@ -11,7 +10,6 @@ import "forge-std/console.sol"; contract PoolLiquidityMonitor is Modifiers { using SafeMath for uint256; - UbiquityPoolFacet public ubiquityPoolFacet; address public defenderRelayer; uint256 public liquidityVertex; bool public monitorPaused; diff --git a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol index 724c0ec8c..4f9aabd78 100644 --- a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol @@ -6,17 +6,152 @@ import "../../../src/dollar/monitors/PoolLiquidityMonitor.sol"; import "../../helpers/LocalTestHelper.sol"; import {DiamondTestSetup} from "../../../test/diamond/DiamondTestSetup.sol"; import {DEFAULT_ADMIN_ROLE} from "../../../src/dollar/libraries/Constants.sol"; +import {MockChainLinkFeed} from "../../../src/dollar/mocks/MockChainLinkFeed.sol"; +import {MockERC20} from "../../../src/dollar/mocks/MockERC20.sol"; +import {MockCurveStableSwapNG} from "../../../src/dollar/mocks/MockCurveStableSwapNG.sol"; +import {MockCurveTwocryptoOptimized} from "../../../src/dollar/mocks/MockCurveTwocryptoOptimized.sol"; contract PoolLiquidityMonitorTest is DiamondTestSetup { PoolLiquidityMonitor monitor; address defenderRelayer = address(0x456); address unauthorized = address(0x123); + MockERC20 collateralToken; + MockERC20 stableToken; + MockERC20 wethToken; + + // mock three ChainLink price feeds, one for each token + MockChainLinkFeed collateralTokenPriceFeed; + MockChainLinkFeed ethUsdPriceFeed; + MockChainLinkFeed stableUsdPriceFeed; + + // mock two curve pools Stablecoin/Dollar and Governance/WETH + MockCurveStableSwapNG curveDollarPlainPool; + MockCurveTwocryptoOptimized curveGovernanceEthPool; + + address user = address(1); + function setUp() public override { super.setUp(); - vm.prank(admin); + vm.startPrank(admin); + + collateralToken = new MockERC20("COLLATERAL", "CLT", 18); + wethToken = new MockERC20("WETH", "WETH", 18); + stableToken = new MockERC20("STABLE", "STABLE", 18); + + collateralTokenPriceFeed = new MockChainLinkFeed(); + ethUsdPriceFeed = new MockChainLinkFeed(); + stableUsdPriceFeed = new MockChainLinkFeed(); + + curveDollarPlainPool = new MockCurveStableSwapNG( + address(stableToken), + address(dollarToken) + ); + + curveGovernanceEthPool = new MockCurveTwocryptoOptimized( + address(governanceToken), + address(wethToken) + ); + + // add collateral token to the pool + uint256 poolCeiling = 50_000e18; // max 50_000 of collateral tokens is allowed + ubiquityPoolFacet.addCollateralToken( + address(collateralToken), + address(collateralTokenPriceFeed), + poolCeiling + ); + + // set collateral price initial feed mock params + collateralTokenPriceFeed.updateMockParams( + 1, // round id + 100_000_000, // answer, 100_000_000 = $1.00 (chainlink 8 decimals answer is converted to 6 decimals pool price) + block.timestamp, // started at + block.timestamp, // updated at + 1 // answered in round + ); + + // set ETH/USD price initial feed mock params + ethUsdPriceFeed.updateMockParams( + 1, // round id + 2000_00000000, // answer, 2000_00000000 = $2000 (8 decimals) + block.timestamp, // started at + block.timestamp, // updated at + 1 // answered in round + ); + + // set stable/USD price feed initial mock params + stableUsdPriceFeed.updateMockParams( + 1, // round id + 100_000_000, // answer, 100_000_000 = $1.00 (8 decimals) + block.timestamp, // started at + block.timestamp, // updated at + 1 // answered in round + ); + + // set ETH/Governance initial price to 20k in Curve pool mock (20k GOV == 1 ETH) + curveGovernanceEthPool.updateMockParams(20_000e18); + + curveDollarPlainPool.updateMockParams(1.01e18); + + // set price feed for collateral token + ubiquityPoolFacet.setCollateralChainLinkPriceFeed( + address(collateralToken), // collateral token address + address(collateralTokenPriceFeed), // price feed address + 1 days // price feed staleness threshold in seconds + ); + + // set price feed for ETH/USD pair + ubiquityPoolFacet.setEthUsdChainLinkPriceFeed( + address(ethUsdPriceFeed), // price feed address + 1 days // price feed staleness threshold in seconds + ); + + // set price feed for stable/USD pair + ubiquityPoolFacet.setStableUsdChainLinkPriceFeed( + address(stableUsdPriceFeed), // price feed address + 1 days // price feed staleness threshold in seconds + ); + + // enable collateral at index 0 + ubiquityPoolFacet.toggleCollateral(0); + // set mint and redeem initial fees + ubiquityPoolFacet.setFees( + 0, // collateral index + 10000, // 1% mint fee + 20000 // 2% redeem fee + ); + // set redemption delay to 2 blocks + ubiquityPoolFacet.setRedemptionDelayBlocks(2); + // set mint price threshold to $1.01 and redeem price to $0.99 + ubiquityPoolFacet.setPriceThresholds(1010000, 990000); + // set collateral ratio to 100% + ubiquityPoolFacet.setCollateralRatio(1_000_000); + // set Governance-ETH pool + ubiquityPoolFacet.setGovernanceEthPoolAddress( + address(curveGovernanceEthPool) + ); + + // set Curve plain pool in manager facet + managerFacet.setStableSwapPlainPoolAddress( + address(curveDollarPlainPool) + ); + poolLiquidityMonitor.setDefenderRelayer(defenderRelayer); + + // stop being admin + vm.stopPrank(); + + // mint 2000 Governance tokens to the user + deal(address(governanceToken), user, 2000e18); + // mint 2000 collateral tokens to the user + collateralToken.mint(address(user), 2000e18); + // user approves the pool to transfer collateral + vm.prank(user); + collateralToken.approve(address(ubiquityPoolFacet), 100e18); + + vm.prank(user); + ubiquityPoolFacet.mintDollar(0, 1e18, 0.9e18, 1e18, 0, true); } function testSetThresholdPercentage() public { @@ -34,8 +169,6 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { } function testDropLiquidityVertex() public { - vm.expectRevert("Insufficient liquidity"); - vm.prank(admin); poolLiquidityMonitor.dropLiquidityVertex(); } @@ -55,6 +188,25 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { poolLiquidityMonitor.togglePaused(); } + function testSetDefenderRelayer() public { + address newRelayer = address(0x789); + + vm.prank(admin); + poolLiquidityMonitor.setDefenderRelayer(newRelayer); + } + + function testUnauthorizedSetDefenderRelayer() public { + address newRelayer = address(0x789); + + vm.expectRevert("Manager: Caller is not admin"); + poolLiquidityMonitor.setDefenderRelayer(newRelayer); + } + + function testCheckLiquidity() public { + vm.prank(defenderRelayer); + poolLiquidityMonitor.checkLiquidityVertex(); + } + function testUnauthorizedCheckLiquidity() public { vm.prank(unauthorized); vm.expectRevert("Not authorized: Only Defender Relayer allowed"); @@ -62,24 +214,24 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { poolLiquidityMonitor.checkLiquidityVertex(); } - function testCheckLiquidity() public { - vm.expectRevert("Insufficient liquidity"); - + function testLiquidityDropBelowVertex() public { vm.prank(defenderRelayer); poolLiquidityMonitor.checkLiquidityVertex(); - } - function testSetDefenderRelayer() public { - address newRelayer = address(0x789); + curveDollarPlainPool.updateMockParams(0.99e18); - vm.prank(admin); - poolLiquidityMonitor.setDefenderRelayer(newRelayer); - } + vm.prank(user); + ubiquityPoolFacet.redeemDollar(0, 5e17, 0, 0); - function testUnauthorizedSetDefenderRelayer() public { - address newRelayer = address(0x789); + // Call the checkLiquidityVertex function to test behavior after the liquidity drop + vm.prank(defenderRelayer); + poolLiquidityMonitor.checkLiquidityVertex(); - vm.expectRevert("Manager: Caller is not admin"); - poolLiquidityMonitor.setDefenderRelayer(newRelayer); + // Assert that the liquidity monitor paused after detecting a large drop + bool monitorPaused = poolLiquidityMonitor.monitorPaused(); + assertTrue( + monitorPaused, + "Monitor should be paused after liquidity drop" + ); } } From dd032cfe69df2e1f99128b52a9c39b1fdf65fdaf Mon Sep 17 00:00:00 2001 From: green Date: Tue, 17 Sep 2024 22:16:34 +0200 Subject: [PATCH 10/26] feat: remove pool-monitor from the diamond --- .../UbiquityPoolSecurityMonitor.sol} | 69 +++++++++++------- .../src/dollar/libraries/Constants.sol | 3 + .../contracts/test/diamond/DiamondTest.t.sol | 2 +- .../test/diamond/DiamondTestSetup.sol | 23 +----- .../monitors/PoolLiquidityMonitorTest.t.sol | 72 ++++++++++--------- 5 files changed, 86 insertions(+), 83 deletions(-) rename packages/contracts/src/dollar/{monitors/PoolLiquidityMonitor.sol => core/UbiquityPoolSecurityMonitor.sol} (50%) diff --git a/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol similarity index 50% rename from packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol rename to packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol index 438c88212..4f29df8d9 100644 --- a/packages/contracts/src/dollar/monitors/PoolLiquidityMonitor.sol +++ b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol @@ -1,16 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {Modifiers} from "../libraries/LibAppStorage.sol"; import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; -import {DEFAULT_ADMIN_ROLE} from "../libraries/Constants.sol"; -import {LibUbiquityPool} from "../libraries/LibUbiquityPool.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {AccessControlFacet} from "../facets/AccessControlFacet.sol"; +import {UbiquityPoolFacet} from "../facets/UbiquityPoolFacet.sol"; + +import "../libraries/Constants.sol"; import "forge-std/console.sol"; -contract PoolLiquidityMonitor is Modifiers { +contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { using SafeMath for uint256; - address public defenderRelayer; + AccessControlFacet public accessControlFacet; + UbiquityPoolFacet public ubiquityPoolFacet; uint256 public liquidityVertex; bool public monitorPaused; uint256 public thresholdPercentage; @@ -20,35 +24,50 @@ contract PoolLiquidityMonitor is Modifiers { event VertexDropped(); event PausedToggled(bool paused); - modifier onlyAuthorized() { + modifier onlyDefender() { require( - msg.sender == defenderRelayer, - "Not authorized: Only Defender Relayer allowed" + accessControlFacet.hasRole(DEFENDER_RELAYER_ROLE, msg.sender), + "Ubiquity Pool Security Monitor: not defender relayer" ); _; } + modifier onlyMonitorAdmin() { + require( + accessControlFacet.hasRole(DEFAULT_ADMIN_ROLE, msg.sender), + "Ubiquity Pool Security Monitor: not admin" + ); + _; + } + + function initialize( + address _accessControlFacet, + address _ubiquityPoolFacet + ) public initializer { + thresholdPercentage = 30; + + accessControlFacet = AccessControlFacet(_accessControlFacet); + ubiquityPoolFacet = UbiquityPoolFacet(_ubiquityPoolFacet); + } + + function _authorizeUpgrade( + address newImplementation + ) internal override onlyMonitorAdmin {} + function setThresholdPercentage( uint256 _newThresholdPercentage - ) external onlyAdmin { + ) external onlyMonitorAdmin { thresholdPercentage = _newThresholdPercentage; } - function setDefenderRelayer( - address _newDefenderRelayer - ) external onlyAdmin { - defenderRelayer = _newDefenderRelayer; - } - - function togglePaused() external onlyAdmin { + function togglePaused() external onlyMonitorAdmin { monitorPaused = !monitorPaused; emit PausedToggled(monitorPaused); } - function dropLiquidityVertex() external onlyAdmin { - uint256 currentCollateralLiquidity = LibUbiquityPool + function dropLiquidityVertex() external onlyMonitorAdmin { + uint256 currentCollateralLiquidity = ubiquityPoolFacet .collateralUsdBalance(); - require(currentCollateralLiquidity > 0, "Insufficient liquidity"); liquidityVertex = currentCollateralLiquidity; @@ -56,8 +75,8 @@ contract PoolLiquidityMonitor is Modifiers { emit VertexDropped(); } - function checkLiquidityVertex() external onlyAuthorized { - uint256 currentCollateralLiquidity = LibUbiquityPool + function checkLiquidityVertex() external onlyDefender { + uint256 currentCollateralLiquidity = ubiquityPoolFacet .collateralUsdBalance(); require(currentCollateralLiquidity > 0, "Insufficient liquidity"); @@ -65,7 +84,6 @@ contract PoolLiquidityMonitor is Modifiers { if (currentCollateralLiquidity > liquidityVertex) { liquidityVertex = currentCollateralLiquidity; - emit LiquidityVertexUpdated(liquidityVertex); } else { uint256 liquidityDiffPercentage = liquidityVertex @@ -74,11 +92,10 @@ contract PoolLiquidityMonitor is Modifiers { .div(liquidityVertex); if (liquidityDiffPercentage >= thresholdPercentage) { - monitorPaused = true; - - // Pause the UbiquityDollarToken - // Pause LibUbiquityPool by disabling collateral + // a) Pause the UbiquityDollarToken + // b) Pause LibUbiquityPool by disabling collateral + monitorPaused = true; emit MonitorPaused( currentCollateralLiquidity, liquidityDiffPercentage diff --git a/packages/contracts/src/dollar/libraries/Constants.sol b/packages/contracts/src/dollar/libraries/Constants.sol index 006e1e9c4..44f6355d6 100644 --- a/packages/contracts/src/dollar/libraries/Constants.sol +++ b/packages/contracts/src/dollar/libraries/Constants.sol @@ -68,6 +68,9 @@ bytes32 constant GOVERNANCE_TOKEN_MANAGER_ROLE = keccak256( "GOVERNANCE_TOKEN_MANAGER_ROLE" ); +/// @dev Role name for Governance token manager +bytes32 constant DEFENDER_RELAYER_ROLE = keccak256("DEFENDER_RELAYER_ROLE"); + /// @dev ETH pseudo address used to distinguish ERC20 tokens and ETH in `LibCollectableDust.sendDust()` address constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; diff --git a/packages/contracts/test/diamond/DiamondTest.t.sol b/packages/contracts/test/diamond/DiamondTest.t.sol index 2ad0a8790..f4b530683 100644 --- a/packages/contracts/test/diamond/DiamondTest.t.sol +++ b/packages/contracts/test/diamond/DiamondTest.t.sol @@ -19,7 +19,7 @@ contract TestDiamond is DiamondTestSetup { } function testHasMultipleFacets() public { - assertEq(facetAddressList.length, 20); + assertEq(facetAddressList.length, 19); } function testFacetsHaveCorrectSelectors() public { diff --git a/packages/contracts/test/diamond/DiamondTestSetup.sol b/packages/contracts/test/diamond/DiamondTestSetup.sol index 3eaab57be..39362a306 100644 --- a/packages/contracts/test/diamond/DiamondTestSetup.sol +++ b/packages/contracts/test/diamond/DiamondTestSetup.sol @@ -33,7 +33,6 @@ import {MockERC20} from "../../src/dollar/mocks/MockERC20.sol"; import {DiamondInit} from "../../src/dollar/upgradeInitializers/DiamondInit.sol"; import {DiamondTestHelper} from "../helpers/DiamondTestHelper.sol"; import {UUPSTestHelper} from "../helpers/UUPSTestHelper.sol"; -import {PoolLiquidityMonitor} from "../../src/dollar/monitors/PoolLiquidityMonitor.sol"; import {CREDIT_NFT_MANAGER_ROLE, CREDIT_TOKEN_BURNER_ROLE, CREDIT_TOKEN_MINTER_ROLE, CURVE_DOLLAR_MANAGER_ROLE, DOLLAR_TOKEN_BURNER_ROLE, DOLLAR_TOKEN_MINTER_ROLE, GOVERNANCE_TOKEN_BURNER_ROLE, GOVERNANCE_TOKEN_MANAGER_ROLE, GOVERNANCE_TOKEN_MINTER_ROLE, STAKING_SHARE_MINTER_ROLE} from "../../src/dollar/libraries/Constants.sol"; /** @@ -64,7 +63,6 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { StakingFacet stakingFacet; StakingFormulasFacet stakingFormulasFacet; UbiquityPoolFacet ubiquityPoolFacet; - PoolLiquidityMonitor poolLiquidityMonitor; // diamond facet implementation instances (should not be used in tests, use only on upgrades) AccessControlFacet accessControlFacetImplementation; @@ -86,7 +84,6 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { StakingFacet stakingFacetImplementation; StakingFormulasFacet stakingFormulasFacetImplementation; UbiquityPoolFacet ubiquityPoolFacetImplementation; - PoolLiquidityMonitor poolLiquidityMonitorImplementation; // facet names with addresses string[] facetNames; @@ -119,7 +116,6 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { bytes4[] selectorsOfStakingFacet; bytes4[] selectorsOfStakingFormulasFacet; bytes4[] selectorsOfUbiquityPoolFacet; - bytes4[] selectorsOfPoolLiquidityMonitor; /// @notice Deploys diamond and connects facets function setUp() public virtual { @@ -189,10 +185,6 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { "/out/UbiquityPoolFacet.sol/UbiquityPoolFacet.json" ); - selectorsOfPoolLiquidityMonitor = getSelectorsFromAbi( - "/out/PoolLiquidityMonitor.sol/PoolLiquidityMonitor.json" - ); - // deploy facet implementation instances accessControlFacetImplementation = new AccessControlFacet(); bondingCurveFacetImplementation = new BondingCurveFacet(); @@ -213,7 +205,6 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { stakingFacetImplementation = new StakingFacet(); stakingFormulasFacetImplementation = new StakingFormulasFacet(); ubiquityPoolFacetImplementation = new UbiquityPoolFacet(); - poolLiquidityMonitorImplementation = new PoolLiquidityMonitor(); // prepare diamond init args diamondInit = new DiamondInit(); @@ -236,8 +227,7 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { "OwnershipFacet", "StakingFacet", "StakingFormulasFacet", - "UbiquityPoolFacet", - "PoolLiquidityMonitor" + "UbiquityPoolFacet" ]; DiamondInit.Args memory initArgs = DiamondInit.Args({ admin: admin, @@ -257,7 +247,7 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { ) }); - FacetCut[] memory cuts = new FacetCut[](20); + FacetCut[] memory cuts = new FacetCut[](19); cuts[0] = ( FacetCut({ @@ -399,14 +389,6 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { }) ); - cuts[19] = ( - FacetCut({ - facetAddress: address(poolLiquidityMonitorImplementation), - action: FacetCutAction.Add, - functionSelectors: selectorsOfPoolLiquidityMonitor - }) - ); - // deploy diamond vm.prank(owner); diamond = new Diamond(_args, cuts); @@ -437,7 +419,6 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { stakingFacet = StakingFacet(address(diamond)); stakingFormulasFacet = StakingFormulasFacet(address(diamond)); ubiquityPoolFacet = UbiquityPoolFacet(address(diamond)); - poolLiquidityMonitor = PoolLiquidityMonitor(address(diamond)); // get all addresses facetAddressList = diamondLoupeFacet.facetAddresses(); diff --git a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol index 4f9aabd78..4257d41c2 100644 --- a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; -import "../../../src/dollar/monitors/PoolLiquidityMonitor.sol"; +import "../../../src/dollar/core/UbiquityPoolSecurityMonitor.sol"; import "../../helpers/LocalTestHelper.sol"; import {DiamondTestSetup} from "../../../test/diamond/DiamondTestSetup.sol"; import {DEFAULT_ADMIN_ROLE} from "../../../src/dollar/libraries/Constants.sol"; @@ -10,9 +10,10 @@ import {MockChainLinkFeed} from "../../../src/dollar/mocks/MockChainLinkFeed.sol import {MockERC20} from "../../../src/dollar/mocks/MockERC20.sol"; import {MockCurveStableSwapNG} from "../../../src/dollar/mocks/MockCurveStableSwapNG.sol"; import {MockCurveTwocryptoOptimized} from "../../../src/dollar/mocks/MockCurveTwocryptoOptimized.sol"; +import {ERC20Ubiquity} from "../../../src/dollar/core/ERC20Ubiquity.sol"; contract PoolLiquidityMonitorTest is DiamondTestSetup { - PoolLiquidityMonitor monitor; + UbiquityPoolSecurityMonitor monitor; address defenderRelayer = address(0x456); address unauthorized = address(0x123); @@ -137,7 +138,14 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { address(curveDollarPlainPool) ); - poolLiquidityMonitor.setDefenderRelayer(defenderRelayer); + accessControlFacet.grantRole(DEFENDER_RELAYER_ROLE, defenderRelayer); + + // Initialize the UbiquityPoolSecurityMonitor contract + monitor = new UbiquityPoolSecurityMonitor(); + monitor.initialize( + address(accessControlFacet), + address(ubiquityPoolFacet) + ); // stop being admin vm.stopPrank(); @@ -155,83 +163,77 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { } function testSetThresholdPercentage() public { - uint256 newThresholdPercentage = 30; + uint256 newThresholdPercentage = 20; vm.prank(admin); - poolLiquidityMonitor.setThresholdPercentage(newThresholdPercentage); + monitor.setThresholdPercentage(newThresholdPercentage); } function testUnauthorizedSetThresholdPercentage() public { uint256 newThresholdPercentage = 30; - vm.expectRevert("Manager: Caller is not admin"); - poolLiquidityMonitor.setThresholdPercentage(newThresholdPercentage); + vm.expectRevert("Ubiquity Pool Security Monitor: not admin"); + monitor.setThresholdPercentage(newThresholdPercentage); } function testDropLiquidityVertex() public { vm.prank(admin); - poolLiquidityMonitor.dropLiquidityVertex(); + monitor.dropLiquidityVertex(); } function testUnauthorizedDropLiquidityVertex() public { - vm.expectRevert("Manager: Caller is not admin"); - poolLiquidityMonitor.dropLiquidityVertex(); + vm.expectRevert("Ubiquity Pool Security Monitor: not admin"); + monitor.dropLiquidityVertex(); } function testTogglePaused() public { vm.prank(admin); - poolLiquidityMonitor.togglePaused(); + monitor.togglePaused(); } function testUnauthorizedTogglePaused() public { - vm.expectRevert("Manager: Caller is not admin"); - poolLiquidityMonitor.togglePaused(); - } - - function testSetDefenderRelayer() public { - address newRelayer = address(0x789); - - vm.prank(admin); - poolLiquidityMonitor.setDefenderRelayer(newRelayer); - } - - function testUnauthorizedSetDefenderRelayer() public { - address newRelayer = address(0x789); - - vm.expectRevert("Manager: Caller is not admin"); - poolLiquidityMonitor.setDefenderRelayer(newRelayer); + vm.expectRevert("Ubiquity Pool Security Monitor: not admin"); + monitor.togglePaused(); } function testCheckLiquidity() public { vm.prank(defenderRelayer); - poolLiquidityMonitor.checkLiquidityVertex(); + monitor.checkLiquidityVertex(); } function testUnauthorizedCheckLiquidity() public { vm.prank(unauthorized); - vm.expectRevert("Not authorized: Only Defender Relayer allowed"); + vm.expectRevert("Ubiquity Pool Security Monitor: not defender relayer"); - poolLiquidityMonitor.checkLiquidityVertex(); + monitor.checkLiquidityVertex(); } function testLiquidityDropBelowVertex() public { vm.prank(defenderRelayer); - poolLiquidityMonitor.checkLiquidityVertex(); + monitor.checkLiquidityVertex(); curveDollarPlainPool.updateMockParams(0.99e18); vm.prank(user); ubiquityPoolFacet.redeemDollar(0, 5e17, 0, 0); - // Call the checkLiquidityVertex function to test behavior after the liquidity drop vm.prank(defenderRelayer); - poolLiquidityMonitor.checkLiquidityVertex(); + monitor.checkLiquidityVertex(); - // Assert that the liquidity monitor paused after detecting a large drop - bool monitorPaused = poolLiquidityMonitor.monitorPaused(); + bool monitorPaused = monitor.monitorPaused(); assertTrue( monitorPaused, "Monitor should be paused after liquidity drop" ); } + + function testCheckLiquidityWhenPaused() public { + vm.prank(admin); + monitor.togglePaused(); + + vm.expectRevert("Monitor paused"); + + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + } } From b5dc0543697a956c36f7eda1e401c4c511126829 Mon Sep 17 00:00:00 2001 From: green Date: Wed, 18 Sep 2024 23:00:15 +0200 Subject: [PATCH 11/26] feat: add pause lib-ubiquity-pool logic --- .../core/UbiquityPoolSecurityMonitor.sol | 24 ++++++++++++++++--- .../PoolLiquidityMonitorTest.t.sol | 6 +++++ 2 files changed, 27 insertions(+), 3 deletions(-) rename packages/contracts/test/dollar/{monitors => core}/PoolLiquidityMonitorTest.t.sol (97%) diff --git a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol index 4f29df8d9..54ce0c8a4 100644 --- a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol +++ b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol @@ -6,7 +6,7 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {AccessControlFacet} from "../facets/AccessControlFacet.sol"; import {UbiquityPoolFacet} from "../facets/UbiquityPoolFacet.sol"; - +import {LibUbiquityPool} from "../libraries/LibUbiquityPool.sol"; import "../libraries/Constants.sol"; import "forge-std/console.sol"; @@ -92,8 +92,10 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { .div(liquidityVertex); if (liquidityDiffPercentage >= thresholdPercentage) { - // a) Pause the UbiquityDollarToken - // b) Pause LibUbiquityPool by disabling collateral + // TODO: Pause the UbiquityDollarToken + + // Pause LibUbiquityPool by disabling collateral + _pauseLibUbiquityPool(); monitorPaused = true; emit MonitorPaused( @@ -103,4 +105,20 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { } } } + + function _pauseLibUbiquityPool() internal { + address[] memory allCollaterals = ubiquityPoolFacet.allCollaterals(); + + for (uint256 i = 0; i < allCollaterals.length; i++) { + try + ubiquityPoolFacet.collateralInformation(allCollaterals[i]) + returns ( + LibUbiquityPool.CollateralInformation memory collateralInfo + ) { + ubiquityPoolFacet.toggleCollateral(collateralInfo.index); + } catch { + continue; + } + } + } } diff --git a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol similarity index 97% rename from packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol rename to packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol index 4257d41c2..1bab8f532 100644 --- a/packages/contracts/test/dollar/monitors/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol @@ -147,6 +147,8 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { address(ubiquityPoolFacet) ); + accessControlFacet.grantRole(DEFAULT_ADMIN_ROLE, address(monitor)); + // stop being admin vm.stopPrank(); @@ -220,7 +222,11 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); + vm.expectRevert("Invalid collateral"); + ubiquityPoolFacet.collateralInformation(address(collateralToken)); + bool monitorPaused = monitor.monitorPaused(); + assertTrue( monitorPaused, "Monitor should be paused after liquidity drop" From 07fc15d8525b06d3e314330e20719a1ee49c400e Mon Sep 17 00:00:00 2001 From: green Date: Thu, 19 Sep 2024 11:42:19 +0200 Subject: [PATCH 12/26] feat: potential reentrancy fixed --- .../contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol index 54ce0c8a4..6e9ff5d2e 100644 --- a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol +++ b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol @@ -92,12 +92,12 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { .div(liquidityVertex); if (liquidityDiffPercentage >= thresholdPercentage) { + monitorPaused = true; // TODO: Pause the UbiquityDollarToken // Pause LibUbiquityPool by disabling collateral _pauseLibUbiquityPool(); - monitorPaused = true; emit MonitorPaused( currentCollateralLiquidity, liquidityDiffPercentage From 327496453c0109759f66fff586734f7f34e83e09 Mon Sep 17 00:00:00 2001 From: green Date: Thu, 19 Sep 2024 12:35:02 +0200 Subject: [PATCH 13/26] feat: perform act pull_request --- .../contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol index 6e9ff5d2e..49eacab4a 100644 --- a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol +++ b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol @@ -93,6 +93,7 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { if (liquidityDiffPercentage >= thresholdPercentage) { monitorPaused = true; + // TODO: Pause the UbiquityDollarToken // Pause LibUbiquityPool by disabling collateral From b2c16f0fbec0ac921c4ebdb0ef44ad0b226e97f4 Mon Sep 17 00:00:00 2001 From: green Date: Fri, 20 Sep 2024 19:29:54 +0200 Subject: [PATCH 14/26] feat: add dollar pause method --- .../core/UbiquityPoolSecurityMonitor.sol | 35 ++++++++++++- .../core/PoolLiquidityMonitorTest.t.sol | 49 ++++++++++++++++++- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol index 49eacab4a..c71c1538a 100644 --- a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol +++ b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol @@ -7,6 +7,8 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U import {AccessControlFacet} from "../facets/AccessControlFacet.sol"; import {UbiquityPoolFacet} from "../facets/UbiquityPoolFacet.sol"; import {LibUbiquityPool} from "../libraries/LibUbiquityPool.sol"; +import {ERC20Ubiquity} from "./ERC20Ubiquity.sol"; +import {ManagerFacet} from "../facets/ManagerFacet.sol"; import "../libraries/Constants.sol"; import "forge-std/console.sol"; @@ -15,6 +17,7 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { AccessControlFacet public accessControlFacet; UbiquityPoolFacet public ubiquityPoolFacet; + ManagerFacet public managerFacet; uint256 public liquidityVertex; bool public monitorPaused; uint256 public thresholdPercentage; @@ -42,18 +45,38 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { function initialize( address _accessControlFacet, - address _ubiquityPoolFacet + address _ubiquityPoolFacet, + address _managerFacet ) public initializer { thresholdPercentage = 30; accessControlFacet = AccessControlFacet(_accessControlFacet); ubiquityPoolFacet = UbiquityPoolFacet(_ubiquityPoolFacet); + managerFacet = ManagerFacet(_managerFacet); } function _authorizeUpgrade( address newImplementation ) internal override onlyMonitorAdmin {} + function setManagerFacet( + address _newManagerFacet + ) external onlyMonitorAdmin { + managerFacet = ManagerFacet(_newManagerFacet); + } + + function setUbiquityPoolFacet( + address _newUbiquityPoolFacet + ) external onlyMonitorAdmin { + ubiquityPoolFacet = UbiquityPoolFacet(_newUbiquityPoolFacet); + } + + function setAccessControlFacet( + address _newAccessControlFacet + ) external onlyMonitorAdmin { + accessControlFacet = AccessControlFacet(_newAccessControlFacet); + } + function setThresholdPercentage( uint256 _newThresholdPercentage ) external onlyMonitorAdmin { @@ -94,7 +117,8 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { if (liquidityDiffPercentage >= thresholdPercentage) { monitorPaused = true; - // TODO: Pause the UbiquityDollarToken + // Pause the UbiquityDollarToken + _pauseUbiquityDollarToken(); // Pause LibUbiquityPool by disabling collateral _pauseLibUbiquityPool(); @@ -122,4 +146,11 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { } } } + + function _pauseUbiquityDollarToken() internal { + ERC20Ubiquity dollarToken = ERC20Ubiquity( + managerFacet.dollarTokenAddress() + ); + dollarToken.pause(); + } } diff --git a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol index 1bab8f532..d9921e8b9 100644 --- a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import "../../../src/dollar/core/UbiquityPoolSecurityMonitor.sol"; import "../../helpers/LocalTestHelper.sol"; import {DiamondTestSetup} from "../../../test/diamond/DiamondTestSetup.sol"; -import {DEFAULT_ADMIN_ROLE} from "../../../src/dollar/libraries/Constants.sol"; +import {DEFAULT_ADMIN_ROLE, PAUSER_ROLE} from "../../../src/dollar/libraries/Constants.sol"; import {MockChainLinkFeed} from "../../../src/dollar/mocks/MockChainLinkFeed.sol"; import {MockERC20} from "../../../src/dollar/mocks/MockERC20.sol"; import {MockCurveStableSwapNG} from "../../../src/dollar/mocks/MockCurveStableSwapNG.sol"; @@ -16,6 +16,9 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { UbiquityPoolSecurityMonitor monitor; address defenderRelayer = address(0x456); address unauthorized = address(0x123); + address newManagerFacet = address(0x457); + address newUbiquityPoolFacet = address(0x458); + address newAccessControlFacet = address(0x459); MockERC20 collateralToken; MockERC20 stableToken; @@ -144,10 +147,12 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { monitor = new UbiquityPoolSecurityMonitor(); monitor.initialize( address(accessControlFacet), - address(ubiquityPoolFacet) + address(ubiquityPoolFacet), + address(managerFacet) ); accessControlFacet.grantRole(DEFAULT_ADMIN_ROLE, address(monitor)); + accessControlFacet.grantRole(PAUSER_ROLE, address(monitor)); // stop being admin vm.stopPrank(); @@ -164,6 +169,36 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { ubiquityPoolFacet.mintDollar(0, 1e18, 0.9e18, 1e18, 0, true); } + function testSetManagerFacet() public { + vm.prank(admin); + monitor.setManagerFacet(newManagerFacet); + } + + function testUnauthorizedSetManagerFacet() public { + vm.expectRevert("Ubiquity Pool Security Monitor: not admin"); + monitor.setManagerFacet(newManagerFacet); + } + + function testSetUbiquityPoolFacet() public { + vm.prank(admin); + monitor.setUbiquityPoolFacet(newUbiquityPoolFacet); + } + + function testUnauthorizedSetUbiquityPoolFacet() public { + vm.expectRevert("Ubiquity Pool Security Monitor: not admin"); + monitor.setUbiquityPoolFacet(newUbiquityPoolFacet); + } + + function testSetAccessControlFacet() public { + vm.prank(admin); + monitor.setAccessControlFacet(newAccessControlFacet); + } + + function testUnauthorizedSetAccessControlFacet() public { + vm.expectRevert("Ubiquity Pool Security Monitor: not admin"); + monitor.setAccessControlFacet(newAccessControlFacet); + } + function testSetThresholdPercentage() public { uint256 newThresholdPercentage = 20; @@ -231,6 +266,16 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { monitorPaused, "Monitor should be paused after liquidity drop" ); + + ERC20Ubiquity dollarToken = ERC20Ubiquity( + managerFacet.dollarTokenAddress() + ); + bool dollarIsPaused = dollarToken.paused(); + + assertTrue( + dollarIsPaused, + "Dollar should be paused after liquidity drop" + ); } function testCheckLiquidityWhenPaused() public { From 428fb6d4a52bacd06ab82c26c38489323c4d1370 Mon Sep 17 00:00:00 2001 From: green Date: Fri, 20 Sep 2024 19:32:29 +0200 Subject: [PATCH 15/26] feat: update dollar pause method --- .../src/dollar/core/UbiquityPoolSecurityMonitor.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol index c71c1538a..1d24e7ffe 100644 --- a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol +++ b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol @@ -148,9 +148,7 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { } function _pauseUbiquityDollarToken() internal { - ERC20Ubiquity dollarToken = ERC20Ubiquity( - managerFacet.dollarTokenAddress() - ); - dollarToken.pause(); + ERC20Ubiquity dollar = ERC20Ubiquity(managerFacet.dollarTokenAddress()); + dollar.pause(); } } From 2ddd382d159e5c2d15291a225d64eca312f4879b Mon Sep 17 00:00:00 2001 From: green Date: Fri, 20 Sep 2024 19:57:22 +0200 Subject: [PATCH 16/26] feat(ubiquity-pool-security-monitor): modularize contract --- .../core/UbiquityPoolSecurityMonitor.sol | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol index 1d24e7ffe..99ce0b076 100644 --- a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol +++ b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol @@ -106,28 +106,38 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { require(!monitorPaused, "Monitor paused"); if (currentCollateralLiquidity > liquidityVertex) { - liquidityVertex = currentCollateralLiquidity; - emit LiquidityVertexUpdated(liquidityVertex); + _updateLiquidityVertex(currentCollateralLiquidity); } else { - uint256 liquidityDiffPercentage = liquidityVertex - .sub(currentCollateralLiquidity) - .mul(100) - .div(liquidityVertex); + _checkThresholdPercentage(currentCollateralLiquidity); + } + } + + function _updateLiquidityVertex(uint256 _newLiquidityVertex) internal { + liquidityVertex = _newLiquidityVertex; + emit LiquidityVertexUpdated(liquidityVertex); + } - if (liquidityDiffPercentage >= thresholdPercentage) { - monitorPaused = true; + function _checkThresholdPercentage( + uint256 _currentCollateralLiquidity + ) internal { + uint256 liquidityDiffPercentage = liquidityVertex + .sub(_currentCollateralLiquidity) + .mul(100) + .div(liquidityVertex); - // Pause the UbiquityDollarToken - _pauseUbiquityDollarToken(); + if (liquidityDiffPercentage >= thresholdPercentage) { + monitorPaused = true; - // Pause LibUbiquityPool by disabling collateral - _pauseLibUbiquityPool(); + // Pause the UbiquityDollarToken + _pauseUbiquityDollarToken(); - emit MonitorPaused( - currentCollateralLiquidity, - liquidityDiffPercentage - ); - } + // Pause LibUbiquityPool by disabling collateral + _pauseLibUbiquityPool(); + + emit MonitorPaused( + _currentCollateralLiquidity, + liquidityDiffPercentage + ); } } From 8ee2a93dc01db13dce7251da7b8a4dd13c3159c6 Mon Sep 17 00:00:00 2001 From: green Date: Sat, 21 Sep 2024 18:00:51 +0200 Subject: [PATCH 17/26] feat: increase test coverage and add multi-collateral tests --- .../core/PoolLiquidityMonitorTest.t.sol | 116 +++++++++++++++++- 1 file changed, 112 insertions(+), 4 deletions(-) diff --git a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol index d9921e8b9..6abc65b8a 100644 --- a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol @@ -21,6 +21,9 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { address newAccessControlFacet = address(0x459); MockERC20 collateralToken; + MockERC20 collateralToken2; + MockERC20 collateralToken3; + MockERC20 stableToken; MockERC20 wethToken; @@ -41,6 +44,9 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { vm.startPrank(admin); collateralToken = new MockERC20("COLLATERAL", "CLT", 18); + collateralToken2 = new MockERC20("COLLATERAL-2", "CLT", 18); + collateralToken3 = new MockERC20("COLLATERAL-3", "CLT", 18); + wethToken = new MockERC20("WETH", "WETH", 18); stableToken = new MockERC20("STABLE", "STABLE", 18); @@ -65,6 +71,16 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { address(collateralTokenPriceFeed), poolCeiling ); + ubiquityPoolFacet.addCollateralToken( + address(collateralToken2), + address(collateralTokenPriceFeed), + poolCeiling + ); + ubiquityPoolFacet.addCollateralToken( + address(collateralToken3), + address(collateralTokenPriceFeed), + poolCeiling + ); // set collateral price initial feed mock params collateralTokenPriceFeed.updateMockParams( @@ -104,6 +120,16 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { address(collateralTokenPriceFeed), // price feed address 1 days // price feed staleness threshold in seconds ); + ubiquityPoolFacet.setCollateralChainLinkPriceFeed( + address(collateralToken2), // collateral token address + address(collateralTokenPriceFeed), // price feed address + 1 days // price feed staleness threshold in seconds + ); + ubiquityPoolFacet.setCollateralChainLinkPriceFeed( + address(collateralToken3), // collateral token address + address(collateralTokenPriceFeed), // price feed address + 1 days // price feed staleness threshold in seconds + ); // set price feed for ETH/USD pair ubiquityPoolFacet.setEthUsdChainLinkPriceFeed( @@ -119,6 +145,9 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { // enable collateral at index 0 ubiquityPoolFacet.toggleCollateral(0); + ubiquityPoolFacet.toggleCollateral(1); + ubiquityPoolFacet.toggleCollateral(2); + // set mint and redeem initial fees ubiquityPoolFacet.setFees( 0, // collateral index @@ -161,12 +190,21 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { deal(address(governanceToken), user, 2000e18); // mint 2000 collateral tokens to the user collateralToken.mint(address(user), 2000e18); + collateralToken2.mint(address(user), 2000e18); + collateralToken3.mint(address(user), 2000e18); + + vm.startPrank(user); // user approves the pool to transfer collateral - vm.prank(user); + // vm.prank(user); collateralToken.approve(address(ubiquityPoolFacet), 100e18); + collateralToken2.approve(address(ubiquityPoolFacet), 100e18); + collateralToken3.approve(address(ubiquityPoolFacet), 100e18); - vm.prank(user); ubiquityPoolFacet.mintDollar(0, 1e18, 0.9e18, 1e18, 0, true); + ubiquityPoolFacet.mintDollar(1, 1e18, 0.9e18, 1e18, 0, true); + ubiquityPoolFacet.mintDollar(2, 1e18, 0.9e18, 1e18, 0, true); + + vm.stopPrank(); } function testSetManagerFacet() public { @@ -245,14 +283,14 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { monitor.checkLiquidityVertex(); } - function testLiquidityDropBelowVertex() public { + function testLiquidityDropBelowVertexThreshold() public { vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); curveDollarPlainPool.updateMockParams(0.99e18); vm.prank(user); - ubiquityPoolFacet.redeemDollar(0, 5e17, 0, 0); + ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); @@ -278,6 +316,76 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { ); } + function testLiquidityDropBelowVertex() public { + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + curveDollarPlainPool.updateMockParams(0.99e18); + + vm.prank(user); + ubiquityPoolFacet.redeemDollar(0, 1e17, 0, 0); + + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + ubiquityPoolFacet.collateralInformation(address(collateralToken)); + + bool monitorPaused = monitor.monitorPaused(); + + assertFalse( + monitorPaused, + "Monitor should Not be paused after liquidity drop" + ); + } + + function testLiquidityDropBelowVertexThresholdAndInvalidCollateral() + public + { + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + curveDollarPlainPool.updateMockParams(0.99e18); + + vm.prank(user); + ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); + + vm.prank(admin); + ubiquityPoolFacet.toggleCollateral(0); + + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + bool monitorPaused = monitor.monitorPaused(); + + assertTrue( + monitorPaused, + "Monitor should be paused after liquidity drop, and any prior manipulation of collateral does not interfere with the ongoing incident management process." + ); + } + + function testLiquidityDropBelowVertexAndInvalidCollateral() public { + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + curveDollarPlainPool.updateMockParams(0.99e18); + + vm.prank(user); + ubiquityPoolFacet.redeemDollar(0, 1e17, 0, 0); + + vm.prank(admin); + ubiquityPoolFacet.toggleCollateral(0); + + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + bool monitorPaused = monitor.monitorPaused(); + + assertFalse( + monitorPaused, + "Monitor should Not be paused after liquidity drop, and any prior manipulation of collateral does not affect it" + ); + } + function testCheckLiquidityWhenPaused() public { vm.prank(admin); monitor.togglePaused(); From 9e865ce9da95b4a551f504a8066b75f5f4fc8f2a Mon Sep 17 00:00:00 2001 From: green Date: Tue, 24 Sep 2024 19:25:13 +0200 Subject: [PATCH 18/26] test: add events testing --- .../core/PoolLiquidityMonitorTest.t.sol | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol index 6abc65b8a..7a77e46ee 100644 --- a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol @@ -38,6 +38,11 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { address user = address(1); + event MonitorPaused(uint256 collateralLiquidity, uint256 diffPercentage); + event VertexDropped(); + event PausedToggled(bool paused); + event LiquidityVertexUpdated(uint256 collateralLiquidity); + function setUp() public override { super.setUp(); @@ -194,8 +199,7 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { collateralToken3.mint(address(user), 2000e18); vm.startPrank(user); - // user approves the pool to transfer collateral - // vm.prank(user); + // user approves the pool to transfer collaterals collateralToken.approve(address(ubiquityPoolFacet), 100e18); collateralToken2.approve(address(ubiquityPoolFacet), 100e18); collateralToken3.approve(address(ubiquityPoolFacet), 100e18); @@ -252,6 +256,9 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { } function testDropLiquidityVertex() public { + vm.expectEmit(true, true, true, false); + emit VertexDropped(); + vm.prank(admin); monitor.dropLiquidityVertex(); } @@ -262,6 +269,9 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { } function testTogglePaused() public { + vm.expectEmit(true, true, true, false); + emit PausedToggled(true); + vm.prank(admin); monitor.togglePaused(); } @@ -272,6 +282,12 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { } function testCheckLiquidity() public { + uint256 currentCollateralLiquidity = ubiquityPoolFacet + .collateralUsdBalance(); + + vm.expectEmit(true, true, true, false); + emit LiquidityVertexUpdated(currentCollateralLiquidity); + vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); } @@ -283,6 +299,25 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { monitor.checkLiquidityVertex(); } + function testLiquidityDropBelowVertexThresholdEvent() public { + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + curveDollarPlainPool.updateMockParams(0.99e18); + + vm.prank(user); + ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); + + uint256 currentCollateralLiquidity = ubiquityPoolFacet + .collateralUsdBalance(); + + vm.expectEmit(true, true, true, false); + emit MonitorPaused(currentCollateralLiquidity, 32); + + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + } + function testLiquidityDropBelowVertexThreshold() public { vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); @@ -387,6 +422,9 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { } function testCheckLiquidityWhenPaused() public { + vm.expectEmit(true, true, true, false); + emit PausedToggled(true); + vm.prank(admin); monitor.togglePaused(); From 3ee67deeaf2a4bfe55deb6d150d3ca7578775922 Mon Sep 17 00:00:00 2001 From: green Date: Wed, 25 Sep 2024 18:22:40 +0200 Subject: [PATCH 19/26] test: increase test coverage --- .../core/PoolLiquidityMonitorTest.t.sol | 106 +++++++++++++++++- 1 file changed, 100 insertions(+), 6 deletions(-) diff --git a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol index 7a77e46ee..64b80fd8c 100644 --- a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol @@ -299,7 +299,9 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { monitor.checkLiquidityVertex(); } - function testLiquidityDropBelowVertexThresholdEvent() public { + function testMonitorPausedEventEmittedAfterLiquidityDropBelowThreshold() + public + { vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); @@ -318,7 +320,26 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { monitor.checkLiquidityVertex(); } - function testLiquidityDropBelowVertexThreshold() public { + function testMonitorPausedRevertAfterLiquidityDropBelowThreshold() public { + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + curveDollarPlainPool.updateMockParams(0.99e18); + + vm.prank(user); + ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); + + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + vm.expectRevert("Monitor paused"); + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + } + + function testMonitorAndDollarPauseAfterLiquidityDropBelowThreshold() + public + { vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); @@ -351,7 +372,7 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { ); } - function testLiquidityDropBelowVertex() public { + function testLiquidityDropDoesNotPauseMonitorBelowThreshold() public { vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); @@ -373,7 +394,7 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { ); } - function testLiquidityDropBelowVertexThresholdAndInvalidCollateral() + function testLiquidityDropPausesMonitorWhenCollateralToggledAfterThreshold() public { vm.prank(defenderRelayer); @@ -398,7 +419,9 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { ); } - function testLiquidityDropBelowVertexAndInvalidCollateral() public { + function testLiquidityDropDoesNotPauseMonitorWhenCollateralToggled() + public + { vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); @@ -421,7 +444,7 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { ); } - function testCheckLiquidityWhenPaused() public { + function testCheckLiquidityRevertsWhenMonitorIsPaused() public { vm.expectEmit(true, true, true, false); emit PausedToggled(true); @@ -433,4 +456,75 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); } + + function testMintDollarRevertsWhenCollateralDisabledDueToLiquidityDrop() + public + { + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + curveDollarPlainPool.updateMockParams(0.99e18); + + vm.prank(user); + ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); + + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + uint256 collateralCount = 3; + for (uint256 i = 0; i < collateralCount; i++) { + vm.expectRevert("Collateral disabled"); + + vm.prank(user); + ubiquityPoolFacet.mintDollar(i, 1e18, 0.9e18, 1e18, 0, true); + } + } + + function testRedeemDollarRevertsWhenCollateralDisabledDueToLiquidityDrop() + public + { + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + curveDollarPlainPool.updateMockParams(0.99e18); + + vm.prank(user); + ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); + + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + vm.expectRevert("Collateral disabled"); + vm.prank(user); + ubiquityPoolFacet.redeemDollar(1, 1e18, 0, 0); + } + + function testDollarTokenRevertsOnTransferWhenPausedDueToLiquidityDrop() + public + { + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + curveDollarPlainPool.updateMockParams(0.99e18); + + vm.prank(user); + ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); + + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); + + bool isPaused = dollarToken.paused(); + assertTrue( + isPaused, + "Expected the Dollar token to be paused after the liquidity drop" + ); + + ERC20Ubiquity dollarToken = ERC20Ubiquity( + managerFacet.dollarTokenAddress() + ); + + vm.expectRevert("Pausable: paused"); + vm.prank(user); + dollarToken.transfer(address(0x123), 1e18); + } } From aca3438a770824f18854f6bd9ec71477dd5f2b61 Mon Sep 17 00:00:00 2001 From: green Date: Wed, 25 Sep 2024 18:33:23 +0200 Subject: [PATCH 20/26] chore: modifying tests configuration --- .../core/PoolLiquidityMonitorTest.t.sol | 33 ++++--------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol index 64b80fd8c..e27d42b18 100644 --- a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol @@ -209,6 +209,9 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { ubiquityPoolFacet.mintDollar(2, 1e18, 0.9e18, 1e18, 0, true); vm.stopPrank(); + + vm.prank(defenderRelayer); + monitor.checkLiquidityVertex(); } function testSetManagerFacet() public { @@ -282,6 +285,9 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { } function testCheckLiquidity() public { + vm.prank(user); + ubiquityPoolFacet.mintDollar(1, 1e18, 0.9e18, 1e18, 0, true); + uint256 currentCollateralLiquidity = ubiquityPoolFacet .collateralUsdBalance(); @@ -302,9 +308,6 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { function testMonitorPausedEventEmittedAfterLiquidityDropBelowThreshold() public { - vm.prank(defenderRelayer); - monitor.checkLiquidityVertex(); - curveDollarPlainPool.updateMockParams(0.99e18); vm.prank(user); @@ -321,9 +324,6 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { } function testMonitorPausedRevertAfterLiquidityDropBelowThreshold() public { - vm.prank(defenderRelayer); - monitor.checkLiquidityVertex(); - curveDollarPlainPool.updateMockParams(0.99e18); vm.prank(user); @@ -340,9 +340,6 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { function testMonitorAndDollarPauseAfterLiquidityDropBelowThreshold() public { - vm.prank(defenderRelayer); - monitor.checkLiquidityVertex(); - curveDollarPlainPool.updateMockParams(0.99e18); vm.prank(user); @@ -373,9 +370,6 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { } function testLiquidityDropDoesNotPauseMonitorBelowThreshold() public { - vm.prank(defenderRelayer); - monitor.checkLiquidityVertex(); - curveDollarPlainPool.updateMockParams(0.99e18); vm.prank(user); @@ -397,9 +391,6 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { function testLiquidityDropPausesMonitorWhenCollateralToggledAfterThreshold() public { - vm.prank(defenderRelayer); - monitor.checkLiquidityVertex(); - curveDollarPlainPool.updateMockParams(0.99e18); vm.prank(user); @@ -422,9 +413,6 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { function testLiquidityDropDoesNotPauseMonitorWhenCollateralToggled() public { - vm.prank(defenderRelayer); - monitor.checkLiquidityVertex(); - curveDollarPlainPool.updateMockParams(0.99e18); vm.prank(user); @@ -460,9 +448,6 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { function testMintDollarRevertsWhenCollateralDisabledDueToLiquidityDrop() public { - vm.prank(defenderRelayer); - monitor.checkLiquidityVertex(); - curveDollarPlainPool.updateMockParams(0.99e18); vm.prank(user); @@ -483,9 +468,6 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { function testRedeemDollarRevertsWhenCollateralDisabledDueToLiquidityDrop() public { - vm.prank(defenderRelayer); - monitor.checkLiquidityVertex(); - curveDollarPlainPool.updateMockParams(0.99e18); vm.prank(user); @@ -502,9 +484,6 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { function testDollarTokenRevertsOnTransferWhenPausedDueToLiquidityDrop() public { - vm.prank(defenderRelayer); - monitor.checkLiquidityVertex(); - curveDollarPlainPool.updateMockParams(0.99e18); vm.prank(user); From 8fabd03de053efc55e14202790058bb3f470dcd3 Mon Sep 17 00:00:00 2001 From: green Date: Sun, 29 Sep 2024 20:36:52 +0200 Subject: [PATCH 21/26] docs: add ubiquitypoolsecuritymonitor off-chain part setup readme --- .../core/UbiquityPoolSecurityMonitor.md | 50 ++++++++++++++++++ .../core/UbiquityPoolSecurityMonitor.sol | 11 ++-- .../core/PoolLiquidityMonitorTest.t.sol | 15 +++++- ...uityPoolSecurityMonitorWorkflow.drawio.png | Bin 0 -> 62971 bytes 4 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.md create mode 100644 utils/UbiquityPoolSecurityMonitorWorkflow.drawio.png diff --git a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.md b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.md new file mode 100644 index 000000000..0d44fed60 --- /dev/null +++ b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.md @@ -0,0 +1,50 @@ +# UbiquityPoolSecurityMonitor Off-Chain part + +A crucial component of the `UbiquityPoolSecurityMonitor` contract workflow is its off-chain integration. The `checkLiquidityVertex()` function must be periodically triggered by OpenZeppelin Defender to ensure continuous liquidity monitoring and security assessments. + +The workflow consists of four key components: + +1. **[OpenZeppelin Actions](https://docs.openzeppelin.com/defender/module/actions)**: Executes a cron job that triggers the Relayer to call the `checkLiquidityVertex()` function. +2. **[OpenZeppelin Relayer](https://docs.openzeppelin.com/defender/module/relayers)**: Performs the transaction that invokes the `checkLiquidityVertex()` function. +3. **UbiquityPoolSecurityMonitor Contract**: Conducts the on-chain liquidity check, takes necessary actions if an incident occurs, and emits the `MonitorPaused` event. +4. **[OpenZeppelin Monitor](https://docs.openzeppelin.com/defender/module/monitor)**: Listens for the `MonitorPaused` event and sends alerts via email or other designated channels. + +### Workflow diagram +![Workflow Diagram](../../../../../utils/UbiquityPoolSecurityMonitorWorkflow.drawio.png) + + +### OpenZeppelin Defender Setup + +To integrate OpenZeppelin Defender with the `UbiquityPoolSecurityMonitor`, follow the steps below: + +#### 1. Relayer Setup + +Complete only **Part 1** of the [OpenZeppelin Defender Relayer tutorial](https://docs.openzeppelin.com/defender/tutorial/relayer). This will configure the Relayer to handle transactions for calling the `checkLiquidityVertex()` function. + +#### 2. Actions Setup + +Follow the [OpenZeppelin Defender Actions tutorial](https://docs.openzeppelin.com/defender/tutorial/actions) to set up Actions. While configuring your Action, choose the Relayer you set up in step 1, and use the following script for your newly created Action: + +```javascript +const { Defender } = require('@openzeppelin/defender-sdk'); + +exports.handler = async function (credentials) { + const client = new Defender(credentials); + + const txRes = await client.relaySigner.sendTransaction({ + to: '0xb60ce3bf27B86d3099F48dbcDB52F5538402EF7B', // Address of UbiquityPoolSecurityMonitor contract + speed: 'fast', + data: '0x9ba8a26c', // Encoded function signature for checkLiquidityVertex() of the UbiquityPoolSecurityMonitor + gasLimit: '80000', + }); + + return txRes.hash; +}; +``` + +#### 3. Monitor Setup + +Follow the [OpenZeppelin Defender Monitor tutorial](https://docs.openzeppelin.com/defender/tutorial/monitor) to configure a Monitor that listens for the MonitorPaused event emitted by the UbiquityPoolSecurityMonitor contract. Set up your alerts using the desired source (e.g., email or other alerting mechanisms). + + + diff --git a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol index 99ce0b076..39670a0a0 100644 --- a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol +++ b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol @@ -22,9 +22,9 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { bool public monitorPaused; uint256 public thresholdPercentage; - event LiquidityVertexUpdated(uint256 collateralLiquidity); + event LiquidityVertexUpdated(uint256 liquidityVertex); + event LiquidityVertexDropped(uint256 liquidityVertex); event MonitorPaused(uint256 collateralLiquidity, uint256 diffPercentage); - event VertexDropped(); event PausedToggled(bool paused); modifier onlyDefender() { @@ -95,19 +95,20 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { liquidityVertex = currentCollateralLiquidity; - emit VertexDropped(); + emit LiquidityVertexDropped(liquidityVertex); } function checkLiquidityVertex() external onlyDefender { + require(!monitorPaused, "Monitor paused"); + uint256 currentCollateralLiquidity = ubiquityPoolFacet .collateralUsdBalance(); require(currentCollateralLiquidity > 0, "Insufficient liquidity"); - require(!monitorPaused, "Monitor paused"); if (currentCollateralLiquidity > liquidityVertex) { _updateLiquidityVertex(currentCollateralLiquidity); - } else { + } else if (currentCollateralLiquidity < liquidityVertex) { _checkThresholdPercentage(currentCollateralLiquidity); } } diff --git a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol index e27d42b18..21513319a 100644 --- a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol @@ -39,7 +39,7 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { address user = address(1); event MonitorPaused(uint256 collateralLiquidity, uint256 diffPercentage); - event VertexDropped(); + event LiquidityVertexDropped(uint256 liquidityVertex); event PausedToggled(bool paused); event LiquidityVertexUpdated(uint256 collateralLiquidity); @@ -259,8 +259,11 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { } function testDropLiquidityVertex() public { + uint256 currentCollateralLiquidity = ubiquityPoolFacet + .collateralUsdBalance(); + vm.expectEmit(true, true, true, false); - emit VertexDropped(); + emit LiquidityVertexDropped(currentCollateralLiquidity); vm.prank(admin); monitor.dropLiquidityVertex(); @@ -408,6 +411,14 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { monitorPaused, "Monitor should be paused after liquidity drop, and any prior manipulation of collateral does not interfere with the ongoing incident management process." ); + + address[] memory allCollaterals = ubiquityPoolFacet.allCollaterals(); + for (uint256 i = 0; i < allCollaterals.length; i++) { + vm.expectRevert("Invalid collateral"); + + vm.prank(user); + ubiquityPoolFacet.collateralInformation(allCollaterals[i]); + } } function testLiquidityDropDoesNotPauseMonitorWhenCollateralToggled() diff --git a/utils/UbiquityPoolSecurityMonitorWorkflow.drawio.png b/utils/UbiquityPoolSecurityMonitorWorkflow.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..c46003db02f5f55a90a53bde714ba3123ea154fb GIT binary patch literal 62971 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

eI&=TY+^?R4u<2-JAz&t$9U_T8{DOg=c5}z;R(D z+z~h#=Z}Xkyvs=MJEYf&;vK26=RgqHebSjD?)H7{pVUu2_gfe5+oYf{jka}N3d>(j zs}C7Wux+`CiE~D#J_Cwf(<~{-jh}4=E#GceBeEH$cu@By*Cj_@!jjdRJ)M-=(1FBK(QBYne&lmxN9v@lG>+HFp3U ze_oxy>5nF^7~G27(FL{jAixz^liwC7X_%Y-k@0gr(oUMvH+y+4oiw9g6kuB{mKC$+ zCg1`n8mFOlMqiJ5x+f|nW>K7Y|gC#VW4Bf1gvpbuPN@Vyo$0)D=_Nx{Yyg%!l5 zv&V1f8}CP~g|wa134*l6svW6aa$9t|F?0o)T=~q;izxkb5mk9LadTdLoHsdh%NeF* zVa3H^Aa*en(cKkA{GoIHtFQHFDu+9p^aW5uedLZAOM=?7Y)gH(Aatr%tCbv5xN>c| z3Dum?u_r|#A7)gHdksx^G%0#^GAP1l@F(njmpYPj_jizw##)@^?n);^O&kxVg!kqw z=Ck`WaKCreOfr8o)-R2T}jM;83md!&y50H|k#_B|(ZJDb zkuc{M4?I@W(~?sq;IVpE@CWzaOCM;I)uoSh+_-N=iL>9%q z;O*H)-b;4G9e?O8`6`bXUFg&K`)sXTiSJip3v-=ELSUYJ0Ne2Ac_5zPV+I)V&`)r5 zK|+0bN-P1eeawItJ>5f{5;9C8bg+d%QhKer%eeFP8xRobE>i%ZE?t6sNFbW= zoL5mUC&P3g`;3P-Whb6BH+tBh=WTyo9 zJ_lS`_8wi;a52_T#kCg{pLBT8anTm~_r?nB6aWE^Uhsby*`kuPp2UI@+;rS| zs@#MD5902)_H^@rM+Q#{qVl}ummG!tV|i{ z*1SxasVonm%8$+8))3bK2)#`eLjOPQ9gv{VmB}yuQusArJ5EmNi#I5~$|5-mKnGCp zu<=joz-r)1&aN4|xG&r1J<19SctFl!eNtY(EUBdscJCL?IrQFp11DAu4$HPv0m&>< zzCszcht3dqR9)l$DI74yET=?8uikBJOyNGndYloczm&~1cJ0~1LUlm{h=04`vRx4Y ztWN6jLsAfu*;++^QKo&02|5?t%OB9CtGp&6gz;xysw0w6qd9MP{2R%U0U#fETF4@` z8l682n-`k`W(s9oR*rX~@1D7MwRNYRi@JTjkPq_&7U0Cj-Z+FrCk z$4;+fda!f)??Hm^Up|&D6o|Zm{(RbqiajoxTqOXE?G;R%0 z4PdGejJBp;2`lu5VSAOLk2Z1L15!)C@i|js;A{cd=x^89zq=lR&2&yllUw9~alm+o{#Lvs1Q0K*5slK-k z)=84RiL|8^hH|TK>y&>4m1JN}{=>C9q3ai?@|vXgrBQjQ+&!yQeKdiDeO;0r(#R4bE_fO;+vt(iGUapX z0qiz(nI8=8+X`b-96L`*DfFwIfg(8QrM3VjX5dt*X-A1}9N>j|!`N27_}qp0wl-mO zUt*>_xm=%xGd_JzIP%@Q&(7CeW(DA`6alI#W!AD3q(OnI5fLpWeypPLwXt}~JQ|aE zHL!gdNdAQ4;mKfIkv5{6Y^$Q#qp!xE*z#=5>pjri(_U!K#_}5v;t(I3IIcDBivnYh z8V@bS{i^y7w|7R-bC1Eo6{gcW$*1)GpM~t6C-dHt6B5Rrq_otDU5W$llJ(gcUQjt2 zR`3XYC0B^oAi_8@!-Ps?1&5ye_Eq-zWC0yc{~d8dwb2?fTZsk1Qq0XQ*O@{NG+9@1 zLOC7ju6)V$HqFQA^F*(P*Lo`}oY?|IkDh+BG77Zy|E=g?NvMz?r>MTf`ZN9}mt&Ys zLR5B9s=SvRYx|>1r8jZ5(WT)-^+yw)l|U^3`Kl5`MklD54g9g_vw^O)Zi~y))s@3T z=|5@YUsHHKP{}ks*`g^q9$lSRW9E6z^0+Pgf-kf%MqW-E+$weSK=SwNz;}Z6_8J+T z!qQ>0{UVGON2*v@A?=!m5vxwOID{c#GC+7cdC&MQruzzi5jY`c{3#SCov>se^>~KS z^}@)*)DmS&4!tQ;da^5<_u09l1y_jy3u|j~GlXF=?wuU-5YVd__|7TbAEf% zAo~%t6K(UW^au|!mNbf6TX7I^>w6da*A}r{xzULLS z9YSB_e>C3@?fR>7AZu(%7lX9(LeI10eP> zG>X4_#{uHwC(4&qs{EBx>(W(K(|K#LMk(zbas3_6Rg~w-&)buur8=k_RgT;%2!8qR zrUyV(zy&h2t!P(6KRa+G+WFv7Mo?J|e?vf5#hd)y1+f*G=mW_|PVeprZO0aM>7b@p z*6$@+e;E7DJ-!&TtiZgRI5dvkF;*c-<|iWbxzaHamzR9-ygU1A`D6ag>t9JsWc=Qg ztG`?5rsgS&GjCFh>vM~*Ye{f%f1&}&cgb(k7ngBR{ z(#>glQw2DlA!+n}5C|^^O@QF-3%ydKd{)nm`A{vK-oKGF7j?K8^0@}%Wz*h>=*`Bk z9q&CP7Ryj0apA3hmR+K3Cg=SzvhpwxMG?xV5)lx;O=$X)QAcj$-eho7 zpEs657qjgWgO;zmK#6Os*2mVWwO~8uvt$v?Ry&N-W++73jBni07l(r8R%1%7gJIl~ z8UaruF6YT#I&~t4A|-ZQ>vO;djEkW~!f88s@G5G;?#5C1uCE(MA2QCJNt4f`W+uVw zrEpoBIq7v?ZZNyV39S{GPuTgRsG=3O_`9V=n-U(a>tD>dd=BRYvkjLxUy?1qXL=o< ziMpCm5giO3UmltR+3un2b|5j@vwl+l!t+OQIaH-hVZ;%<-#i;tZs#Z*>HA$!2&37H zx1*P?&#JH}!14EP!&$Y8&RY}C^YCj^zvnnHp$ZH9D>7e9PEsg{8Cm$qos7>2m*D4* zTNjZLy6O7`y9R^(rq1g#1^{LJv;It1myP+hcYtVRT}UGx(kkRT=T=DSB}AcjGRlDf zk>}=)aBbJO#_Z5V=|V(Mb5tPmFGWm8T+yL!AKQ%(_ab`?;d&pN$GN*K%!t~a@9S%&l84tO1@oGj zg*UD;1o2LT^xxGDxA~P)Br%K)1o0&=Kan0(O*qOg8P?}Ewql#NC@`5H$SJRdoxy@r z`$bkxp#k6Yu;x~O<0Y{o`i<4Rxgv#pYkaA{SMdJU_3Dal#-8#`$+I{!+=0I(o3l@f z+{-8=J+97A0xs}#!PdJzdSGXNrWdwwN>C0_(xP`2uCR4_Y0(o+#qYM!vTw5KL`5s* zLCe7Pa~9sj4dC9MKpf;m#oG81S2Q-T+7)5@m6-`uR#?ph^}JJYJ`@q*Fqk21Bb3ut zES|boPIP_t>%oq-A&oacg8E#J_|EWj+?C%kl=T~z%)PzTbbb5ywqecjDZ=5;M7But z@SSwTEJEn8rYVW3tx94fDY?Vw_>xSF-SD`HwD}of;VC5QN|~xHhpD$;9~`zS4^>z! zUew$1$A}ITpeU-!y%)l8)8VdWEgvv&2GuT>RefY(PX9t%AAmx|KuXQ)YPH7=8%Nl> zPL~dcvTbfvi!@FS03)&>`GzJ4UL0~ z(|Af6B;Xbbb%?CVc+s8?j+Yye+nY4h8X;$^{zZ{G@c!>k5|4zQ{8dJYilIZ~G!u$O z?=J9p#9BR)jSYs*(6G=!;yv9Ga!d!1b8?@Tj=H2nJ&Ma8;-Ywg_IJ@Pocccr!0+0 zQA0>ac5p;4@7?Tx&R|Ua29s1sJmTGbe93G7(yP=snCBo((ZU7jS-qcm!4lQvSjxKn zl3rraz;^;IpLdpa*7A~v1koxYe>y)r;sDy020&M@j)2(6;C1*Gzy6aC3qs%A>4v54 z?w(f@;G=ii}X>huMm12b2>+s$S10 z^%!gBNpu`o;mm8GIe%pq)Zd>{_z}pV%F(>Z*-(MJ6x%Xa{9)gO2SDO0^zuIPBcwNxFqPkK z+&+skKCI)|M&_`9Ri&3O97`y*U!#2i|S=nArx|K@-_mDYNP8?+s;xJ{3n24bv#H;U7=E$5K- zY$)2KNBPm86e0Ff+VF2~b)R{#mBCqR%@d!VS>It|ewuuzwXvs2)GWOY6ZqiNdYqgg zXW3&C@@s@aJHv`&T5cS>M6GtC``YM(y`tVL_Fi>Z+-gO(f^@aa`ekX+iLY0&-FTx# z{K@w>wPq!%W;(gXOs37rrKIt%sY+scHpEb6&EJ^?6M1|dHWfk0dHqP$gebnL4gTcy z9N>;5n0T+=W^P0=uDli=1rj*M{&JawTr%d~Y8z-ptpnYQ;D2KO|3hTwf2GH1T3Br~>&| z2Hlh4D+Ubf2tUQ_L?@NCLIsb`JOL9;nzw`YF*FZT=q{&$ng!oO*m)W0I+qcs;bf3g zjS0rKe*JDd52y-xb5%i#hng+GB}DsSL}>yco_Q&;uX}D?NxbzNmit`>qe#@(3_P+>ZojW+n~qZRa--< z;14LqsKBT4$UW`4-cftZYS0a#UG5KBC>o%3|8lkH>&}V2z(SKb&d{$v4+uDzXrl`Cid25r8R+_5nywL6h2&U@qZyZfm4ik|7uVY z3kyrS?eB{($=@$DqRwjx>(zwTBcqCII&a>y z+-=&)TD^Y%M^35-2CW!q6u}hC@jfO{-g5iVd+X}j`Aau+dDeZcMZh#Gxho+xGnAjq z`VE-XZD2@6Q_I9cUowF66ZnYgV9t9th`o!2ahV@Tx_zTLyGVfiAJ`nq0TG~nM`M+1>v^&(-7eR(XlJVz-BsSYbNx;W z0O|3d#y7qh`R*c@GHcD$X@Z+iFkL1b?Vlp(bZX^F zv}~2YKPInmBU`T%<#v*kjR5kq)n4>PJq5D0xu?)usZxMFsxX(=yRQV-o7tv~ZfVq9 zSi#aSuz>D8QnqAs7Azwb^h`b~_OkbY5(q?KQU@~tWEGNDDz5pK>|3Oe9O(jH-hZlk zoHy?2=x{Eg`(gMEv02y_|5Ce-*BOh$is!I{a`FShUm#5;%YBEv zsQY0FK(T+EZ!Y9^ir8=V`UYpD)6528Nj~nuxcnHC@pXy}^ewj?@@^{~yWopD=(DvS z8leez`Q&~3d&pZ=g_~I3n@Z%k72|RmP;V%0tf9}zsMKE6-Ca4WoW=eBefMhQ5I4K8nW$wanXGuK+}fGKy0s% z!b3obIm}q8C1h!0_dN`ycKRG#zXR_$b=H3$q>%bY%6>Y84Y=68K;7|G2^FSnAeOt* z4QK(19VvAebMK$74{z>&b&Hs@i*MEXlXQjz@{&?ZyrX<56oR`26KZTcY#6NaJcjyL z!N%DbM;S5IhlBY`Gsmve7h-@WpIOGMHH&}jf{i4c+LPm#Z(Is{E1Rg?dlF1aEY=Z} z7?CwJC@?rUcq6Ut8YdZg-1Y;d7aBq048kzh!T(jb5V9(d6hn!sQF**f2V`n@!abTD zLO0EXHeTpH2$*o%E~+4Lwi6xM|J<BIwDW={3)w&Z;=J9j>%empTPOKh9CX)~=Ja@ExARC(_G~_>-qJBN3GKM=JN?RIB1J2i;0VhJk+* z*wxBziu!R_aPShVFC#Q}_8HcDQnN_)J>WXJ#*0c^zejAc?Q(c5-;f=ZF4a{Bf7y~k zSmWWAM>;I;qUFwi#DOQ>fwN>Tcrd;l1QTV24a7JNzB7V*RG+HidOyW*{1HPB71;6Q zP9UeN0Ipfy=f&z7{)yeK_`}6wqy%8s@6w&uEk!HczzA35B?0kvA;}Rn=0Y@0rr3Sl z7@4!*Aw@4L%T9QQ5UC~+jDWR!Gh?)FJri_eWePLKpVZA+X?Okeb~h%LJmV%*`puzC zAFVKmM$&g~(psAZ)@JS)u#srIN`5gRbVKl=1sezEHPBy2#& zvL8hKj9}Erl=~MvG4p91E(XIBX&D9*QaodK{Ewv1NyZ&SWqo5leZAm&9P5i`ioDGb&R~pm7)~X0tj6gzgJ6eQEONfhQ6A>Nbk?#| zGh|IgI$KPuAQD=l+6aY6G=knPgEcvk`@fs-i0oua&~}%G^@G(oJg~=Z&-hFx>fIaQ zum$nkNJVEvp9$@s>ajForA0I-i}8=KqXz_frV^xkmXEix`EhBapmn;0dh!8p{lt4r z5IB1SCLe*#>+$1tquPId2N%PdWGk7UT&_1OOwF_q=TgV#^q-b`%;?8UZZyKG6h5}C&#H`CKQM0z<{&( zg`|*RzQDiblvc#+qixRXTKiy40@xaGh|cV7XPX1QU)}t-zRd@!i;u+rQYNKPTFitp ztWDBcd#mgK?QZ{bl@27!9O80pnQ^lhZ};BZCQp&moy-G~-(0tZ2iw;xlD);t#LPTi zIZgB`+u89no`TTq%B;Nft;yam7Aic9QY>$x+Z?S4Emv2sQf>G5jMau~)tK3NaIZay z$>}W@?-2!ZnB+6y@Sp#fShgJxi*({D=WzKLmmscvMJ?)0<|nv`8yzX?jTIPMLw=;} zwM|h)XFqduyw`gMz;(~rt4IP~?%&euVv=sB1TkVoW65^reV(>|(q#Qm#`z_C?4dy< znXjG)wMHSNbsC@fs`#dtxd7>`la#4oyTXx_vi7yqone3Imkp6t+|4 caeQ~*1+&mLw~#mS9`K_iuMR1fHT(Gg0QGOX5dZ)H literal 0 HcmV?d00001 From a5a153226a5f5f735c707042eb5d22a611175bfe Mon Sep 17 00:00:00 2001 From: green Date: Sun, 29 Sep 2024 20:43:28 +0200 Subject: [PATCH 22/26] fix: kebab-case --- ...quity-pool-security-monitor-workflow.drawio.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename utils/{UbiquityPoolSecurityMonitorWorkflow.drawio.png => ubiquity-pool-security-monitor-workflow.drawio.png} (100%) diff --git a/utils/UbiquityPoolSecurityMonitorWorkflow.drawio.png b/utils/ubiquity-pool-security-monitor-workflow.drawio.png similarity index 100% rename from utils/UbiquityPoolSecurityMonitorWorkflow.drawio.png rename to utils/ubiquity-pool-security-monitor-workflow.drawio.png From e024fa00402500944f7443b9314689137e6362ef Mon Sep 17 00:00:00 2001 From: green Date: Sun, 29 Sep 2024 20:46:07 +0200 Subject: [PATCH 23/26] fix: kebab-case --- ... => ubiquity-pool-security-monitor-workflow.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename utils/{ubiquity-pool-security-monitor-workflow.drawio.png => ubiquity-pool-security-monitor-workflow.png} (100%) diff --git a/utils/ubiquity-pool-security-monitor-workflow.drawio.png b/utils/ubiquity-pool-security-monitor-workflow.png similarity index 100% rename from utils/ubiquity-pool-security-monitor-workflow.drawio.png rename to utils/ubiquity-pool-security-monitor-workflow.png From 9e89435e16184237beb5b59426400fd9c59ec11e Mon Sep 17 00:00:00 2001 From: green Date: Sun, 29 Sep 2024 20:50:28 +0200 Subject: [PATCH 24/26] fix: diagram path for ubiquity-pool-security-monitor --- .../contracts/src/dollar/core/UbiquityPoolSecurityMonitor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.md b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.md index 0d44fed60..0b265d8e8 100644 --- a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.md +++ b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.md @@ -10,7 +10,7 @@ The workflow consists of four key components: 4. **[OpenZeppelin Monitor](https://docs.openzeppelin.com/defender/module/monitor)**: Listens for the `MonitorPaused` event and sends alerts via email or other designated channels. ### Workflow diagram -![Workflow Diagram](../../../../../utils/UbiquityPoolSecurityMonitorWorkflow.drawio.png) +![Workflow Diagram](../../../../../utils/ubiquity-pool-security-monitor-workflow.png) ### OpenZeppelin Defender Setup From 26699873ab7174adee8ba9f81f574d3131a62a87 Mon Sep 17 00:00:00 2001 From: green Date: Mon, 30 Sep 2024 16:47:47 +0200 Subject: [PATCH 25/26] docs: add docs to pool contract, update readme --- .../core/UbiquityPoolSecurityMonitor.md | 13 +- .../core/UbiquityPoolSecurityMonitor.sol | 156 +++++++++++++++++- .../core/PoolLiquidityMonitorTest.t.sol | 2 +- 3 files changed, 163 insertions(+), 8 deletions(-) diff --git a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.md b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.md index 0b265d8e8..284ee1981 100644 --- a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.md +++ b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.md @@ -23,7 +23,10 @@ Complete only **Part 1** of the [OpenZeppelin Defender Relayer tutorial](https:/ #### 2. Actions Setup -Follow the [OpenZeppelin Defender Actions tutorial](https://docs.openzeppelin.com/defender/tutorial/actions) to set up Actions. While configuring your Action, choose the Relayer you set up in step 1, and use the following script for your newly created Action: +Follow the [OpenZeppelin Defender Actions tutorial](https://docs.openzeppelin.com/defender/tutorial/actions) to set up Actions. While configuring your Action: + - choose the Relayer you set up in step 1 + - choose `Schedule` as trigger + - use the following script for your newly created Action: ```javascript const { Defender } = require('@openzeppelin/defender-sdk'); @@ -32,7 +35,7 @@ exports.handler = async function (credentials) { const client = new Defender(credentials); const txRes = await client.relaySigner.sendTransaction({ - to: '0xb60ce3bf27B86d3099F48dbcDB52F5538402EF7B', // Address of UbiquityPoolSecurityMonitor contract + to: '0x0000000000000000000000000000000000000000', // Replace with the actual UbiquityPoolSecurityMonitor contract address speed: 'fast', data: '0x9ba8a26c', // Encoded function signature for checkLiquidityVertex() of the UbiquityPoolSecurityMonitor gasLimit: '80000', @@ -44,7 +47,11 @@ exports.handler = async function (credentials) { #### 3. Monitor Setup -Follow the [OpenZeppelin Defender Monitor tutorial](https://docs.openzeppelin.com/defender/tutorial/monitor) to configure a Monitor that listens for the MonitorPaused event emitted by the UbiquityPoolSecurityMonitor contract. Set up your alerts using the desired source (e.g., email or other alerting mechanisms). +In **Settings -> Notifications**, configure the desired channels you want to use for managing notifications for your monitor. + +Follow the [OpenZeppelin Defender Monitor tutorial](https://docs.openzeppelin.com/defender/tutorial/monitor) to configure a Monitor that listens for the MonitorPaused event emitted by the UbiquityPoolSecurityMonitor contract. +You will need to pass the ABI array of the **UbiquityPoolSecurityMonitor** contract. Once the ABI is provided, you will be able to choose and subscribe to any event emitted by the contract. +Then, in the **Monitor's alert section**, select the appropriate alert option for your setup. diff --git a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol index 39670a0a0..4678d8450 100644 --- a/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol +++ b/packages/contracts/src/dollar/core/UbiquityPoolSecurityMonitor.sol @@ -15,18 +15,67 @@ import "forge-std/console.sol"; contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { using SafeMath for uint256; + /** + * @notice Instance of AccessControlFacet used for role-based access control. + */ AccessControlFacet public accessControlFacet; + + /** + * @notice Instance of UbiquityPoolFacet used to interact with the pool's functionalities. + */ UbiquityPoolFacet public ubiquityPoolFacet; + + /** + * @notice Instance of ManagerFacet used to interact with manager-related functionalities. + */ ManagerFacet public managerFacet; + + /** + * @notice The highest recorded collateral liquidity value (in USD) used as a reference point. + */ uint256 public liquidityVertex; + + /** + * @notice Flag indicating whether the liquidity monitor is paused. + */ bool public monitorPaused; + + /** + * @notice The threshold percentage at which liquidity differences are considered critical. + * @dev Default is set to 30%. + */ uint256 public thresholdPercentage; + /** + * @notice Emitted when the liquidity vertex is updated to a new value. + * @param liquidityVertex The new liquidity vertex value (in USD). + */ event LiquidityVertexUpdated(uint256 liquidityVertex); + + /** + * @notice Emitted when the liquidity vertex is manually dropped. + * @param liquidityVertex The dropped liquidity vertex value (in USD). + */ event LiquidityVertexDropped(uint256 liquidityVertex); + + /** + * @notice Emitted when the monitor pauses due to a liquidity drop exceeding the threshold. + * @param collateralLiquidity The current collateral liquidity (in USD) when the monitor pauses. + * @param diffPercentage The percentage difference between the current liquidity and the vertex. + */ event MonitorPaused(uint256 collateralLiquidity, uint256 diffPercentage); + + /** + * @notice Emitted when the monitor's paused state is toggled. + * @param paused Boolean flag indicating the new paused state (true = paused, false = active). + */ event PausedToggled(bool paused); + /** + * @notice Modifier that restricts access to functions to only addresses with the DEFENDER_RELAYER_ROLE. + * @dev This role is required for relayer functions in the security monitor system. + * If the caller does not have the required role, the transaction is reverted. + */ modifier onlyDefender() { require( accessControlFacet.hasRole(DEFENDER_RELAYER_ROLE, msg.sender), @@ -35,6 +84,11 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { _; } + /** + * @notice Modifier that restricts access to functions to only addresses with the DEFAULT_ADMIN_ROLE. + * @dev This role is needed for administrative tasks, such as managing settings or configurations. + * If the caller does not have the admin role, the transaction is reverted. + */ modifier onlyMonitorAdmin() { require( accessControlFacet.hasRole(DEFAULT_ADMIN_ROLE, msg.sender), @@ -43,6 +97,14 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { _; } + /** + * @notice Initializes the UbiquityPoolSecurityMonitor contract. + * @param _accessControlFacet The address of the AccessControlFacet contract for managing roles. + * @param _ubiquityPoolFacet The address of the UbiquityPoolFacet contract for pool interactions. + * @param _managerFacet The address of the ManagerFacet contract for manager-related interactions. + * @dev Sets the default threshold percentage to 30% and assigns the provided facet contracts. + * This function is only called once during the initialization of the upgradeable contract. + */ function initialize( address _accessControlFacet, address _ubiquityPoolFacet, @@ -55,39 +117,69 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { managerFacet = ManagerFacet(_managerFacet); } - function _authorizeUpgrade( - address newImplementation - ) internal override onlyMonitorAdmin {} - + /** + * @notice Updates the ManagerFacet contract used by the monitor. + * @param _newManagerFacet The address of the new ManagerFacet contract. + * @dev This function is restricted to addresses with the DEFAULT_ADMIN_ROLE via the `onlyMonitorAdmin` modifier. + */ function setManagerFacet( address _newManagerFacet ) external onlyMonitorAdmin { managerFacet = ManagerFacet(_newManagerFacet); } + /** + * @notice Updates the UbiquityPoolFacet contract used by the monitor. + * @param _newUbiquityPoolFacet The address of the new UbiquityPoolFacet contract. + * @dev This function is restricted to addresses with the DEFAULT_ADMIN_ROLE via the `onlyMonitorAdmin` modifier. + */ function setUbiquityPoolFacet( address _newUbiquityPoolFacet ) external onlyMonitorAdmin { ubiquityPoolFacet = UbiquityPoolFacet(_newUbiquityPoolFacet); } + /** + * @notice Updates the AccessControlFacet contract used by the monitor. + * @param _newAccessControlFacet The address of the new AccessControlFacet contract. + * @dev This function is restricted to addresses with the DEFAULT_ADMIN_ROLE via the `onlyMonitorAdmin` modifier. + */ function setAccessControlFacet( address _newAccessControlFacet ) external onlyMonitorAdmin { accessControlFacet = AccessControlFacet(_newAccessControlFacet); } + /** + * @notice Updates the threshold percentage used to detect significant liquidity drops. + * @param _newThresholdPercentage The new threshold percentage to be set. + * @dev This function is restricted to addresses with the DEFAULT_ADMIN_ROLE via the `onlyMonitorAdmin` modifier. + */ function setThresholdPercentage( uint256 _newThresholdPercentage ) external onlyMonitorAdmin { thresholdPercentage = _newThresholdPercentage; } + /** + * @notice Toggles the paused state of the liquidity monitor. + * @dev This function is restricted to addresses with the DEFAULT_ADMIN_ROLE via the `onlyMonitorAdmin` modifier. + * Emits the `PausedToggled` event with the updated paused state. + */ function togglePaused() external onlyMonitorAdmin { monitorPaused = !monitorPaused; emit PausedToggled(monitorPaused); } + /** + * @notice Resets the liquidity vertex to the current collateral liquidity in the pool. + * @dev This function is used to restart the monitor and reset the liquidity vertex after a + * significant liquidity drop incident. It ensures that the new vertex is set to the + * current collateral liquidity. + * Emits the `LiquidityVertexDropped` event with the updated liquidity vertex value. + * Requires the current collateral liquidity to be greater than zero. + * @dev This function is restricted to addresses with the DEFAULT_ADMIN_ROLE via the `onlyMonitorAdmin` modifier. + */ function dropLiquidityVertex() external onlyMonitorAdmin { uint256 currentCollateralLiquidity = ubiquityPoolFacet .collateralUsdBalance(); @@ -98,6 +190,16 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { emit LiquidityVertexDropped(liquidityVertex); } + /** + * @notice Checks the current collateral liquidity and compares it with the recorded liquidity vertex. + * @dev This function ensures that the liquidity monitor is not paused and compares the current collateral + * liquidity in the pool against the stored liquidity vertex: + * - If the current liquidity exceeds the vertex, the vertex is updated. + * - If the current liquidity is below the vertex, the function checks whether the drop exceeds + * the configured threshold percentage. + * @dev Requires the current collateral liquidity to be greater than zero and ensures the monitor is not paused. + * This function is restricted to addresses with the DEFENDER_RELAYER_ROLE via the `onlyDefender` modifier. + */ function checkLiquidityVertex() external onlyDefender { require(!monitorPaused, "Monitor paused"); @@ -113,11 +215,30 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { } } + /** + * @notice Updates the liquidity vertex to a new value when the current liquidity reaches a new higher value. + * @param _newLiquidityVertex The new collateral liquidity value to set as the liquidity vertex. + * @dev This internal function updates the recorded liquidity vertex to the provided value and + * emits the `LiquidityVertexUpdated` event. It is used when the current collateral liquidity + * exceeds the previously recorded vertex, ensuring that the vertex always reflects the highest + * observed liquidity level. + */ function _updateLiquidityVertex(uint256 _newLiquidityVertex) internal { liquidityVertex = _newLiquidityVertex; emit LiquidityVertexUpdated(liquidityVertex); } + /** + * @notice Checks if the difference between the current collateral liquidity and the liquidity vertex + * exceeds the configured threshold percentage. + * @param _currentCollateralLiquidity The current collateral liquidity in the pool. + * @dev This internal function is used when the current collateral liquidity is lower than the + * recorded liquidity vertex. It calculates the percentage difference and, if the difference + * exceeds the threshold percentage, the monitor is paused, the UbiquityDollarToken is paused, + * and collateral in the Ubiquity Pool is disabled. + * Emits the `MonitorPaused` event when the monitor is paused due to a significant liquidity drop. + * This event is caught by the defender monitor, which alerts about the liquidity issue after detecting it. + */ function _checkThresholdPercentage( uint256 _currentCollateralLiquidity ) internal { @@ -142,6 +263,15 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { } } + /** + * @notice Pauses all collaterals in the Ubiquity Pool. + * @dev This internal function retrieves all collateral addresses from the UbiquityPoolFacet + * and attempts to pause each collateral by toggling its state. If any collateral information + * cannot be retrieved, it is assumed that the collateral may already be paused, and the function + * continues to the next collateral without reverting. + * The purpose of this function is to disable all collateral operations when a significant + * liquidity issue is detected and the monitor is paused. + */ function _pauseLibUbiquityPool() internal { address[] memory allCollaterals = ubiquityPoolFacet.allCollaterals(); @@ -153,13 +283,31 @@ contract UbiquityPoolSecurityMonitor is Initializable, UUPSUpgradeable { ) { ubiquityPoolFacet.toggleCollateral(collateralInfo.index); } catch { + // Assume collateral is already paused if information cannot be retrieved continue; } } } + /** + * @notice Pauses the UbiquityDollarToken. + * @dev This internal function pauses the UbiquityDollarToken by calling its `pause` function. + * It retrieves the UbiquityDollarToken contract address via the ManagerFacet and pauses it + * to prevent further transactions involving the dollar token during a significant liquidity issue. + */ function _pauseUbiquityDollarToken() internal { ERC20Ubiquity dollar = ERC20Ubiquity(managerFacet.dollarTokenAddress()); dollar.pause(); } + + /** + * @notice Authorizes an upgrade to a new contract implementation. + * @param newImplementation The address of the new implementation contract. + * @dev This function is protected by the `onlyMonitorAdmin` modifier, meaning only an admin + * can authorize contract upgrades. This is an internal function that overrides UUPSUpgradeable's + * _authorizeUpgrade function. + */ + function _authorizeUpgrade( + address newImplementation + ) internal override onlyMonitorAdmin {} } diff --git a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol index 21513319a..91c9322bc 100644 --- a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol @@ -148,7 +148,7 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { 1 days // price feed staleness threshold in seconds ); - // enable collateral at index 0 + // enable collateral at index 0,1,2 ubiquityPoolFacet.toggleCollateral(0); ubiquityPoolFacet.toggleCollateral(1); ubiquityPoolFacet.toggleCollateral(2); From b67985ef9e839bb399084dd8eca5c3319ff2e1fa Mon Sep 17 00:00:00 2001 From: green Date: Tue, 1 Oct 2024 14:13:00 +0200 Subject: [PATCH 26/26] docs: add docs to security monitor tests --- ...ol => UbiquityPoolSecurityMonitorTest.sol} | 132 ++++++++++++++++-- 1 file changed, 121 insertions(+), 11 deletions(-) rename packages/contracts/test/dollar/core/{PoolLiquidityMonitorTest.t.sol => UbiquityPoolSecurityMonitorTest.sol} (65%) diff --git a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol b/packages/contracts/test/dollar/core/UbiquityPoolSecurityMonitorTest.sol similarity index 65% rename from packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol rename to packages/contracts/test/dollar/core/UbiquityPoolSecurityMonitorTest.sol index 91c9322bc..3f95097cf 100644 --- a/packages/contracts/test/dollar/core/PoolLiquidityMonitorTest.t.sol +++ b/packages/contracts/test/dollar/core/UbiquityPoolSecurityMonitorTest.sol @@ -12,7 +12,7 @@ import {MockCurveStableSwapNG} from "../../../src/dollar/mocks/MockCurveStableSw import {MockCurveTwocryptoOptimized} from "../../../src/dollar/mocks/MockCurveTwocryptoOptimized.sol"; import {ERC20Ubiquity} from "../../../src/dollar/core/ERC20Ubiquity.sol"; -contract PoolLiquidityMonitorTest is DiamondTestSetup { +contract UbiquityPoolSecurityMonitorTest is DiamondTestSetup { UbiquityPoolSecurityMonitor monitor; address defenderRelayer = address(0x456); address unauthorized = address(0x123); @@ -258,15 +258,25 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { monitor.setThresholdPercentage(newThresholdPercentage); } + /** + * @notice Tests the dropLiquidityVertex function and ensures the correct event is emitted. + * @dev Simulates a call from an account with the DEFAULT_ADMIN_ROLE to drop the liquidity vertex. + * Verifies that the current collateral liquidity is set as the new liquidity vertex and the + * LiquidityVertexDropped event is emitted with the correct value. + */ function testDropLiquidityVertex() public { + // Get the current collateral liquidity from the UbiquityPoolFacet uint256 currentCollateralLiquidity = ubiquityPoolFacet .collateralUsdBalance(); + // Expect the LiquidityVertexDropped event to be emitted with the current collateral liquidity vm.expectEmit(true, true, true, false); emit LiquidityVertexDropped(currentCollateralLiquidity); + // Simulate the admin account calling the dropLiquidityVertex function vm.prank(admin); monitor.dropLiquidityVertex(); + // The LiquidityVertexDropped event should be emitted with the correct liquidity value } function testUnauthorizedDropLiquidityVertex() public { @@ -287,16 +297,25 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { monitor.togglePaused(); } + /** + * @notice Tests the update of liquidity vertex after the collateral liquidity is increased via minting. + * @dev Simulates a user increasing liquidity by calling mintDollar and then checks if the liquidity vertex + * is updated correctly when the defender relayer calls checkLiquidityVertex. + */ function testCheckLiquidity() public { + // Simulate the user minting dollars to increase the collateral liquidity vm.prank(user); ubiquityPoolFacet.mintDollar(1, 1e18, 0.9e18, 1e18, 0, true); + // Fetch the updated collateral liquidity after minting uint256 currentCollateralLiquidity = ubiquityPoolFacet .collateralUsdBalance(); + // Expect the LiquidityVertexUpdated event to be emitted with the new liquidity value vm.expectEmit(true, true, true, false); emit LiquidityVertexUpdated(currentCollateralLiquidity); + // Simulate the defender relayer calling checkLiquidityVertex to update the liquidity vertex vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); } @@ -308,6 +327,11 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { monitor.checkLiquidityVertex(); } + /** + * @notice Tests that the `MonitorPaused` event is emitted when the liquidity drops below the configured threshold. + * @dev Simulates a scenario where the collateral liquidity drops below the threshold by redeeming dollars, + * and checks if the monitor pauses and emits the `MonitorPaused` event. + */ function testMonitorPausedEventEmittedAfterLiquidityDropBelowThreshold() public { @@ -316,157 +340,223 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { vm.prank(user); ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); + // Fetch the current collateral liquidity after redemption uint256 currentCollateralLiquidity = ubiquityPoolFacet .collateralUsdBalance(); + // Expect the MonitorPaused event to be emitted with the current liquidity and percentage drop vm.expectEmit(true, true, true, false); - emit MonitorPaused(currentCollateralLiquidity, 32); + emit MonitorPaused(currentCollateralLiquidity, 32); // 32 represents the percentage difference + // Simulate the defender relayer calling checkLiquidityVertex to trigger the liquidity check vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); } + /** + * @notice Tests that the `checkLiquidityVertex` function reverts with "Monitor paused" after the monitor is paused due to liquidity dropping below the threshold. + * @dev Simulates a drop in collateral liquidity by redeeming dollars and ensures that once the liquidity drop exceeds the threshold, + * the monitor is paused and subsequent calls to `checkLiquidityVertex` revert with the message "Monitor paused". + */ function testMonitorPausedRevertAfterLiquidityDropBelowThreshold() public { curveDollarPlainPool.updateMockParams(0.99e18); + // Simulate a user redeeming dollars, leading to a decrease in collateral liquidity vm.prank(user); ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); + // Simulate the defender relayer calling checkLiquidityVertex to trigger the monitor pause vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); + // Expect a revert with "Monitor paused" message when trying to check liquidity again vm.expectRevert("Monitor paused"); vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); } + /** + * @notice Tests that both the monitor and the Ubiquity Dollar are paused after a liquidity drop below the threshold. + * @dev Simulates a collateral price drop and ensures that: + * - The monitor is paused after the liquidity drop. + * - The collateral information is no longer valid and reverts with "Invalid collateral". + * - The Ubiquity Dollar token is paused after the liquidity drop. + */ function testMonitorAndDollarPauseAfterLiquidityDropBelowThreshold() public { curveDollarPlainPool.updateMockParams(0.99e18); + // Simulate a user redeeming dollars, leading to a decrease in collateral liquidity vm.prank(user); ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); + // Simulate the defender relayer calling checkLiquidityVertex to trigger the monitor pause vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); + // Expect a revert with "Invalid collateral" when trying to retrieve collateral information vm.expectRevert("Invalid collateral"); ubiquityPoolFacet.collateralInformation(address(collateralToken)); + // Assert that the monitor is paused bool monitorPaused = monitor.monitorPaused(); - assertTrue( monitorPaused, "Monitor should be paused after liquidity drop" ); + // Assert that the Ubiquity Dollar token is paused ERC20Ubiquity dollarToken = ERC20Ubiquity( managerFacet.dollarTokenAddress() ); bool dollarIsPaused = dollarToken.paused(); - assertTrue( dollarIsPaused, "Dollar should be paused after liquidity drop" ); } - function testLiquidityDropDoesNotPauseMonitorBelowThreshold() public { + /** + * @notice Tests that the monitor is not paused when the liquidity drop does not exceed the configured threshold. + * @dev Simulates a small collateral liquidity drop by redeeming dollars and ensures that: + * - The monitor does not pause if the liquidity drop remains above the threshold. + * - Collateral information remains valid and accessible after the liquidity drop. + */ + function testLiquidityDropDoesNotPauseMonitor() public { curveDollarPlainPool.updateMockParams(0.99e18); + // Simulate a user redeeming a small amount of dollars, causing a minor decrease in collateral liquidity vm.prank(user); ubiquityPoolFacet.redeemDollar(0, 1e17, 0, 0); + // Simulate the defender relayer calling checkLiquidityVertex to verify the monitor status vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); + // Ensure collateral information remains valid after the minor liquidity drop ubiquityPoolFacet.collateralInformation(address(collateralToken)); + // Assert that the monitor is not paused after the liquidity drop bool monitorPaused = monitor.monitorPaused(); - assertFalse( monitorPaused, "Monitor should Not be paused after liquidity drop" ); } - function testLiquidityDropPausesMonitorWhenCollateralToggledAfterThreshold() - public - { + /** + * @notice Tests that the monitor is paused after a significant liquidity drop, even when collateral was toggled before. + * @dev Simulates a scenario where collateral liquidity drops below the threshold and collateral is toggled prior to the incident. + * Ensures that: + * - The monitor pauses after the liquidity drop. + * - Any collateral that was toggled prior to the liquidity check does not interfere with the monitor's behavior. + * - Collateral information becomes inaccessible after the monitor is paused. + */ + function testLiquidityDropPausesMonitorWhenCollateralToggled() public { curveDollarPlainPool.updateMockParams(0.99e18); + // Simulate a user redeeming dollars, causing a significant liquidity drop vm.prank(user); ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); + // Simulate the admin toggling the collateral state vm.prank(admin); ubiquityPoolFacet.toggleCollateral(0); + // Simulate the defender relayer calling checkLiquidityVertex to trigger the monitor pause vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); + // Assert that the monitor is paused after the liquidity drop bool monitorPaused = monitor.monitorPaused(); - assertTrue( monitorPaused, "Monitor should be paused after liquidity drop, and any prior manipulation of collateral does not interfere with the ongoing incident management process." ); + // Ensure that collateral information is inaccessible after the monitor is paused address[] memory allCollaterals = ubiquityPoolFacet.allCollaterals(); for (uint256 i = 0; i < allCollaterals.length; i++) { vm.expectRevert("Invalid collateral"); + // Simulate the user trying to access collateral information, expecting a revert vm.prank(user); ubiquityPoolFacet.collateralInformation(allCollaterals[i]); } } + /** + * @notice Tests that the monitor is not paused when the liquidity drop does not exceed the threshold, even if collateral is toggled. + * @dev Simulates a scenario where collateral liquidity drops but not enough to trigger the monitor pause. + * Ensures that: + * - The monitor remains active after a minor liquidity drop. + * - Any collateral that was toggled prior to the liquidity check does not affect the monitor’s behavior. + */ function testLiquidityDropDoesNotPauseMonitorWhenCollateralToggled() public { curveDollarPlainPool.updateMockParams(0.99e18); + // Simulate a user redeeming a small amount of dollars, causing a minor liquidity drop vm.prank(user); ubiquityPoolFacet.redeemDollar(0, 1e17, 0, 0); + // Simulate the admin toggling the collateral state vm.prank(admin); ubiquityPoolFacet.toggleCollateral(0); + // Simulate the defender relayer calling checkLiquidityVertex to verify the monitor status vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); + // Assert that the monitor is not paused after the minor liquidity drop bool monitorPaused = monitor.monitorPaused(); - assertFalse( monitorPaused, "Monitor should Not be paused after liquidity drop, and any prior manipulation of collateral does not affect it" ); } + /** + * @notice Tests that `checkLiquidityVertex` reverts with "Monitor paused" when the monitor is manually paused. + * @dev Simulates pausing the monitor and ensures that any subsequent calls to `checkLiquidityVertex` revert with the appropriate error message. + */ function testCheckLiquidityRevertsWhenMonitorIsPaused() public { + // Expect the PausedToggled event to be emitted when the monitor is paused vm.expectEmit(true, true, true, false); emit PausedToggled(true); + // Simulate the admin manually pausing the monitor vm.prank(admin); monitor.togglePaused(); + // Expect a revert with "Monitor paused" when checkLiquidityVertex is called while the monitor is paused vm.expectRevert("Monitor paused"); + // Simulate the defender relayer attempting to check liquidity while the monitor is paused vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); } + /** + * @notice Tests that the `mintDollar` function reverts with "Collateral disabled" due to a liquidity drop. + * @dev Simulates a liquidity drop below the threshold and ensures that collateral is disabled, causing subsequent attempts to mint dollars to fail. + */ function testMintDollarRevertsWhenCollateralDisabledDueToLiquidityDrop() public { curveDollarPlainPool.updateMockParams(0.99e18); + // Simulate a user redeeming dollars, causing a significant liquidity drop vm.prank(user); ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); + // Simulate the defender relayer calling checkLiquidityVertex to trigger the monitor pause and disable collateral vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); + // Attempt to mint dollars for each collateral, expecting a revert with "Collateral disabled" uint256 collateralCount = 3; for (uint256 i = 0; i < collateralCount; i++) { vm.expectRevert("Collateral disabled"); @@ -476,44 +566,64 @@ contract PoolLiquidityMonitorTest is DiamondTestSetup { } } + /** + * @notice Tests that the `redeemDollar` function reverts with "Collateral disabled" due to a liquidity drop. + * @dev Simulates a liquidity drop below the threshold and ensures that collateral is disabled, causing subsequent attempts to redeem dollars to fail. + */ function testRedeemDollarRevertsWhenCollateralDisabledDueToLiquidityDrop() public { curveDollarPlainPool.updateMockParams(0.99e18); + // Simulate a user redeeming dollars, causing a significant liquidity drop vm.prank(user); ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); + // Simulate the defender relayer calling checkLiquidityVertex to trigger the monitor pause and disable collateral vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); + // Expect a revert with "Collateral disabled" when trying to redeem dollars after the collateral is disabled vm.expectRevert("Collateral disabled"); + + // Simulate the user attempting to redeem dollars, which should revert due to disabled collateral vm.prank(user); ubiquityPoolFacet.redeemDollar(1, 1e18, 0, 0); } + /** + * @notice Tests that the Ubiquity Dollar token reverts transfers with "Pausable: paused" when the token is paused due to a liquidity drop. + * @dev Simulates a liquidity drop below the threshold and ensures that the Ubiquity Dollar token is paused, preventing transfers. + */ function testDollarTokenRevertsOnTransferWhenPausedDueToLiquidityDrop() public { curveDollarPlainPool.updateMockParams(0.99e18); + // Simulate a user redeeming dollars, causing a significant liquidity drop vm.prank(user); ubiquityPoolFacet.redeemDollar(0, 1e18, 0, 0); + // Simulate the defender relayer calling checkLiquidityVertex to trigger the monitor pause and pause the dollar token vm.prank(defenderRelayer); monitor.checkLiquidityVertex(); + // Assert that the Dollar token is paused after the liquidity drop bool isPaused = dollarToken.paused(); assertTrue( isPaused, "Expected the Dollar token to be paused after the liquidity drop" ); + // Get the Ubiquity Dollar token contract ERC20Ubiquity dollarToken = ERC20Ubiquity( managerFacet.dollarTokenAddress() ); + // Expect a revert with "Pausable: paused" when trying to transfer the paused token vm.expectRevert("Pausable: paused"); + + // Simulate the user attempting to transfer the paused dollar token, which should revert vm.prank(user); dollarToken.transfer(address(0x123), 1e18); }