Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Lite PSM and Splitter emergency spells #2

Merged
merged 18 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ TBD.
| Set `Clip` breaker | :white_check_mark: | :white_check_mark: |
| Disable `DDM` | :white_check_mark: | :x: |
| Stop `OSM` | :white_check_mark: | :white_check_mark: |
| Halt `PSM` | :white_check_mark: | :x: |
| Stop `Splitter` | :x: | :white_check_mark: |

### Wipe `AutoLine`

Expand All @@ -65,6 +67,14 @@ Disables a Direct Deposit Module (`DIRECT_{ID}_PLAN`), preventing further debt f

Stops the specified Oracle Security Module (`PIP_{GEM}`) instances, preventing updates in their price feeds.

### Halt `PSM`

Halts swaps on the `PSM`, with optional direction (only `GEM` buys, only `GEM` sells, both).

### Stop `Splitter`

Disables the smart burn engine.

## Design

Emergency spells are meant to be as ABI-compatible with regular spells as possible, to allow Governance to reuse any
Expand Down
117 changes: 117 additions & 0 deletions src/lite-psm-halt/SingleLitePsmHaltSpell.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: © 2024 Dai Foundation <www.daifoundation.org>
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pragma solidity ^0.8.16;

import {DssEmergencySpell} from "../DssEmergencySpell.sol";

enum Flow {
SELL, // Halt only selling gems
BUY, // Halt only buying gems
BOTH // Halt both
}

interface LitePsmMomLike {
function halt(address psm, Flow what) external;
}

interface LitePsmLike {
function wards(address) external view returns (uint256);
function tin() external view returns (uint256);
function tout() external view returns (uint256);
function HALTED() external view returns (uint256);
function ilk() external view returns (bytes32);
}

/// @title Lite PSM Halt Emergency Spell
/// @notice Will halt trading on MCD_LITE_PSM_USDC_A, can halt only gem buys, sells, or both.
/// @custom:authors [Oddaf]
/// @custom:reviewers []
/// @custom:auditors []
/// @custom:bounties []
contract SingleLitePsmHaltSpell is DssEmergencySpell {
LitePsmMomLike public immutable litePsmMom = LitePsmMomLike(_log.getAddress("LITE_PSM_MOM"));
LitePsmLike public immutable psm;
Flow public immutable flow;

event Halt(Flow what);

constructor(address _psm, Flow _flow) {
psm = LitePsmLike(_psm);
flow = _flow;
}

function _flowToString(Flow _flow) internal pure returns (string memory) {
if (_flow == Flow.SELL) return "SELL";
if (_flow == Flow.BUY) return "BUY";
if (_flow == Flow.BOTH) return "BOTH";
return "";
}

function description() external view returns (string memory) {
return string(abi.encodePacked("Emergency Spell | ", psm.ilk(), " | halt: ", _flowToString(flow)));
}

/**
* @notice Halts trading on LitePSM
*/
function _emergencyActions() internal override {
litePsmMom.halt(address(psm), flow);
emit Halt(flow);
}

/**
* @notice Returns whether the spell is done or not.
* @dev Checks if the swaps have been halted on the psm.
* The spell would revert if any of the following conditions holds:
* 1. LitePsmMom is not a ward of LitePsm
* 2. Call to LitePsm `HALTED()` reverts (likely not a LitePsm)
* In both cases, it returns `true`, meaning no further action can be taken at the moment.
*/
function done() external view returns (bool) {
try psm.wards(address(litePsmMom)) returns (uint256 ward) {
// Ignore LitePsm instances that have not relied on LitePsmMom.
if (ward == 0) {
return true;
}
} catch {
// If the call failed, it means the contract is most likely not a LitePsm instance.
return true;
}

try psm.HALTED() returns (uint256 halted) {
if (flow == Flow.SELL) {
return psm.tin() == halted;
}
if (flow == Flow.BUY) {
return psm.tout() == halted;
}

return psm.tin() == halted && psm.tout() == halted;
} catch {
// If the call failed, it means the contract is most likely not a LitePsm instance.
return true;
}
}
}

