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

Add helper to set protocol fees by factory #897

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
8ade135
checkpoint
EndymionJkb Aug 17, 2024
37e7d85
Merge branch 'main' into factory-fee
EndymionJkb Aug 17, 2024
11e2a7e
Merge branch 'main' into factory-fee
EndymionJkb Aug 17, 2024
d354182
checkpoint - most general
EndymionJkb Aug 17, 2024
963b158
refactor: add interface, and simplify (remove completely generic pool…
EndymionJkb Aug 18, 2024
507dfb5
lint
EndymionJkb Aug 18, 2024
68b65e2
test: add tests for new fee controller getter
EndymionJkb Aug 18, 2024
b2089b6
test: add percentages provider tests
EndymionJkb Aug 18, 2024
d384e0e
docs: clarify permission requirements
EndymionJkb Aug 18, 2024
0814c98
refactor: use constants in test instead of hard-coding
EndymionJkb Aug 18, 2024
9e9a6f2
refactor: remove unnecessary permission set
EndymionJkb Aug 18, 2024
4b2d40d
refactor: add event
EndymionJkb Aug 19, 2024
e428ffd
Merge branch 'main' into factory-fee
EndymionJkb Aug 23, 2024
1301cd4
Merge branch 'main' into factory-fee
EndymionJkb Aug 23, 2024
b1552f0
Merge branch 'main' into factory-fee
EndymionJkb Aug 26, 2024
c4bcf2c
Merge branch 'main' into factory-fee
EndymionJkb Aug 27, 2024
b0b09f5
Merge branch 'main' into factory-fee
EndymionJkb Aug 28, 2024
557b97d
chore: update gas
EndymionJkb Aug 28, 2024
9310d7e
Merge branch 'main' into factory-fee
EndymionJkb Aug 28, 2024
7e44de5
Merge branch 'main' into factory-fee
EndymionJkb Aug 28, 2024
13658d0
Merge branch 'main' into factory-fee
EndymionJkb Aug 28, 2024
d559356
Merge branch 'main' into factory-fee
EndymionJkb Aug 30, 2024
227a52e
Merge branch 'main' into factory-fee
EndymionJkb Aug 30, 2024
0dd8cbf
Merge branch 'main' into factory-fee
EndymionJkb Sep 1, 2024
2ba62a0
Merge branch 'main' into factory-fee
EndymionJkb Sep 2, 2024
f99fb6e
Merge branch 'main' into factory-fee
EndymionJkb Sep 2, 2024
5ce27b3
Merge branch 'main' into factory-fee
EndymionJkb Sep 2, 2024
e4c32d4
fix: import
EndymionJkb Sep 2, 2024
a621374
Merge branch 'main' into factory-fee
EndymionJkb Sep 4, 2024
81da628
refactor: expose the precision check
EndymionJkb Sep 4, 2024
fa46b99
feat: validate precision in percentages provider
EndymionJkb Sep 4, 2024
77591d5
Merge branch 'main' into factory-fee
EndymionJkb Sep 4, 2024
a089892
Merge branch 'main' into factory-fee
EndymionJkb Sep 12, 2024
8e99571
Merge branch 'main' into factory-fee
EndymionJkb Sep 12, 2024
e3be8ee
Merge branch 'main' into factory-fee
EndymionJkb Sep 16, 2024
b2556d4
Merge branch 'main' into factory-fee
EndymionJkb Sep 18, 2024
d243d8c
Merge branch 'main' into factory-fee
EndymionJkb Sep 19, 2024
18f1a51
Merge branch 'main' into factory-fee
EndymionJkb Sep 20, 2024
d02643f
Merge branch 'main' into factory-fee
EndymionJkb Sep 23, 2024
2def1d8
Merge branch 'main' into factory-fee
EndymionJkb Sep 24, 2024
6b0a96c
Merge branch 'main' into factory-fee
EndymionJkb Sep 24, 2024
2a78301
Merge branch 'main' into factory-fee
EndymionJkb Sep 25, 2024
01f9464
Merge branch 'main' into factory-fee
EndymionJkb Sep 27, 2024
0c62abc
Merge branch 'main' into factory-fee
EndymionJkb Sep 27, 2024
048182c
Merge branch 'main' into factory-fee
EndymionJkb Oct 1, 2024
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
7 changes: 7 additions & 0 deletions pkg/interfaces/contracts/vault/IProtocolFeeController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ interface IProtocolFeeController {
/// @dev Returns the main Vault address.
function vault() external view returns (IVault);

/**
* @dev Return the maximum swap and yield protocol fee percentages.
* @return maxProtocolSwapFeePercentage The maximum protocol swap fee percentage
* @return maxProtocolYieldFeePercentage The maximum protocol yield fee percentage
*/
function getMaximumProtocolFeePercentages() external pure returns (uint256, uint256);

/// @dev Collects aggregate fees from the Vault for a given pool.
function collectAggregateFees(address pool) external;

Expand Down
76 changes: 76 additions & 0 deletions pkg/interfaces/contracts/vault/IProtocolFeePercentagesProvider.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

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

interface IProtocolFeePercentagesProvider {
/**
* @notice `setFactorySpecificProtocolFeePercentages` has not been called for this factory address.
* @dev This error can by thrown by `getFactorySpecificProtocolFeePercentages` or
* `setProtocolFeePercentagesForPools`, as both require that valid fee percentages have been set.
*
* @param factory The unregistered factory address
*/
error FactoryNotRegistered(address factory);

/**
* @notice The factory address provided is not a valid `IBasePoolFactory`.
* @dev This means it responds incorrectly to `isPoolFromFactory` (e.g., always responds true). If it doesn't
* implement `isPoolFromFactory` or isn't a contract at all, calls on `setFactorySpecificProtocolFeePercentages`
* will revert with no data.
*
* @param factory The address of the invalid factory
*/
error InvalidFactory(address factory);

/**
* @notice The given pool is not from the expected factory.
* @dev Occurs when one of the pools supplied to `setProtocolFeePercentagesForPools` is not from the given factory.
* @param pool The address of the unrecognized pool
* @param factory The address of the factory
*/
error PoolNotFromFactory(address pool, address factory);

/**
* @notice Get the address of the `ProtocolFeeController` used to set fees.
* @return protocolFeeController The address of the fee controller
*/
function getProtocolFeeController() external view returns (IProtocolFeeController);

/**
* @notice Query the protocol fee percentages for a given factory.
* @param factory The address of the factory
* @return protocolSwapFeePercentage The protocol swap fee percentage set for that factory
* @return protocolYieldFeePercentage The protocol yield fee percentage set for that factory
*/
function getFactorySpecificProtocolFeePercentages(
address factory
) external view returns (uint256 protocolSwapFeePercentage, uint256 protocolYieldFeePercentage);

/**
* @notice Assign intended protocol fee percentages for a given factory.
* @dev This is a permissioned call. After the fee percentages have been set, and governance has granted
* this contract permission to set fee percentages on pools, anyone can call `setProtocolFeePercentagesForPools`
* to update the fee percentages on a set of pools from that factory.
*
* @param factory The address of the factory
* @param protocolSwapFeePercentage The new protocol swap fee percentage
* @param protocolYieldFeePercentage The new protocol yield fee percentage
*/
function setFactorySpecificProtocolFeePercentages(
address factory,
uint256 protocolSwapFeePercentage,
uint256 protocolYieldFeePercentage
) external;

/**
* @notice Update the protocol fees for a set of pools from a given factory.
* @dev This call is permissionless. Anyone can update the fee percentages, once they're set by governance.
* Note that goverance must also grant this contract permmission to set protocol fee percentages on pools.
*
* @param factory The address of the factory
* @param pools The pools whose fees will be set according to `setFactorySpecificProtocolFeePercentages`
*/
function setProtocolFeePercentagesForPools(address factory, address[] memory pools) external;
}
7 changes: 7 additions & 0 deletions pkg/vault/contracts/ProtocolFeeController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ contract ProtocolFeeController is
bool isOverride;
}

// Note that the `ProtocolFeePercentagesProvider` assumes the maximum fee bounds are constant.

// Maximum protocol swap fee percentage. FixedPoint.ONE corresponds to a 100% fee.
uint256 internal constant _MAX_PROTOCOL_SWAP_FEE_PERCENTAGE = 50e16; // 50%

Expand Down Expand Up @@ -161,6 +163,11 @@ contract ProtocolFeeController is
return _vault;
}

