Skip to content

Commit

Permalink
Merge pull request #10 from makerdao/feat/batched-ilk-spells
Browse files Browse the repository at this point in the history
Feat: add grouped ilk emergency spells
  • Loading branch information
amusingaxl authored Dec 3, 2024
2 parents 4e14e95 + ceffca9 commit 0129bcd
Show file tree
Hide file tree
Showing 8 changed files with 1,008 additions and 55 deletions.
37 changes: 25 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,14 @@ bespoke spell in record time.

## 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 @@ -222,14 +222,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 @@ -240,7 +253,7 @@ 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
131 changes: 131 additions & 0 deletions src/DssGroupedEmergencySpell.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// 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, and therefore, could not overwrite the PauseProxy state through delegate call even
/// if used incorrectly.
bytes32[] private ilkList;

/// @param _ilks The list of ilks for which the spell should be applicable
/// @dev The list size must be at least 1.
/// The grouped spell is meant to be used for ilks that are a variation of the 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, "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 list length, the iteration will be automatically capped.
/// @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) {
for (uint256 i = 0; i < ilkList.length; i++) {
if (!_done(ilkList[i])) {
return false;
}
}
return true;
}

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

0 comments on commit 0129bcd

Please sign in to comment.