-
Notifications
You must be signed in to change notification settings - Fork 46
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
Balancer pause helper #1211
base: main
Are you sure you want to change the base?
Balancer pause helper #1211
Changes from all commits
f890de9
9755e8b
ddc05ff
dfa1ba3
604ec24
4792b3b
eaa8665
b87af61
3adf961
a2dd9dd
ebabee2
7f330c4
8fe6eaa
afe4dbd
f180ea6
fa109d8
3ac8cd7
6ddf620
9216f1c
487fb9f
b6b7bff
ae91018
ed2717b
493cc3e
2e31465
82a8b59
5d379f3
5330ae6
05008fa
4431f7a
a3ecf67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,153 @@ | ||||||||
// SPDX-License-Identifier: GPL-3.0-or-later | ||||||||
|
||||||||
pragma solidity ^0.8.24; | ||||||||
|
||||||||
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; | ||||||||
|
||||||||
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; | ||||||||
import { IVaultAdmin } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultAdmin.sol"; | ||||||||
import { SingletonAuthentication } from "@balancer-labs/v3-vault/contracts/SingletonAuthentication.sol"; | ||||||||
|
||||||||
contract PauseHelper is SingletonAuthentication { | ||||||||
using EnumerableSet for EnumerableSet.AddressSet; | ||||||||
|
||||||||
/** | ||||||||
* @notice Cannot add a pool that is already there. | ||||||||
* @param pool Address of the pool being added | ||||||||
*/ | ||||||||
error PoolAlreadyInPausableSet(address pool); | ||||||||
|
||||||||
/** | ||||||||
* @notice Cannot remove a pool that was not added. | ||||||||
* @param pool Address of the pool being removed | ||||||||
*/ | ||||||||
error PoolNotInPausableSet(address pool); | ||||||||
|
||||||||
/// @notice An index is beyond the current bounds of the set. | ||||||||
error IndexOutOfBounds(); | ||||||||
|
||||||||
/** | ||||||||
* @notice Emitted when a pool is added to the list of pools that can be paused. | ||||||||
* @param pool Address of the pool that was added | ||||||||
*/ | ||||||||
event PoolAddedToPausableSet(address pool); | ||||||||
|
||||||||
/** | ||||||||
* @notice Emitted when a pool is removed from the list of pools that can be paused. | ||||||||
* @param pool Address of the pool that was removed | ||||||||
*/ | ||||||||
event PoolRemovedFromPausableSet(address pool); | ||||||||
|
||||||||
EnumerableSet.AddressSet private _pausablePools; | ||||||||
|
||||||||
constructor(IVault vault) SingletonAuthentication(vault) { | ||||||||
// solhint-disable-previous-line no-empty-blocks | ||||||||
} | ||||||||
|
||||||||
/*************************************************************************** | ||||||||
Manage Pools | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
ASCII art :) |
||||||||
***************************************************************************/ | ||||||||
|
||||||||
/** | ||||||||
* @notice Add pools to the list of pools that can be paused. | ||||||||
* @dev This is a permissioned function. Only authorized accounts (e.g., monitoring service providers) may add | ||||||||
* pools to the pause list. | ||||||||
* | ||||||||
* @param newPools List of pools to add | ||||||||
*/ | ||||||||
function addPools(address[] calldata newPools) external authenticate { | ||||||||
elshan-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
uint256 length = newPools.length; | ||||||||
|
||||||||
for (uint256 i = 0; i < length; i++) { | ||||||||
elshan-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
address pool = newPools[i]; | ||||||||
if (_pausablePools.add(pool) == false) { | ||||||||
revert PoolAlreadyInPausableSet(pool); | ||||||||
} | ||||||||
|
||||||||
emit PoolAddedToPausableSet(pool); | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @notice Remove pools from the list of pools that can be paused. | ||||||||
* @dev This is a permissioned function. Only authorized accounts (e.g., monitoring service providers) may remove | ||||||||
* pools from the pause list. | ||||||||
* | ||||||||
* @param pools List of pools to remove | ||||||||
*/ | ||||||||
function removePools(address[] memory pools) public authenticate { | ||||||||
uint256 length = pools.length; | ||||||||
for (uint256 i = 0; i < length; i++) { | ||||||||
elshan-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
address pool = pools[i]; | ||||||||
if (_pausablePools.remove(pool) == false) { | ||||||||
revert PoolNotInPausableSet(pool); | ||||||||
} | ||||||||
|
||||||||
emit PoolRemovedFromPausableSet(pool); | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @notice Pause a set of pools. | ||||||||
* @dev This is a permissioned function. Governance must first grant this contract permission to call `pausePool` | ||||||||
* on the Vault, then grant another account permission to call `pausePools` here. Note that this is not necessarily | ||||||||
* the same account that can add or remove pools from the pausable list. | ||||||||
* | ||||||||
* Note that there is no `unpause`. This is a helper contract designed to react quickly to emergencies. Unpausing | ||||||||
* is a more deliberate action that should be performed by accounts approved by governance for this purpose, or by | ||||||||
* the individual pools' pause managers. | ||||||||
* | ||||||||
* @param pools List of pools to pause | ||||||||
*/ | ||||||||
function pausePools(address[] memory pools) public authenticate { | ||||||||
uint256 length = pools.length; | ||||||||
for (uint256 i = 0; i < length; i++) { | ||||||||
elshan-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
address pool = pools[i]; | ||||||||
if (_pausablePools.contains(pool) == false) { | ||||||||
revert PoolNotInPausableSet(pool); | ||||||||
} | ||||||||
|
||||||||
getVault().pausePool(pool); | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
/*************************************************************************** | ||||||||
Getters | ||||||||
***************************************************************************/ | ||||||||
|
||||||||
/** | ||||||||
* @notice Get the number of pools. | ||||||||
* @dev Needed to support pagination in case the list is too long to process in a single transaction. | ||||||||
* @return poolCount The current number of pools in the pausable list | ||||||||
*/ | ||||||||
function getPoolsCount() external view returns (uint256) { | ||||||||
return _pausablePools.length(); | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @notice Check whether a pool is in the list of pausable pools. | ||||||||
* @param pool Pool to check | ||||||||
* @return isPausable True if the pool is in the list, false otherwise | ||||||||
*/ | ||||||||
function hasPool(address pool) external view returns (bool) { | ||||||||
return _pausablePools.contains(pool); | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @notice Get a range of pools. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Still think we should clarify this. |
||||||||
* @param from Start index | ||||||||
* @param to End index | ||||||||
* @return pools List of pools | ||||||||
*/ | ||||||||
function getPools(uint256 from, uint256 to) public view returns (address[] memory pools) { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we also want a That way it supports 3 ways of using it:
|
||||||||
uint256 poolLength = _pausablePools.length(); | ||||||||
if (from > to || to > poolLength || from >= poolLength) { | ||||||||
revert IndexOutOfBounds(); | ||||||||
} | ||||||||
|
||||||||
pools = new address[](to - from); | ||||||||
for (uint256 i = from; i < to; i++) { | ||||||||
pools[i - from] = _pausablePools.at(i); | ||||||||
} | ||||||||
} | ||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
// SPDX-License-Identifier: GPL-2.0-or-later | ||
|
||
pragma solidity ^0.8.24; | ||
|
||
import { PoolMock } from "@balancer-labs/v3-vault/contracts/test/PoolMock.sol"; | ||
import { PoolFactoryMock } from "@balancer-labs/v3-vault/contracts/test/PoolFactoryMock.sol"; | ||
import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol"; | ||
import { IVaultErrors } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultErrors.sol"; | ||
import { IAuthentication } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IAuthentication.sol"; | ||
|
||
import { PauseHelper } from "../../contracts/PauseHelper.sol"; | ||
|
||
contract PauseHelperTest is BaseVaultTest { | ||
PauseHelper pauseHelper; | ||
|
||
function setUp() public virtual override { | ||
BaseVaultTest.setUp(); | ||
|
||
address[] memory owners = new address[](1); | ||
owners[0] = address(this); | ||
|
||
pauseHelper = new PauseHelper(vault); | ||
|
||
authorizer.grantRole(pauseHelper.getActionId(pauseHelper.addPools.selector), address(this)); | ||
authorizer.grantRole(pauseHelper.getActionId(pauseHelper.removePools.selector), address(this)); | ||
authorizer.grantRole(pauseHelper.getActionId(pauseHelper.pausePools.selector), address(this)); | ||
|
||
authorizer.grantRole(vault.getActionId(vault.pausePool.selector), address(pauseHelper)); | ||
} | ||
|
||
function testAddPoolsWithTwoBatches() public { | ||
elshan-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
assertEq(pauseHelper.getPoolsCount(), 0, "Initial pool count non-zero"); | ||
|
||
// Add first batch of pools | ||
address[] memory firstPools = _generatePools(10); | ||
for (uint256 i = 0; i < firstPools.length; i++) { | ||
vm.expectEmit(); | ||
emit PauseHelper.PoolAddedToPausableSet(firstPools[i]); | ||
} | ||
|
||
pauseHelper.addPools(firstPools); | ||
|
||
assertEq(pauseHelper.getPoolsCount(), firstPools.length, "Pools count should be 10"); | ||
for (uint256 i = 0; i < firstPools.length; i++) { | ||
assertTrue(pauseHelper.hasPool(firstPools[i])); | ||
} | ||
|
||
// Add second batch of pools | ||
address[] memory secondPools = _generatePools(10); | ||
for (uint256 i = 0; i < secondPools.length; i++) { | ||
vm.expectEmit(); | ||
emit PauseHelper.PoolAddedToPausableSet(secondPools[i]); | ||
} | ||
|
||
pauseHelper.addPools(secondPools); | ||
assertEq(pauseHelper.getPoolsCount(), firstPools.length + secondPools.length, "Pools count should be 20"); | ||
|
||
for (uint256 i = 0; i < secondPools.length; i++) { | ||
assertTrue(pauseHelper.hasPool(secondPools[i])); | ||
} | ||
elshan-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
assertFalse(pauseHelper.hasPool(address(pauseHelper)), "Has invalid pool"); | ||
assertFalse(pauseHelper.hasPool(address(0)), "Has zero address pool"); | ||
} | ||
|
||
function testDoubleAddOnePool() public { | ||
assertEq(pauseHelper.getPoolsCount(), 0, "Initial pool count non-zero"); | ||
|
||
address[] memory pools = new address[](2); | ||
pools[0] = address(0x1); | ||
pools[1] = address(0x1); | ||
|
||
vm.expectRevert(abi.encodeWithSelector(PauseHelper.PoolAlreadyInPausableSet.selector, pools[1])); | ||
pauseHelper.addPools(pools); | ||
} | ||
|
||
function testAddPoolWithoutPermission() public { | ||
authorizer.revokeRole(pauseHelper.getActionId(pauseHelper.addPools.selector), address(this)); | ||
|
||
vm.expectRevert(IAuthentication.SenderNotAllowed.selector); | ||
pauseHelper.addPools(new address[](0)); | ||
} | ||
|
||
function testRemovePools() public { | ||
assertEq(pauseHelper.getPoolsCount(), 0, "Initial pool count non-zero"); | ||
|
||
address[] memory pools = _addPools(10); | ||
assertEq(pauseHelper.getPoolsCount(), 10, "Pools count should be 10"); | ||
|
||
for (uint256 i = 0; i < pools.length; i++) { | ||
vm.expectEmit(); | ||
emit PauseHelper.PoolRemovedFromPausableSet(pools[i]); | ||
} | ||
|
||
pauseHelper.removePools(pools); | ||
|
||
assertEq(pauseHelper.getPoolsCount(), 0, "End pool count non-zero"); | ||
|
||
for (uint256 i = 0; i < pools.length; i++) { | ||
assertFalse(pauseHelper.hasPool(pools[i])); | ||
} | ||
} | ||
|
||
function testRemoveNotExistingPool() public { | ||
_addPools(10); | ||
|
||
vm.expectRevert(abi.encodeWithSelector(PauseHelper.PoolNotInPausableSet.selector, address(0x00))); | ||
pauseHelper.removePools(new address[](1)); | ||
} | ||
|
||
function testRemovePoolWithoutPermission() public { | ||
address[] memory pools = _addPools(10); | ||
|
||
authorizer.revokeRole(pauseHelper.getActionId(pauseHelper.removePools.selector), address(this)); | ||
|
||
vm.expectRevert(IAuthentication.SenderNotAllowed.selector); | ||
pauseHelper.removePools(pools); | ||
} | ||
|
||
function testPause() public { | ||
elshan-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
address[] memory pools = _addPools(10); | ||
|
||
pauseHelper.pausePools(pools); | ||
|
||
for (uint256 i = 0; i < pools.length; i++) { | ||
assertTrue(vault.isPoolPaused(pools[i]), "Pool should be paused"); | ||
} | ||
} | ||
|
||
function testDoublePauseOnePool() public { | ||
address[] memory pools = _addPools(2); | ||
pools[1] = pools[0]; | ||
|
||
vm.expectRevert(abi.encodeWithSelector(IVaultErrors.PoolPaused.selector, pools[1])); | ||
pauseHelper.pausePools(pools); | ||
} | ||
|
||
function testPauseIfPoolIsNotInList() public { | ||
_addPools(10); | ||
|
||
vm.expectRevert(abi.encodeWithSelector(PauseHelper.PoolNotInPausableSet.selector, address(0x00))); | ||
pauseHelper.pausePools(new address[](1)); | ||
} | ||
|
||
function testPauseWithoutPermission() public { | ||
elshan-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
address[] memory pools = _addPools(10); | ||
|
||
authorizer.revokeRole(pauseHelper.getActionId(pauseHelper.pausePools.selector), address(this)); | ||
|
||
vm.expectRevert(IAuthentication.SenderNotAllowed.selector); | ||
pauseHelper.pausePools(pools); | ||
} | ||
|
||
function testPauseWithoutVaultPermission() public { | ||
address[] memory pools = _addPools(1); | ||
|
||
authorizer.revokeRole(vault.getActionId(vault.pausePool.selector), address(pauseHelper)); | ||
|
||
vm.expectRevert(IAuthentication.SenderNotAllowed.selector); | ||
pauseHelper.pausePools(pools); | ||
} | ||
|
||
function testGetPools() public { | ||
address[] memory pools = _addPools(10); | ||
address[] memory storedPools = pauseHelper.getPools(0, 10); | ||
|
||
for (uint256 i = 0; i < pools.length; i++) { | ||
assertEq(pools[i], storedPools[i], "Stored pool should be the same as the added pool"); | ||
} | ||
elshan-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
storedPools = pauseHelper.getPools(3, 5); | ||
|
||
for (uint256 i = 3; i < 5; i++) { | ||
assertEq(pools[i], storedPools[i - 3], "Stored pool should be the same as the added pool (partial)"); | ||
} | ||
} | ||
|
||
function _generatePools(uint256 length) internal returns (address[] memory pools) { | ||
pools = new address[](length); | ||
for (uint256 i = 0; i < length; i++) { | ||
pools[i] = PoolFactoryMock(poolFactory).createPool("Test", "TEST"); | ||
PoolFactoryMock(poolFactory).registerTestPool( | ||
pools[i], | ||
vault.buildTokenConfig(tokens), | ||
poolHooksContract, | ||
lp | ||
); | ||
} | ||
} | ||
|
||
function _addPools(uint256 length) internal returns (address[] memory pools) { | ||
pools = _generatePools(length); | ||
|
||
pauseHelper.addPools(pools); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we have an IPauseHelper, that defines all errors, events, and document all functions? Just to make this file cleaner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the same vein, is this confusable with Vault pausing? (i.e., will people think you can also pause the Vault with this?) Maybe it should be called
PausePoolHelper
.