diff --git a/pkg/interfaces/contracts/vault/IProtocolFeeController.sol b/pkg/interfaces/contracts/vault/IProtocolFeeController.sol index 5abd6ce59..ef4ff1c06 100644 --- a/pkg/interfaces/contracts/vault/IProtocolFeeController.sol +++ b/pkg/interfaces/contracts/vault/IProtocolFeeController.sol @@ -171,6 +171,13 @@ interface IProtocolFeeController { */ function vault() external view returns (IVault); + /** + * @notice 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); + /** * @notice Collects aggregate fees from the Vault for a given pool. * @param pool The pool with aggregate fees @@ -290,6 +297,13 @@ interface IProtocolFeeController { */ function updateProtocolYieldFeePercentage(address pool) external; + /** + * @notice Ensure the proposed fee can be stored in the Vault without precision loss. + * @dev Fees are stored with 24 bit precision. The function will revert with `FeePrecisionTooHigh` if invalid. + * @param feePercentage The percentage to be checked + */ + function ensureValidPrecision(uint256 feePercentage) external pure; + /*************************************************************************** Permissioned Functions ***************************************************************************/ diff --git a/pkg/interfaces/contracts/vault/IProtocolFeePercentagesProvider.sol b/pkg/interfaces/contracts/vault/IProtocolFeePercentagesProvider.sol new file mode 100644 index 000000000..72d3f7bde --- /dev/null +++ b/pkg/interfaces/contracts/vault/IProtocolFeePercentagesProvider.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IProtocolFeeController } from "./IProtocolFeeController.sol"; + +interface IProtocolFeePercentagesProvider { + /** + * @notice Protocol fee percentages have been set for the given factory. + * @param factory The pool factory + * @param protocolSwapFeePercentage The protocol swap fee percentage intended for pools from this factory + * @param protocolYieldFeePercentage The protocol yield fee percentage intended for pools from this factory + */ + event FactorySpecificProtocolFeePercentagesSet( + address indexed factory, + uint256 protocolSwapFeePercentage, + uint256 protocolYieldFeePercentage + ); + + /// @notice The protocol fee controller was configured with an incorrect Vault address. + error WrongProtocolFeeControllerDeployment(); + + /** + * @notice Fees can only be set on recognized factories (i.e., registered in the `BalancerContractRegistry`). + * @param factory The address of the unknown factory + */ + error UnknownFactory(address factory); + + /** + * @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. + * You need to set the factory fees before you can apply them to pools from that factory. + * + * @param factory The factory address where fees have not been set + */ + error FactoryFeesNotSet(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 governance must also grant this contract permission 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; +} diff --git a/pkg/pool-weighted/test/gas/.hardhat-snapshots/[WeightedPool - Standard - BatchRouter] add liquidity unbalanced using swapExactIn - warm slots b/pkg/pool-weighted/test/gas/.hardhat-snapshots/[WeightedPool - Standard - BatchRouter] add liquidity unbalanced using swapExactIn - warm slots index afdce9710..bfb6c3c2f 100644 --- a/pkg/pool-weighted/test/gas/.hardhat-snapshots/[WeightedPool - Standard - BatchRouter] add liquidity unbalanced using swapExactIn - warm slots +++ b/pkg/pool-weighted/test/gas/.hardhat-snapshots/[WeightedPool - Standard - BatchRouter] add liquidity unbalanced using swapExactIn - warm slots @@ -1 +1 @@ -189.4k \ No newline at end of file +189.3k \ No newline at end of file diff --git a/pkg/pool-weighted/test/gas/.hardhat-snapshots/[WeightedPool - WithRate] remove liquidity proportional b/pkg/pool-weighted/test/gas/.hardhat-snapshots/[WeightedPool - WithRate] remove liquidity proportional index 01db6fe9f..c4f48aed1 100644 --- a/pkg/pool-weighted/test/gas/.hardhat-snapshots/[WeightedPool - WithRate] remove liquidity proportional +++ b/pkg/pool-weighted/test/gas/.hardhat-snapshots/[WeightedPool - WithRate] remove liquidity proportional @@ -1 +1 @@ -222.2k \ No newline at end of file +222.1k \ No newline at end of file diff --git a/pkg/vault/contracts/ProtocolFeeController.sol b/pkg/vault/contracts/ProtocolFeeController.sol index b3e92c35c..b947155ab 100644 --- a/pkg/vault/contracts/ProtocolFeeController.sol +++ b/pkg/vault/contracts/ProtocolFeeController.sol @@ -86,6 +86,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 public constant MAX_PROTOCOL_SWAP_FEE_PERCENTAGE = 50e16; // 50% @@ -145,7 +147,7 @@ contract ProtocolFeeController is if (newSwapFeePercentage > MAX_PROTOCOL_SWAP_FEE_PERCENTAGE) { revert ProtocolSwapFeePercentageTooHigh(); } - _ensureValidPrecision(newSwapFeePercentage); + ensureValidPrecision(newSwapFeePercentage); _; } @@ -154,7 +156,7 @@ contract ProtocolFeeController is if (newYieldFeePercentage > MAX_PROTOCOL_YIELD_FEE_PERCENTAGE) { revert ProtocolYieldFeePercentageTooHigh(); } - _ensureValidPrecision(newYieldFeePercentage); + ensureValidPrecision(newYieldFeePercentage); _; } @@ -185,6 +187,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.encodeCall(ProtocolFeeController.collectAggregateFeesHook, pool)); @@ -641,6 +648,18 @@ contract ProtocolFeeController is _withdrawPoolCreatorFees(pool, _getPoolCreator(pool)); } + /// @inheritdoc IProtocolFeeController + function ensureValidPrecision(uint256 feePercentage) public pure { + // Primary fee percentages are 18-decimal values, stored here in 64 bits, and calculated with full 256-bit + // precision. However, the resulting aggregate fees are stored in the Vault with 24-bit precision, which + // corresponds to 0.00001% resolution (i.e., a fee can be 1%, 1.00001%, 1.00002%, but not 1.000005%). + // Ensure there will be no precision loss in the Vault - which would lead to a discrepancy between the + // aggregate fee calculated here and that stored in the Vault. + if ((feePercentage / FEE_SCALING_FACTOR) * FEE_SCALING_FACTOR != feePercentage) { + revert IVaultErrors.FeePrecisionTooHigh(); + } + } + function _withdrawPoolCreatorFees(address pool, address recipient) private { (IERC20[] memory poolTokens, uint256 numTokens) = _getPoolTokensAndCount(pool); @@ -693,15 +712,4 @@ contract ProtocolFeeController is emit ProtocolYieldFeePercentageChanged(pool, newProtocolYieldFeePercentage); } - - function _ensureValidPrecision(uint256 feePercentage) private pure { - // Primary fee percentages are 18-decimal values, stored here in 64 bits, and calculated with full 256-bit - // precision. However, the resulting aggregate fees are stored in the Vault with 24-bit precision, which - // corresponds to 0.00001% resolution (i.e., a fee can be 1%, 1.00001%, 1.00002%, but not 1.000005%). - // Ensure there will be no precision loss in the Vault - which would lead to a discrepancy between the - // aggregate fee calculated here and that stored in the Vault. - if ((feePercentage / FEE_SCALING_FACTOR) * FEE_SCALING_FACTOR != feePercentage) { - revert IVaultErrors.FeePrecisionTooHigh(); - } - } } diff --git a/pkg/vault/contracts/ProtocolFeePercentagesProvider.sol b/pkg/vault/contracts/ProtocolFeePercentagesProvider.sol new file mode 100644 index 000000000..caf6ebd1f --- /dev/null +++ b/pkg/vault/contracts/ProtocolFeePercentagesProvider.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import { IProtocolFeeController } from "@balancer-labs/v3-interfaces/contracts/vault/IProtocolFeeController.sol"; +import { IBasePoolFactory } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePoolFactory.sol"; +import { + IProtocolFeePercentagesProvider +} from "@balancer-labs/v3-interfaces/contracts/vault/IProtocolFeePercentagesProvider.sol"; +import { + IBalancerContractRegistry, + ContractType +} from "@balancer-labs/v3-interfaces/contracts/standalone-utils/IBalancerContractRegistry.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; + +import { SingletonAuthentication } from "./SingletonAuthentication.sol"; + +contract ProtocolFeePercentagesProvider is IProtocolFeePercentagesProvider, SingletonAuthentication { + using SafeCast for uint256; + + /** + * @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 areFactoryFeesSet Flag indicating fees have been set (allows zero values) + */ + struct FactoryProtocolFees { + uint64 protocolSwapFeePercentage; + uint64 protocolYieldFeePercentage; + bool areFactoryFeesSet; + } + + IBalancerContractRegistry private immutable _trustedContractRegistry; + 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, + IBalancerContractRegistry trustedContractRegistry + ) SingletonAuthentication(vault) { + _protocolFeeController = protocolFeeController; + _trustedContractRegistry = trustedContractRegistry; + + 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(); + } + + // Ensure precision checks will pass. + _protocolFeeController.ensureValidPrecision(protocolSwapFeePercentage); + _protocolFeeController.ensureValidPrecision(protocolYieldFeePercentage); + + // Ensure the factory is valid. + if (_trustedContractRegistry.isActiveBalancerContract(ContractType.POOL_FACTORY, factory) == false) { + revert UnknownFactory(factory); + } + + // Store the default fee percentages, and mark the factory as registered. + _factoryDefaultFeePercentages[IBasePoolFactory(factory)] = FactoryProtocolFees({ + protocolSwapFeePercentage: protocolSwapFeePercentage.toUint64(), + protocolYieldFeePercentage: protocolYieldFeePercentage.toUint64(), + areFactoryFeesSet: true + }); + + emit FactorySpecificProtocolFeePercentagesSet(factory, protocolSwapFeePercentage, protocolYieldFeePercentage); + } + + /// @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.areFactoryFeesSet == false) { + revert FactoryFeesNotSet(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); + } +} diff --git a/pkg/vault/contracts/test/PoolFactoryMock.sol b/pkg/vault/contracts/test/PoolFactoryMock.sol index e13f38ebb..597882b37 100644 --- a/pkg/vault/contracts/test/PoolFactoryMock.sol +++ b/pkg/vault/contracts/test/PoolFactoryMock.sol @@ -168,6 +168,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; diff --git a/pkg/vault/test/foundry/ProtocolFeeController.t.sol b/pkg/vault/test/foundry/ProtocolFeeController.t.sol index 4fe88578d..17d247f4b 100644 --- a/pkg/vault/test/foundry/ProtocolFeeController.t.sol +++ b/pkg/vault/test/foundry/ProtocolFeeController.t.sol @@ -77,6 +77,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), diff --git a/pkg/vault/test/foundry/ProtocolFeePercentagesProvider.t.sol b/pkg/vault/test/foundry/ProtocolFeePercentagesProvider.t.sol new file mode 100644 index 000000000..d883ab6f5 --- /dev/null +++ b/pkg/vault/test/foundry/ProtocolFeePercentagesProvider.t.sol @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +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 { IVaultErrors } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultErrors.sol"; +import { IPoolInfo } from "@balancer-labs/v3-interfaces/contracts/pool-utils/IPoolInfo.sol"; +import { + IProtocolFeePercentagesProvider +} from "@balancer-labs/v3-interfaces/contracts/vault/IProtocolFeePercentagesProvider.sol"; +import { + IBalancerContractRegistry, + ContractType +} from "@balancer-labs/v3-interfaces/contracts/standalone-utils/IBalancerContractRegistry.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; + +import { BalancerContractRegistry } from "@balancer-labs/v3-standalone-utils/contracts/BalancerContractRegistry.sol"; + +import { ProtocolFeePercentagesProvider } from "../../contracts/ProtocolFeePercentagesProvider.sol"; +import { PoolFactoryMock } from "../../contracts/test/PoolFactoryMock.sol"; + +import { BaseVaultTest } from "./utils/BaseVaultTest.sol"; + +contract ProtocolFeePercentagesProviderTest is BaseVaultTest { + address internal constant INVALID_ADDRESS = address(0x1234); + + IProtocolFeePercentagesProvider internal percentagesProvider; + BalancerContractRegistry internal trustedContractRegistry; + + IAuthentication internal percentagesProviderAuth; + IAuthentication internal feeControllerAuth; + + uint256 internal maxSwapFeePercentage; + uint256 internal maxYieldFeePercentage; + + address[] internal pools; + + function setUp() public override { + BaseVaultTest.setUp(); + + trustedContractRegistry = new BalancerContractRegistry(vault); + percentagesProvider = new ProtocolFeePercentagesProvider(vault, feeController, trustedContractRegistry); + + // Mark the poolFactory as trusted, so that operations on it won't fail. + authorizer.grantRole( + trustedContractRegistry.getActionId(BalancerContractRegistry.registerBalancerContract.selector), + admin + ); + authorizer.grantRole( + trustedContractRegistry.getActionId(BalancerContractRegistry.deprecateBalancerContract.selector), + admin + ); + vm.prank(admin); + trustedContractRegistry.registerBalancerContract(ContractType.POOL_FACTORY, "MockFactory", poolFactory); + + percentagesProviderAuth = IAuthentication(address(percentagesProvider)); + feeControllerAuth = IAuthentication(address(feeController)); + + (maxSwapFeePercentage, maxYieldFeePercentage) = feeController.getMaximumProtocolFeePercentages(); + + // Ensure we aren't comparing to 0. + require(maxSwapFeePercentage > 0, "Zero swap fee percentage"); + require(maxYieldFeePercentage > 0, "Zero yield fee percentage"); + + pools = new address[](1); + pools[0] = pool; + } + + function testInvalidConstruction() public { + vm.expectRevert(IProtocolFeePercentagesProvider.WrongProtocolFeeControllerDeployment.selector); + new ProtocolFeePercentagesProvider(IVault(INVALID_ADDRESS), feeController, trustedContractRegistry); + } + + function testGetProtocolFeeController() public view { + assertEq( + address(percentagesProvider.getProtocolFeeController()), + address(feeController), + "Wrong protocol fee controller" + ); + } + + function testGetFactorySpecificProtocolFeePercentagesUnregisteredFactory() public { + vm.expectRevert( + abi.encodeWithSelector(IProtocolFeePercentagesProvider.FactoryFeesNotSet.selector, INVALID_ADDRESS) + ); + percentagesProvider.getFactorySpecificProtocolFeePercentages(INVALID_ADDRESS); + } + + function testSetFactorySpecificProtocolFeePercentageNoPermission() public { + vm.expectRevert(IAuthentication.SenderNotAllowed.selector); + percentagesProvider.setFactorySpecificProtocolFeePercentages( + poolFactory, + maxSwapFeePercentage, + maxYieldFeePercentage + ); + } + + function testRevertWhenSetFactorySpecificProtocolFeePercentageInvalidFactory() public { + _grantPermissions(); + + vm.expectRevert(); + vm.prank(admin); + percentagesProvider.setFactorySpecificProtocolFeePercentages( + INVALID_ADDRESS, + maxSwapFeePercentage, + maxYieldFeePercentage + ); + } + + function testSetFactorySpecificProtocolFeePercentageBadFactory() public { + _grantPermissions(); + + // Cause `isPoolFromFactory` to return "true" for address(0). + PoolFactoryMock(poolFactory).manualSetPoolFromFactory(address(0)); + + vm.prank(admin); + trustedContractRegistry.deprecateBalancerContract(poolFactory); + + vm.expectRevert(abi.encodeWithSelector(IProtocolFeePercentagesProvider.UnknownFactory.selector, poolFactory)); + vm.prank(admin); + percentagesProvider.setFactorySpecificProtocolFeePercentages( + poolFactory, + maxSwapFeePercentage, + maxYieldFeePercentage + ); + } + + function testSetFactorySpecificProtocolFeePercentageInvalidSwap() public { + _grantPermissions(); + + vm.expectRevert(IProtocolFeeController.ProtocolSwapFeePercentageTooHigh.selector); + vm.prank(admin); + percentagesProvider.setFactorySpecificProtocolFeePercentages( + poolFactory, + maxSwapFeePercentage + 1, + maxYieldFeePercentage + ); + } + + function testSetFactorySpecificProtocolFeePercentageHighPrecisionSwap() public { + _grantPermissions(); + + vm.expectRevert(IVaultErrors.FeePrecisionTooHigh.selector); + vm.prank(admin); + percentagesProvider.setFactorySpecificProtocolFeePercentages( + poolFactory, + 1e16 + 234234234, + maxYieldFeePercentage + ); + } + + function testSetFactorySpecificProtocolFeePercentageInvalidYield() public { + _grantPermissions(); + + vm.expectRevert(IProtocolFeeController.ProtocolYieldFeePercentageTooHigh.selector); + vm.prank(admin); + percentagesProvider.setFactorySpecificProtocolFeePercentages( + poolFactory, + maxSwapFeePercentage, + maxYieldFeePercentage + 1 + ); + } + + function testSetFactorySpecificProtocolFeePercentageHighPrecisionYield() public { + _grantPermissions(); + + vm.expectRevert(IProtocolFeeController.ProtocolYieldFeePercentageTooHigh.selector); + vm.prank(admin); + percentagesProvider.setFactorySpecificProtocolFeePercentages( + poolFactory, + 1e16 + 234234234, + maxYieldFeePercentage + 1 + ); + } + + function testSetFactorySpecificProtocolFeePercentages() public { + _grantPermissions(); + + // Ensure that they are different, so the test doesn't pass accidentally. + uint256 yieldFeePercentage = maxSwapFeePercentage / 2; + + vm.expectEmit(); + emit IProtocolFeePercentagesProvider.FactorySpecificProtocolFeePercentagesSet( + poolFactory, + maxSwapFeePercentage, + yieldFeePercentage + ); + + vm.prank(admin); + percentagesProvider.setFactorySpecificProtocolFeePercentages( + poolFactory, + maxSwapFeePercentage, + yieldFeePercentage + ); + + (uint256 actualSwapFeePercentage, uint256 actualYieldFeePercentage) = percentagesProvider + .getFactorySpecificProtocolFeePercentages(poolFactory); + assertEq(actualSwapFeePercentage, maxSwapFeePercentage, "Wrong factory swap fee percentage"); + assertEq(actualYieldFeePercentage, yieldFeePercentage, "Wrong factory swap fee percentage"); + } + + function testSetProtocolFeePercentagesForPoolsUnregisteredFactory() public { + vm.expectRevert( + abi.encodeWithSelector(IProtocolFeePercentagesProvider.FactoryFeesNotSet.selector, INVALID_ADDRESS) + ); + percentagesProvider.setProtocolFeePercentagesForPools(INVALID_ADDRESS, pools); + } + + function testSetProtocolFeePercentagesForPoolsUnknownPool() public { + _grantPermissions(); + + vm.prank(admin); + percentagesProvider.setFactorySpecificProtocolFeePercentages( + poolFactory, + maxSwapFeePercentage, + maxYieldFeePercentage + ); + + pools = new address[](2); + pools[0] = pool; + pools[1] = INVALID_ADDRESS; + + vm.expectRevert( + abi.encodeWithSelector( + IProtocolFeePercentagesProvider.PoolNotFromFactory.selector, + INVALID_ADDRESS, + poolFactory + ) + ); + + percentagesProvider.setProtocolFeePercentagesForPools(poolFactory, pools); + } + + function testSetProtocolFeePercentagesForPools() public { + _grantPermissions(); + + // Use random odd values to ensure we're setting them. + uint256 expectedSwapFeePercentage = 5.28e16; + uint256 expectedYieldFeePercentage = 3.14e16; + + vm.prank(admin); + percentagesProvider.setFactorySpecificProtocolFeePercentages( + poolFactory, + expectedSwapFeePercentage, + expectedYieldFeePercentage + ); + + // These should be zero initially. Since there is no pool creator here, the aggregate fees = protocol fees. + (uint256 originalSwapFeePercentage, uint256 originalYieldFeePercentage) = IPoolInfo(pool) + .getAggregateFeePercentages(); + assertEq(originalSwapFeePercentage, 0, "Non-zero original swap fee percentage"); + assertEq(originalYieldFeePercentage, 0, "Non-zero original yield fee percentage"); + + // Permissionless call to set fee percentages by factory. + percentagesProvider.setProtocolFeePercentagesForPools(poolFactory, pools); + + (uint256 currentSwapFeePercentage, uint256 currentYieldFeePercentage) = IPoolInfo(pool) + .getAggregateFeePercentages(); + assertEq(currentSwapFeePercentage, expectedSwapFeePercentage, "Non-zero original swap fee percentage"); + assertEq(currentYieldFeePercentage, expectedYieldFeePercentage, "Non-zero original yield fee percentage"); + } + + function _grantPermissions() private { + // Allow calling `setFactorySpecificProtocolFeePercentages` on the provider. + authorizer.grantRole( + percentagesProviderAuth.getActionId( + IProtocolFeePercentagesProvider.setFactorySpecificProtocolFeePercentages.selector + ), + admin + ); + + // Allow the provider to call the underlying functions on the fee controller. + authorizer.grantRole( + feeControllerAuth.getActionId(IProtocolFeeController.setProtocolSwapFeePercentage.selector), + address(percentagesProvider) + ); + authorizer.grantRole( + feeControllerAuth.getActionId(IProtocolFeeController.setProtocolYieldFeePercentage.selector), + address(percentagesProvider) + ); + } +}