Skip to content

Commit

Permalink
Add flag to enable / disable joins and exits in ManagedPool. (#2070)
Browse files Browse the repository at this point in the history
Add flag to enable / disable joins and exits in `ManagedPool`.

Co-authored-by: Tom French <[email protected]>
Co-authored-by: Nicolás Venturo <[email protected]>
  • Loading branch information
3 people authored Nov 29, 2022
1 parent f6c4a58 commit d67a0e6
Show file tree
Hide file tree
Showing 15 changed files with 349 additions and 19 deletions.
1 change: 1 addition & 0 deletions pkg/balancer-js/src/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const balancerErrorCodes: Record<string, string> = {
'355': 'INVALID_CIRCUIT_BREAKER_BOUNDS',
'356': 'CIRCUIT_BREAKER_TRIPPED',
'357': 'MALICIOUS_QUERY_REVERT',
'358': 'JOINS_EXITS_DISABLED',
'400': 'REENTRANCY',
'401': 'SENDER_NOT_ALLOWED',
'402': 'PAUSED',
Expand Down
15 changes: 15 additions & 0 deletions pkg/interfaces/contracts/pool-utils/IManagedPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface IManagedPool is IBasePool {
uint256[] endWeights
);
event SwapEnabledSet(bool swapEnabled);
event JoinExitEnabledSet(bool joinExitEnabled);
event MustAllowlistLPsSet(bool mustAllowlistLPs);
event AllowlistAddressAdded(address indexed member);
event AllowlistAddressRemoved(address indexed member);
Expand Down Expand Up @@ -145,6 +146,20 @@ interface IManagedPool is IBasePool {
uint256[] memory endWeights
);

// Join and Exit enable/disable

/**
* @notice Enable or disable joins and exits. Note that this does not affect Recovery Mode exits.
* @dev Emits the JoinExitEnabledSet event. This is a permissioned function.
* @param joinExitEnabled - The new value of the join/exit enabled flag.
*/
function setJoinExitEnabled(bool joinExitEnabled) external;

/**
* @notice Returns whether joins and exits are enabled.
*/
function getJoinExitEnabled() external view returns (bool);

// Swap enable/disable

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ library Errors {
uint256 internal constant INVALID_CIRCUIT_BREAKER_BOUNDS = 355;
uint256 internal constant CIRCUIT_BREAKER_TRIPPED = 356;
uint256 internal constant MALICIOUS_QUERY_REVERT = 357;
uint256 internal constant JOINS_EXITS_DISABLED = 358;

// Lib
uint256 internal constant REENTRANCY = 400;
Expand Down
33 changes: 27 additions & 6 deletions pkg/pool-utils/contracts/controllers/ManagedPoolController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,34 +34,38 @@ contract ManagedPoolController is BasePoolController {
using SafeERC20 for IERC20;
using WordCodec for bytes32;

// There are six managed pool rights: all corresponding to permissioned functions of ManagedPool.
// There are seven managed pool rights: all corresponding to permissioned functions of ManagedPool.
struct ManagedPoolRights {
bool canChangeWeights;
bool canDisableSwaps;
bool canSetMustAllowlistLPs;
bool canSetCircuitBreakers;
bool canChangeTokens;
bool canChangeMgmtFees;
bool canDisableJoinExit;
}

// The minimum weight change duration could be replaced with more sophisticated rate-limiting.
uint256 internal immutable _minWeightChangeDuration;

/* solhint-disable max-line-length */
// Immutable controller state - the first 16 bits are reserved as a bitmap for permission flags
// (3 used in the base class; 6 used here), and the remaining 240 bits can be used by derived classes
// (3 used in the base class; 7 used here), and the remaining 240 bits can be used by derived classes
// to store any other immutable data.
//
// Managed Pool Controller Permissions | Base Controller Permissions ]
// [ 240 | 7 bits | 1 bit | 1 bit | 1 bit | 1 bit | 1 bit | 1 bit | 1 bit | 1 bit | 1 bit ]
// [unused|reserved| mgmt fee | tokens | breakers | LPs | swaps | weights | metadata | swap fee | transfer ]
// |MSB LSB|
// [ Managed Pool Controller Permissions | Base Controller Permissions ]
// [ 240 | 6 bits | 1 bit | 1 bit | 1 bit | 1 bit | 1 bit | 1 bit | 1 bit | 1 bit | 1 bit | 1 bit ]
// [unused|reserved| join-exit | mgmt fee | tokens | breakers | LPs | swaps | weights | metadata | swap fee | transfer ]
// |MSB LSB|
/* solhint-enable max-line-length */

uint256 private constant _CHANGE_WEIGHTS_OFFSET = 3;
uint256 private constant _DISABLE_SWAPS_OFFSET = 4;
uint256 private constant _MUST_ALLOWLIST_LPS_OFFSET = 5;
uint256 private constant _CIRCUIT_BREAKERS_OFFSET = 6;
uint256 private constant _CHANGE_TOKENS_OFFSET = 7;
uint256 private constant _CHANGE_MGMT_FEES_OFFSET = 8;
uint256 private constant _DISABLE_JOIN_EXIT_OFFSET = 9;

/**
* @dev Pass in the `BasePoolRights` and `ManagedPoolRights` structures, to form the complete set of
Expand Down Expand Up @@ -91,6 +95,7 @@ contract ManagedPoolController is BasePoolController {
// Needed to avoid "stack too deep"
return
permissions
.insertBool(managedRights.canDisableJoinExit, _DISABLE_JOIN_EXIT_OFFSET)
.insertBool(managedRights.canChangeMgmtFees, _CHANGE_MGMT_FEES_OFFSET)
.insertBool(managedRights.canChangeTokens, _CHANGE_TOKENS_OFFSET)
.insertBool(managedRights.canSetCircuitBreakers, _CIRCUIT_BREAKERS_OFFSET);
Expand Down Expand Up @@ -138,6 +143,13 @@ contract ManagedPoolController is BasePoolController {
return _controllerState.decodeBool(_CHANGE_MGMT_FEES_OFFSET);
}

/**
* @dev Getter for the canDisableJoinExit permission.
*/
function canDisableJoinExit() public view returns (bool) {
return _controllerState.decodeBool(_DISABLE_JOIN_EXIT_OFFSET);
}

/**
* @dev Getter for the minimum weight change duration.
*/
Expand Down Expand Up @@ -226,4 +238,13 @@ contract ManagedPoolController is BasePoolController {

return IManagedPool(pool).setManagementAumFeePercentage(managementAumFeePercentage);
}

/**
* @dev Pass a call to ManagedPool's setJoinExitEnabled through to the underlying pool.
*/
function setJoinExitEnabled(bool joinExitEnabled) external virtual onlyManager withBoundPool {
_require(canDisableJoinExit(), Errors.FEATURE_DISABLED);

IManagedPool(pool).setJoinExitEnabled(joinExitEnabled);
}
}
36 changes: 31 additions & 5 deletions pkg/pool-utils/test/ManagedPoolController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ async function deployControllerAndPool(
canSetCircuitBreakers = true,
canChangeTokens = true,
canChangeMgmtFees = true,
canDisableJoinExit = true,
swapEnabledOnStart = true,
protocolSwapFeePercentage = MAX_UINT256
) {
Expand All @@ -77,6 +78,7 @@ async function deployControllerAndPool(
canSetCircuitBreakers: canSetCircuitBreakers,
canChangeTokens: canChangeTokens,
canChangeMgmtFees: canChangeMgmtFees,
canDisableJoinExit: canDisableJoinExit,
};

poolController = await deploy('ManagedPoolController', {
Expand Down Expand Up @@ -130,6 +132,7 @@ describe('ManagedPoolController', function () {
expect(await poolController.canSetCircuitBreakers()).to.be.true;
expect(await poolController.canChangeTokens()).to.be.true;
expect(await poolController.canChangeManagementFees()).to.be.true;
expect(await poolController.canDisableJoinExit()).to.be.true;
});

it('sets the minimum weight change duration', async () => {
Expand Down Expand Up @@ -159,6 +162,18 @@ describe('ManagedPoolController', function () {
});
});

describe('set join / exit enabled', () => {
it('lets the manager disable joins and exits', async () => {
await poolController.connect(manager).setJoinExitEnabled(false);

expect(await pool.getJoinExitEnabled(manager)).to.be.false;
});

it('reverts if non-manager disables joins and exits', async () => {
await expect(poolController.connect(other).setJoinExitEnabled(false)).to.be.revertedWith('CALLER_IS_NOT_OWNER');
});
});

describe('set swap enabled', () => {
it('lets the manager disable trading', async () => {
await poolController.connect(manager).setSwapEnabled(false);
Expand Down Expand Up @@ -292,7 +307,7 @@ describe('ManagedPoolController', function () {

context('with canChangeMgmtFees set to false', () => {
sharedBeforeEach('deploy controller (canChangeMgmtFees false)', async () => {
await deployControllerAndPool(true, true, true, true, true, false, true, true, false);
await deployControllerAndPool(true, true, true, true, true, true, true, true, false);
await poolController.initialize(pool.address);
});

Expand All @@ -303,9 +318,20 @@ describe('ManagedPoolController', function () {
});
});

context('with canDisableJoinExit set to false', () => {
sharedBeforeEach('deploy controller (canDisableJoinExit false)', async () => {
await deployControllerAndPool(true, true, true, true, true, true, true, true, true, false);
await poolController.initialize(pool.address);
});

it('reverts if the manager disables swaps', async () => {
await expect(poolController.connect(manager).setJoinExitEnabled(false)).to.be.revertedWith('FEATURE_DISABLED');
});
});

context('with public swaps disabled (on start)', () => {
sharedBeforeEach('deploy controller (swapEnabledOnStart false)', async () => {
await deployControllerAndPool(true, true, true, true, true, false, true, true, true, false);
await deployControllerAndPool(true, true, true, true, true, true, true, true, true, true, false);
await poolController.initialize(pool.address);
await allTokens.approve({ from: manager, to: await pool.getVault() });
const initialBalances = Array(allTokens.length).fill(fp(1));
Expand Down Expand Up @@ -338,7 +364,7 @@ describe('ManagedPoolController', function () {

context('with canSetCircuitBreakers set to false', () => {
sharedBeforeEach('deploy controller (canSetCircuitBreakers false)', async () => {
await deployControllerAndPool(true, true, true, true, true, false, false);
await deployControllerAndPool(true, true, true, true, true, true, false);
});

it('sets the set circuit breakers permission', async () => {
Expand All @@ -348,7 +374,7 @@ describe('ManagedPoolController', function () {

context('with canChangeTokens set to false', () => {
sharedBeforeEach('deploy controller (canChangeTokens false)', async () => {
await deployControllerAndPool(true, true, true, true, true, false, true, false);
await deployControllerAndPool(true, true, true, true, true, true, true, false);
});

it('sets the change tokens permission', async () => {
Expand All @@ -358,7 +384,7 @@ describe('ManagedPoolController', function () {

context('with canChangeMgmtFees set to false', () => {
sharedBeforeEach('deploy controller (canChangeMgmtFees false)', async () => {
await deployControllerAndPool(true, true, true, true, true, false, false, false, false);
await deployControllerAndPool(true, true, true, true, true, true, true, true, false);
});

it('sets the set management fee permission', async () => {
Expand Down
15 changes: 15 additions & 0 deletions pkg/pool-weighted/contracts/managed/ManagedPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ contract ManagedPool is ManagedPoolSettings {
uint256 actualSupply,
bytes32 poolState
) internal view returns (uint256) {
// Check whether joins are enabled.
_require(ManagedPoolStorageLib.getJoinExitsEnabled(poolState), Errors.JOINS_EXITS_DISABLED);

// We first query data needed to perform the joinswap, i.e. the token weight and scaling factor as well as the
// Pool's swap fee.
(uint256 tokenInWeight, uint256 scalingFactorTokenIn) = _getTokenInfo(
Expand Down Expand Up @@ -256,6 +259,9 @@ contract ManagedPool is ManagedPoolSettings {
uint256 actualSupply,
bytes32 poolState
) internal view returns (uint256) {
// Check whether exits are enabled.
_require(ManagedPoolStorageLib.getJoinExitsEnabled(poolState), Errors.JOINS_EXITS_DISABLED);

// We first query data needed to perform the exitswap, i.e. the token weight and scaling factor as well as the
// Pool's swap fee.
(uint256 tokenOutWeight, uint256 scalingFactorTokenOut) = _getTokenInfo(
Expand Down Expand Up @@ -542,6 +548,10 @@ contract ManagedPool is ManagedPoolSettings {
bytes memory userData
) internal view returns (uint256, uint256[] memory) {
bytes32 poolState = _getPoolState();

// Check whether joins are enabled.
_require(ManagedPoolStorageLib.getJoinExitsEnabled(poolState), Errors.JOINS_EXITS_DISABLED);

WeightedPoolUserData.JoinKind kind = userData.joinKind();

// If swaps are disabled, only proportional joins are allowed. All others involve implicit swaps, and alter
Expand Down Expand Up @@ -646,6 +656,11 @@ contract ManagedPool is ManagedPoolSettings {
bytes memory userData
) internal view virtual returns (uint256, uint256[] memory) {
bytes32 poolState = _getPoolState();

// Check whether exits are enabled. Recovery mode exits are not blocked by this check, since they are routed
// through a different codepath at the base pool layer.
_require(ManagedPoolStorageLib.getJoinExitsEnabled(poolState), Errors.JOINS_EXITS_DISABLED);

WeightedPoolUserData.ExitKind kind = userData.exitKind();

// If swaps are disabled, only proportional exits are allowed. All others involve implicit swaps, and alter
Expand Down
20 changes: 20 additions & 0 deletions pkg/pool-weighted/contracts/managed/ManagedPoolSettings.sol
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ abstract contract ManagedPoolSettings is NewBasePool, ProtocolFeeCache, IManaged

// If true, only addresses on the manager-controlled allowlist may join the pool.
_setMustAllowlistLPs(params.mustAllowlistLPs);

// Joins and exits are enabled by default on start.
_setJoinExitEnabled(true);
}

function _getPoolState() internal view returns (bytes32) {
Expand Down Expand Up @@ -376,6 +379,22 @@ abstract contract ManagedPoolSettings is NewBasePool, ProtocolFeeCache, IManaged
emit GradualWeightUpdateScheduled(startTime, endTime, startWeights, endWeights);
}

// Join / Exit Enabled

function getJoinExitEnabled() external view override returns (bool) {
return ManagedPoolStorageLib.getJoinExitsEnabled(_poolState);
}

function setJoinExitEnabled(bool joinExitEnabled) external override authenticate whenNotPaused {
_setJoinExitEnabled(joinExitEnabled);
}

function _setJoinExitEnabled(bool joinExitEnabled) private {
_poolState = ManagedPoolStorageLib.setJoinExitsEnabled(_poolState, joinExitEnabled);

emit JoinExitEnabledSet(joinExitEnabled);
}

// Swap Enabled

function getSwapEnabled() external view override returns (bool) {
Expand Down Expand Up @@ -828,6 +847,7 @@ abstract contract ManagedPoolSettings is NewBasePool, ProtocolFeeCache, IManaged
return
(actionId == getActionId(ManagedPoolSettings.updateWeightsGradually.selector)) ||
(actionId == getActionId(ManagedPoolSettings.updateSwapFeeGradually.selector)) ||
(actionId == getActionId(ManagedPoolSettings.setJoinExitEnabled.selector)) ||
(actionId == getActionId(ManagedPoolSettings.setSwapEnabled.selector)) ||
(actionId == getActionId(ManagedPoolSettings.addAllowedAddress.selector)) ||
(actionId == getActionId(ManagedPoolSettings.removeAllowedAddress.selector)) ||
Expand Down
26 changes: 22 additions & 4 deletions pkg/pool-weighted/contracts/managed/ManagedPoolStorageLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ library ManagedPoolStorageLib {
// Store non-token-based values:
// Start/end timestamps for gradual weight and swap fee updates
// Start/end values of the swap fee
// Flags for the LP allowlist, enabling/disabling trading, and recovery mode
// Flags for the LP allowlist, enabling/disabling trading, enabling/disabling joins and exits, and recovery mode
//
// [ 1 bit | 1 bit | 1 bit | 1 bit | 62 bits | 62 bits | 32 bits | 32 bits | 32 bits | 32 bits ]
// [ unused | recovery | LP flag | swap flag | end swap fee | start swap fee | end fee time | start fee time | end wgt | start wgt ]
// |MSB LSB|
// [ 1 bit | 1 bit | 1 bit | 1 bit | 62 bits | 62 bits | 32 bits | 32 bits | 32 bits | 32 bits ]
// [ join-exit flag | recovery | LP flag | swap flag | end swap fee | start swap fee | end fee time | start fee time | end wgt | start wgt ]
// |MSB LSB|
/* solhint-enable max-line-length */
uint256 private constant _WEIGHT_START_TIME_OFFSET = 0;
uint256 private constant _WEIGHT_END_TIME_OFFSET = _WEIGHT_START_TIME_OFFSET + _TIMESTAMP_WIDTH;
Expand All @@ -45,13 +45,22 @@ library ManagedPoolStorageLib {
uint256 private constant _SWAP_ENABLED_OFFSET = _SWAP_FEE_END_PCT_OFFSET + _SWAP_FEE_PCT_WIDTH;
uint256 private constant _MUST_ALLOWLIST_LPS_OFFSET = _SWAP_ENABLED_OFFSET + 1;
uint256 private constant _RECOVERY_MODE_OFFSET = _MUST_ALLOWLIST_LPS_OFFSET + 1;
uint256 private constant _JOIN_EXIT_ENABLED_OFFSET = _RECOVERY_MODE_OFFSET + 1;

uint256 private constant _TIMESTAMP_WIDTH = 32;
// 2**60 ~= 1.1e18 so this is sufficient to store the full range of potential swap fees.
uint256 private constant _SWAP_FEE_PCT_WIDTH = 62;

// Getters

/**
* @notice Returns whether the Pool allows regular joins and exits (recovery exits not included).
* @param poolState - The byte32 state of the Pool.
*/
function getJoinExitsEnabled(bytes32 poolState) internal pure returns (bool) {
return poolState.decodeBool(_JOIN_EXIT_ENABLED_OFFSET);
}

/**
* @notice Returns whether the Pool is currently in Recovery Mode.
* @param poolState - The byte32 state of the Pool.
Expand Down Expand Up @@ -143,6 +152,15 @@ library ManagedPoolStorageLib {

// Setters

/**
* @notice Sets the "Joins/Exits enabled" flag to `enabled`.
* @param poolState - The byte32 state of the Pool.
* @param enabled - A boolean flag for whether Joins and Exits are to be enabled.
*/
function setJoinExitsEnabled(bytes32 poolState, bool enabled) internal pure returns (bytes32) {
return poolState.insertBool(enabled, _JOIN_EXIT_ENABLED_OFFSET);
}

/**
* @notice Sets the "Recovery Mode enabled" flag to `enabled`.
* @param poolState - The byte32 state of the Pool.
Expand Down
Loading

0 comments on commit d67a0e6

Please sign in to comment.