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 33 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
37 changes: 25 additions & 12 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 | Grouped | Multi-ilk / Global |
| :---------- | :--------: | :-----: | :----------------: |
| 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,14 +120,27 @@ 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. Grouped: 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.


### About storage variables in `DssGroupedEmergencySpell`

Regular spell actions are executed through a `delegatecall` from `MCD_PAUSE_PROXY`. For that reason, they usually should
not have storage variables, as they would be accessing and interacting with `MCD_PAUSE_PROXY`'s storage, not their own.

However, Emergency Spells are not required to interact with `MCD_PAUSE` and `MCD_PAUSE_PROXY` at all. They execute
actions through regular `call` on `Mom` contracts, so we do not have this limitation.

Even if the contract is somehow misused and used as a regular spell, interacting with `MCD_PAUSE`, there would not be a
problem because the storage should not be changed outside the constructor by the concrete implementations.

### About the `done()` function

Conforming spells have a [`done`][spell-done] public storage variable which is `false` when the spell is deployed and
Expand All @@ -137,7 +150,7 @@ An emergency spell is not meant to be cast, but it can be scheduled multiple tim
storage variable, it becomes a getter function that will return:
- `false`: if the emergency spell can be scheduled in the current state, given it is lifted to the hat.
- `true`: if the desired effects of the spell can be verified or if there is anything that would prevent the spell from
being scheduled (i.e.: bad system config)
being scheduled (i.e.: bad system config).

