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

Balancer pause helper #1211

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f890de9
add first iteration
elshan-eth Jan 6, 2025
9755e8b
add tests
elshan-eth Jan 6, 2025
ddc05ff
add tests
elshan-eth Jan 7, 2025
dfa1ba3
fix codestyle
elshan-eth Jan 7, 2025
604ec24
remove safe
elshan-eth Jan 8, 2025
4792b3b
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 14, 2025
eaa8665
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 14, 2025
b87af61
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 14, 2025
3adf961
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 14, 2025
a2dd9dd
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 14, 2025
ebabee2
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 14, 2025
7f330c4
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 14, 2025
8fe6eaa
fist part of fixes
elshan-eth Jan 14, 2025
afe4dbd
Merge branch 'balancer-pause-helper' of https://github.com/balancer/b…
elshan-eth Jan 14, 2025
f180ea6
fixes
elshan-eth Jan 14, 2025
fa109d8
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 20, 2025
3ac8cd7
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 20, 2025
6ddf620
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 20, 2025
9216f1c
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 20, 2025
487fb9f
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 20, 2025
b6b7bff
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 20, 2025
ae91018
Update pkg/standalone-utils/contracts/PauseHelper.sol
elshan-eth Jan 20, 2025
ed2717b
small fixes
elshan-eth Jan 20, 2025
493cc3e
Merge branch 'balancer-pause-helper' of https://github.com/balancer/b…
elshan-eth Jan 20, 2025
2e31465
small fixes
elshan-eth Jan 20, 2025
82a8b59
Fix rounding of 2CLP pools (#1193)
joaobrunoah Jan 8, 2025
5d379f3
Medusa swap tests (#1167)
elshan-eth Jan 8, 2025
5330ae6
Alternative LBP initialization (#1210)
jubeira Jan 8, 2025
05008fa
Restructuring LiquidityApproximation and E2ESwap tests (#1080)
joaobrunoah Jan 10, 2025
4431f7a
Merge branch 'main' into balancer-pause-helper
EndymionJkb Jan 29, 2025
a3ecf67
fix: merge conflict
EndymionJkb Jan 29, 2025
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
153 changes: 153 additions & 0 deletions pkg/standalone-utils/contracts/PauseHelper.sol
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 {
Copy link
Contributor

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.

Copy link
Collaborator

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.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Manage Pools
Manage Pools

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @notice Get a range of pools.
* @notice Get a range of pools.
* @dev Indexes are 0-based and [start, end) (i.e., inclusive of `start`; exclusive of `end`).

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also want a getPoolAt(uint256 index)?
Could also have a getPools() returns (address[] memory pools) that just returns them all.

That way it supports 3 ways of using it:

  1. simple getPools() if you know there isn't a pagination issue;
  2. generic iteration: for(i = 0; i < getPoolsCount(); ++i) { address pool = getPoolAt(i); }
  3. pagination if needed, using getPools(from, to);

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);
}
}
}
196 changes: 196 additions & 0 deletions pkg/standalone-utils/test/foundry/PauseHelper.t.sol
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);
}
}