Skip to content

Commit

Permalink
Merge pull request #2 from makerdao/feat/psm-splitter-emergency-spells
Browse files Browse the repository at this point in the history
Add Lite PSM and Splitter emergency spells
  • Loading branch information
amusingaxl authored Nov 11, 2024
2 parents 1feca1d + fab5046 commit c02f3af
Show file tree
Hide file tree
Showing 5 changed files with 459 additions and 0 deletions.
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

### Overview
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 | Stop 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

0 comments on commit c02f3af

Please sign in to comment.