/// @inheritdoc IProtocolFeeController
function getMaximumProtocolFeePercentages() external pure returns (uint256, uint256) {
return (_MAX_PROTOCOL_SWAP_FEE_PERCENTAGE, _MAX_PROTOCOL_YIELD_FEE_PERCENTAGE);
}

/// @inheritdoc IProtocolFeeController
function collectAggregateFees(address pool) public {
_vault.unlock(abi.encodeWithSelector(ProtocolFeeController.collectAggregateFeesHook.selector, pool));
Expand Down
139 changes: 139 additions & 0 deletions pkg/vault/contracts/ProtocolFeePercentagesProvider.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";

import {
IProtocolFeePercentagesProvider
} from "@balancer-labs/v3-interfaces/contracts/vault/IProtocolFeePercentagesProvider.sol";
import { IProtocolFeeController } from "@balancer-labs/v3-interfaces/contracts/vault/IProtocolFeeController.sol";
import { IBasePoolFactory } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePoolFactory.sol";
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";

import {
SingletonAuthentication
} from "@balancer-labs/v3-solidity-utils/contracts/helpers/SingletonAuthentication.sol";

contract ProtocolFeePercentagesProvider is IProtocolFeePercentagesProvider, SingletonAuthentication {
using SafeCast for uint256;

/// @notice The protocol fee controller was configured with an incorrect Vault address.
error WrongProtocolFeeControllerDeployment();

/**
* @dev Data structure to store default protocol fees by factory. Fee percentages are 18-decimal floating point
* numbers, so we know they fit in 64 bits, allowing the fees to be stored in a single slot.
*
* @param protocolSwapFee The protocol swap fee
* @param protocolYieldFee The protocol yield fee
* @param isFactoryRegistered Flag indicating fees have been set (allows zero values)
*/
struct FactoryProtocolFees {
uint64 protocolSwapFeePercentage;
uint64 protocolYieldFeePercentage;
bool isFactoryRegistered;
}

IProtocolFeeController private immutable _protocolFeeController;

uint256 private immutable _maxProtocolSwapFeePercentage;
uint256 private immutable _maxProtocolYieldFeePercentage;

// Factory address => FactoryProtocolFees
mapping(IBasePoolFactory => FactoryProtocolFees) private _factoryDefaultFeePercentages;

constructor(IVault vault, IProtocolFeeController protocolFeeController) SingletonAuthentication(vault) {
_protocolFeeController = protocolFeeController;

if (protocolFeeController.vault() != vault) {
revert WrongProtocolFeeControllerDeployment();
}

// These values are constant in the `ProtocolFeeController`.
(_maxProtocolSwapFeePercentage, _maxProtocolYieldFeePercentage) = protocolFeeController
.getMaximumProtocolFeePercentages();
}

/// @inheritdoc IProtocolFeePercentagesProvider
function getProtocolFeeController() external view returns (IProtocolFeeController) {
return _protocolFeeController;
}

/// @inheritdoc IProtocolFeePercentagesProvider
function getFactorySpecificProtocolFeePercentages(
address factory
) external view returns (uint256 protocolSwapFeePercentage, uint256 protocolYieldFeePercentage) {
FactoryProtocolFees memory factoryFees = _getValidatedProtocolFees(factory);

protocolSwapFeePercentage = factoryFees.protocolSwapFeePercentage;
protocolYieldFeePercentage = factoryFees.protocolYieldFeePercentage;
}

/// @inheritdoc IProtocolFeePercentagesProvider
function setFactorySpecificProtocolFeePercentages(
address factory,
uint256 protocolSwapFeePercentage,
uint256 protocolYieldFeePercentage
) external authenticate {
// Validate the fee percentages; don't store values that the `ProtocolFeeCollector` will reject.
if (protocolSwapFeePercentage > _maxProtocolSwapFeePercentage) {
revert IProtocolFeeController.ProtocolSwapFeePercentageTooHigh();
}

if (protocolYieldFeePercentage > _maxProtocolYieldFeePercentage) {
revert IProtocolFeeController.ProtocolYieldFeePercentageTooHigh();
}

// Best effort check that `factory` is the address of an IBasePoolFactory.
bool poolFromFactory = IBasePoolFactory(factory).isPoolFromFactory(address(0));
if (poolFromFactory) {
revert InvalidFactory(factory);
}

// Store the default fee percentages, and mark the factory as registered.
_factoryDefaultFeePercentages[IBasePoolFactory(factory)] = FactoryProtocolFees({
protocolSwapFeePercentage: protocolSwapFeePercentage.toUint64(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Technically don't need SafeCast here, as I've validated the range above; could just cast it. But we don't care too much about gas here.

protocolYieldFeePercentage: protocolYieldFeePercentage.toUint64(),
isFactoryRegistered: true
});
}

/// @inheritdoc IProtocolFeePercentagesProvider
function setProtocolFeePercentagesForPools(address factory, address[] memory pools) external {
FactoryProtocolFees memory factoryFees = _getValidatedProtocolFees(factory);

for (uint256 i = 0; i < pools.length; ++i) {
address currentPool = pools[i];

if (IBasePoolFactory(factory).isPoolFromFactory(currentPool) == false) {
revert PoolNotFromFactory(currentPool, factory);
}

_setPoolProtocolFees(
currentPool,
factoryFees.protocolSwapFeePercentage,
factoryFees.protocolYieldFeePercentage
);
}
}

function _getValidatedProtocolFees(address factory) private view returns (FactoryProtocolFees memory factoryFees) {
factoryFees = _factoryDefaultFeePercentages[IBasePoolFactory(factory)];

if (factoryFees.isFactoryRegistered == false) {
revert FactoryNotRegistered(factory);
}
}

// These are permissioned functions on `ProtocolFeeController`, so governance will need to allow this contract
// to call `setProtocolSwapFeePercentage` and `setProtocolYieldFeePercentage`.
function _setPoolProtocolFees(
address pool,
uint256 protocolSwapFeePercentage,
uint256 protocolYieldFeePercentage
) private {
_protocolFeeController.setProtocolSwapFeePercentage(pool, protocolSwapFeePercentage);
_protocolFeeController.setProtocolYieldFeePercentage(pool, protocolYieldFeePercentage);
}
}
4 changes: 4 additions & 0 deletions pkg/vault/contracts/test/PoolFactoryMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ contract PoolFactoryMock is IBasePoolFactory, SingletonAuthentication, FactoryWi
);
}

