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 7 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` | n/a | :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
98 changes: 98 additions & 0 deletions src/lite-psm-halt/SingleLitePsmHaltSpell.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// 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 tin() external view returns (uint256);
function tout() external view returns (uint256);
function HALTED() external view returns (uint256);
}

/// @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"));
Flow public immutable flow;
address public immutable psm = _log.getAddress("MCD_LITE_PSM_USDC_A");

event Halt(Flow what);

constructor(Flow _flow) {
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 | MCD_LITE_PSM_USDC_A halt: ", flowToString(flow)));
}

/**
* @notice Halts trading on LitePSM
*/
function _emergencyActions() internal override {
litePsmMom.halt(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.
*/
function done() external view returns (bool) {
uint256 halted = LitePsmLike(psm).HALTED();

if (flow == Flow.SELL || flow == Flow.BOTH) {
if (LitePsmLike(psm).tin() != halted) return false;
}

if (flow == Flow.BUY || flow == Flow.BOTH) {
if (LitePsmLike(psm).tout() != halted) return false;
}

return true;
}
}

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

function deploy(Flow flow) external returns (address spell) {
spell = address(new SingleLitePsmHaltSpell(flow));
emit Deploy(flow, spell);
}
}
111 changes: 111 additions & 0 deletions src/lite-psm-halt/SingleLitePsmHaltSpell.t.integration.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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, LitePsmLike, Flow} from "./SingleLitePsmHaltSpell.sol";

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 {
testPsmHaltOnSchedule(Flow.BUY);
}

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

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

function testPsmHaltOnSchedule(Flow flow) internal {
DssEmergencySpellLike spell = DssEmergencySpellLike(factory.deploy(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 testRevertPsmHaltWhenItDoesNotHaveTheHat() public {
Flow flow = Flow.BOTH;
DssEmergencySpellLike spell = DssEmergencySpellLike(factory.deploy(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);
}
57 changes: 57 additions & 0 deletions src/splitter-stop/SplitterStopSpell.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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 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 SplitterDisabled();

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

/**
* @notice Returns whether the spell is done or not.
* @dev Checks if `splitter.hop() == type(uint).max` (disabled).
*/
function done() external view returns (bool) {
return splitter.hop() == type(uint256).max;
}
}
62 changes: 62 additions & 0 deletions src/splitter-stop/SplitterStopSpell.t.integration.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// 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 {SplitterStopSpell, SplitterLike} from "./SplitterStopSpell.sol";

contract SplitterStopSpellTest is DssTest {
using stdStorage for StdStorage;

address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F;
DssInstance dss;
address chief;
SplitterLike splitter;
SplitterStopSpell spell;

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

dss = MCD.loadFromChainlog(CHAINLOG);
MCD.giveAdminAccess(dss);
chief = dss.chainlog.getAddress("MCD_ADM");
splitter = SplitterLike(dss.chainlog.getAddress("MCD_SPLIT"));
spell = new SplitterStopSpell();

stdstore.target(chief).sig("hat()").checked_write(address(spell));

vm.makePersistent(chief);
}

function testSplitterStopOnSchedule() public {
uint256 preHop = splitter.hop();
assertTrue(preHop != type(uint256).max, "before: Splitter already stopped");
assertFalse(spell.done(), "before: spell already done");

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

spell.schedule();

uint256 postHop = splitter.hop();
assertEq(postHop, type(uint256).max, "after: Splitter not stopped");
assertTrue(spell.done(), "after: spell not done");
}

function testRevertSplitterStopWhenItDoesNotHaveTheHat() public {
stdstore.target(chief).sig("hat()").checked_write(address(0));

uint256 preHop = splitter.hop();
assertTrue(preHop != type(uint256).max, "before: Splitter already stopped");
assertFalse(spell.done(), "before: spell already done");

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

uint256 postHop = splitter.hop();
assertEq(postHop, preHop, "after: Splitter stopped unexpectedly");
assertFalse(spell.done(), "after: spell done unexpectedly");
}

event SplitterDisabled();
}