-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from makerdao/feat/psm-splitter-emergency-spells
Add Lite PSM and Splitter emergency spells
- Loading branch information
Showing
5 changed files
with
459 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
153
src/lite-psm-halt/SingleLitePsmHaltSpell.t.integration.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.