From ee2295af59d99c604686cd7a5a94932b15c36c88 Mon Sep 17 00:00:00 2001 From: amusingaxl <112016538+amusingaxl@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:40:20 -0300 Subject: [PATCH 1/8] feat(auto-line-wipe): add hardcoded multi-ilk spells --- lib/dss-test | 2 +- src/auto-line-wipe/EthAutoLineWipeSpell.sol | 92 +++++++++ .../EthAutoLineWipeSpell.t.integration.sol | 169 +++++++++++++++++ src/auto-line-wipe/WbtcAutoLineWipeSpell.sol | 92 +++++++++ .../WbtcAutoLineWipeSpell.t.integration.sol | 177 ++++++++++++++++++ .../WstethAutoLineWipeSpell.sol | 89 +++++++++ .../WstethAutoLineWipeSpell.t.integration.sol | 146 +++++++++++++++ 7 files changed, 766 insertions(+), 1 deletion(-) create mode 100644 src/auto-line-wipe/EthAutoLineWipeSpell.sol create mode 100644 src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol create mode 100644 src/auto-line-wipe/WbtcAutoLineWipeSpell.sol create mode 100644 src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol create mode 100644 src/auto-line-wipe/WstethAutoLineWipeSpell.sol create mode 100644 src/auto-line-wipe/WstethAutoLineWipeSpell.t.integration.sol diff --git a/lib/dss-test b/lib/dss-test index 6d4029d..dd3ff09 160000 --- a/lib/dss-test +++ b/lib/dss-test @@ -1 +1 @@ -Subproject commit 6d4029dc373d4e2af2404016e0a834e53d976264 +Subproject commit dd3ff0970cded87ebd875120220225e72cd8c75a diff --git a/src/auto-line-wipe/EthAutoLineWipeSpell.sol b/src/auto-line-wipe/EthAutoLineWipeSpell.sol new file mode 100644 index 0000000..7121060 --- /dev/null +++ b/src/auto-line-wipe/EthAutoLineWipeSpell.sol @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// 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 . +pragma solidity ^0.8.16; + +import {DssEmergencySpell} from "../DssEmergencySpell.sol"; + +interface LineMomLike { + function autoLine() external view returns (address); + function ilks(bytes32 ilk) external view returns (uint256); + function wipe(bytes32 ilk) external returns (uint256); +} + +interface AutoLineLike { + function ilks(bytes32 ilk) + external + view + returns (uint256 maxLine, uint256 gap, uint48 ttl, uint48 last, uint48 lastInc); + function wards(address who) external view returns (uint256); +} + +interface VatLike { + function ilks(bytes32 ilk) + external + view + returns (uint256 Art, uint256 rate, uint256 spot, uint256 line, uint256 dust); + function wards(address who) external view returns (uint256); +} + +contract EthAutoLineWipeSpell is DssEmergencySpell { + LineMomLike public immutable lineMom = LineMomLike(_log.getAddress("LINE_MOM")); + AutoLineLike public immutable autoLine = AutoLineLike(LineMomLike(_log.getAddress("LINE_MOM")).autoLine()); + VatLike public immutable vat = VatLike(_log.getAddress("MCD_VAT")); + bytes32 internal immutable ETH_A = "ETH-A"; + bytes32 internal immutable ETH_B = "ETH-B"; + bytes32 internal immutable ETH_C = "ETH-C"; + + event Wipe(bytes32 indexed ilk); + + function description() external view returns (string memory) { + return string(abi.encodePacked("Emergency Spell | Auto-Line Wipe: ", ETH_A, ", ", ETH_B, ", ", ETH_C)); + } + + function _emergencyActions() internal override { + lineMom.wipe(ETH_A); + lineMom.wipe(ETH_B); + lineMom.wipe(ETH_C); + + emit Wipe(ETH_A); + emit Wipe(ETH_B); + emit Wipe(ETH_C); + } + + /** + * @notice Returns whether the spell is done or not. + * @dev Checks if the all ilks have been wiped from auto-line and vat line is zero for all ilks. + * The spell would revert if any of the following conditions holds: + * 1. LineMom is not ward on Vat + * 2. LineMom is not ward on AutoLine + * 3. The ilk has not been added to AutoLine + * In such cases, it returns `true`, meaning no further action can be taken at the moment. + */ + function done() external view returns (bool) { + return _done(ETH_A) && _done(ETH_B) && _done(ETH_C); + } + + /** + * @notice Returns whether the spell is done or not. + */ + function _done(bytes32 _ilk) internal view returns (bool) { + if (vat.wards(address(lineMom)) == 0 || autoLine.wards(address(lineMom)) == 0 || lineMom.ilks(_ilk) == 0) { + return true; + } + + (,,, uint256 line,) = vat.ilks(_ilk); + (uint256 maxLine, uint256 gap, uint48 ttl, uint48 last, uint48 lastInc) = autoLine.ilks(_ilk); + + return line == 0 && maxLine == 0 && gap == 0 && ttl == 0 && last == 0 && lastInc == 0; + } +} diff --git a/src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol b/src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol new file mode 100644 index 0000000..43e2514 --- /dev/null +++ b/src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// 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 . +pragma solidity ^0.8.16; + +import {stdStorage, StdStorage} from "forge-std/Test.sol"; +import {DssTest, DssInstance, MCD} from "dss-test/DssTest.sol"; +import {DssEmergencySpellLike} from "../DssEmergencySpell.sol"; +import {EthAutoLineWipeSpell} from "./EthAutoLineWipeSpell.sol"; + +interface AutoLineLike { + function ilks(bytes32 ilk) + external + view + returns (uint256 maxLine, uint256 gap, uint48 ttl, uint48 last, uint48 lastInc); +} + +interface LineMomLike { + function delIlk(bytes32 ilk) external; +} + +interface VatLike { + function file(bytes32 ilk, bytes32 what, uint256 data) external; +} + +contract EthAutoLineWipeSpellTest is DssTest { + using stdStorage for StdStorage; + + address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; + DssInstance dss; + address pauseProxy; + VatLike vat; + address chief; + bytes32 ETH_A = "ETH-A"; + bytes32 ETH_B = "ETH-B"; + bytes32 ETH_C = "ETH-C"; + LineMomLike lineMom; + AutoLineLike autoLine; + DssEmergencySpellLike spell; + + function setUp() public { + vm.createSelectFork("mainnet"); + + dss = MCD.loadFromChainlog(CHAINLOG); + MCD.giveAdminAccess(dss); + pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); + vat = VatLike(dss.chainlog.getAddress("MCD_VAT")); + chief = dss.chainlog.getAddress("MCD_ADM"); + lineMom = LineMomLike(dss.chainlog.getAddress("LINE_MOM")); + autoLine = AutoLineLike(dss.chainlog.getAddress("MCD_IAM_AUTO_LINE")); + spell = new EthAutoLineWipeSpell(); + + stdstore.target(chief).sig("hat()").checked_write(address(spell)); + + vm.makePersistent(chief); + } + + function testAutoLineWipeOnSchedule() public { + uint256 pmaxLine; + uint256 pgap; + + (pmaxLine, pgap,,,) = autoLine.ilks(ETH_A); + assertGt(pmaxLine, 0, "ETH-A before: auto-line already wiped"); + assertGt(pgap, 0, "ETH-A before: auto-line already wiped"); + assertFalse(spell.done(), "ETH-A before: spell already done"); + + (pmaxLine, pgap,,,) = autoLine.ilks(ETH_B); + assertGt(pmaxLine, 0, "ETH-B before: auto-line already wiped"); + assertGt(pgap, 0, "ETH-B before: auto-line already wiped"); + assertFalse(spell.done(), "ETH-B before: spell already done"); + + (pmaxLine, pgap,,,) = autoLine.ilks(ETH_C); + assertGt(pmaxLine, 0, "ETH-C before: auto-line already wiped"); + assertGt(pgap, 0, "ETH-C before: auto-line already wiped"); + assertFalse(spell.done(), "ETH-C before: spell already done"); + + vm.expectEmit(true, true, true, false); + emit Wipe(ETH_A); + emit Wipe(ETH_B); + emit Wipe(ETH_C); + spell.schedule(); + + uint256 maxLine; + uint256 gap; + + (maxLine, gap,,,) = autoLine.ilks(ETH_A); + assertEq(maxLine, 0, "ETH-A after: auto-line not wiped (maxLine)"); + assertEq(gap, 0, "ETH-A after: auto-line not wiped (gap)"); + assertTrue(spell.done(), "ETH-A after: spell not done"); + + (maxLine, gap,,,) = autoLine.ilks(ETH_B); + assertEq(maxLine, 0, "ETH-B after: auto-line not wiped (maxLine)"); + assertEq(gap, 0, "ETH-B after: auto-line not wiped (gap)"); + assertTrue(spell.done(), "ETH-B after: spell not done"); + + (maxLine, gap,,,) = autoLine.ilks(ETH_C); + assertEq(maxLine, 0, "ETH-C after: auto-line not wiped (maxLine)"); + assertEq(gap, 0, "ETH-C after: auto-line not wiped (gap)"); + assertTrue(spell.done(), "ETH-C after: spell not done"); + } + + function testDoneWhenIlkIsNotAddedToLineMom() public { + uint256 before = vm.snapshotState(); + + vm.prank(pauseProxy); + lineMom.delIlk(ETH_A); + assertFalse(spell.done(), "ETH-A spell done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + lineMom.delIlk(ETH_B); + assertFalse(spell.done(), "ETH-B spell done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + lineMom.delIlk(ETH_C); + assertFalse(spell.done(), "ETH-C spell done"); + vm.revertToState(before); + + vm.startPrank(pauseProxy); + lineMom.delIlk(ETH_A); + lineMom.delIlk(ETH_B); + lineMom.delIlk(ETH_C); + assertTrue(spell.done(), "spell not done done"); + } + + function testDoneWhenAutoLineIsNotActiveButLineIsNonZero() public { + uint256 before = vm.snapshotState(); + + spell.schedule(); + assertTrue(spell.done(), "before: spell not done"); + + vm.prank(pauseProxy); + vat.file(ETH_A, "line", 10 ** 45); + assertFalse(spell.done(), "ETH-A after: spell still done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + vat.file(ETH_B, "line", 10 ** 45); + assertFalse(spell.done(), "ETH-B after: spell still done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + vat.file(ETH_C, "line", 10 ** 45); + assertFalse(spell.done(), "ETH-C after: spell still done"); + vm.revertToState(before); + } + + function testRevertAutoLineWipeWhenItDoesNotHaveTheHat() public { + stdstore.target(chief).sig("hat()").checked_write(address(0)); + + vm.expectRevert(); + spell.schedule(); + } + + event Wipe(bytes32 indexed ilk); +} diff --git a/src/auto-line-wipe/WbtcAutoLineWipeSpell.sol b/src/auto-line-wipe/WbtcAutoLineWipeSpell.sol new file mode 100644 index 0000000..d6bcb87 --- /dev/null +++ b/src/auto-line-wipe/WbtcAutoLineWipeSpell.sol @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// 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 . +pragma solidity ^0.8.16; + +import {DssEmergencySpell} from "../DssEmergencySpell.sol"; + +interface LineMomLike { + function autoLine() external view returns (address); + function ilks(bytes32 ilk) external view returns (uint256); + function wipe(bytes32 ilk) external returns (uint256); +} + +interface AutoLineLike { + function ilks(bytes32 ilk) + external + view + returns (uint256 maxLine, uint256 gap, uint48 ttl, uint48 last, uint48 lastInc); + function wards(address who) external view returns (uint256); +} + +interface VatLike { + function ilks(bytes32 ilk) + external + view + returns (uint256 Art, uint256 rate, uint256 spot, uint256 line, uint256 dust); + function wards(address who) external view returns (uint256); +} + +contract WbtcAutoLineWipeSpell is DssEmergencySpell { + LineMomLike public immutable lineMom = LineMomLike(_log.getAddress("LINE_MOM")); + AutoLineLike public immutable autoLine = AutoLineLike(LineMomLike(_log.getAddress("LINE_MOM")).autoLine()); + VatLike public immutable vat = VatLike(_log.getAddress("MCD_VAT")); + bytes32 internal immutable WBTC_A = "WBTC-A"; + bytes32 internal immutable WBTC_B = "WBTC-B"; + bytes32 internal immutable WBTC_C = "WBTC-C"; + + event Wipe(bytes32 indexed ilk); + + function description() external view returns (string memory) { + return string(abi.encodePacked("Emergency Spell | Auto-Line Wipe: ", WBTC_A, ", ", WBTC_B, ", ", WBTC_C)); + } + + function _emergencyActions() internal override { + lineMom.wipe(WBTC_A); + lineMom.wipe(WBTC_B); + lineMom.wipe(WBTC_C); + + emit Wipe(WBTC_A); + emit Wipe(WBTC_B); + emit Wipe(WBTC_C); + } + + /** + * @notice Returns whether the spell is done or not. + * @dev Checks if the all ilks have been wiped from auto-line and vat line is zero for all ilks. + * The spell would revert if any of the following conditions holds: + * 1. LineMom is not ward on Vat + * 2. LineMom is not ward on AutoLine + * 3. The ilk has not been added to AutoLine + * In such cases, it returns `true`, meaning no further action can be taken at the moment. + */ + function done() external view returns (bool) { + return _done(WBTC_A) && _done(WBTC_B) && _done(WBTC_C); + } + + /** + * @notice Returns whether the spell is done or not. + */ + function _done(bytes32 _ilk) internal view returns (bool) { + if (vat.wards(address(lineMom)) == 0 || autoLine.wards(address(lineMom)) == 0 || lineMom.ilks(_ilk) == 0) { + return true; + } + + (,,, uint256 line,) = vat.ilks(_ilk); + (uint256 maxLine, uint256 gap, uint48 ttl, uint48 last, uint48 lastInc) = autoLine.ilks(_ilk); + + return line == 0 && maxLine == 0 && gap == 0 && ttl == 0 && last == 0 && lastInc == 0; + } +} diff --git a/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol b/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol new file mode 100644 index 0000000..e913976 --- /dev/null +++ b/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// 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 . +pragma solidity ^0.8.16; + +import {stdStorage, StdStorage} from "forge-std/Test.sol"; +import {DssTest, DssInstance, MCD} from "dss-test/DssTest.sol"; +import {DssEmergencySpellLike} from "../DssEmergencySpell.sol"; +import {WbtcAutoLineWipeSpell} from "./WbtcAutoLineWipeSpell.sol"; + +interface AutoLineLike { + function ilks(bytes32 ilk) + external + view + returns (uint256 maxLine, uint256 gap, uint48 ttl, uint48 last, uint48 lastInc); + function setIlk(bytes32 ilk, uint256 maxLine, uint256 gap, uint256 ttl) external; +} + +interface LineMomLike { + function delIlk(bytes32 ilk) external; +} + +interface VatLike { + function file(bytes32 ilk, bytes32 what, uint256 data) external; +} + +contract WbtcAutoLineWipeSpellTest is DssTest { + using stdStorage for StdStorage; + + address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; + DssInstance dss; + address pauseProxy; + VatLike vat; + address chief; + bytes32 WBTC_A = "WBTC-A"; + bytes32 WBTC_B = "WBTC-B"; + bytes32 WBTC_C = "WBTC-C"; + LineMomLike lineMom; + AutoLineLike autoLine; + DssEmergencySpellLike spell; + + function setUp() public { + vm.createSelectFork("mainnet"); + + dss = MCD.loadFromChainlog(CHAINLOG); + MCD.giveAdminAccess(dss); + pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); + vat = VatLike(dss.chainlog.getAddress("MCD_VAT")); + chief = dss.chainlog.getAddress("MCD_ADM"); + lineMom = LineMomLike(dss.chainlog.getAddress("LINE_MOM")); + autoLine = AutoLineLike(dss.chainlog.getAddress("MCD_IAM_AUTO_LINE")); + spell = new WbtcAutoLineWipeSpell(); + + stdstore.target(chief).sig("hat()").checked_write(address(spell)); + + vm.makePersistent(chief); + } + + function testAutoLineWipeOnSchedule() public { + uint256 pmaxLine; + uint256 pgap; + + // WBTC debt ceiling was set to zero when this tests was written, so we need to overwrite the state. + vm.startPrank(pauseProxy); + autoLine.setIlk(WBTC_A, 1, 1, 1); + autoLine.setIlk(WBTC_B, 1, 1, 1); + autoLine.setIlk(WBTC_C, 1, 1, 1); + vm.stopPrank(); + + (pmaxLine, pgap,,,) = autoLine.ilks(WBTC_A); + assertGt(pmaxLine, 0, "WBTC-A before: auto-line already wiped"); + assertGt(pgap, 0, "WBTC-A before: auto-line already wiped"); + assertFalse(spell.done(), "WBTC-A before: spell already done"); + + (pmaxLine, pgap,,,) = autoLine.ilks(WBTC_B); + assertGt(pmaxLine, 0, "WBTC-B before: auto-line already wiped"); + assertGt(pgap, 0, "WBTC-B before: auto-line already wiped"); + assertFalse(spell.done(), "WBTC-B before: spell already done"); + + (pmaxLine, pgap,,,) = autoLine.ilks(WBTC_C); + assertGt(pmaxLine, 0, "WBTC-C before: auto-line already wiped"); + assertGt(pgap, 0, "WBTC-C before: auto-line already wiped"); + assertFalse(spell.done(), "WBTC-C before: spell already done"); + + vm.expectEmit(true, true, true, false); + emit Wipe(WBTC_A); + emit Wipe(WBTC_B); + emit Wipe(WBTC_C); + spell.schedule(); + + uint256 maxLine; + uint256 gap; + + (maxLine, gap,,,) = autoLine.ilks(WBTC_A); + assertEq(maxLine, 0, "WBTC-A after: auto-line not wiped (maxLine)"); + assertEq(gap, 0, "WBTC-A after: auto-line not wiped (gap)"); + assertTrue(spell.done(), "WBTC-A after: spell not done"); + + (maxLine, gap,,,) = autoLine.ilks(WBTC_B); + assertEq(maxLine, 0, "WBTC-B after: auto-line not wiped (maxLine)"); + assertEq(gap, 0, "WBTC-B after: auto-line not wiped (gap)"); + assertTrue(spell.done(), "WBTC-B after: spell not done"); + + (maxLine, gap,,,) = autoLine.ilks(WBTC_C); + assertEq(maxLine, 0, "WBTC-C after: auto-line not wiped (maxLine)"); + assertEq(gap, 0, "WBTC-C after: auto-line not wiped (gap)"); + assertTrue(spell.done(), "WBTC-C after: spell not done"); + } + + function testDoneWhenIlkIsNotAddedToLineMom() public { + uint256 before = vm.snapshotState(); + + vm.prank(pauseProxy); + lineMom.delIlk(WBTC_A); + assertFalse(spell.done(), "WBTC-A spell done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + lineMom.delIlk(WBTC_B); + assertFalse(spell.done(), "WBTC-B spell done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + lineMom.delIlk(WBTC_C); + assertFalse(spell.done(), "WBTC-C spell done"); + vm.revertToState(before); + + vm.startPrank(pauseProxy); + lineMom.delIlk(WBTC_A); + lineMom.delIlk(WBTC_B); + lineMom.delIlk(WBTC_C); + assertTrue(spell.done(), "spell not done done"); + } + + function testDoneWhenAutoLineIsNotActiveButLineIsNonZero() public { + uint256 before = vm.snapshotState(); + + spell.schedule(); + assertTrue(spell.done(), "before: spell not done"); + + vm.prank(pauseProxy); + vat.file(WBTC_A, "line", 10 ** 45); + assertFalse(spell.done(), "WBTC-A after: spell still done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + vat.file(WBTC_B, "line", 10 ** 45); + assertFalse(spell.done(), "WBTC-B after: spell still done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + vat.file(WBTC_C, "line", 10 ** 45); + assertFalse(spell.done(), "WBTC-C after: spell still done"); + vm.revertToState(before); + } + + function testRevertAutoLineWipeWhenItDoesNotHaveTheHat() public { + stdstore.target(chief).sig("hat()").checked_write(address(0)); + + vm.expectRevert(); + spell.schedule(); + } + + event Wipe(bytes32 indexed ilk); +} diff --git a/src/auto-line-wipe/WstethAutoLineWipeSpell.sol b/src/auto-line-wipe/WstethAutoLineWipeSpell.sol new file mode 100644 index 0000000..46f9bca --- /dev/null +++ b/src/auto-line-wipe/WstethAutoLineWipeSpell.sol @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// 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 . +pragma solidity ^0.8.16; + +import {DssEmergencySpell} from "../DssEmergencySpell.sol"; + +interface LineMomLike { + function autoLine() external view returns (address); + function ilks(bytes32 ilk) external view returns (uint256); + function wipe(bytes32 ilk) external returns (uint256); +} + +interface AutoLineLike { + function ilks(bytes32 ilk) + external + view + returns (uint256 maxLine, uint256 gap, uint48 ttl, uint48 last, uint48 lastInc); + function wards(address who) external view returns (uint256); +} + +interface VatLike { + function ilks(bytes32 ilk) + external + view + returns (uint256 Art, uint256 rate, uint256 spot, uint256 line, uint256 dust); + function wards(address who) external view returns (uint256); +} + +contract WstethAutoLineWipeSpell is DssEmergencySpell { + LineMomLike public immutable lineMom = LineMomLike(_log.getAddress("LINE_MOM")); + AutoLineLike public immutable autoLine = AutoLineLike(LineMomLike(_log.getAddress("LINE_MOM")).autoLine()); + VatLike public immutable vat = VatLike(_log.getAddress("MCD_VAT")); + bytes32 internal immutable WSTETH_A = "WSTETH-A"; + bytes32 internal immutable WSTETH_B = "WSTETH-B"; + + event Wipe(bytes32 indexed ilk); + + function description() external view returns (string memory) { + return string(abi.encodePacked("Emergency Spell | Auto-Line Wipe: ", WSTETH_A, ", ", WSTETH_B)); + } + + function _emergencyActions() internal override { + lineMom.wipe(WSTETH_A); + lineMom.wipe(WSTETH_B); + + emit Wipe(WSTETH_A); + emit Wipe(WSTETH_B); + } + + /** + * @notice Returns whwstether the spell is done or not. + * @dev Checks if the all ilks have been wiped from auto-line and vat line is zero for all ilks. + * The spell would revert if any of the following conditions holds: + * 1. LineMom is not ward on Vat + * 2. LineMom is not ward on AutoLine + * 3. The ilk has not been added to AutoLine + * In such cases, it returns `true`, meaning no further action can be taken at the moment. + */ + function done() external view returns (bool) { + return _done(WSTETH_A) && _done(WSTETH_B); + } + + /** + * @notice Returns whwstether the spell is done or not. + */ + function _done(bytes32 _ilk) internal view returns (bool) { + if (vat.wards(address(lineMom)) == 0 || autoLine.wards(address(lineMom)) == 0 || lineMom.ilks(_ilk) == 0) { + return true; + } + + (,,, uint256 line,) = vat.ilks(_ilk); + (uint256 maxLine, uint256 gap, uint48 ttl, uint48 last, uint48 lastInc) = autoLine.ilks(_ilk); + + return line == 0 && maxLine == 0 && gap == 0 && ttl == 0 && last == 0 && lastInc == 0; + } +} diff --git a/src/auto-line-wipe/WstethAutoLineWipeSpell.t.integration.sol b/src/auto-line-wipe/WstethAutoLineWipeSpell.t.integration.sol new file mode 100644 index 0000000..26ba8e5 --- /dev/null +++ b/src/auto-line-wipe/WstethAutoLineWipeSpell.t.integration.sol @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// 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 . +pragma solidity ^0.8.16; + +import {stdStorage, StdStorage} from "forge-std/Test.sol"; +import {DssTest, DssInstance, MCD} from "dss-test/DssTest.sol"; +import {DssEmergencySpellLike} from "../DssEmergencySpell.sol"; +import {WstethAutoLineWipeSpell} from "./WstethAutoLineWipeSpell.sol"; + +interface AutoLineLike { + function ilks(bytes32 ilk) + external + view + returns (uint256 maxLine, uint256 gap, uint48 ttl, uint48 last, uint48 lastInc); +} + +interface LineMomLike { + function delIlk(bytes32 ilk) external; +} + +interface VatLike { + function file(bytes32 ilk, bytes32 what, uint256 data) external; +} + +contract WstethAutoLineWipeSpellTest is DssTest { + using stdStorage for StdStorage; + + address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; + DssInstance dss; + address pauseProxy; + VatLike vat; + address chief; + bytes32 WSTETH_A = "WSTETH-A"; + bytes32 WSTETH_B = "WSTETH-B"; + LineMomLike lineMom; + AutoLineLike autoLine; + DssEmergencySpellLike spell; + + function setUp() public { + vm.createSelectFork("mainnet"); + + dss = MCD.loadFromChainlog(CHAINLOG); + MCD.giveAdminAccess(dss); + pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); + vat = VatLike(dss.chainlog.getAddress("MCD_VAT")); + chief = dss.chainlog.getAddress("MCD_ADM"); + lineMom = LineMomLike(dss.chainlog.getAddress("LINE_MOM")); + autoLine = AutoLineLike(dss.chainlog.getAddress("MCD_IAM_AUTO_LINE")); + spell = new WstethAutoLineWipeSpell(); + + stdstore.target(chief).sig("hat()").checked_write(address(spell)); + + vm.makePersistent(chief); + } + + function testAutoLineWipeOnSchedule() public { + uint256 pmaxLine; + uint256 pgap; + + (pmaxLine, pgap,,,) = autoLine.ilks(WSTETH_A); + assertGt(pmaxLine, 0, "WSTETH-A before: auto-line already wiped"); + assertGt(pgap, 0, "WSTETH-A before: auto-line already wiped"); + assertFalse(spell.done(), "WSTETH-A before: spell already done"); + + (pmaxLine, pgap,,,) = autoLine.ilks(WSTETH_B); + assertGt(pmaxLine, 0, "WSTETH-B before: auto-line already wiped"); + assertGt(pgap, 0, "WSTETH-B before: auto-line already wiped"); + assertFalse(spell.done(), "WSTETH-B before: spell already done"); + + vm.expectEmit(true, true, true, false); + emit Wipe(WSTETH_A); + emit Wipe(WSTETH_B); + spell.schedule(); + + uint256 maxLine; + uint256 gap; + + (maxLine, gap,,,) = autoLine.ilks(WSTETH_A); + assertEq(maxLine, 0, "WSTETH-A after: auto-line not wiped (maxLine)"); + assertEq(gap, 0, "WSTETH-A after: auto-line not wiped (gap)"); + assertTrue(spell.done(), "WSTETH-A after: spell not done"); + + (maxLine, gap,,,) = autoLine.ilks(WSTETH_B); + assertEq(maxLine, 0, "WSTETH-B after: auto-line not wiped (maxLine)"); + assertEq(gap, 0, "WSTETH-B after: auto-line not wiped (gap)"); + assertTrue(spell.done(), "WSTETH-B after: spell not done"); + } + + function testDoneWhenIlkIsNotAddedToLineMom() public { + uint256 before = vm.snapshotState(); + + vm.prank(pauseProxy); + lineMom.delIlk(WSTETH_A); + assertFalse(spell.done(), "WSTETH-A spell done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + lineMom.delIlk(WSTETH_B); + assertFalse(spell.done(), "WSTETH-B spell done"); + vm.revertToState(before); + + vm.startPrank(pauseProxy); + lineMom.delIlk(WSTETH_A); + lineMom.delIlk(WSTETH_B); + assertTrue(spell.done(), "spell not done done"); + } + + function testDoneWhenAutoLineIsNotActiveButLineIsNonZero() public { + uint256 before = vm.snapshotState(); + + spell.schedule(); + assertTrue(spell.done(), "before: spell not done"); + + vm.prank(pauseProxy); + vat.file(WSTETH_A, "line", 10 ** 45); + assertFalse(spell.done(), "WSTETH-A after: spell still done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + vat.file(WSTETH_B, "line", 10 ** 45); + assertFalse(spell.done(), "WSTETH-B after: spell still done"); + vm.revertToState(before); + } + + function testRevertAutoLineWipeWhenItDoesNotHaveTheHat() public { + stdstore.target(chief).sig("hat()").checked_write(address(0)); + + vm.expectRevert(); + spell.schedule(); + } + + event Wipe(bytes32 indexed ilk); +} From b0bedc39d5a5bfa17616cf88309eebb7e4d758a5 Mon Sep 17 00:00:00 2001 From: amusingaxl <112016538+amusingaxl@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:21:01 -0300 Subject: [PATCH 2/8] refactor: fix description declaration, docs and tests --- src/auto-line-wipe/EthAutoLineWipeSpell.sol | 14 ++++++-------- .../EthAutoLineWipeSpell.t.integration.sol | 2 ++ src/auto-line-wipe/WbtcAutoLineWipeSpell.sol | 14 ++++++-------- .../WbtcAutoLineWipeSpell.t.integration.sol | 2 ++ src/auto-line-wipe/WstethAutoLineWipeSpell.sol | 12 +++++------- .../WstethAutoLineWipeSpell.t.integration.sol | 1 + 6 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/auto-line-wipe/EthAutoLineWipeSpell.sol b/src/auto-line-wipe/EthAutoLineWipeSpell.sol index 7121060..697c2b4 100644 --- a/src/auto-line-wipe/EthAutoLineWipeSpell.sol +++ b/src/auto-line-wipe/EthAutoLineWipeSpell.sol @@ -43,16 +43,14 @@ contract EthAutoLineWipeSpell is DssEmergencySpell { LineMomLike public immutable lineMom = LineMomLike(_log.getAddress("LINE_MOM")); AutoLineLike public immutable autoLine = AutoLineLike(LineMomLike(_log.getAddress("LINE_MOM")).autoLine()); VatLike public immutable vat = VatLike(_log.getAddress("MCD_VAT")); - bytes32 internal immutable ETH_A = "ETH-A"; - bytes32 internal immutable ETH_B = "ETH-B"; - bytes32 internal immutable ETH_C = "ETH-C"; + bytes32 internal constant ETH_A = "ETH-A"; + bytes32 internal constant ETH_B = "ETH-B"; + bytes32 internal constant ETH_C = "ETH-C"; + string public constant description = + string(abi.encodePacked("Emergency Spell | Auto-Line Wipe: ", ETH_A, ", ", ETH_B, ", ", ETH_C)); event Wipe(bytes32 indexed ilk); - function description() external view returns (string memory) { - return string(abi.encodePacked("Emergency Spell | Auto-Line Wipe: ", ETH_A, ", ", ETH_B, ", ", ETH_C)); - } - function _emergencyActions() internal override { lineMom.wipe(ETH_A); lineMom.wipe(ETH_B); @@ -77,7 +75,7 @@ contract EthAutoLineWipeSpell is DssEmergencySpell { } /** - * @notice Returns whether the spell is done or not. + * @notice Returns whether the spell is done or not for the specified ilk. */ function _done(bytes32 _ilk) internal view returns (bool) { if (vat.wards(address(lineMom)) == 0 || autoLine.wards(address(lineMom)) == 0 || lineMom.ilks(_ilk) == 0) { diff --git a/src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol b/src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol index 43e2514..eeef594 100644 --- a/src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol +++ b/src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol @@ -88,7 +88,9 @@ contract EthAutoLineWipeSpellTest is DssTest { vm.expectEmit(true, true, true, false); emit Wipe(ETH_A); + vm.expectEmit(true, true, true, false); emit Wipe(ETH_B); + vm.expectEmit(true, true, true, false); emit Wipe(ETH_C); spell.schedule(); diff --git a/src/auto-line-wipe/WbtcAutoLineWipeSpell.sol b/src/auto-line-wipe/WbtcAutoLineWipeSpell.sol index d6bcb87..a04aaa3 100644 --- a/src/auto-line-wipe/WbtcAutoLineWipeSpell.sol +++ b/src/auto-line-wipe/WbtcAutoLineWipeSpell.sol @@ -43,16 +43,14 @@ contract WbtcAutoLineWipeSpell is DssEmergencySpell { LineMomLike public immutable lineMom = LineMomLike(_log.getAddress("LINE_MOM")); AutoLineLike public immutable autoLine = AutoLineLike(LineMomLike(_log.getAddress("LINE_MOM")).autoLine()); VatLike public immutable vat = VatLike(_log.getAddress("MCD_VAT")); - bytes32 internal immutable WBTC_A = "WBTC-A"; - bytes32 internal immutable WBTC_B = "WBTC-B"; - bytes32 internal immutable WBTC_C = "WBTC-C"; + bytes32 internal constant WBTC_A = "WBTC-A"; + bytes32 internal constant WBTC_B = "WBTC-B"; + bytes32 internal constant WBTC_C = "WBTC-C"; + string public constant description = + string(abi.encodePacked("Emergency Spell | Auto-Line Wipe: ", WBTC_A, ", ", WBTC_B, ", ", WBTC_C)); event Wipe(bytes32 indexed ilk); - function description() external view returns (string memory) { - return string(abi.encodePacked("Emergency Spell | Auto-Line Wipe: ", WBTC_A, ", ", WBTC_B, ", ", WBTC_C)); - } - function _emergencyActions() internal override { lineMom.wipe(WBTC_A); lineMom.wipe(WBTC_B); @@ -77,7 +75,7 @@ contract WbtcAutoLineWipeSpell is DssEmergencySpell { } /** - * @notice Returns whether the spell is done or not. + * @notice Returns whether the spell is done or not for the specified ilk. */ function _done(bytes32 _ilk) internal view returns (bool) { if (vat.wards(address(lineMom)) == 0 || autoLine.wards(address(lineMom)) == 0 || lineMom.ilks(_ilk) == 0) { diff --git a/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol b/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol index e913976..99bf920 100644 --- a/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol +++ b/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol @@ -96,7 +96,9 @@ contract WbtcAutoLineWipeSpellTest is DssTest { vm.expectEmit(true, true, true, false); emit Wipe(WBTC_A); + vm.expectEmit(true, true, true, false); emit Wipe(WBTC_B); + vm.expectEmit(true, true, true, false); emit Wipe(WBTC_C); spell.schedule(); diff --git a/src/auto-line-wipe/WstethAutoLineWipeSpell.sol b/src/auto-line-wipe/WstethAutoLineWipeSpell.sol index 46f9bca..9134d89 100644 --- a/src/auto-line-wipe/WstethAutoLineWipeSpell.sol +++ b/src/auto-line-wipe/WstethAutoLineWipeSpell.sol @@ -43,15 +43,13 @@ contract WstethAutoLineWipeSpell is DssEmergencySpell { LineMomLike public immutable lineMom = LineMomLike(_log.getAddress("LINE_MOM")); AutoLineLike public immutable autoLine = AutoLineLike(LineMomLike(_log.getAddress("LINE_MOM")).autoLine()); VatLike public immutable vat = VatLike(_log.getAddress("MCD_VAT")); - bytes32 internal immutable WSTETH_A = "WSTETH-A"; - bytes32 internal immutable WSTETH_B = "WSTETH-B"; + bytes32 internal constant WSTETH_A = "WSTETH-A"; + bytes32 internal constant WSTETH_B = "WSTETH-B"; + string public constant description = + string(abi.encodePacked("Emergency Spell | Auto-Line Wipe: ", WSTETH_A, ", ", WSTETH_B)); event Wipe(bytes32 indexed ilk); - function description() external view returns (string memory) { - return string(abi.encodePacked("Emergency Spell | Auto-Line Wipe: ", WSTETH_A, ", ", WSTETH_B)); - } - function _emergencyActions() internal override { lineMom.wipe(WSTETH_A); lineMom.wipe(WSTETH_B); @@ -74,7 +72,7 @@ contract WstethAutoLineWipeSpell is DssEmergencySpell { } /** - * @notice Returns whwstether the spell is done or not. + * @notice Returns whether the spell is done or not for the specified ilk. */ function _done(bytes32 _ilk) internal view returns (bool) { if (vat.wards(address(lineMom)) == 0 || autoLine.wards(address(lineMom)) == 0 || lineMom.ilks(_ilk) == 0) { diff --git a/src/auto-line-wipe/WstethAutoLineWipeSpell.t.integration.sol b/src/auto-line-wipe/WstethAutoLineWipeSpell.t.integration.sol index 26ba8e5..2cd9f0d 100644 --- a/src/auto-line-wipe/WstethAutoLineWipeSpell.t.integration.sol +++ b/src/auto-line-wipe/WstethAutoLineWipeSpell.t.integration.sol @@ -82,6 +82,7 @@ contract WstethAutoLineWipeSpellTest is DssTest { vm.expectEmit(true, true, true, false); emit Wipe(WSTETH_A); + vm.expectEmit(true, true, true, false); emit Wipe(WSTETH_B); spell.schedule(); From 520b472422810ce53fdb86f4919143cf649bdafb Mon Sep 17 00:00:00 2001 From: amusingaxl <112016538+amusingaxl@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:16:44 -0300 Subject: [PATCH 3/8] fix(autoline-wipe): broken initial state for `testDoneWhenIlkIsNotAddedToLineMom` --- src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol b/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol index 99bf920..25c47d3 100644 --- a/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol +++ b/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol @@ -122,6 +122,13 @@ contract WbtcAutoLineWipeSpellTest is DssTest { } function testDoneWhenIlkIsNotAddedToLineMom() public { + // WBTC debt ceiling was set to zero when this tests was written, so we need to overwrite the state. + vm.startPrank(pauseProxy); + autoLine.setIlk(WBTC_A, 1, 1, 1); + autoLine.setIlk(WBTC_B, 1, 1, 1); + autoLine.setIlk(WBTC_C, 1, 1, 1); + vm.stopPrank(); + uint256 before = vm.snapshotState(); vm.prank(pauseProxy); From 8cf50e7a96203a48e0c22ad179a54f18a55d8abc Mon Sep 17 00:00:00 2001 From: amusingaxl <112016538+amusingaxl@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:17:11 -0300 Subject: [PATCH 4/8] feat: add hardcoded multi-ilk clip breaker spells --- src/clip-breaker/EthClipBreakerSpell.sol | 100 +++++++++++++ .../EthClipBreakerSpell.t.integration.sol | 141 ++++++++++++++++++ src/clip-breaker/WbtcClipBreakerSpell.sol | 100 +++++++++++++ .../WbtcClipBreakerSpell.t.integration.sol | 141 ++++++++++++++++++ src/clip-breaker/WstethClipBreakerSpell.sol | 98 ++++++++++++ .../WstethClipBreakerSpell.t.integration.sol | 129 ++++++++++++++++ 6 files changed, 709 insertions(+) create mode 100644 src/clip-breaker/EthClipBreakerSpell.sol create mode 100644 src/clip-breaker/EthClipBreakerSpell.t.integration.sol create mode 100644 src/clip-breaker/WbtcClipBreakerSpell.sol create mode 100644 src/clip-breaker/WbtcClipBreakerSpell.t.integration.sol create mode 100644 src/clip-breaker/WstethClipBreakerSpell.sol create mode 100644 src/clip-breaker/WstethClipBreakerSpell.t.integration.sol diff --git a/src/clip-breaker/EthClipBreakerSpell.sol b/src/clip-breaker/EthClipBreakerSpell.sol new file mode 100644 index 0000000..b330824 --- /dev/null +++ b/src/clip-breaker/EthClipBreakerSpell.sol @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// 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 . +pragma solidity ^0.8.16; + +import {DssEmergencySpell} from "../DssEmergencySpell.sol"; + +interface ClipperMomLike { + function setBreaker(address clip, uint256 level, uint256 delay) external; +} + +interface ClipLike { + function stopped() external view returns (uint256); + function wards(address who) external view returns (uint256); +} + +interface IlkRegistryLike { + function xlip(bytes32 ilk) external view returns (address); +} + +contract EthClipBreakerSpell is DssEmergencySpell { + /// @dev During an emergency, set the breaker level to 3 to prevent `kick()`, `redo()` and `take()`. + uint256 public constant BREAKER_LEVEL = 3; + /// @dev The delay is not applicable for level 3 breakers, so we set it to zero. + uint256 public constant BREAKER_DELAY = 0; + + ClipperMomLike public immutable clipperMom = ClipperMomLike(_log.getAddress("CLIPPER_MOM")); + IlkRegistryLike public immutable ilkReg = IlkRegistryLike(_log.getAddress("ILK_REGISTRY")); + bytes32 internal constant ETH_A = "ETH-A"; + bytes32 internal constant ETH_B = "ETH-B"; + bytes32 internal constant ETH_C = "ETH-C"; + string public constant description = + string(abi.encodePacked("Emergency Spell | Set Clip Breaker: ", ETH_A, ", ", ETH_B, ", ", ETH_C)); + + event SetBreaker(bytes32 indexed ilk, address indexed clip); + + function _emergencyActions() internal override { + _setBreaker(ETH_A); + _setBreaker(ETH_B); + _setBreaker(ETH_C); + } + + function _setBreaker(bytes32 _ilk) internal { + address clip = ilkReg.xlip(_ilk); + clipperMom.setBreaker(clip, BREAKER_LEVEL, BREAKER_DELAY); + emit SetBreaker(_ilk, clip); + } + + /** + * @notice Returns whether the spell is done for all ilks or not. + * @dev Checks if all Clip instances have stopped = 3. + * The spell would revert if any of the following conditions holds: + * 1. Clip is set to address(0) + * 2. ClipperMom is not a ward on Clip + * 3. Clip does not implement the `stopped` function + * In such cases, it returns `true`, meaning no further action can be taken at the moment. + */ + function done() external view returns (bool) { + return _done(ETH_A) && _done(ETH_B) && _done(ETH_C); + } + + /** + * @notice Returns whether the spell is done or not for the specified ilk. + */ + function _done(bytes32 _ilk) internal view returns (bool) { + address clip = ilkReg.xlip(_ilk); + if (clip == address(0)) { + return true; + } + + try ClipLike(clip).wards(address(clipperMom)) returns (uint256 ward) { + // Ignore Clip instances that have not relied on ClipperMom. + if (ward == 0) { + return true; + } + } catch { + // If the call failed, it means the contract is most likely not a Clip instance. + return true; + } + + try ClipLike(clip).stopped() returns (uint256 stopped) { + return stopped == BREAKER_LEVEL; + } catch { + // If the call failed, it means the contract is most likely not a Clip instance. + return true; + } + } +} diff --git a/src/clip-breaker/EthClipBreakerSpell.t.integration.sol b/src/clip-breaker/EthClipBreakerSpell.t.integration.sol new file mode 100644 index 0000000..9ae7f84 --- /dev/null +++ b/src/clip-breaker/EthClipBreakerSpell.t.integration.sol @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// 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 . +pragma solidity ^0.8.16; + +import {stdStorage, StdStorage} from "forge-std/Test.sol"; +import {DssTest, DssInstance, MCD} from "dss-test/DssTest.sol"; +import {DssEmergencySpellLike} from "../DssEmergencySpell.sol"; +import {EthClipBreakerSpell} from "./EthClipBreakerSpell.sol"; + +interface IlkRegistryLike { + function xlip(bytes32 ilk) external view returns (address); + function file(bytes32 ilk, bytes32 what, address data) external; +} + +interface ClipperMomLike { + function setBreaker(address clip, uint256 level, uint256 delay) external; +} + +interface ClipLike { + function stopped() external view returns (uint256); + function deny(address who) external; +} + +contract EthClipBreakerSpellTest is DssTest { + using stdStorage for StdStorage; + + address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; + DssInstance dss; + address pauseProxy; + address chief; + IlkRegistryLike ilkReg; + bytes32 ETH_A = "ETH-A"; + bytes32 ETH_B = "ETH-B"; + bytes32 ETH_C = "ETH-C"; + ClipperMomLike clipperMom; + ClipLike clipA; + ClipLike clipB; + ClipLike clipC; + DssEmergencySpellLike spell; + + function setUp() public { + vm.createSelectFork("mainnet"); + + dss = MCD.loadFromChainlog(CHAINLOG); + MCD.giveAdminAccess(dss); + pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); + chief = dss.chainlog.getAddress("MCD_ADM"); + ilkReg = IlkRegistryLike(dss.chainlog.getAddress("ILK_REGISTRY")); + clipperMom = ClipperMomLike(dss.chainlog.getAddress("CLIPPER_MOM")); + clipA = ClipLike(ilkReg.xlip(ETH_A)); + clipB = ClipLike(ilkReg.xlip(ETH_B)); + clipC = ClipLike(ilkReg.xlip(ETH_C)); + spell = new EthClipBreakerSpell(); + + stdstore.target(chief).sig("hat()").checked_write(address(spell)); + + vm.makePersistent(chief); + } + + function testClipBreakerOnSchedule() public { + assertEq(clipA.stopped(), 0, "ETH-A before: clip already stopped"); + assertFalse(spell.done(), "ETH-A before: spell already done"); + assertEq(clipB.stopped(), 0, "ETH-B before: clip already stopped"); + assertFalse(spell.done(), "ETH-B before: spell already done"); + assertEq(clipC.stopped(), 0, "ETH-C before: clip already stopped"); + assertFalse(spell.done(), "ETH-C before: spell already done"); + + vm.expectEmit(true, true, true, true); + emit SetBreaker(ETH_A, address(clipA)); + vm.expectEmit(true, true, true, true); + emit SetBreaker(ETH_B, address(clipB)); + vm.expectEmit(true, true, true, true); + emit SetBreaker(ETH_C, address(clipC)); + spell.schedule(); + + assertEq(clipA.stopped(), 3, "ETH-A after: clip not stopped"); + assertTrue(spell.done(), "ETH-A after: spell not done"); + assertEq(clipB.stopped(), 3, "ETH-B after: clip not stopped"); + assertTrue(spell.done(), "ETH-B after: spell not done"); + assertEq(clipC.stopped(), 3, "ETH-C after: clip not stopped"); + assertTrue(spell.done(), "ETH-C after: spell not done"); + } + + function testDoneWhenClipIsNotSetInIlkReg() public { + vm.startPrank(pauseProxy); + ilkReg.file(ETH_A, "xlip", address(0)); + ilkReg.file(ETH_B, "xlip", address(0)); + ilkReg.file(ETH_C, "xlip", address(0)); + vm.stopPrank(); + + assertTrue(spell.done(), "spell not done"); + } + + function testDoneWhenClipperMomIsNotWardInClip() public { + uint256 before = vm.snapshotState(); + + vm.prank(pauseProxy); + clipA.deny(address(clipperMom)); + assertFalse(spell.done(), "ETH-A spell already done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + clipB.deny(address(clipperMom)); + assertFalse(spell.done(), "ETH-B spell already done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + clipC.deny(address(clipperMom)); + assertFalse(spell.done(), "ETH-C spell already done"); + vm.revertToState(before); + + vm.startPrank(pauseProxy); + clipA.deny(address(clipperMom)); + clipB.deny(address(clipperMom)); + clipC.deny(address(clipperMom)); + vm.stopPrank(); + assertTrue(spell.done(), "after: spell not done"); + } + + function testRevertClipBreakerWhenItDoesNotHaveTheHat() public { + stdstore.target(chief).sig("hat()").checked_write(address(0)); + + vm.expectRevert(); + spell.schedule(); + } + + event SetBreaker(bytes32 indexed ilk, address indexed clip); +} diff --git a/src/clip-breaker/WbtcClipBreakerSpell.sol b/src/clip-breaker/WbtcClipBreakerSpell.sol new file mode 100644 index 0000000..b98e102 --- /dev/null +++ b/src/clip-breaker/WbtcClipBreakerSpell.sol @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// 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 . +pragma solidity ^0.8.16; + +import {DssEmergencySpell} from "../DssEmergencySpell.sol"; + +interface ClipperMomLike { + function setBreaker(address clip, uint256 level, uint256 delay) external; +} + +interface ClipLike { + function stopped() external view returns (uint256); + function wards(address who) external view returns (uint256); +} + +interface IlkRegistryLike { + function xlip(bytes32 ilk) external view returns (address); +} + +contract WbtcClipBreakerSpell is DssEmergencySpell { + /// @dev During an emergency, set the breaker level to 3 to prevent `kick()`, `redo()` and `take()`. + uint256 public constant BREAKER_LEVEL = 3; + /// @dev The delay is not applicable for level 3 breakers, so we set it to zero. + uint256 public constant BREAKER_DELAY = 0; + + ClipperMomLike public immutable clipperMom = ClipperMomLike(_log.getAddress("CLIPPER_MOM")); + IlkRegistryLike public immutable ilkReg = IlkRegistryLike(_log.getAddress("ILK_REGISTRY")); + bytes32 internal constant WBTC_A = "WBTC-A"; + bytes32 internal constant WBTC_B = "WBTC-B"; + bytes32 internal constant WBTC_C = "WBTC-C"; + string public constant description = + string(abi.encodePacked("Emergency Spell | Set Clip Breaker: ", WBTC_A, ", ", WBTC_B, ", ", WBTC_C)); + + event SetBreaker(bytes32 indexed ilk, address indexed clip); + + function _emergencyActions() internal override { + _setBreaker(WBTC_A); + _setBreaker(WBTC_B); + _setBreaker(WBTC_C); + } + + function _setBreaker(bytes32 _ilk) internal { + address clip = ilkReg.xlip(_ilk); + clipperMom.setBreaker(clip, BREAKER_LEVEL, BREAKER_DELAY); + emit SetBreaker(_ilk, clip); + } + + /** + * @notice Returns whether the spell is done for all ilks or not. + * @dev Checks if all Clip instances have stopped = 3. + * The spell would revert if any of the following conditions holds: + * 1. Clip is set to address(0) + * 2. ClipperMom is not a ward on Clip + * 3. Clip does not implement the `stopped` function + * In such cases, it returns `true`, meaning no further action can be taken at the moment. + */ + function done() external view returns (bool) { + return _done(WBTC_A) && _done(WBTC_B) && _done(WBTC_C); + } + + /** + * @notice Returns whether the spell is done or not for the specified ilk. + */ + function _done(bytes32 _ilk) internal view returns (bool) { + address clip = ilkReg.xlip(_ilk); + if (clip == address(0)) { + return true; + } + + try ClipLike(clip).wards(address(clipperMom)) returns (uint256 ward) { + // Ignore Clip instances that have not relied on ClipperMom. + if (ward == 0) { + return true; + } + } catch { + // If the call failed, it means the contract is most likely not a Clip instance. + return true; + } + + try ClipLike(clip).stopped() returns (uint256 stopped) { + return stopped == BREAKER_LEVEL; + } catch { + // If the call failed, it means the contract is most likely not a Clip instance. + return true; + } + } +} diff --git a/src/clip-breaker/WbtcClipBreakerSpell.t.integration.sol b/src/clip-breaker/WbtcClipBreakerSpell.t.integration.sol new file mode 100644 index 0000000..1b5bf69 --- /dev/null +++ b/src/clip-breaker/WbtcClipBreakerSpell.t.integration.sol @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// 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 . +pragma solidity ^0.8.16; + +import {stdStorage, StdStorage} from "forge-std/Test.sol"; +import {DssTest, DssInstance, MCD} from "dss-test/DssTest.sol"; +import {DssEmergencySpellLike} from "../DssEmergencySpell.sol"; +import {WbtcClipBreakerSpell} from "./WbtcClipBreakerSpell.sol"; + +interface IlkRegistryLike { + function xlip(bytes32 ilk) external view returns (address); + function file(bytes32 ilk, bytes32 what, address data) external; +} + +interface ClipperMomLike { + function setBreaker(address clip, uint256 level, uint256 delay) external; +} + +interface ClipLike { + function stopped() external view returns (uint256); + function deny(address who) external; +} + +contract WbtcClipBreakerSpellTest is DssTest { + using stdStorage for StdStorage; + + address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; + DssInstance dss; + address pauseProxy; + address chief; + IlkRegistryLike ilkReg; + bytes32 WBTC_A = "WBTC-A"; + bytes32 WBTC_B = "WBTC-B"; + bytes32 WBTC_C = "WBTC-C"; + ClipperMomLike clipperMom; + ClipLike clipA; + ClipLike clipB; + ClipLike clipC; + DssEmergencySpellLike spell; + + function setUp() public { + vm.createSelectFork("mainnet"); + + dss = MCD.loadFromChainlog(CHAINLOG); + MCD.giveAdminAccess(dss); + pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); + chief = dss.chainlog.getAddress("MCD_ADM"); + ilkReg = IlkRegistryLike(dss.chainlog.getAddress("ILK_REGISTRY")); + clipperMom = ClipperMomLike(dss.chainlog.getAddress("CLIPPER_MOM")); + clipA = ClipLike(ilkReg.xlip(WBTC_A)); + clipB = ClipLike(ilkReg.xlip(WBTC_B)); + clipC = ClipLike(ilkReg.xlip(WBTC_C)); + spell = new WbtcClipBreakerSpell(); + + stdstore.target(chief).sig("hat()").checked_write(address(spell)); + + vm.makePersistent(chief); + } + + function testClipBreakerOnSchedule() public { + assertEq(clipA.stopped(), 0, "WBTC-A before: clip already stopped"); + assertFalse(spell.done(), "WBTC-A before: spell already done"); + assertEq(clipB.stopped(), 0, "WBTC-B before: clip already stopped"); + assertFalse(spell.done(), "WBTC-B before: spell already done"); + assertEq(clipC.stopped(), 0, "WBTC-C before: clip already stopped"); + assertFalse(spell.done(), "WBTC-C before: spell already done"); + + vm.expectEmit(true, true, true, true); + emit SetBreaker(WBTC_A, address(clipA)); + vm.expectEmit(true, true, true, true); + emit SetBreaker(WBTC_B, address(clipB)); + vm.expectEmit(true, true, true, true); + emit SetBreaker(WBTC_C, address(clipC)); + spell.schedule(); + + assertEq(clipA.stopped(), 3, "WBTC-A after: clip not stopped"); + assertTrue(spell.done(), "WBTC-A after: spell not done"); + assertEq(clipB.stopped(), 3, "WBTC-B after: clip not stopped"); + assertTrue(spell.done(), "WBTC-B after: spell not done"); + assertEq(clipC.stopped(), 3, "WBTC-C after: clip not stopped"); + assertTrue(spell.done(), "WBTC-C after: spell not done"); + } + + function testDoneWhenClipIsNotSetInIlkReg() public { + vm.startPrank(pauseProxy); + ilkReg.file(WBTC_A, "xlip", address(0)); + ilkReg.file(WBTC_B, "xlip", address(0)); + ilkReg.file(WBTC_C, "xlip", address(0)); + vm.stopPrank(); + + assertTrue(spell.done(), "spell not done"); + } + + function testDoneWhenClipperMomIsNotWardInClip() public { + uint256 before = vm.snapshotState(); + + vm.prank(pauseProxy); + clipA.deny(address(clipperMom)); + assertFalse(spell.done(), "WBTC-A spell already done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + clipB.deny(address(clipperMom)); + assertFalse(spell.done(), "WBTC-B spell already done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + clipC.deny(address(clipperMom)); + assertFalse(spell.done(), "WBTC-C spell already done"); + vm.revertToState(before); + + vm.startPrank(pauseProxy); + clipA.deny(address(clipperMom)); + clipB.deny(address(clipperMom)); + clipC.deny(address(clipperMom)); + vm.stopPrank(); + assertTrue(spell.done(), "after: spell not done"); + } + + function testRevertClipBreakerWhenItDoesNotHaveTheHat() public { + stdstore.target(chief).sig("hat()").checked_write(address(0)); + + vm.expectRevert(); + spell.schedule(); + } + + event SetBreaker(bytes32 indexed ilk, address indexed clip); +} diff --git a/src/clip-breaker/WstethClipBreakerSpell.sol b/src/clip-breaker/WstethClipBreakerSpell.sol new file mode 100644 index 0000000..8dd9bcb --- /dev/null +++ b/src/clip-breaker/WstethClipBreakerSpell.sol @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// 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 . +pragma solidity ^0.8.16; + +import {DssEmergencySpell} from "../DssEmergencySpell.sol"; + +interface ClipperMomLike { + function setBreaker(address clip, uint256 level, uint256 delay) external; +} + +interface ClipLike { + function stopped() external view returns (uint256); + function wards(address who) external view returns (uint256); +} + +interface IlkRegistryLike { + function xlip(bytes32 ilk) external view returns (address); +} + +contract WstethClipBreakerSpell is DssEmergencySpell { + /// @dev During an emergency, set the breaker level to 3 to prevent `kick()`, `redo()` and `take()`. + uint256 public constant BREAKER_LEVEL = 3; + /// @dev The delay is not applicable for level 3 breakers, so we set it to zero. + uint256 public constant BREAKER_DELAY = 0; + + ClipperMomLike public immutable clipperMom = ClipperMomLike(_log.getAddress("CLIPPER_MOM")); + IlkRegistryLike public immutable ilkReg = IlkRegistryLike(_log.getAddress("ILK_REGISTRY")); + bytes32 internal constant WSTETH_A = "WSTETH-A"; + bytes32 internal constant WSTETH_B = "WSTETH-B"; + string public constant description = + string(abi.encodePacked("Emergency Spell | Set Clip Breaker: ", WSTETH_A, ", ", WSTETH_B)); + + event SetBreaker(bytes32 indexed ilk, address indexed clip); + + function _emergencyActions() internal override { + _setBreaker(WSTETH_A); + _setBreaker(WSTETH_B); + } + + function _setBreaker(bytes32 _ilk) internal { + address clip = ilkReg.xlip(_ilk); + clipperMom.setBreaker(clip, BREAKER_LEVEL, BREAKER_DELAY); + emit SetBreaker(_ilk, clip); + } + + /** + * @notice Returns whether the spell is done for all ilks or not. + * @dev Checks if all Clip instances have stopped = 3. + * The spell would revert if any of the following conditions holds: + * 1. Clip is set to address(0) + * 2. ClipperMom is not a ward on Clip + * 3. Clip does not implement the `stopped` function + * In such cases, it returns `true`, meaning no further action can be taken at the moment. + */ + function done() external view returns (bool) { + return _done(WSTETH_A) && _done(WSTETH_B); + } + + /** + * @notice Returns whether the spell is done or not for the specified ilk. + */ + function _done(bytes32 _ilk) internal view returns (bool) { + address clip = ilkReg.xlip(_ilk); + if (clip == address(0)) { + return true; + } + + try ClipLike(clip).wards(address(clipperMom)) returns (uint256 ward) { + // Ignore Clip instances that have not relied on ClipperMom. + if (ward == 0) { + return true; + } + } catch { + // If the call failed, it means the contract is most likely not a Clip instance. + return true; + } + + try ClipLike(clip).stopped() returns (uint256 stopped) { + return stopped == BREAKER_LEVEL; + } catch { + // If the call failed, it means the contract is most likely not a Clip instance. + return true; + } + } +} diff --git a/src/clip-breaker/WstethClipBreakerSpell.t.integration.sol b/src/clip-breaker/WstethClipBreakerSpell.t.integration.sol new file mode 100644 index 0000000..e96b67f --- /dev/null +++ b/src/clip-breaker/WstethClipBreakerSpell.t.integration.sol @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: © 2024 Dai Foundation +// 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 . +pragma solidity ^0.8.16; + +import {stdStorage, StdStorage} from "forge-std/Test.sol"; +import {DssTest, DssInstance, MCD} from "dss-test/DssTest.sol"; +import {DssEmergencySpellLike} from "../DssEmergencySpell.sol"; +import {WstethClipBreakerSpell} from "./WstethClipBreakerSpell.sol"; + +interface IlkRegistryLike { + function xlip(bytes32 ilk) external view returns (address); + function file(bytes32 ilk, bytes32 what, address data) external; +} + +interface ClipperMomLike { + function setBreaker(address clip, uint256 level, uint256 delay) external; +} + +interface ClipLike { + function stopped() external view returns (uint256); + function deny(address who) external; +} + +contract WstethClipBreakerSpellTest is DssTest { + using stdStorage for StdStorage; + + address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; + DssInstance dss; + address pauseProxy; + address chief; + IlkRegistryLike ilkReg; + bytes32 WSTETH_A = "WSTETH-A"; + bytes32 WSTETH_B = "WSTETH-B"; + ClipperMomLike clipperMom; + ClipLike clipA; + ClipLike clipB; + DssEmergencySpellLike spell; + + function setUp() public { + vm.createSelectFork("mainnet"); + + dss = MCD.loadFromChainlog(CHAINLOG); + MCD.giveAdminAccess(dss); + pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); + chief = dss.chainlog.getAddress("MCD_ADM"); + ilkReg = IlkRegistryLike(dss.chainlog.getAddress("ILK_REGISTRY")); + clipperMom = ClipperMomLike(dss.chainlog.getAddress("CLIPPER_MOM")); + clipA = ClipLike(ilkReg.xlip(WSTETH_A)); + clipB = ClipLike(ilkReg.xlip(WSTETH_B)); + spell = new WstethClipBreakerSpell(); + + stdstore.target(chief).sig("hat()").checked_write(address(spell)); + + vm.makePersistent(chief); + } + + function testClipBreakerOnSchedule() public { + assertEq(clipA.stopped(), 0, "WSTETH-A before: clip already stopped"); + assertFalse(spell.done(), "WSTETH-A before: spell already done"); + assertEq(clipB.stopped(), 0, "WSTETH-B before: clip already stopped"); + assertFalse(spell.done(), "WSTETH-B before: spell already done"); + + vm.expectEmit(true, true, true, true); + emit SetBreaker(WSTETH_A, address(clipA)); + vm.expectEmit(true, true, true, true); + emit SetBreaker(WSTETH_B, address(clipB)); + spell.schedule(); + + assertEq(clipA.stopped(), 3, "WSTETH-A after: clip not stopped"); + assertTrue(spell.done(), "WSTETH-A after: spell not done"); + assertEq(clipB.stopped(), 3, "WSTETH-B after: clip not stopped"); + assertTrue(spell.done(), "WSTETH-B after: spell not done"); + } + + function testDoneWhenClipIsNotSetInIlkReg() public { + vm.startPrank(pauseProxy); + ilkReg.file(WSTETH_A, "xlip", address(0)); + ilkReg.file(WSTETH_B, "xlip", address(0)); + vm.stopPrank(); + + assertTrue(spell.done(), "spell not done"); + } + + function testDoneWhenClipperMomIsNotWardInClip() public { + uint256 before = vm.snapshotState(); + + vm.prank(pauseProxy); + clipA.deny(address(clipperMom)); + assertFalse(spell.done(), "WSTETH-A spell already done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + clipB.deny(address(clipperMom)); + assertFalse(spell.done(), "WSTETH-B spell already done"); + vm.revertToState(before); + + vm.prank(pauseProxy); + assertFalse(spell.done(), "WSTETH-C spell already done"); + vm.revertToState(before); + + vm.startPrank(pauseProxy); + clipA.deny(address(clipperMom)); + clipB.deny(address(clipperMom)); + vm.stopPrank(); + assertTrue(spell.done(), "after: spell not done"); + } + + function testRevertClipBreakerWhenItDoesNotHaveTheHat() public { + stdstore.target(chief).sig("hat()").checked_write(address(0)); + + vm.expectRevert(); + spell.schedule(); + } + + event SetBreaker(bytes32 indexed ilk, address indexed clip); +} From 1043d6d987c81a758e94a6c4f7ca9b8731d17c2c Mon Sep 17 00:00:00 2001 From: amusingaxl <112016538+amusingaxl@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:17:26 -0300 Subject: [PATCH 5/8] docs: update README with the latest features --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index dbd9a28..7c40a31 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,12 @@ TBD. ## Implemented Actions -| Description | Single ilk | Multi ilk | -| :---------- | :--------: | :-------: | -| Wipe `AutoLine` | :white_check_mark: | :white_check_mark: | -| Set `Clip` breaker | :white_check_mark: | :white_check_mark: | -| Disable `DDM` | :white_check_mark: | :x: | -| Stop `OSM` | :white_check_mark: | :white_check_mark: | +| Description | Single ilk | Multi ilk | Related ilks | +| :---------- | :--------: | :-------: | :-------: | +| Wipe `AutoLine` | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Set `Clip` breaker | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Disable `DDM` | :white_check_mark: | :x: | :x: | +| Stop `OSM` | :white_check_mark: | :white_check_mark: | :x: | ### Wipe `AutoLine` @@ -107,10 +107,11 @@ constructor. [spell-tag]: https://github.com/makerdao/dss-exec-lib/blob/69b658f35d8618272cd139dfc18c5713caf6b96b/src/DssExec.sol#L75 -Some types of emergency spells may come in 2 flavors: +Some types of emergency spells may come in 3 flavors: -1. Single ilk: applies the desired spell action for a single pre-defined ilk. -1. Multi ilk: applies the desired spell action for all applicable ilks. +1. Single-ilk: applies the desired spell action to a single pre-defined ilk. +1. Multi-ilk: applies the desired spell action to all applicable ilks. +1. Hardcoded Multi-ilk: applies the desired spell action to a hardcoded list of retlated ilks (i.e.: `ETH-A`, `ETH-B` and `ETH-C`) Furthermore, this repo provides on-chain factories for single ilk emergency spells to make it easier to deploy for new ilks. From 07b4aa4f9a202e9ed782716bab979cd3b694bc26 Mon Sep 17 00:00:00 2001 From: amusingaxl <112016538+amusingaxl@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:56:22 +0200 Subject: [PATCH 6/8] fix: apply suggestions from code review Co-authored-by: oddaf <106770775+oddaf@users.noreply.github.com> --- src/auto-line-wipe/EthAutoLineWipeSpell.sol | 2 +- src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol | 2 +- src/auto-line-wipe/WbtcAutoLineWipeSpell.sol | 2 +- src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol | 4 ++-- src/auto-line-wipe/WstethAutoLineWipeSpell.sol | 4 ++-- src/auto-line-wipe/WstethAutoLineWipeSpell.t.integration.sol | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/auto-line-wipe/EthAutoLineWipeSpell.sol b/src/auto-line-wipe/EthAutoLineWipeSpell.sol index 697c2b4..3473820 100644 --- a/src/auto-line-wipe/EthAutoLineWipeSpell.sol +++ b/src/auto-line-wipe/EthAutoLineWipeSpell.sol @@ -63,7 +63,7 @@ contract EthAutoLineWipeSpell is DssEmergencySpell { /** * @notice Returns whether the spell is done or not. - * @dev Checks if the all ilks have been wiped from auto-line and vat line is zero for all ilks. + * @dev Checks if all the ilks have been wiped from auto-line and vat line is zero for all ilks. * The spell would revert if any of the following conditions holds: * 1. LineMom is not ward on Vat * 2. LineMom is not ward on AutoLine diff --git a/src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol b/src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol index eeef594..0bc9081 100644 --- a/src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol +++ b/src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol @@ -135,7 +135,7 @@ contract EthAutoLineWipeSpellTest is DssTest { lineMom.delIlk(ETH_A); lineMom.delIlk(ETH_B); lineMom.delIlk(ETH_C); - assertTrue(spell.done(), "spell not done done"); + assertTrue(spell.done(), "spell not done"); } function testDoneWhenAutoLineIsNotActiveButLineIsNonZero() public { diff --git a/src/auto-line-wipe/WbtcAutoLineWipeSpell.sol b/src/auto-line-wipe/WbtcAutoLineWipeSpell.sol index a04aaa3..7cda5ad 100644 --- a/src/auto-line-wipe/WbtcAutoLineWipeSpell.sol +++ b/src/auto-line-wipe/WbtcAutoLineWipeSpell.sol @@ -63,7 +63,7 @@ contract WbtcAutoLineWipeSpell is DssEmergencySpell { /** * @notice Returns whether the spell is done or not. - * @dev Checks if the all ilks have been wiped from auto-line and vat line is zero for all ilks. + * @dev Checks if all the ilks have been wiped from auto-line and vat line is zero for all ilks. * The spell would revert if any of the following conditions holds: * 1. LineMom is not ward on Vat * 2. LineMom is not ward on AutoLine diff --git a/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol b/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol index 25c47d3..b46521e 100644 --- a/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol +++ b/src/auto-line-wipe/WbtcAutoLineWipeSpell.t.integration.sol @@ -72,7 +72,7 @@ contract WbtcAutoLineWipeSpellTest is DssTest { uint256 pmaxLine; uint256 pgap; - // WBTC debt ceiling was set to zero when this tests was written, so we need to overwrite the state. + // WBTC debt ceiling was set to zero when this test was written, so we need to overwrite the state. vm.startPrank(pauseProxy); autoLine.setIlk(WBTC_A, 1, 1, 1); autoLine.setIlk(WBTC_B, 1, 1, 1); @@ -150,7 +150,7 @@ contract WbtcAutoLineWipeSpellTest is DssTest { lineMom.delIlk(WBTC_A); lineMom.delIlk(WBTC_B); lineMom.delIlk(WBTC_C); - assertTrue(spell.done(), "spell not done done"); + assertTrue(spell.done(), "spell not done"); } function testDoneWhenAutoLineIsNotActiveButLineIsNonZero() public { diff --git a/src/auto-line-wipe/WstethAutoLineWipeSpell.sol b/src/auto-line-wipe/WstethAutoLineWipeSpell.sol index 9134d89..84a8b1f 100644 --- a/src/auto-line-wipe/WstethAutoLineWipeSpell.sol +++ b/src/auto-line-wipe/WstethAutoLineWipeSpell.sol @@ -59,8 +59,8 @@ contract WstethAutoLineWipeSpell is DssEmergencySpell { } /** - * @notice Returns whwstether the spell is done or not. - * @dev Checks if the all ilks have been wiped from auto-line and vat line is zero for all ilks. + * @notice Returns whether the spell is done or not. + * @dev Checks if all the ilks have been wiped from auto-line and vat line is zero for all ilks. * The spell would revert if any of the following conditions holds: * 1. LineMom is not ward on Vat * 2. LineMom is not ward on AutoLine diff --git a/src/auto-line-wipe/WstethAutoLineWipeSpell.t.integration.sol b/src/auto-line-wipe/WstethAutoLineWipeSpell.t.integration.sol index 2cd9f0d..d494aea 100644 --- a/src/auto-line-wipe/WstethAutoLineWipeSpell.t.integration.sol +++ b/src/auto-line-wipe/WstethAutoLineWipeSpell.t.integration.sol @@ -116,7 +116,7 @@ contract WstethAutoLineWipeSpellTest is DssTest { vm.startPrank(pauseProxy); lineMom.delIlk(WSTETH_A); lineMom.delIlk(WSTETH_B); - assertTrue(spell.done(), "spell not done done"); + assertTrue(spell.done(), "spell not done"); } function testDoneWhenAutoLineIsNotActiveButLineIsNonZero() public { From 55f5c69b775fd7fe6bf957d44fc73d68e3f74610 Mon Sep 17 00:00:00 2001 From: amusingaxl <112016538+amusingaxl@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:58:14 -0300 Subject: [PATCH 7/8] refactor(wsteth-clip-breaker): remove unused parts of test case --- src/clip-breaker/WstethClipBreakerSpell.t.integration.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/clip-breaker/WstethClipBreakerSpell.t.integration.sol b/src/clip-breaker/WstethClipBreakerSpell.t.integration.sol index e96b67f..19ff370 100644 --- a/src/clip-breaker/WstethClipBreakerSpell.t.integration.sol +++ b/src/clip-breaker/WstethClipBreakerSpell.t.integration.sol @@ -107,10 +107,6 @@ contract WstethClipBreakerSpellTest is DssTest { assertFalse(spell.done(), "WSTETH-B spell already done"); vm.revertToState(before); - vm.prank(pauseProxy); - assertFalse(spell.done(), "WSTETH-C spell already done"); - vm.revertToState(before); - vm.startPrank(pauseProxy); clipA.deny(address(clipperMom)); clipB.deny(address(clipperMom)); From 423051206635f00d7e8dba42fe100c4a8fe9796f Mon Sep 17 00:00:00 2001 From: amusingaxl <112016538+amusingaxl@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:09:21 -0300 Subject: [PATCH 8/8] fix: typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 546b3ab..83daff5 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ TBD. | Disable `DDM` | :white_check_mark: | :x: | :x: | | Stop `OSM` | :white_check_mark: | :white_check_mark: | :x: | | Halt `LitePSM` | :white_check_mark: | :x: | :x: | -| Stop `Splitter` | :x: | :white_check_mark: | :x | +| Stop `Splitter` | :x: | :white_check_mark: | :x: | ### Wipe `AutoLine`