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

Fee controller migration contracts #1299

Open
wants to merge 50 commits into
base: protocol-fee-controller-v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
a062f77
feat: add getters for pool creator data, and explicit pool registrati…
EndymionJkb Feb 16, 2025
529ac79
feat: add governance-scripts package
EndymionJkb Feb 16, 2025
b058b23
feat: add protocol fee controller migration
EndymionJkb Feb 16, 2025
b30b537
refactor: don't pass in pool creator to initialize
EndymionJkb Feb 16, 2025
eed79d8
refactor: remove directory that generates strange errors
EndymionJkb Feb 17, 2025
33ce5e6
refactor: remove redundant (and problematic) `_poolCreators` storage
EndymionJkb Feb 17, 2025
ab3df83
refactor: remove unused error
EndymionJkb Feb 17, 2025
395c7bc
refactor: don't collect and withdraw fees
EndymionJkb Feb 17, 2025
8d4409d
feat: add explicit pool registration and new pool creator event
EndymionJkb Feb 17, 2025
71b466b
test: add tests for new features
EndymionJkb Feb 17, 2025
42be536
docs: update migration comments for simple one
EndymionJkb Feb 17, 2025
ccae6fd
feat: add registerInMigration helper
EndymionJkb Feb 17, 2025
22102e1
test: add tests for new migration function
EndymionJkb Feb 17, 2025
01a081c
feat: add level 2 migration contract
EndymionJkb Feb 17, 2025
7ebb82f
refactor: simplify to pass old fee controller instead of the values (…
EndymionJkb Feb 18, 2025
a7999dd
test: new migration interface
EndymionJkb Feb 18, 2025
746a210
refactor: reuse original migration functions
EndymionJkb Feb 18, 2025
dff7d50
refactor: rename migration function
EndymionJkb Feb 18, 2025
f8e9652
refactor: add migration role
EndymionJkb Feb 18, 2025
57d48d2
refactor: simplify migrations
EndymionJkb Feb 18, 2025
da0e2b9
refactor: simplify migratePool
EndymionJkb Feb 18, 2025
c00dccc
lint
EndymionJkb Feb 18, 2025
90adc71
refactor: don't need FixedPoint
EndymionJkb Feb 18, 2025
570a075
test: adjust to new migration signature
EndymionJkb Feb 18, 2025
56d791d
test: remove unnecessary pool creator storage from fee controller
EndymionJkb Feb 18, 2025
25b8162
refactor: make virtual in case we need a v3 someday
EndymionJkb Feb 18, 2025
ecb07f0
Merge branch 'main' into fee-controller-script
EndymionJkb Feb 18, 2025
e7c6149
refactor: move basic authorizer
EndymionJkb Feb 18, 2025
c735ec2
refactor: complete move of basic authorizer
EndymionJkb Feb 18, 2025
3b04170
Merge branch 'main' into fee-controller-script
EndymionJkb Feb 18, 2025
c4639be
feat: new protocol fee controller, suitable for migrations
EndymionJkb Feb 19, 2025
4da42c5
test: update tests for new functions
EndymionJkb Feb 19, 2025
c4324fb
refactor: remove pool creator from ProtocolFeeControllerMock
EndymionJkb Feb 19, 2025
0a5c484
chore: update bytecode
EndymionJkb Feb 19, 2025
ebae2ab
chore: update gas
EndymionJkb Feb 19, 2025
7793e6d
docs: fix comments and decrease diffs
EndymionJkb Feb 19, 2025
c1be315
Merge branch 'protocol-fee-controller-v2' into fee-controller-script
EndymionJkb Feb 19, 2025
0e8bd5a
test: adjust tests for fee controller pool creator
EndymionJkb Feb 19, 2025
84f0b1e
Merge branch 'protocol-fee-controller-v2' into fee-controller-script
EndymionJkb Feb 19, 2025
cde1d1e
refactor: remove unnecessary import
EndymionJkb Feb 19, 2025
2f14d8b
Merge branch 'protocol-fee-controller-v2' into fee-controller-script
EndymionJkb Feb 19, 2025
27da6d2
Merge branch 'main' into protocol-fee-controller-v2
EndymionJkb Feb 19, 2025
d52eb97
Merge branch 'protocol-fee-controller-v2' into fee-controller-script
EndymionJkb Feb 19, 2025
af318e8
Merge branch 'main' into fee-controller-script
EndymionJkb Feb 20, 2025
b62843d
refactor: migratePools is now permissionless
EndymionJkb Feb 20, 2025
1510a2d
Merge branch 'main' into fee-controller-script
EndymionJkb Feb 21, 2025
9eff06c
temp: to accommodate fork tests, allow the fee controller to be set l…
EndymionJkb Feb 21, 2025
2086a6b
refactor: reduce to a single migration script
EndymionJkb Feb 22, 2025
7a33dd2
Merge branch 'protocol-fee-controller-v3' into fee-controller-script
EndymionJkb Feb 22, 2025
940dcaa
docs: clarify comments
EndymionJkb Feb 22, 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
1 change: 1 addition & 0 deletions pkg/governance-scripts/.solhintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
contracts/test/
17 changes: 17 additions & 0 deletions pkg/governance-scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# <img src="../../logo.svg" alt="Balancer" height="128px">

# Balancer V3 Governance Scripts

In order to manage the Balancer protocol, Balancer DAO must sometimes execute somewhat complex sets of actions which if executed incorrectly could result in governance losing control of key powers over the protocol, opening up vulnerabilities by granting powerful permissions improperly, etc.

In order to prevent this, complex governance actions may be enacted through script contracts. These have a number of benefits over performing actions directly through the multisig wallet.

- The contract can be easily tested prior to the execution on mainnet to ensure that it produces the correct result.
- It's much simpler to verify the behavior of Solidity code matches the proposal specification relative to a series of raw function calls.
- The contract may only be triggered once, ensuring that any powers granted to it can't be used in future for another purpose unilaterally.

This package contains the source code for these script contracts to form a record of major technical actions Balancer DAO has taken.

## Licensing

[GNU General Public License Version 3 (GPL v3)](../../LICENSE).
16 changes: 16 additions & 0 deletions pkg/governance-scripts/contracts/IBasicAuthorizer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

import { IAuthorizer } from "@balancer-labs/v3-interfaces/contracts/vault/IAuthorizer.sol";

interface IBasicAuthorizer is IAuthorizer {
// solhint-disable-next-line func-name-mixedcase
function DEFAULT_ADMIN_ROLE() external view returns (bytes32);

function grantRole(bytes32 role, address account) external;

function revokeRole(bytes32 role, address account) external;

function renounceRole(bytes32 role, address account) external;
}
153 changes: 153 additions & 0 deletions pkg/governance-scripts/contracts/ProtocolFeeControllerMigration.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 { IAuthentication } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IAuthentication.sol";
import { IProtocolFeeController } from "@balancer-labs/v3-interfaces/contracts/vault/IProtocolFeeController.sol";
import { PoolRoleAccounts, PoolConfig } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol";
import { IVaultAdmin } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultAdmin.sol";
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";

import { ProtocolFeeController } from "@balancer-labs/v3-vault/contracts/ProtocolFeeController.sol";
import {
ReentrancyGuardTransient
} from "@balancer-labs/v3-solidity-utils/contracts/openzeppelin/ReentrancyGuardTransient.sol";

import { IBasicAuthorizer } from "./IBasicAuthorizer.sol";

/**
* @notice Migrate from the original ProtocolFeeController to one with extra events.
* @dev These events enable tracking pool protocol fees under all circumstances (in particular, when protocol fees are
* initially turned off). It also adds some infrastructure that makes future migrations easier, and removes redundant
* poolCreator storage.
*
* This simple migration assumes:
* 1) There are no pools with pool creators
* 2) There are no pools with protocol fee exemptions or overrides
* 3) Migrating the complete list of pools can be done in a single transaction.
*
* These simplifications enable simply calling `migrateFeeController` once with the complete list of pools.
*
* After the migration, the Vault will point to the new fee controller, and any collection thereafter will go there.
* If there are any residual fee amounts in the old fee controller (i.e., that were collected but not withdrawn),
* governance will still need to withdraw from the old fee controller. Otherwise, no further interaction with the old
* controller is necessary.
*
* Associated with `20250221-protocol-fee-controller-migration`.
*/
contract ProtocolFeeControllerMigration is ReentrancyGuardTransient {
IProtocolFeeController public immutable oldFeeController;
IProtocolFeeController public immutable newFeeController;

IVault public immutable vault;

// IAuthorizer with interface for granting/revoking roles.
IBasicAuthorizer internal immutable _authorizer;

// Set when the operation is complete and all permissions have been renounced.
bool internal _finalized;

/**
* @notice Attempt to deploy this contract with invalid parameters.
* @dev ProtocolFeeController contracts return the address of the Vault they were deployed with. Ensure that both
* the old and new controllers reference the same vault.
*/
error InvalidFeeController();

/// @notice Migration can only be performed once.
error AlreadyMigrated();

constructor(IVault _vault, IProtocolFeeController _newFeeController) {
oldFeeController = _vault.getProtocolFeeController();

// Ensure valid fee controllers. Also ensure that we are not trying to operate on the current fee controller.
if (_newFeeController.vault() != _vault || _newFeeController == oldFeeController) {
revert InvalidFeeController();
}

vault = _vault;
newFeeController = _newFeeController;

_authorizer = IBasicAuthorizer(address(vault.getAuthorizer()));
}

/**
* @notice Permissionless migration function.
* @dev Call this with the full set of pools to perform the migration. After this runs, the Vault will point to the
* new fee controller, which will have a copy of all the relevant state from the old controller. Also, all
* permissions will be revoked, and the contract will be disabled.
*
* @param pools The complete set of pools to migrate
*/
function migrateFeeController(address[] memory pools) external virtual nonReentrant {
if (_finalized) {
revert AlreadyMigrated();
}

_finalized = true;

_migrateGlobalPercentages();

// This simple migration assumes that:
// 1) There are no pool creators, so no state related to pool creator fees (and no fees to be withdrawn).
// 2) There are no protocol fee exempt pools or governance overrides
// (i.e., all override flags are false, and all pool fees match current global values).
//
// At the end of this process, since there are no pool creators, token balances should all be zero, unless
// there are "left over" protocol fees that have been collected but not withdrawn. Governance would then
// still have to withdraw from the old fee controller.
//
// For future migrations, when we might have pool creator fees, the pool creators would still need to withdraw
// them from the old controller themselves.
for (uint256 i = 0; i < pools.length; ++i) {
address pool = pools[i];

// Set pool-specific values. This assumes there are no fee exempt pools or overrides.
newFeeController.updateProtocolSwapFeePercentage(pool);
newFeeController.updateProtocolYieldFeePercentage(pool);
}

// Update the fee controller in the Vault.
_migrateFeeController();

// Revoke all permissions.
_authorizer.renounceRole(_authorizer.DEFAULT_ADMIN_ROLE(), address(this));
}

function _migrateGlobalPercentages() internal {
// Grant global fee percentage permissions to set on new controller.
bytes32 swapFeeRole = IAuthentication(address(newFeeController)).getActionId(
IProtocolFeeController.setGlobalProtocolSwapFeePercentage.selector
);

bytes32 yieldFeeRole = IAuthentication(address(newFeeController)).getActionId(
IProtocolFeeController.setGlobalProtocolYieldFeePercentage.selector
);

_authorizer.grantRole(swapFeeRole, address(this));
_authorizer.grantRole(yieldFeeRole, address(this));

// Copy percentages to new controller.
uint256 globalSwapFeePercentage = oldFeeController.getGlobalProtocolSwapFeePercentage();
uint256 globalYieldFeePercentage = oldFeeController.getGlobalProtocolYieldFeePercentage();

newFeeController.setGlobalProtocolSwapFeePercentage(globalSwapFeePercentage);
newFeeController.setGlobalProtocolYieldFeePercentage(globalYieldFeePercentage);

// Revoke permissions.
_authorizer.renounceRole(swapFeeRole, address(this));
_authorizer.renounceRole(yieldFeeRole, address(this));
}

function _migrateFeeController() internal {
bytes32 setFeeControllerRole = IAuthentication(address(vault)).getActionId(
IVaultAdmin.setProtocolFeeController.selector
);

_authorizer.grantRole(setFeeControllerRole, address(this));

vault.setProtocolFeeController(newFeeController);

_authorizer.renounceRole(setFeeControllerRole, address(this));
}
}
111 changes: 111 additions & 0 deletions pkg/governance-scripts/contracts/ProtocolFeeControllerMigrationV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

import { IAuthentication } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IAuthentication.sol";
import { IProtocolFeeController } from "@balancer-labs/v3-interfaces/contracts/vault/IProtocolFeeController.sol";
import { PoolRoleAccounts, PoolConfig } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol";
import { IVaultAdmin } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultAdmin.sol";
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";

import { SingletonAuthentication } from "@balancer-labs/v3-vault/contracts/SingletonAuthentication.sol";
import { ProtocolFeeController } from "@balancer-labs/v3-vault/contracts/ProtocolFeeController.sol";

import { ProtocolFeeControllerMigration } from "./ProtocolFeeControllerMigration.sol";
import { IBasicAuthorizer } from "./IBasicAuthorizer.sol";

/**
* @notice Migrate from the original ProtocolFeeController to one with extra events.
* @dev These events enable tracking pool protocol fees under all circumstances (in particular, when protocol fees are
* initially turned off).
*
* After deployment, call `migratePools` as many times as necessary. The list must be generated externally, as pools
* are not iterable on-chain. The batch interface allows an unlimited number of pools to be migrated; it's possible
* there might be too many to migrate in a single call.
*
* The first time `migratePools` is called, the contract will first copy the global (pool-independent data). This could
* be done in a separate stage, but we're trying to keep the contract simple, vs. duplicating the staging coordinator
* system of v2 just yet.
*
* When all pools have been migrated, call `finalizeMigration` to disable further migration, update the address in the
* Vault, and renounce all permissions. While `migratePools` is permissionless, this call must be permissioned to
* prevent premature termination in case multiple transactions are required to migrate all the pools.
*
* Associated with `20250221-protocol-fee-controller-migration` (fork test only).
*/
contract ProtocolFeeControllerMigrationV2 is ProtocolFeeControllerMigration, SingletonAuthentication {
// Set after the global percentages have been transferred (on the first call to `migratePools`).
bool internal _globalPercentagesMigrated;

// ActionId for permission required in `migratePools`.
bytes32 internal _migrationRole;

/// @notice Cannot call the base contract migration; it is invalid for this migration.
error WrongMigrationVersion();

constructor(
IVault _vault,
IProtocolFeeController _newFeeController
) ProtocolFeeControllerMigration(_vault, _newFeeController) SingletonAuthentication(_vault) {
_migrationRole = IAuthentication(address(newFeeController)).getActionId(
ProtocolFeeController.migratePool.selector
);

// Grant permission required in `migratePools`.
_authorizer.grantRole(_migrationRole, address(this));
}

/**
* @notice Migrate pools from the old fee controller to the new one.
* @dev THis can be called multiple times, if there are too many pools for a single transaction. Note that the
* first time this is called, it will migrate the global fee percentages, then proceed with the first set of pools.
*
* @param pools The set of pools to be migrated in this call
*/
function migratePools(address[] memory pools) external nonReentrant {
if (_finalized) {
revert AlreadyMigrated();
}

// Migrate the global percentages only once, before the first set of pools.
if (_globalPercentagesMigrated == false) {
_globalPercentagesMigrated = true;

_migrateGlobalPercentages();
}

// This more complex migration allows for pool creators and overrides, and uses the new features in the second
// deployment of the `ProtocolFeeController`.
//
// At the end of this process, governance must still withdraw any leftover protocol fees from the old
// controller (i.e., that have been collected but not withdrawn). Pool creators likewise would still need to
// withdraw any leftover pool creator fees from the old controller.
for (uint256 i = 0; i < pools.length; ++i) {
// This function is not in the public interface.
ProtocolFeeController(address(newFeeController)).migratePool(pools[i]);
}
}

function finalizeMigration() external authenticate {
if (_finalized) {
revert AlreadyMigrated();
}

_finalized = true;

// Remove permission to migrate pools.
_authorizer.renounceRole(_migrationRole, address(this));

// Update the fee controller in the Vault.
_migrateFeeController();

// Revoke all permissions.
_authorizer.renounceRole(_authorizer.DEFAULT_ADMIN_ROLE(), address(this));
}

/// @inheritdoc ProtocolFeeControllerMigration
function migrateFeeController(address[] memory) external pure override {
// The one-step migration does not work in this version, with pool creators and overrides.
revert WrongMigrationVersion();
}
}
48 changes: 48 additions & 0 deletions pkg/governance-scripts/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
[profile.default]
src = 'contracts'
out = 'forge-artifacts'
libs = ['node_modules']
test = 'test/foundry'
cache_path = 'forge-cache'
allow_paths = ['../', '../../node_modules/']
ffi = true
fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}]
remappings = [
'vault/=../vault/',
'pool-weighted/=../pool-weighted/',
'solidity-utils/=../solidity-utils/',
'ds-test/=../../node_modules/forge-std/lib/ds-test/src/',
'forge-std/=../../node_modules/forge-std/src/',
'@openzeppelin/=../../node_modules/@openzeppelin/',
'permit2/=../../node_modules/permit2/',
'@balancer-labs/=../../node_modules/@balancer-labs/',
'forge-gas-snapshot/=../../node_modules/forge-gas-snapshot/src/'
]
optimizer = true
optimizer_runs = 999
solc_version = '0.8.26'
auto_detect_solc = false
evm_version = 'cancun'
ignored_error_codes = [2394, 5574, 3860] # Transient storage, code size
allow_internal_expect_revert = true

[fuzz]
runs = 10000
max_test_rejects = 60000

[profile.forkfuzz.fuzz]
runs = 1000
max_test_rejects = 60000

[profile.coverage.fuzz]
runs = 100
max_test_rejects = 60000

[profile.intense.fuzz]
verbosity = 3
runs = 100000
max_test_rejects = 600000

[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
sepolia = "${SEPOLIA_RPC_URL}"
20 changes: 20 additions & 0 deletions pkg/governance-scripts/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HardhatUserConfig } from 'hardhat/config';
import { hardhatBaseConfig } from '@balancer-labs/v3-common';

import '@nomicfoundation/hardhat-toolbox';
import '@nomicfoundation/hardhat-ethers';
import '@typechain/hardhat';

import 'hardhat-ignore-warnings';
import 'hardhat-gas-reporter';
import 'hardhat-contract-sizer';
import { warnings } from '@balancer-labs/v3-common/hardhat-base-config';

const config: HardhatUserConfig = {
solidity: {
compilers: hardhatBaseConfig.compilers,
},
warnings,
};

export default config;
Loading
Loading