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: add grouped ilk emergency spells #10

Merged
merged 36 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ee2295a
feat(auto-line-wipe): add hardcoded multi-ilk spells
amusingaxl Nov 6, 2024
b0bedc3
refactor: fix description declaration, docs and tests
amusingaxl Nov 6, 2024
520b472
fix(autoline-wipe): broken initial state for `testDoneWhenIlkIsNotAdd…
amusingaxl Nov 6, 2024
8cf50e7
feat: add hardcoded multi-ilk clip breaker spells
amusingaxl Nov 6, 2024
1043d6d
docs: update README with the latest features
amusingaxl Nov 6, 2024
07b4aa4
fix: apply suggestions from code review
amusingaxl Nov 11, 2024
55f5c69
refactor(wsteth-clip-breaker): remove unused parts of test case
amusingaxl Nov 11, 2024
36aecba
Merge branch 'master' into feat/hardcoded-multi-ilks
amusingaxl Nov 11, 2024
4230512
fix: typo
amusingaxl Nov 11, 2024
60b79de
Merge branch 'master' into feat/hardcoded-multi-ilks
amusingaxl Nov 21, 2024
bd840a6
refactor: align hard-coded line-wipe spells with the remaining
amusingaxl Nov 21, 2024
3e9f52d
chore: udpate contracts metadata
amusingaxl Nov 21, 2024
b34a584
refactor: rename Multi* spells to Universal*
amusingaxl Nov 21, 2024
ae35324
chore: udpate @title and @custom:reviewers
amusingaxl Nov 21, 2024
ab2f20d
refactor: normalize format of natspec comments
amusingaxl Nov 21, 2024
d87f197
feat: add multi-ilk emergency spells
amusingaxl Nov 21, 2024
08056e6
refactor: remove hardcoded ilks spells
amusingaxl Nov 21, 2024
c6b59d1
docs: fix comment on constructor
amusingaxl Nov 21, 2024
ec3f66b
refactor: rename `listSize` to `len`
amusingaxl Nov 21, 2024
d8d1f3a
refactor: extract common logic for multi-ilks spells into an abstract…
amusingaxl Nov 22, 2024
cce9fdd
docs: fix typo in natspec
amusingaxl Nov 22, 2024
9bf471b
refactor(DssMultiIlkEmregencySpell): add missing interface
amusingaxl Nov 22, 2024
0fe2d61
tests: add coverage for DssMultiIlkEmergencySpell
amusingaxl Nov 22, 2024
4d37201
refactor: rename LitePSM halt spell factory
amusingaxl Nov 22, 2024
2b03d60
fix: broken tests after refactor
amusingaxl Nov 22, 2024
84bd1de
refactor: rename to fit the previous established convention
amusingaxl Nov 24, 2024
728ab24
refactor: revert changes on contracts that have already been audited
amusingaxl Nov 24, 2024
1e417cd
refactor: rename ilk list size constraints vars
amusingaxl Nov 25, 2024
d542bd6
docs: remove wrong natspec section
amusingaxl Nov 25, 2024
3213078
docs: update README impelemented actions categories
amusingaxl Nov 25, 2024
380c4ca
docs: fix table heading in README
amusingaxl Nov 25, 2024
7826814
refactor: rename `Batched` spells as `Grouped`; switch to dynamic sto…
amusingaxl Nov 28, 2024
5c0a6b8
fix: standardize natspec comment style
amusingaxl Nov 28, 2024
8c3eda7
fix: apply suggestions from code review
amusingaxl Nov 29, 2024
00951d4
Merge branch 'master' into feat/batched-ilk-spells
amusingaxl Nov 29, 2024
ceffca9
refactor: fix formatting
amusingaxl Nov 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 | Batched | Multi |
| :---------- | :--------: | :-----: | :---: |
| 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: | :x: | :white_check_mark: |
| Halt `LitePSM` | :white_check_mark: | :x: | :x: |
| Stop `Splitter` | :x: | :x: | :white_check_mark: |

### 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. Batched: applies the desired spell action to a list of related ilks (i.e.: `ETH-A`, `ETH-B` and `ETH-C`)
1. Multi: applies the desired spell action to all applicable ilks.

Furthermore, this repo provides on-chain factories for single ilk emergency spells to make it easier to deploy for new
ilks.
Expand Down
137 changes: 137 additions & 0 deletions src/DssBatchedEmergencySpell.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// 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, DssEmergencySpellLike} from "./DssEmergencySpell.sol";

interface DssBatchedEmergencySpellLike is DssEmergencySpellLike {
function ilks() external view returns (bytes32[] memory);
}

/// @title Batched Emergency Spell
/// @notice Defines the base implementation for batched emergency spells.
/// @custom:authors [amusingaxl]
/// @custom:reviewers []
/// @custom:auditors []
/// @custom:bounties []
abstract contract DssBatchedEmergencySpell is DssEmergencySpell, DssBatchedEmergencySpellLike {
/// @dev The min size for the list of ilks
uint256 public constant MIN_ILKS = 2;
/// @dev The max size for the list of ilks
uint256 public constant MAX_ILKS = 3;
amusingaxl marked this conversation as resolved.
Show resolved Hide resolved

/// @dev The total number of ilks in the spell.
uint256 internal immutable _totalIlks;
/// @dev The 0th ilk to which the spell should be applicable.
bytes32 internal immutable _ilk0;
/// @dev The 1st ilk to which the spell should be applicable.
bytes32 internal immutable _ilk1;
/// @dev The 2nd ilk to which the spell should be applicable.
bytes32 internal immutable _ilk2;

/// @param _ilks The list of ilks for which the spell should be applicable
/// @dev The list size is be at least 2 and less than or equal to 3.
/// The batched spell is meant to be used for ilks that are a variation of tha same collateral gem
/// (i.e.: ETH-A, ETH-B, ETH-C)
/// There has never been a case where MCD onboarded 4 or more ilks for the same collateral gem.
/// For cases where there is only one ilk for the same collateral gem, use the single-ilk version.
constructor(bytes32[] memory _ilks) {
// This is a workaround to Solidity's lack of support for immutable arrays, as described in
// https://github.com/ethereum/solidity/issues/12587
uint256 len = _ilks.length;
require(len >= MIN_ILKS, "DssBatchedEmergencySpell/too-few-ilks");
require(len <= MAX_ILKS, "DssBatchedEmergencySpell/too-many-ilks");
_totalIlks = len;

_ilk0 = _ilks[0];
_ilk1 = _ilks[1];
// Only ilk2 is not guaranteed to exist.
_ilk2 = len > 2 ? _ilks[2] : bytes32(0);
}

/// @notice Returns the list of ilks to which the spell is applicable.
/// @return _ilks The list of ilks
function ilks() public view returns (bytes32[] memory _ilks) {
_ilks = new bytes32[](_totalIlks);
_ilks[0] = _ilk0;
_ilks[1] = _ilk1;
if (_totalIlks > 2) {
_ilks[2] = _ilk2;
}
}

/// @notice Returns the spell description.
function description() external view returns (string memory) {
// Join the list of ilks into a comma-separated string
string memory buf = string.concat(_bytes32ToString(_ilk0), ", ", _bytes32ToString(_ilk1));
if (_totalIlks > 2) {
buf = string.concat(buf, ", ", _bytes32ToString(_ilk2));
}

return string.concat(_descriptionPrefix(), " ", buf);
}

/// @notice Converts a bytes32 value into a string.
function _bytes32ToString(bytes32 src) internal pure returns (string memory res) {
uint256 len = 0;
while (src[len] != 0 && len < 32) {
len++;
}
assembly {
res := mload(0x40)
// new "memory end" including padding (the string isn't larger than 32 bytes)
mstore(0x40, add(res, 0x40))
// store len in memory
mstore(res, len)
// write actual data
mstore(add(res, 0x20), src)
}
}

/// @dev Returns the description prefix to compose the final description.
function _descriptionPrefix() internal view virtual returns (string memory);

/// @inheritdoc DssEmergencySpell
function _emergencyActions() internal override {
_emergencyActions(_ilk0);
_emergencyActions(_ilk1);
if (_totalIlks > 2) {
_emergencyActions(_ilk2);
}
}

/// @notice Executes the emergency actions for the specified ilk.
/// @param _ilk The ilk to set the related Clip breaker.
function _emergencyActions(bytes32 _ilk) internal virtual;

/// @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.
/// @return res Whether the spells is done or not.
function done() external view returns (bool res) {
res = _done(_ilk0) && _done(_ilk1);
if (_totalIlks > 2) {
res = res && _done(_ilk2);
}
}

/// @notice Returns whether the spell is done or not for the specified ilk.
function _done(bytes32 _ilk) internal view virtual returns (bool);
}
124 changes: 124 additions & 0 deletions src/DssBatchedEmergencySpell.t.integration.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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, GodMode} from "dss-test/DssTest.sol";
import {DssBatchedEmergencySpell} from "./DssBatchedEmergencySpell.sol";

contract DssBatchedEmergencySpellImpl is DssBatchedEmergencySpell {
mapping(bytes32 => bool) internal _isDone;

function setDone(bytes32 ilk, bool val) external {
_isDone[ilk] = val;
}

function _descriptionPrefix() internal pure override returns (string memory) {
return "Batched Emergency Spell:";
}

event EmergencyAction(bytes32 indexed ilk);

constructor(bytes32[] memory _ilks) DssBatchedEmergencySpell(_ilks) {}

function _emergencyActions(bytes32 ilk) internal override {
emit EmergencyAction(ilk);
}

function _done(bytes32 ilk) internal view override returns (bool) {
return _isDone[ilk];
}
}

contract DssBatchedEmergencySpellTest is DssTest {
address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F;
DssInstance dss;
DssBatchedEmergencySpellImpl spell2;
DssBatchedEmergencySpellImpl spell3;
address pause;

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

dss = MCD.loadFromChainlog(CHAINLOG);
MCD.giveAdminAccess(dss);
pause = dss.chainlog.getAddress("MCD_PAUSE");

bytes32[] memory ilks2 = new bytes32[](2);
ilks2[0] = "WSTETH-A";
ilks2[1] = "WSTETH-B";
spell2 = new DssBatchedEmergencySpellImpl(ilks2);
bytes32[] memory ilks3 = new bytes32[](3);
ilks3[0] = "ETH-A";
ilks3[1] = "ETH-B";
ilks3[2] = "ETH-C";
spell3 = new DssBatchedEmergencySpellImpl(ilks3);
}

function testDescription() public view {
assertEq(spell2.description(), "Batched Emergency Spell: WSTETH-A, WSTETH-B");
assertEq(spell3.description(), "Batched Emergency Spell: ETH-A, ETH-B, ETH-C");
}

function testEmergencyActions() public {
vm.expectEmit(true, true, true, true);
emit EmergencyAction("WSTETH-A");
vm.expectEmit(true, true, true, true);
emit EmergencyAction("WSTETH-B");
spell2.schedule();

vm.expectEmit(true, true, true, true);
emit EmergencyAction("ETH-A");
vm.expectEmit(true, true, true, true);
emit EmergencyAction("ETH-B");
vm.expectEmit(true, true, true, true);
emit EmergencyAction("ETH-C");
spell3.schedule();
}

function testDone() public {
assertFalse(spell2.done(), "spell2 unexpectedly done");
assertFalse(spell3.done(), "spell2 unexpectedly done");

{
// Tweak spell2 so it is considered done for WSTETH-A...
spell2.setDone("WSTETH-A", true);
// ... in this case it should still return false
assertFalse(spell2.done(), "spell2 unexpectedly done");
// Then set done for WSTETH-B...
spell2.setDone("WSTETH-B", true);
// ... new the spell must finally return true
assertTrue(spell2.done(), "spell2 not done");
}

{
// Tweak spell3 so it is considered done for ETH-A...
spell3.setDone("ETH-A", true);
// ... in this case it should still return false
assertFalse(spell3.done(), "spell3 unexpectedly done");
// Then set done for ETH-B...
spell3.setDone("ETH-B", true);
// ... it should still return false
assertFalse(spell3.done(), "spell3 unexpectedly done");
// Then set done for ETH-C...
spell3.setDone("ETH-C", true);
// ... new the spell must finally return true
assertTrue(spell3.done(), "spell3 not done");
}
}

event EmergencyAction(bytes32 indexed ilk);
}
Loading