contract SingleLitePsmHaltSpellFactory {
event Deploy(address psm, Flow indexed flow, address spell);

function deploy(address psm, Flow flow) external returns (address spell) {
spell = address(new SingleLitePsmHaltSpell(psm, flow));
emit Deploy(psm, flow, spell);
}
}
153 changes: 153 additions & 0 deletions src/lite-psm-halt/SingleLitePsmHaltSpell.t.integration.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.16;

import {stdStorage, StdStorage} from "forge-std/Test.sol";
import {DssTest, DssInstance, MCD, GodMode} from "dss-test/DssTest.sol";
import {DssEmergencySpellLike} from "../DssEmergencySpell.sol";
import {SingleLitePsmHaltSpellFactory, Flow} from "./SingleLitePsmHaltSpell.sol";

interface LitePsmLike {
function deny(address) external;
function tin() external view returns (uint256);
function tout() external view returns (uint256);
function HALTED() external view returns (uint256);
}

contract MockAuth {
function wards(address) external pure returns (uint256) {
return 1;
}
}

contract MockPsmHaltedReverts is MockAuth {
function HALTED() external pure {
revert();
}
}

contract SingleLitePsmHaltSpellTest is DssTest {
using stdStorage for StdStorage;

address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F;
DssInstance dss;
address chief;
address litePsmMom;
LitePsmLike psm;
SingleLitePsmHaltSpellFactory factory;

function setUp() public {
vm.createSelectFork("mainnet");

dss = MCD.loadFromChainlog(CHAINLOG);
MCD.giveAdminAccess(dss);
chief = dss.chainlog.getAddress("MCD_ADM");
litePsmMom = dss.chainlog.getAddress("LITE_PSM_MOM");
psm = LitePsmLike(dss.chainlog.getAddress("MCD_LITE_PSM_USDC_A"));
factory = new SingleLitePsmHaltSpellFactory();
}

function testPsmHaltOnScheduleBuy() public {
_checkPsmHaltOnSchedule(Flow.BUY);
}

function testPsmHaltOnScheduleSell() public {
_checkPsmHaltOnSchedule(Flow.SELL);
}

function testPsmHaltOnScheduleBoth() public {
_checkPsmHaltOnSchedule(Flow.BOTH);
}

function _checkPsmHaltOnSchedule(Flow flow) internal {
DssEmergencySpellLike spell = DssEmergencySpellLike(factory.deploy(address(psm), flow));
stdstore.target(chief).sig("hat()").checked_write(address(spell));
vm.makePersistent(chief);

uint256 preTin = psm.tin();
uint256 preTout = psm.tout();
uint256 halted = psm.HALTED();

if (flow == Flow.SELL || flow == Flow.BOTH) {
assertNotEq(preTin, halted, "before: PSM SELL already halted");
}
if (flow == Flow.BUY || flow == Flow.BOTH) {
assertNotEq(preTout, halted, "before: PSM BUY already halted");
}
assertFalse(spell.done(), "before: spell already done");

vm.expectEmit(true, true, true, false, address(spell));
emit Halt(flow);

spell.schedule();

uint256 postTin = psm.tin();
uint256 postTout = psm.tout();

if (flow == Flow.SELL || flow == Flow.BOTH) {
assertEq(postTin, halted, "after: PSM SELL not halted (tin)");
}
if (flow == Flow.BUY || flow == Flow.BOTH) {
assertEq(postTout, halted, "after: PSM BUY not halted (tout)");
}

assertTrue(spell.done(), "after: spell not done");
}

function testDoneWhenLitePsmMomIsNotWardInPsm() public {
DssEmergencySpellLike spell = DssEmergencySpellLike(factory.deploy(address(psm), Flow.BUY));

address pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY");
vm.prank(pauseProxy);
psm.deny(address(litePsmMom));

assertTrue(spell.done(), "spell not done");
}

function testDoneWhenLitePsmDoesNotImplementHalted() public {
psm = LitePsmLike(address(new MockAuth()));
DssEmergencySpellLike spell = DssEmergencySpellLike(factory.deploy(address(psm), Flow.BUY));

assertTrue(spell.done(), "spell not done");
}

function testDoneWhenLitePsmHaltedReverts() public {
psm = LitePsmLike(address(new MockPsmHaltedReverts()));
DssEmergencySpellLike spell = DssEmergencySpellLike(factory.deploy(address(psm), Flow.BUY));

assertTrue(spell.done(), "spell not done");
}

function testRevertPsmHaltWhenItDoesNotHaveTheHat() public {
Flow flow = Flow.BOTH;
DssEmergencySpellLike spell = DssEmergencySpellLike(factory.deploy(address(psm), flow));

uint256 preTin = psm.tin();
uint256 preTout = psm.tout();
uint256 halted = psm.HALTED();

if (flow == Flow.SELL || flow == Flow.BOTH) {
assertNotEq(preTin, halted, "before: PSM SELL already halted");
}
if (flow == Flow.BUY || flow == Flow.BOTH) {
assertNotEq(preTout, halted, "before: PSM BUY already halted");
}
assertFalse(spell.done(), "before: spell already done");

vm.expectRevert();
spell.schedule();

uint256 postTin = psm.tin();
uint256 postTout = psm.tout();

if (flow == Flow.SELL || flow == Flow.BOTH) {
assertEq(postTin, preTin, "after: PSM SELL halted unexpectedly (tin)");
}
if (flow == Flow.BUY || flow == Flow.BOTH) {
assertEq(postTout, preTout, "after: PSM BUY halted unexpectedly (tout)");
}

assertFalse(spell.done(), "after: spell done unexpectedly");
}

event Halt(Flow what);
}
77 changes: 77 additions & 0 deletions src/splitter-stop/SplitterStopSpell.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: © 2024 Dai Foundation <www.daifoundation.org>
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pragma solidity ^0.8.16;

