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 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
32 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
7d430ee
small fixes
elshan-eth Feb 10, 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
144 changes: 144 additions & 0 deletions pkg/standalone-utils/contracts/PauseHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// 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 {
elshan-eth marked this conversation as resolved.
Show resolved Hide resolved
using EnumerableSet for EnumerableSet.AddressSet;

/**
* @notice Revert if the pool is already in the list of pools
* @param pool Pool that tried to be added
elshan-eth marked this conversation as resolved.
Show resolved Hide resolved
*/
error PoolExistInPausableSet(address pool);
elshan-eth marked this conversation as resolved.
Show resolved Hide resolved

/**
* @notice Revert if the pool is not in the list of pools
* @param pool Pool that not found
*/
error PoolNotFoundInPausableSet(address pool);
elshan-eth marked this conversation as resolved.
Show resolved Hide resolved

/// @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 Pool that was added
elshan-eth marked this conversation as resolved.
Show resolved Hide resolved
*/
event PoolAddedToPausableSet(address pool);

/**
* @notice Emitted when a pool is removed from the list of pools that can be paused
* @param pool Pool that was removed
elshan-eth marked this conversation as resolved.
Show resolved Hide resolved
*/
event PoolRemovedFromPausableSet(address pool);

EnumerableSet.AddressSet private _poolSet;
elshan-eth marked this conversation as resolved.
Show resolved Hide resolved

constructor(IVault vault) SingletonAuthentication(vault) {
// solhint-disable-previous-line no-empty-blocks
}

/***************************************************************************
Manage Pools
elshan-eth marked this conversation as resolved.
Show resolved Hide resolved
***************************************************************************/

/**
* @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
elshan-eth marked this conversation as resolved.
Show resolved Hide resolved
*/
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
if (_poolSet.add(newPools[i]) == false) {
revert PoolExistInPausableSet(newPools[i]);
}

emit PoolAddedToPausableSet(newPools[i]);
}
}

/**
* @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
elshan-eth marked this conversation as resolved.
Show resolved Hide resolved
*/
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
if (_poolSet.remove(pools[i]) == false) {
revert PoolNotFoundInPausableSet(pools[i]);
}

emit PoolRemovedFromPausableSet(pools[i]);
}
}

/**
* @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
if (_poolSet.contains(pools[i]) == false) {
revert PoolNotFoundInPausableSet(pools[i]);
}

getVault().pausePool(pools[i]);
}
}

// -------------------------- Getters --------------------------
elshan-eth marked this conversation as resolved.
Show resolved Hide resolved
/**
* @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 _poolSet.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 _poolSet.contains(pool);
}

/**
* @notice Get a range of pools
elshan-eth marked this conversation as resolved.
Show resolved Hide resolved
* @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);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I couldn't come up with a reason why getPoolAt(uint256 index) might be needed. It seems unnecessary to me.

As for getPools(), I think that if it can potentially break at some point, it's better not to implement such a function.

uint256 poolLength = _poolSet.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] = _poolSet.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.PoolExistInPausableSet.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.PoolNotFoundInPausableSet.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.PoolNotFoundInPausableSet.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);
}
}
Loading