From 403d531dff41c0eb801f65bca6bf5ef336e2bbd1 Mon Sep 17 00:00:00 2001 From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Date: Sat, 17 Jan 2026 11:54:25 +0100 Subject: [PATCH 1/5] fix: better name for already shutdown error --- contracts/src/PriceFeeds/FXPriceFeed.sol | 4 ++-- contracts/test/FXPriceFeed.t.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/PriceFeeds/FXPriceFeed.sol b/contracts/src/PriceFeeds/FXPriceFeed.sol index af615a8ae..cd1c85463 100644 --- a/contracts/src/PriceFeeds/FXPriceFeed.sol +++ b/contracts/src/PriceFeeds/FXPriceFeed.sol @@ -36,7 +36,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { bool public isShutdown; /// @notice Thrown when the attempting to shutdown the contract when it is already shutdown - error IsShutDown(); + error AlreadyShutdown(); /// @notice Thrown when a non-watchdog address attempts to shutdown the contract error CallerNotWatchdog(); /// @notice Thrown when a zero address is provided as a parameter @@ -145,7 +145,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { * - The shutdown state is permanent and cannot be reversed */ function shutdown() external { - if (isShutdown) revert IsShutDown(); + if (isShutdown) revert AlreadyShutdown(); if (msg.sender != watchdogAddress) revert CallerNotWatchdog(); isShutdown = true; diff --git a/contracts/test/FXPriceFeed.t.sol b/contracts/test/FXPriceFeed.t.sol index e3b0a0a11..767555555 100644 --- a/contracts/test/FXPriceFeed.t.sol +++ b/contracts/test/FXPriceFeed.t.sol @@ -296,7 +296,7 @@ contract FXPriceFeedTest is Test { function test_shutdown_whenAlreadyShutdown_shouldRevert() initialized public { vm.prank(watchdog); fxPriceFeed.shutdown(); - vm.expectRevert(FXPriceFeed.IsShutDown.selector); + vm.expectRevert(FXPriceFeed.AlreadyShutdown.selector); fxPriceFeed.shutdown(); vm.stopPrank(); } From 1f4a659dd9eacb2c73df388a757c2359bdc322d8 Mon Sep 17 00:00:00 2001 From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:10:19 +0100 Subject: [PATCH 2/5] feat: add l2Sequencer check --- contracts/src/BorrowerOperations.sol | 7 +++ contracts/src/Interfaces/IOracleAdapter.sol | 7 +++ contracts/src/Interfaces/IPriceFeed.sol | 1 + contracts/src/PriceFeeds/FXPriceFeed.sol | 63 ++++++++++++++++++- contracts/src/TroveManager.sol | 8 +++ contracts/test/FXPriceFeed.t.sol | 15 ++++- .../test/TestContracts/MockFXPriceFeed.sol | 9 +++ 7 files changed, 106 insertions(+), 4 deletions(-) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 959dbbbc8..c6fae3ad1 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -129,6 +129,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio error MinInterestRateChangePeriodTooLow(); error NewOracleFailureDetected(); error BatchSharesRatioTooLow(); + error L2SequencerDown(); event TroveManagerAddressChanged(address _newTroveManagerAddress); event GasPoolAddressChanged(address _gasPoolAddress); @@ -1382,6 +1383,12 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } + function _requireL2SequencerIsUp() internal view { + if (!priceFeed.isL2SequencerUp()) { + revert L2SequencerDown(); + } + } + function _requireDelegateCallSucceeded(bool success, bytes memory data) internal pure { if (!success) { assembly { diff --git a/contracts/src/Interfaces/IOracleAdapter.sol b/contracts/src/Interfaces/IOracleAdapter.sol index 20fab15d4..083d33ca4 100644 --- a/contracts/src/Interfaces/IOracleAdapter.sol +++ b/contracts/src/Interfaces/IOracleAdapter.sol @@ -16,4 +16,11 @@ interface IOracleAdapter { * @return denominator The denominator of the rate */ function getFXRateIfValid(address rateFeedID) external view returns (uint256 numerator, uint256 denominator); + + /** + * @notice Returns true if the L2 sequencer has been up and operational for at least the specified duration. + * @param since The minimum number of seconds the L2 sequencer must have been up (e.g., 1 hours = 3600). + * @return up True if the sequencer has been up for at least `since` seconds, false otherwise + */ + function isL2SequencerUp(uint256 since) external view returns (bool up); } diff --git a/contracts/src/Interfaces/IPriceFeed.sol b/contracts/src/Interfaces/IPriceFeed.sol index c9bb33a1a..63c813fd8 100644 --- a/contracts/src/Interfaces/IPriceFeed.sol +++ b/contracts/src/Interfaces/IPriceFeed.sol @@ -4,4 +4,5 @@ pragma solidity ^0.8.0; interface IPriceFeed { function fetchPrice() external returns (uint256); + function isL2SequencerUp() external view returns (bool); } diff --git a/contracts/src/PriceFeeds/FXPriceFeed.sol b/contracts/src/PriceFeeds/FXPriceFeed.sol index cd1c85463..439ca9e63 100644 --- a/contracts/src/PriceFeeds/FXPriceFeed.sol +++ b/contracts/src/PriceFeeds/FXPriceFeed.sol @@ -23,6 +23,9 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { /// @notice The identifier address for the specific rate feed to query address public rateFeedID; + /// @notice The grace period for the L2 sequencer to recover from failure + uint256 public l2SequencerGracePeriod; + /// @notice The watchdog contract address authorized to trigger emergency shutdown address public watchdogAddress; @@ -35,18 +38,30 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { /// @notice Whether the contract has been shutdown due to an oracle failure bool public isShutdown; - /// @notice Thrown when the attempting to shutdown the contract when it is already shutdown + /// @notice Thrown when the attempting to shutdown an already shutdown contract error AlreadyShutdown(); /// @notice Thrown when a non-watchdog address attempts to shutdown the contract - error CallerNotWatchdog(); + error OnlyWatchdog(); /// @notice Thrown when a zero address is provided as a parameter error ZeroAddress(); + /// @notice Thrown when an invalid grace period is provided + error InvalidL2SequencerGracePeriod(); + + /// @notice Emitted when the OracleAdapter contract is updated + /// @param _oldOracleAdapterAddress The previous OracleAdapter contract + /// @param _newOracleAdapterAddress The new OracleAdapter contract + event OracleAdapterUpdated(address indexed _oldOracleAdapterAddress, address indexed _newOracleAdapterAddress); /// @notice Emitted when the rate feed ID is updated /// @param _oldRateFeedID The previous rate feed ID /// @param _newRateFeedID The new rate feed ID event RateFeedIDUpdated(address indexed _oldRateFeedID, address indexed _newRateFeedID); + /// @notice Emitted when the L2 sequencer grace period is updated + /// @param _oldL2SequencerGracePeriod The previous L2 sequencer grace period + /// @param _newL2SequencerGracePeriod The new L2 sequencer grace period + event L2SequencerGracePeriodUpdated(uint256 indexed _oldL2SequencerGracePeriod, uint256 indexed _newL2SequencerGracePeriod); + /// @notice Emitted when the watchdog address is updated /// @param _oldWatchdogAddress The previous watchdog address /// @param _newWatchdogAddress The new watchdog address @@ -69,6 +84,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { * @notice Initializes the FXPriceFeed contract * @param _oracleAdapterAddress The address of the OracleAdapter contract * @param _rateFeedID The address of the rate feed ID + * @param _l2SequencerGracePeriod The grace period for the L2 sequencer to recover from failure * @param _borrowerOperationsAddress The address of the BorrowerOperations contract * @param _watchdogAddress The address of the watchdog contract * @param _initialOwner The address of the initial owner @@ -76,6 +92,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { function initialize( address _oracleAdapterAddress, address _rateFeedID, + uint256 _l2SequencerGracePeriod, address _borrowerOperationsAddress, address _watchdogAddress, address _initialOwner @@ -88,6 +105,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { oracleAdapter = IOracleAdapter(_oracleAdapterAddress); rateFeedID = _rateFeedID; + l2SequencerGracePeriod = _l2SequencerGracePeriod; borrowerOperations = IBorrowerOperations(_borrowerOperationsAddress); watchdogAddress = _watchdogAddress; @@ -96,6 +114,24 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { _transferOwnership(_initialOwner); } + + /** + * @notice Sets the OracleAdapter contract + * @param _newOracleAdapterAddress The address of the new OracleAdapter contract + */ + function setOracleAdapter(address _newOracleAdapterAddress) external onlyOwner { + if (_newOracleAdapterAddress == address(0)) revert ZeroAddress(); + + address oldOracleAdapter = address(oracleAdapter); + oracleAdapter = IOracleAdapter(_newOracleAdapterAddress); + + emit OracleAdapterUpdated(oldOracleAdapter, _newOracleAdapterAddress); + } + + /** + * @notice Sets the rate feed ID to be queried + * @param _newRateFeedID The address of the new rate feed ID + */ function setRateFeedID(address _newRateFeedID) external onlyOwner { if (_newRateFeedID == address(0)) revert ZeroAddress(); @@ -105,6 +141,19 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { emit RateFeedIDUpdated(oldRateFeedID, _newRateFeedID); } + /** + * @notice Sets the L2 sequencer grace period + * @param _newL2SequencerGracePeriod The new L2 sequencer grace period (in seconds) + */ + function setL2SequencerGracePeriod(uint256 _newL2SequencerGracePeriod) external onlyOwner { + if (_newL2SequencerGracePeriod == 0) revert InvalidL2SequencerGracePeriod(); + + uint256 oldL2SequencerGracePeriod = l2SequencerGracePeriod; + l2SequencerGracePeriod = _newL2SequencerGracePeriod; + + emit L2SequencerGracePeriodUpdated(oldL2SequencerGracePeriod, _newL2SequencerGracePeriod); + } + /** * @notice Sets the watchdog address * @param _newWatchdogAddress The address of the new watchdog contract @@ -118,6 +167,14 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { emit WatchdogAddressUpdated(oldWatchdogAddress, _newWatchdogAddress); } + /** + * @notice Checks if the L2 sequencer is up and the grace period has passed + * @return True if the L2 sequencer is up and the grace period has passed, false otherwise + */ + function isL2SequencerUp() public view returns (bool) { + return oracleAdapter.isL2SequencerUp(l2SequencerGracePeriod); + } + /** * @notice Fetches the price of the FX rate, if valid * @dev If the contract is shutdown due to oracle failure, the last valid price is returned @@ -146,7 +203,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { */ function shutdown() external { if (isShutdown) revert AlreadyShutdown(); - if (msg.sender != watchdogAddress) revert CallerNotWatchdog(); + if (msg.sender != watchdogAddress) revert OnlyWatchdog(); isShutdown = true; borrowerOperations.shutdownFromOracleFailure(); diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 7349299c0..a896cb4f9 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -163,6 +163,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { error NotEnoughBoldBalance(); error MinCollNotReached(uint256 _coll); error BatchSharesRatioTooHigh(); + error L2SequencerDown(); // --- Events --- @@ -402,6 +403,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { * Attempt to liquidate a custom list of troves provided by the caller. */ function batchLiquidateTroves(uint256[] memory _troveArray) public override { + _requireL2SequencerIsUp(); if (_troveArray.length == 0) { revert EmptyData(); } @@ -1198,6 +1200,12 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } } + function _requireL2SequencerIsUp() internal view { + if (!priceFeed.isL2SequencerUp()) { + revert L2SequencerDown(); + } + } + // --- Trove property getters --- function getUnbackedPortionPriceAndRedeemability() external returns (uint256, uint256, bool) { diff --git a/contracts/test/FXPriceFeed.t.sol b/contracts/test/FXPriceFeed.t.sol index 767555555..afa83cf47 100644 --- a/contracts/test/FXPriceFeed.t.sol +++ b/contracts/test/FXPriceFeed.t.sol @@ -41,7 +41,9 @@ contract FXPriceFeedTest is Test { FXPriceFeed public fxPriceFeed; MockOracleAdapter public mockOracleAdapter; MockBorrowerOperations public mockBorrowerOperations; + MockFXPriceFeed public mockFXPriceFeed; + uint256 public l2SequencerGracePeriod = 6 hours; address public rateFeedID = makeAddr("rateFeedID"); address public watchdog = makeAddr("watchdog"); address public owner = makeAddr("owner"); @@ -54,6 +56,7 @@ contract FXPriceFeedTest is Test { fxPriceFeed.initialize( address(mockOracleAdapter), rateFeedID, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -78,6 +81,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -91,6 +95,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(0), rateFeedID, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -104,6 +109,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), address(0), + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -117,6 +123,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + l2SequencerGracePeriod, address(0), watchdog, owner @@ -130,6 +137,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + l2SequencerGracePeriod, address(mockBorrowerOperations), address(0), owner @@ -143,6 +151,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, address(0) @@ -157,6 +166,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -164,6 +174,7 @@ contract FXPriceFeedTest is Test { assertEq(address(newFeed.oracleAdapter()), address(mockOracleAdapter)); assertEq(newFeed.rateFeedID(), rateFeedID); + assertEq(newFeed.l2SequencerGracePeriod(), l2SequencerGracePeriod); assertEq(address(newFeed.borrowerOperations()), address(mockBorrowerOperations)); assertEq(newFeed.watchdogAddress(), watchdog); assertEq(newFeed.owner(), owner); @@ -176,6 +187,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -185,6 +197,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + l2SequencerGracePeriod, address(mockBorrowerOperations), watchdog, owner @@ -274,7 +287,7 @@ contract FXPriceFeedTest is Test { address notWatchdog = makeAddr("notWatchdog"); vm.prank(notWatchdog); - vm.expectRevert(FXPriceFeed.CallerNotWatchdog.selector); + vm.expectRevert(FXPriceFeed.OnlyWatchdog.selector); fxPriceFeed.shutdown(); vm.stopPrank(); } diff --git a/contracts/test/TestContracts/MockFXPriceFeed.sol b/contracts/test/TestContracts/MockFXPriceFeed.sol index 6d2d0e753..1c4b7a493 100644 --- a/contracts/test/TestContracts/MockFXPriceFeed.sol +++ b/contracts/test/TestContracts/MockFXPriceFeed.sol @@ -13,6 +13,7 @@ contract MockFXPriceFeed is IMockFXPriceFeed { string private _revertMsg = "MockFXPriceFeed: no valid price"; uint256 private _price = 200 * 1e18; bool private _hasValidPrice = true; + bool private _isL2SequencerUp = true; function getPrice() external view override returns (uint256) { return _price; @@ -26,12 +27,20 @@ contract MockFXPriceFeed is IMockFXPriceFeed { _price = price; } + function setL2SequencerUp(bool up) external { + _isL2SequencerUp = up; + } + function fetchPrice() external view override returns (uint256) { require(_hasValidPrice, _revertMsg); return _price; } + function isL2SequencerUp() external view override returns (bool) { + return _isL2SequencerUp; + } + function REVERT_MSG() external view override returns (string memory) { return _revertMsg; } From 0f3fc2d62641f21db22af64358116d7d5adf6b05 Mon Sep 17 00:00:00 2001 From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Date: Sat, 17 Jan 2026 14:10:23 +0100 Subject: [PATCH 3/5] test: add tests to fx price feed --- contracts/test/FXPriceFeed.t.sol | 83 ++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/contracts/test/FXPriceFeed.t.sol b/contracts/test/FXPriceFeed.t.sol index afa83cf47..b41e85771 100644 --- a/contracts/test/FXPriceFeed.t.sol +++ b/contracts/test/FXPriceFeed.t.sol @@ -21,6 +21,7 @@ contract MockBorrowerOperations { contract MockOracleAdapter { uint256 numerator; uint256 denominator; + bool public sequencerUp = true; function setFXRate(uint256 _numerator, uint256 _denominator) external { numerator = _numerator; @@ -30,6 +31,14 @@ contract MockOracleAdapter { function getFXRateIfValid(address) external view returns (uint256, uint256) { return (numerator, denominator); } + + function setIsL2SequencerUp(bool _isUp) external { + sequencerUp = _isUp; + } + + function isL2SequencerUp(uint256) external view returns (bool) { + return sequencerUp; + } } @@ -37,6 +46,8 @@ contract FXPriceFeedTest is Test { event WatchdogAddressUpdated(address indexed _oldWatchdogAddress, address indexed _newWatchdogAddress); event FXPriceFeedShutdown(); + event OracleAdapterUpdated(address indexed _oldOracleAdapterAddress, address indexed _newOracleAdapterAddress); + event L2SequencerGracePeriodUpdated(uint256 indexed _oldL2SequencerGracePeriod, uint256 indexed _newL2SequencerGracePeriod); FXPriceFeed public fxPriceFeed; MockOracleAdapter public mockOracleAdapter; @@ -68,6 +79,7 @@ contract FXPriceFeedTest is Test { function setUp() public { mockOracleAdapter = new MockOracleAdapter(); mockOracleAdapter.setFXRate(mockRateNumerator, mockRateDenominator); + mockOracleAdapter.setIsL2SequencerUp(true); mockBorrowerOperations = new MockBorrowerOperations(); @@ -313,4 +325,75 @@ contract FXPriceFeedTest is Test { fxPriceFeed.shutdown(); vm.stopPrank(); } + + function test_setOracleAdapter_whenCalledByNonOwner_shouldRevert() initialized public { + address notOwner = makeAddr("notOwner"); + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + fxPriceFeed.setOracleAdapter(makeAddr("newOracleAdapter")); + vm.stopPrank(); + } + + function test_setOracleAdapter_whenNewAddressIsZero_shouldRevert() initialized public { + vm.prank(owner); + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + fxPriceFeed.setOracleAdapter(address(0)); + vm.stopPrank(); + } + + function test_setOracleAdapter_whenCalledByOwner_shouldSucceed() initialized public { + address newOracleAdapter = makeAddr("newOracleAdapter"); + + vm.prank(owner); + vm.expectEmit(); + emit OracleAdapterUpdated(address(mockOracleAdapter), newOracleAdapter); + fxPriceFeed.setOracleAdapter(newOracleAdapter); + vm.stopPrank(); + + assertEq(address(fxPriceFeed.oracleAdapter()), newOracleAdapter); + } + + function test_setL2SequencerGracePeriod_whenCalledByNonOwner_shouldRevert() initialized public { + address notOwner = makeAddr("notOwner"); + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + fxPriceFeed.setL2SequencerGracePeriod(12 hours); + vm.stopPrank(); + } + + function test_setL2SequencerGracePeriod_whenNewPeriodIsZero_shouldRevert() initialized public { + vm.prank(owner); + vm.expectRevert(FXPriceFeed.InvalidL2SequencerGracePeriod.selector); + fxPriceFeed.setL2SequencerGracePeriod(0); + vm.stopPrank(); + } + + function test_setL2SequencerGracePeriod_whenCalledByOwner_shouldSucceed() initialized public { + uint256 oldGracePeriod = fxPriceFeed.l2SequencerGracePeriod(); + uint256 newGracePeriod = 12 hours; + + vm.prank(owner); + vm.expectEmit(); + emit L2SequencerGracePeriodUpdated(oldGracePeriod, newGracePeriod); + fxPriceFeed.setL2SequencerGracePeriod(newGracePeriod); + vm.stopPrank(); + + assertEq(fxPriceFeed.l2SequencerGracePeriod(), newGracePeriod); + } + + function test_isL2SequencerUp_whenSequencerIsUp_shouldReturnTrue() initialized public { + mockOracleAdapter.setIsL2SequencerUp(true); + + bool result = fxPriceFeed.isL2SequencerUp(); + assertTrue(result); + } + + function test_isL2SequencerUp_whenSequencerIsDown_shouldReturnFalse() initialized public { + mockOracleAdapter.setIsL2SequencerUp(false); + + bool result = fxPriceFeed.isL2SequencerUp(); + assertFalse(result); + } } From 6d5f79543f1b695d22cef2cdec7ddc57e2eece18 Mon Sep 17 00:00:00 2001 From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Date: Sat, 17 Jan 2026 14:59:22 +0100 Subject: [PATCH 4/5] revert: remove changes from borrower ops --- contracts/src/BorrowerOperations.sol | 7 ------- 1 file changed, 7 deletions(-) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index c6fae3ad1..959dbbbc8 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -129,7 +129,6 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio error MinInterestRateChangePeriodTooLow(); error NewOracleFailureDetected(); error BatchSharesRatioTooLow(); - error L2SequencerDown(); event TroveManagerAddressChanged(address _newTroveManagerAddress); event GasPoolAddressChanged(address _gasPoolAddress); @@ -1383,12 +1382,6 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - function _requireL2SequencerIsUp() internal view { - if (!priceFeed.isL2SequencerUp()) { - revert L2SequencerDown(); - } - } - function _requireDelegateCallSucceeded(bool success, bytes memory data) internal pure { if (!success) { assembly { From 3f123c19a5602aa3b6faf9a0353bd64f2f5e3e30 Mon Sep 17 00:00:00 2001 From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:17:19 +0100 Subject: [PATCH 5/5] test: add liquidation test --- .../TestContracts/Interfaces/IMockFXPriceFeed.sol | 1 + contracts/test/troveManager.t.sol | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol b/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol index f7e0be41e..d2a05e163 100644 --- a/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol +++ b/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol @@ -9,4 +9,5 @@ interface IMockFXPriceFeed is IPriceFeed { function setPrice(uint256 _price) external; function getPrice() external view returns (uint256); function setValidPrice(bool valid) external; + function setL2SequencerUp(bool up) external; } diff --git a/contracts/test/troveManager.t.sol b/contracts/test/troveManager.t.sol index 621383f88..a63b80915 100644 --- a/contracts/test/troveManager.t.sol +++ b/contracts/test/troveManager.t.sol @@ -217,4 +217,19 @@ contract TroveManagerTest is DevTestSetup { liquidatedTroves[1] = troveIDs.B; troveManager.batchLiquidateTroves(liquidatedTroves); } + + function testLiquidationRevertsWhenL2SequencerIsDown() public { + priceFeed.setPrice(2000e18); + uint256 ATroveId = openTroveNoHints100pct(A, 100 ether, 100_000e18, 1e17); + uint256 BTroveId = openTroveNoHints100pct(B, 100 ether, 100_000e18, 1e17); + + + priceFeed.setPrice(1_000e18); + priceFeed.setL2SequencerUp(false); + + vm.startPrank(A); + vm.expectRevert(TroveManager.L2SequencerDown.selector); + troveManager.liquidate(ATroveId); + vm.stopPrank(); + } }