Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat: Hardcoded Multi-ilk Spells #4

Closed
wants to merge 10 commits into from
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ TBD.

## Implemented Actions

| Description | Single ilk | Multi ilk |
| :---------- | :--------: | :-------: |
| Wipe `line` | :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: |
| Halt `PSM` | :white_check_mark: | :x: |
| Stop `Splitter` | :x: | :white_check_mark: |
| Description | Single ilk | Multi ilk | Related ilks |
| :---------- | :--------: | :-------: | :-------: |
| Wipe `line` | :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: |
| Halt `LitePSM` | :white_check_mark: | :x: | :x: |
| Stop `Splitter` | :x: | :white_check_mark: | :x: |

### Wipe `line`

Expand Down Expand Up @@ -120,10 +120,11 @@ constructor.</sub>

[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.
Expand Down
90 changes: 90 additions & 0 deletions src/auto-line-wipe/EthAutoLineWipeSpell.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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 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 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 _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 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
* 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 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) {
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;
}
}
171 changes: 171 additions & 0 deletions src/auto-line-wipe/EthAutoLineWipeSpell.t.integration.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// 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 {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);
vm.expectEmit(true, true, true, false);
emit Wipe(ETH_B);
vm.expectEmit(true, true, true, false);
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");
}

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);
}
90 changes: 90 additions & 0 deletions src/auto-line-wipe/WbtcAutoLineWipeSpell.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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 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 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 _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 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
* 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 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) {
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;
}
}
Loading