function manualSetPoolFromFactory(address pool) external {
_isPoolFromFactory[pool] = true;
}

function _getDefaultLiquidityManagement() private pure returns (LiquidityManagement memory) {
LiquidityManagement memory liquidityManagement;
liquidityManagement.enableAddLiquidityCustom = true;
Expand Down
8 changes: 8 additions & 0 deletions pkg/vault/test/foundry/ProtocolFeeController.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ contract ProtocolFeeControllerTest is BaseVaultTest {
assertEq(feeAmounts[1], 0, "Collected creator fee amount [1] is non-zero");
}

function testGetMaximumProtocolFeePercentages() public view {
(uint256 maxSwapFeePercentage, uint256 maxYieldFeePercentage) = feeController
.getMaximumProtocolFeePercentages();

assertEq(maxSwapFeePercentage, MAX_PROTOCOL_SWAP_FEE_PCT, "Wrong maximum swap fee percentage");
assertEq(maxYieldFeePercentage, MAX_PROTOCOL_YIELD_FEE_PCT, "Wrong maximum yield fee percentage");
}

function testSetGlobalProtocolSwapFeePercentageRange() public {
authorizer.grantRole(
feeControllerAuth.getActionId(IProtocolFeeController.setGlobalProtocolSwapFeePercentage.selector),
Expand Down
Loading
Loading