This library has not been audited, tested, or even finished.
sol-pause
makes it easy to implement a catch-all "panic button" for use during incident response.
You should use something like sol-pause
if:
- you intend to have pausable contracts in production
- you want to the option to remove the "pausability" of your contracts once you feel more confident
- you're not sure how to configure "pausability" alongside the other roles/permissions your system has
- you want a solution to all of this that doesn't add much complexity
When building complex, multi-contract systems, it's pretty common to include some form of admin control to safeguard the protocol. This role can typically tweak protocol parameters or perform contract upgrades, which can be very useful as a way to recover a system when a bug is discovered. However, these setups are NOT optimal to respond to on-chain incidents!
Because "upgradability" is a security-critical feature, it's often placed in control of a high-threshold multisig. This means that triggering any functions this role holds tends to be very slow. Here's a typical "pause all the contracts" flow for multisig-based setups:
The time between confirming there's a problem and pausing all the contracts can be very high because of the number of people that need to coordinate to execute a transaction. If a pause needs to happen while multiple signers are asleep, they may not be reachable for hours. This is a good thing in the context of contract upgrades - these should be slow and difficult to trigger.
BUT: incidents can happen in minutes, not hours. By the time you manage to get your signers together, it may be too late. And if your attacker is smart, they'll do their research and wait until you're most likely to be asleep.
sol-pause
separates "pausing" from the other roles of an upgrader multisig, enabling pauses to be triggered by just one person (a "Pauser") rather than requiring multiple signers. Any number of Pausers can be added to the PauseController
, and when any one Pauser calls PauseController.pauseAll()
, all of the contracts in the system are immediately paused. This setup is designed to give your contracts a very simple "panic button" - something that a single informed, trusted individual can quickly trigger to immediately halt all your contracts:
A risk you're accepting by using a setup like this is that each Pauser is only secured by a single key, which could be lost of stolen. To mitigate this risk, Pausers are restricted to a minimal role. The ONLY thing a Pauser can do is call pauseAll()
- they have absolutely no other power.
The PauseController
owner (which can be your high threshold multisig) is required to do anything else:
- unpausing contracts
- adding/removing Pausers
- adding/removing Pausable contracts
- upgrading or burning the
PauseController
The above access controls were designed with a handful of scenarios in mind:
- If a Pauser key is lost, it can easily be swapped out by the
PauseController
owner (seesetPauser
). As long as you have other Pausers, the other Pausers can still trigger a pause if needed. - If a Pauser key is stolen, the worst case scenario is that an attacker is able to pause all your contracts. The
PauseController
owner will have to be used to revoke the Pauser's permissions and unpause the contracts (viasetPauser
andunpauseAll
, respectively).- This is not a good situation to be in, but the situation is at least a known, testable worst-case - and you can prepare for it! Assuming you've covered ALL your user-facing methods with
whenNotPaused
modifiers, it shouldn't directly lead to fund loss. It's important that you don't leave user-facing methods open -- and carefully consider this risk when deciding whether you wantsol-pause
for your contracts.
- This is not a good situation to be in, but the situation is at least a known, testable worst-case - and you can prepare for it! Assuming you've covered ALL your user-facing methods with
- If you add new contracts to the system, the
PauseController
owner can easily add them to the list of contracts to be paused (seeaddPausables
) - If you want to change how the
PauseController
works, or remove it from the system entirely, thePauseController
owner can usemigrateAll
to update thePauseController
address in all your system's contracts at once.
Things sol-pause
does NOT help with:
- Proxy-level bugs: Implementation-level pauses don't affect a proxy. Fortunately these aren't common, and can be planned for by having a pre-prepared contract upgrade ready for exactly this scenario.
- Multisig compromise: Pauser keys are designed to be lower security and swappable by a higher-security root of trust. If the root of trust (your multisig) is compromised,
sol-pause
can't fix that. - Giving Pausers additional access in the system: Pauser keys should ONLY be used for pausing. If you grant Pauser addresses other permissions, the consequences of key loss/compromise could be worse.
- Failing to cover ALL user-facing methods, or failing to register
Pausable
contracts in thePauseController
: self-explanatory.
Contract upgrades are slow, methodical processes - for good reason. Upgrades require lots of time to think, examine the problem, and test the solution. If you get it wrong, there's a big risk you make things worse.
Knowing there's a problem is easy - you get a DM from samczsun and check etherscan. Knowing how to patch the problem is very hard. As such, contract upgrades should never be your first response to an active incident.
Pausable contracts inherit from src/Pausable.sol
and may be paused (or unpaused) by the PauseController
. Pausable.sol
is a minimal wrapper around OpenZeppelin's PausableUpgradeable
, and works exactly the same way: when paused, any methods protected by the whenNotPaused
modifier are locked, preventing access/state changes until unpaused.
The PauseController
is intended to be deployed as a standalone contract (i.e. not behind a proxy) and is configured with a list of pausable contracts.
The PauseController
owner has access to the following methods:
PauseController.pauseAll()
callspause()
on each pausable contractPauseController.unpauseAll()
callsunpause()
on each pausable contractPauseController.addPausables(address[])
: adds one or more pausable contracts to the controllerPauseController.removePausables(address[])
: removes one or more pausable contracts from the controllerPauseController.setPauser(address,bool)
: Adds/removes a Pauser from the controllerPauseController.migrateAll(address)
: CallsupdatePauseController
on each pausable contract
Pausers have access to the following method:
PauseController.pauseAll()
: iterates over the pausable contracts and callspause()
on each