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

Dynamic fee setter (WeightedPool) #2558

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
21 changes: 21 additions & 0 deletions pkg/interfaces/contracts/pool-weighted/WeightedPoolUserData.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,25 @@ library WeightedPoolUserData {
{
(, amountsOut, maxBPTAmountIn) = abi.decode(self, (ExitKind, uint256[], uint256));
}

// function related to custom fee
function tokenInForExactBptOutCustomFee(bytes memory self) internal pure returns (uint256 customFee) {
(, , , customFee) = abi.decode(self, (JoinKind, uint256, uint256, uint256));
}

function exactTokensInForBptOutCustomFee(bytes memory self) internal pure returns (uint256 customFee) {
(, , , customFee) = abi.decode(self, (JoinKind, uint256[], uint256, uint256));
}

function exactBptInForTokenOutCustomFee(bytes memory self) internal pure returns (uint256 customFee) {
(, , , customFee) = abi.decode(self, (ExitKind, uint256, uint256, uint256));
}

function bptInForExactTokensOutCustomFee(bytes memory self) internal pure returns (uint256 customFee) {
(, , , customFee) = abi.decode(self, (ExitKind, uint256[], uint256, uint256));
}

function swapCustomFee(bytes memory self) internal pure returns (uint256 customFee) {
(customFee) = abi.decode(self, (uint256));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ library Errors {
uint256 internal constant SAFE_CAST_VALUE_CANT_FIT_UINT64 = 442;
uint256 internal constant UNHANDLED_FEE_TYPE = 443;
uint256 internal constant BURN_FROM_ZERO = 444;
uint256 internal constant INVALID_INPUT_ADDRESS = 445;
uint256 internal constant INVALID_OPERATION_TYPE = 446;

// Vault
uint256 internal constant INVALID_POOL_ID = 500;
Expand Down
3 changes: 3 additions & 0 deletions pkg/interfaces/contracts/vault/IBasePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ interface IBasePool is IPoolSwapStructs {
* Contracts implementing this function should check that the caller is indeed the Vault before performing any
* state-changing operations, such as minting pool shares.
*/
// enum for transaction type
enum OperationType { JOIN, EXIT, SWAP }

function onJoinPool(
bytes32 poolId,
address sender,
Expand Down
4 changes: 2 additions & 2 deletions pkg/pool-utils/contracts/BaseGeneralPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ abstract contract BaseGeneralPool is IGeneralPool, BasePool {
uint256[] memory scalingFactors
) internal virtual returns (uint256) {
// Fees are subtracted before scaling, to reduce the complexity of the rounding direction analysis.
swapRequest.amount = _subtractSwapFeeAmount(swapRequest.amount);
swapRequest.amount = _subtractSwapFeeAmount(swapRequest.amount, getSwapFeePercentage());

_upscaleArray(balances, scalingFactors);
swapRequest.amount = _upscale(swapRequest.amount, scalingFactors[indexIn]);
Expand All @@ -81,7 +81,7 @@ abstract contract BaseGeneralPool is IGeneralPool, BasePool {
amountIn = _downscaleUp(amountIn, scalingFactors[indexIn]);

// Fees are added after scaling happens, to reduce the complexity of the rounding direction analysis.
return _addSwapFeeAmount(amountIn);
return _addSwapFeeAmount(amountIn, getSwapFeePercentage());
}

/*
Expand Down
6 changes: 4 additions & 2 deletions pkg/pool-utils/contracts/BaseMinimalSwapInfoPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ abstract contract BaseMinimalSwapInfoPool is IMinimalSwapInfoPool, BasePool {
uint256 balanceTokenOut
) public override onlyVault(request.poolId) returns (uint256) {
_beforeSwapJoinExit();
uint256 _fee = getSwapFeePercentage(request.userData, OperationType.SWAP);
emit SwapFeePercentageChanged(_fee);

uint256 scalingFactorTokenIn = _scalingFactor(request.tokenIn);
uint256 scalingFactorTokenOut = _scalingFactor(request.tokenOut);
Expand All @@ -44,7 +46,7 @@ abstract contract BaseMinimalSwapInfoPool is IMinimalSwapInfoPool, BasePool {

if (request.kind == IVault.SwapKind.GIVEN_IN) {
// Fees are subtracted before scaling, to reduce the complexity of the rounding direction analysis.
request.amount = _subtractSwapFeeAmount(request.amount);
request.amount = _subtractSwapFeeAmount(request.amount, _fee);

// All token amounts are upscaled.
request.amount = _upscale(request.amount, scalingFactorTokenIn);
Expand All @@ -63,7 +65,7 @@ abstract contract BaseMinimalSwapInfoPool is IMinimalSwapInfoPool, BasePool {
amountIn = _downscaleUp(amountIn, scalingFactorTokenIn);

// Fees are added after scaling happens, to reduce the complexity of the rounding direction analysis.
return _addSwapFeeAmount(amountIn);
return _addSwapFeeAmount(amountIn, _fee);
}
}

Expand Down
14 changes: 10 additions & 4 deletions pkg/pool-utils/contracts/BasePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ abstract contract BasePool is
return _miscData.decodeUint(_SWAP_FEE_PERCENTAGE_OFFSET, _SWAP_FEE_PERCENTAGE_BIT_LENGTH);
}

// overloaded method implementation
function getSwapFeePercentage(bytes memory, OperationType) public view virtual returns (uint256) {
// override the function as per the need in the derived classes
return getSwapFeePercentage();
}

/**
* @notice Return the ProtocolFeesCollector contract.
* @dev This is immutable, and retrieved from the Vault on construction. (It is also immutable in the Vault.)
Expand Down Expand Up @@ -592,17 +598,17 @@ abstract contract BasePool is
/**
* @dev Adds swap fee amount to `amount`, returning a higher value.
*/
function _addSwapFeeAmount(uint256 amount) internal view returns (uint256) {
function _addSwapFeeAmount(uint256 amount, uint256 fee) internal pure returns (uint256) {
// This returns amount + fee amount, so we round up (favoring a higher fee amount).
return amount.divUp(getSwapFeePercentage().complement());
return amount.divUp(fee.complement());
}

/**
* @dev Subtracts swap fee amount from `amount`, returning a lower value.
*/
function _subtractSwapFeeAmount(uint256 amount) internal view returns (uint256) {
function _subtractSwapFeeAmount(uint256 amount, uint256 fee) internal pure returns (uint256) {
// This returns amount - fee amount, so we round up (favoring a higher fee amount).
uint256 feeAmount = amount.mulUp(getSwapFeePercentage());
uint256 feeAmount = amount.mulUp(fee);
return amount.sub(feeAmount);
}

Expand Down
6 changes: 3 additions & 3 deletions pkg/pool-utils/test/BasePool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ describe('BasePool', function () {
const swapFeePercentage = fp(0.003);
const pool = await deployBasePool({ swapFeePercentage });

expect(await pool.getSwapFeePercentage()).to.equal(swapFeePercentage);
expect(await pool['getSwapFeePercentage()']()).to.equal(swapFeePercentage);
});
});

Expand All @@ -169,7 +169,7 @@ describe('BasePool', function () {
it('can change the swap fee', async () => {
await pool.connect(sender).setSwapFeePercentage(newSwapFeePercentage);

expect(await pool.getSwapFeePercentage()).to.equal(newSwapFeePercentage);
expect(await pool['getSwapFeePercentage()']()).to.equal(newSwapFeePercentage);
});

it('emits an event', async () => {
Expand Down Expand Up @@ -815,7 +815,7 @@ describe('BasePool', function () {
});

it('stores the swap fee pct in the most-significant 64 bits', async () => {
expect(await pool.getSwapFeePercentage()).to.equal(swapFeePercentage);
expect(await pool['getSwapFeePercentage()']()).to.equal(swapFeePercentage);

const swapFeeHex = swapFeePercentage.toHexString().slice(2); // remove 0x
const expectedMiscData = swapFeeHex.padStart(16, '0').padEnd(64, '0'); // pad first 8 bytes and fill with zeros
Expand Down
72 changes: 60 additions & 12 deletions pkg/pool-weighted/contracts/BaseWeightedPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,28 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {

(uint256 preJoinExitSupply, uint256 preJoinExitInvariant) = _beforeJoinExit(balances, normalizedWeights);

uint256 swapFeePercentage = getSwapFeePercentage(userData, OperationType.JOIN);
(uint256 bptAmountOut, uint256[] memory amountsIn) = _doJoin(
sender,
balances,
normalizedWeights,
scalingFactors,
preJoinExitSupply,
swapFeePercentage,
userData
);

// _doJoin performs actions specific to type of join
// but it's a view function so can not emit event

WeightedPoolUserData.JoinKind kind = userData.joinKind();
if (
kind == WeightedPoolUserData.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT ||
kind == WeightedPoolUserData.JoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT
) {
emit SwapFeePercentageChanged(swapFeePercentage);
}

_afterJoinExit(
preJoinExitInvariant,
balances,
Expand All @@ -248,14 +261,23 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
uint256[] memory normalizedWeights,
uint256[] memory scalingFactors,
uint256 totalSupply,
uint256 swapFeePercentage,
bytes memory userData
) internal view virtual returns (uint256, uint256[] memory) {
WeightedPoolUserData.JoinKind kind = userData.joinKind();

if (kind == WeightedPoolUserData.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT) {
return _joinExactTokensInForBPTOut(balances, normalizedWeights, scalingFactors, totalSupply, userData);
return
_joinExactTokensInForBPTOut(
balances,
normalizedWeights,
scalingFactors,
totalSupply,
swapFeePercentage,
userData
);
} else if (kind == WeightedPoolUserData.JoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT) {
return _joinTokenInForExactBPTOut(balances, normalizedWeights, totalSupply, userData);
return _joinTokenInForExactBPTOut(balances, normalizedWeights, totalSupply, swapFeePercentage, userData);
} else if (kind == WeightedPoolUserData.JoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT) {
return _joinAllTokensInForExactBPTOut(balances, totalSupply, userData);
} else {
Expand All @@ -268,8 +290,9 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
uint256[] memory normalizedWeights,
uint256[] memory scalingFactors,
uint256 totalSupply,
uint256 swapFeePercentage,
bytes memory userData
) private view returns (uint256, uint256[] memory) {
) private pure returns (uint256, uint256[] memory) {
(uint256[] memory amountsIn, uint256 minBPTAmountOut) = userData.exactTokensInForBptOut();
InputHelpers.ensureInputLengthMatch(balances.length, amountsIn.length);

Expand All @@ -280,7 +303,7 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
normalizedWeights,
amountsIn,
totalSupply,
getSwapFeePercentage()
swapFeePercentage
);

_require(bptAmountOut >= minBPTAmountOut, Errors.BPT_OUT_MIN_AMOUNT);
Expand All @@ -292,8 +315,9 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
uint256[] memory balances,
uint256[] memory normalizedWeights,
uint256 totalSupply,
uint256 swapFeePercentage,
bytes memory userData
) private view returns (uint256, uint256[] memory) {
) private pure returns (uint256, uint256[] memory) {
(uint256 bptAmountOut, uint256 tokenIndex) = userData.tokenInForExactBptOut();
// Note that there is no maximum amountIn parameter: this is handled by `IVault.joinPool`.

Expand All @@ -304,7 +328,7 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
normalizedWeights[tokenIndex],
bptAmountOut,
totalSupply,
getSwapFeePercentage()
swapFeePercentage
);

// We join in a single token, so we initialize amountsIn with zeros
Expand Down Expand Up @@ -344,15 +368,28 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {

(uint256 preJoinExitSupply, uint256 preJoinExitInvariant) = _beforeJoinExit(balances, normalizedWeights);

uint256 swapFeePercentage = getSwapFeePercentage(userData, OperationType.EXIT);
(uint256 bptAmountIn, uint256[] memory amountsOut) = _doExit(
sender,
balances,
normalizedWeights,
scalingFactors,
preJoinExitSupply,
swapFeePercentage,
userData
);

// _doExit performs actions specific to type of exit
// but it's a view function so can not emit event

WeightedPoolUserData.ExitKind kind = userData.exitKind();
if (
kind == WeightedPoolUserData.ExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT ||
kind == WeightedPoolUserData.ExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT
) {
emit SwapFeePercentageChanged(swapFeePercentage);
}

_afterJoinExit(
preJoinExitInvariant,
balances,
Expand All @@ -376,16 +413,25 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
uint256[] memory normalizedWeights,
uint256[] memory scalingFactors,
uint256 totalSupply,
uint256 swapFeePercentage,
bytes memory userData
) internal view virtual returns (uint256, uint256[] memory) {
WeightedPoolUserData.ExitKind kind = userData.exitKind();

if (kind == WeightedPoolUserData.ExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT) {
return _exitExactBPTInForTokenOut(balances, normalizedWeights, totalSupply, userData);
return _exitExactBPTInForTokenOut(balances, normalizedWeights, totalSupply, swapFeePercentage, userData);
} else if (kind == WeightedPoolUserData.ExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT) {
return _exitExactBPTInForTokensOut(balances, totalSupply, userData);
} else if (kind == WeightedPoolUserData.ExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT) {
return _exitBPTInForExactTokensOut(balances, normalizedWeights, scalingFactors, totalSupply, userData);
return
_exitBPTInForExactTokensOut(
balances,
normalizedWeights,
scalingFactors,
totalSupply,
swapFeePercentage,
userData
);
} else {
_revert(Errors.UNHANDLED_EXIT_KIND);
}
Expand All @@ -395,8 +441,9 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
uint256[] memory balances,
uint256[] memory normalizedWeights,
uint256 totalSupply,
uint256 swapFeePercentage,
bytes memory userData
) private view returns (uint256, uint256[] memory) {
) private pure returns (uint256, uint256[] memory) {
(uint256 bptAmountIn, uint256 tokenIndex) = userData.exactBptInForTokenOut();
// Note that there is no minimum amountOut parameter: this is handled by `IVault.exitPool`.

Expand All @@ -407,7 +454,7 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
normalizedWeights[tokenIndex],
bptAmountIn,
totalSupply,
getSwapFeePercentage()
swapFeePercentage
);

// This is an exceptional situation in which the fee is charged on a token out instead of a token in.
Expand Down Expand Up @@ -436,8 +483,9 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
uint256[] memory normalizedWeights,
uint256[] memory scalingFactors,
uint256 totalSupply,
uint256 swapFeePercentage,
bytes memory userData
) private view returns (uint256, uint256[] memory) {
) private pure returns (uint256, uint256[] memory) {
(uint256[] memory amountsOut, uint256 maxBPTAmountIn) = userData.bptInForExactTokensOut();
InputHelpers.ensureInputLengthMatch(amountsOut.length, balances.length);
_upscaleArray(amountsOut, scalingFactors);
Expand All @@ -448,7 +496,7 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
normalizedWeights,
amountsOut,
totalSupply,
getSwapFeePercentage()
swapFeePercentage
);
_require(bptAmountIn <= maxBPTAmountIn, Errors.BPT_IN_MAX_AMOUNT);

Expand Down
52 changes: 52 additions & 0 deletions pkg/pool-weighted/contracts/CustomFeeAuthorizer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;

import "@balancer-labs/v2-interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol";

contract CustomFeeAuthorizer {
event FeeSetterAdded(address indexed feeSetter);
event FeeSetterRemoved(address indexed feeSetter);

mapping(address => bool) private _isCustomFeeSetter;
bool private immutable _isCustomFeeEnabled;
address public solver;

modifier onlySolver() {
_require(solver == msg.sender, Errors.SENDER_NOT_ALLOWED);
_;
}

constructor(bool isCustomFeeEnabled) {
_isCustomFeeEnabled = isCustomFeeEnabled;
}

function isCustomFeeEnabled() public view returns (bool) {
return _isCustomFeeEnabled;
}

function canSetCustomFee(address setterAddress) public view returns (bool isAuthorized) {
if (_isCustomFeeEnabled) {
isAuthorized = (_isCustomFeeSetter[setterAddress] || solver == setterAddress);
} else {
isAuthorized = false;
}
}

function addCustomFeeSetter(address newCustomFeeSetter) public onlySolver {
_require(newCustomFeeSetter != address(0), Errors.INVALID_INPUT_ADDRESS);
_require(_isCustomFeeEnabled, Errors.FEATURE_DISABLED);
_isCustomFeeSetter[newCustomFeeSetter] = true;
emit FeeSetterAdded(newCustomFeeSetter);
}

function removeCustomFeeSetter(address customFeeSetter) public onlySolver {
_require(customFeeSetter != msg.sender, Errors.SENDER_NOT_ALLOWED);
_isCustomFeeSetter[customFeeSetter] = false;
emit FeeSetterRemoved(customFeeSetter);
}

function _setSolverAddress(address _solver) internal {
_require(_solver != address(0), Errors.INVALID_INPUT_ADDRESS);
solver = _solver;
}
}
Loading
Loading