Generally speaking, `done` should almost always return `false` for any emergency spell. If it returns `true` it means it
has just been scheduled or there is most likely something wrong with the modules touched by it. The exception is the
Expand Down
70 changes: 27 additions & 43 deletions src/DssEmergencySpell.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ abstract contract DssEmergencySpell is DssEmergencySpellLike {
/// @dev The chainlog contract reference.
ChainlogLike internal constant _log = ChainlogLike(0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F);

// @dev The reference to the `pause` contract.
/// @dev The reference to the `pause` contract.
address public immutable pause = _log.getAddress("MCD_PAUSE");
/// @dev The chainlog address.
address public constant log = address(_log);
Expand All @@ -63,72 +63,56 @@ abstract contract DssEmergencySpell is DssEmergencySpellLike {
bytes public constant sig = abi.encodeWithSelector(DssAction.execute.selector);
/// @dev Emergency spells should not expire.
uint256 public constant expiration = type(uint256).max;
// @dev An emergency spell does not need to be cast, as all actions happen during the schedule phase.
// Notice that cast is usually not supposed to revert, so it is implemented as a no-op.
/// @dev An emergency spell does not need to be cast, as all actions happen during the schedule phase.
/// Notice that cast is usually not supposed to revert, so it is implemented as a no-op.
uint256 internal immutable _nextCastTime = type(uint256).max;
// @dev Office Hours is always `false` for emergency spells.
/// @dev Office Hours is always `false` for emergency spells.
bool public constant officeHours = false;
// @dev `action` is expected to return a valid address.
// We also implement the `DssAction` interface in this contract.
/// @dev `action` is expected to return a valid address.
/// We also implement the `DssAction` interface in this contract.
address public immutable action = address(this);

/**
* @dev In regular spells, `tag` is an immutable variable with the code hash of the spell action.
* It specifically uses a separate contract for spell action because `tag` is immutable and the code hash of
* the contract being initialized is not accessible in the constructor.
* Since we do not have a separate contract for actions in Emergency Spells, `tag` has to be turned into a
* getter function instead of an immutable variable.
* @return The contract codehash.
*/
/// @dev In regular spells, `tag` is an immutable variable with the code hash of the spell action.
/// It specifically uses a separate contract for spell action because `tag` is immutable and the code hash of
/// the contract being initialized is not accessible in the constructor.
/// Since we do not have a separate contract for actions in Emergency Spells, `tag` has to be turned into a
/// getter function instead of an immutable variable.
/// @return The contract codehash.
function tag() external view returns (bytes32) {
return address(this).codehash;
}

/**
* @notice Triggers the emergency actions of the spell.
* @dev Emergency spells are triggered when scheduled.
* This function maintains the name for compatibility with regular spells, however nothing is actually being
* scheduled. Emergency spells take effect immediately, so there is no need to call `pause.plot()`.
*/
/// @notice Triggers the emergency actions of the spell.
/// @dev Emergency spells are triggered when scheduled.
/// This function maintains the name for compatibility with regular spells, however nothing is actually being
/// scheduled. Emergency spells take effect immediately, so there is no need to call `pause.plot()`.
function schedule() external {
_emergencyActions();
}

/**
* @notice Implements the emergency actions to be triggered by the spell.
*/
/// @notice Implements the emergency actions to be triggered by the spell.
function _emergencyActions() internal virtual;

/**
* @notice Returns `_nextCastTime`.
* @dev This function exists only to keep interface compatibility with regular spells.
*/
/// @notice Returns `_nextCastTime`.
/// @dev This function exists only to keep interface compatibility with regular spells.
function nextCastTime() external view returns (uint256 castTime) {
return _nextCastTime;
}

/**
* @notice No-op.
* @dev This function exists only to keep interface compatibility with regular spells.
*/
/// @notice No-op.
/// @dev This function exists only to keep interface compatibility with regular spells.
function cast() external {}

/**
* @notice No-op.
* @dev This function exists only to keep interface compatibility with regular spells.
*/
/// @notice No-op.
/// @dev This function exists only to keep interface compatibility with regular spells.
function execute() external {}

/**
* @notice No-op.
* @dev This function exists only to keep interface compatibility with regular spells.
*/
/// @notice No-op.
/// @dev This function exists only to keep interface compatibility with regular spells.
function actions() external {}

/**
* @notice Returns `nextCastTime`, regardless of the input parameter.
* @dev This function exists only to keep interface compatibility with regular spells.
*/
/// @notice Returns `nextCastTime`, regardless of the input parameter.
/// @dev This function exists only to keep interface compatibility with regular spells.
function nextCastTime(uint256) external view returns (uint256 castTime) {
return _nextCastTime;
}
Expand Down
129 changes: 129 additions & 0 deletions src/DssGroupedEmergencySpell.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// 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 DssGroupedEmergencySpellLike is DssEmergencySpellLike {
function ilks() external view returns (bytes32[] memory);
function emergencyActionsInBatch(uint256 start, uint256 end) external;
}

/// @title Grouped Emergency Spell
/// @notice Defines the base implementation for grouped emergency spells.
/// @custom:authors [amusingaxl]
/// @custom:reviewers []
/// @custom:auditors []
/// @custom:bounties []
abstract contract DssGroupedEmergencySpell is DssEmergencySpell, DssGroupedEmergencySpellLike {
/// @dev The min size for the list of ilks
uint256 private constant MIN_ILKS = 1;

/// @notice The list of ilks to which the spell is applicable.
/// @dev While spells should not have storage variables, we can make an exception here because this spell should not
/// change its own storage, an therefore, could not overwrite the PauseProxy state through delegate call even
amusingaxl marked this conversation as resolved.
Show resolved Hide resolved
/// if used incorrectly.
bytes32[] private ilkList;

/// @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.
amusingaxl marked this conversation as resolved.
Show resolved Hide resolved
/// The grouped spell is meant to be used for ilks that are a variation of tha same collateral gem
amusingaxl marked this conversation as resolved.
Show resolved Hide resolved
/// (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, "DssGroupedEmergencySpell/too-few-ilks");

ilkList = _ilks;
}

/// @notice Returns the list of ilks to which the spell is applicable.
function ilks() external view returns (bytes32[] memory) {
return ilkList;
}

/// @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 = _bytes32ToString(ilkList[0]);
// Start from one because the first item was already added.
for (uint256 i = 1; i < ilkList.length; i++) {
buf = string.concat(buf, ", ", _bytes32ToString(ilkList[i]));
}

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 {
for (uint256 i = 0; i < ilkList.length; i++) {
_emergencyActions(ilkList[i]);
}
}

/// @notice Executes the emergency actions for all ilks in the batch.
/// @dev This is an escape hatch to prevent the spell from being blocked in case it would hit the block gas limit.
/// In case `end` is greater than the ilk registry length, the iteration will be automatically capped.
amusingaxl marked this conversation as resolved.
Show resolved Hide resolved
/// @param start The index to start the iteration (inclusive).
/// @param end The index to stop the iteration (inclusive).
function emergencyActionsInBatch(uint256 start, uint256 end) external {
end = end > ilkList.length - 1 ? ilkList.length - 1 : end;
require(start <= end, "DssGroupedEmergencySpell/bad-iteration");

for (uint256 i = start; i <= end; i++) {
_emergencyActions(ilkList[i]);
}
}

/// @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.
/// @return res Whether the spells is done or not.
function done() external view returns (bool res) {
res = true;
for (uint256 i = 0; i < ilkList.length; i++) {
res = res && _done(ilkList[i]);
}
}
amusingaxl marked this conversation as resolved.
Show resolved Hide resolved

/// @notice Returns whether the spell is done or not for the specified ilk.
function _done(bytes32 _ilk) internal view virtual returns (bool);
}
Loading
Loading