import {DssEmergencySpell} from "../DssEmergencySpell.sol";

interface SplitterMomLike {
function stop() external;
}

interface SplitterLike {
function wards(address) external view returns (uint256);
function hop() external view returns (uint256);
}

/// @title Splitter Stop Emergency Spell
/// @notice Will disable the Splitter (Smart Burn Engine, former Flap auctions)
/// @custom:authors [Oddaf]
/// @custom:reviewers []
/// @custom:auditors []
/// @custom:bounties []
contract SplitterStopSpell is DssEmergencySpell {
string public constant override description = "Emergency Spell | Disable Splitter";

SplitterMomLike public immutable splitterMom = SplitterMomLike(_log.getAddress("SPLITTER_MOM"));
SplitterLike public immutable splitter = SplitterLike(_log.getAddress("MCD_SPLIT"));

event Stop();

/**
* @notice Disables Splitter
*/
function _emergencyActions() internal override {
splitterMom.stop();
emit Stop();
}

/**
* @notice Returns whether the spell is done or not.
* @dev Checks if `splitter.hop() == type(uint).max` (disabled).
* The spell would revert if any of the following conditions holds:
* 1. SplitterMom is not a ward of Splitter
* 2. Call to Splitter `hop()` reverts (likely not a Splitter)
* In both cases, it returns `true`, meaning no further action can be taken at the moment.
*/
function done() external view returns (bool) {
try splitter.wards(address(splitterMom)) returns (uint256 ward) {
// Ignore Splitter instances that have not relied on SplitterMom.
if (ward == 0) {
return true;
}
} catch {
// If the call failed, it means the contract is most likely not a Splitter instance.
return true;
}

try splitter.hop() returns (uint256 hop) {
return hop == type(uint256).max;
} catch {
// If the call failed, it means the contract is most likely not a Splitter instance.
return true;
}
}
}
Loading