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

More efficient dynamic fee setter #1

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 33 additions & 0 deletions pkg/interfaces/contracts/pool-weighted/WeightedPoolUserData.sol
Expand Up @@ -68,4 +68,37 @@ 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));
}
}
3 changes: 3 additions & 0 deletions pkg/interfaces/contracts/vault/IBasePool.sol
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 }
Copy link

Choose a reason for hiding this comment

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

@markusbkoch do we want this feature to apply to weighted pools, or to every pool type?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

every pool type


function onJoinPool(
bytes32 poolId,
address sender,
Expand Down
4 changes: 2 additions & 2 deletions pkg/pool-utils/contracts/BaseGeneralPool.sol
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
8 changes: 5 additions & 3 deletions pkg/pool-utils/contracts/BaseMinimalSwapInfoPool.sol
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 Expand Up @@ -99,4 +101,4 @@ abstract contract BaseMinimalSwapInfoPool is IMinimalSwapInfoPool, BasePool {
uint256 balanceTokenIn,
uint256 balanceTokenOut
) internal virtual returns (uint256);
}
}
18 changes: 13 additions & 5 deletions pkg/pool-utils/contracts/BasePool.sol
Expand Up @@ -173,6 +173,13 @@ 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 +599,18 @@ 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 Expand Up @@ -784,4 +792,4 @@ abstract contract BasePool is
}
}
}
}
}
26 changes: 22 additions & 4 deletions pkg/pool-weighted/contracts/BaseWeightedPool.sol
Expand Up @@ -225,6 +225,15 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
userData
);

// _doJoin performs actions specific to type of join
// but it's a view function so can not emit event
Comment on lines +228 to +229
Copy link

Choose a reason for hiding this comment

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

I'd consider making _doJoin non-view if needed. I'll give this some more thought.

Copy link
Owner

Choose a reason for hiding this comment

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

Have you thought about this? Should we make it a non-view function?

Copy link

Choose a reason for hiding this comment

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

After some more thought, I think it's fine as it is.


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(getSwapFeePercentage(userData, OperationType.JOIN));
Copy link

Choose a reason for hiding this comment

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

Same as above; we're getting the fee % inside _doJoin as well. Maybe it's better to just save it and pass it along.

Copy link
Owner

Choose a reason for hiding this comment

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

If I create an extra variable it gives stack too deep error.

}

_afterJoinExit(
preJoinExitInvariant,
balances,
Expand Down Expand Up @@ -280,7 +289,7 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
normalizedWeights,
amountsIn,
totalSupply,
getSwapFeePercentage()
getSwapFeePercentage(userData, OperationType.JOIN)
);

_require(bptAmountOut >= minBPTAmountOut, Errors.BPT_OUT_MIN_AMOUNT);
Expand All @@ -304,7 +313,7 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
normalizedWeights[tokenIndex],
bptAmountOut,
totalSupply,
getSwapFeePercentage()
getSwapFeePercentage(userData, OperationType.JOIN)
);

// We join in a single token, so we initialize amountsIn with zeros
Expand Down Expand Up @@ -353,6 +362,15 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
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(getSwapFeePercentage(userData, OperationType.EXIT));
Copy link

Choose a reason for hiding this comment

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

Same as above (store and pass)

}

_afterJoinExit(
preJoinExitInvariant,
balances,
Expand Down Expand Up @@ -407,7 +425,7 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
normalizedWeights[tokenIndex],
bptAmountIn,
totalSupply,
getSwapFeePercentage()
getSwapFeePercentage(userData, OperationType.EXIT)
);

// This is an exceptional situation in which the fee is charged on a token out instead of a token in.
Expand Down Expand Up @@ -448,7 +466,7 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool {
normalizedWeights,
amountsOut,
totalSupply,
getSwapFeePercentage()
getSwapFeePercentage(userData, OperationType.EXIT)
);
_require(bptAmountIn <= maxBPTAmountIn, Errors.BPT_IN_MAX_AMOUNT);

Expand Down
50 changes: 50 additions & 0 deletions pkg/pool-weighted/contracts/CustomFeeAuthorizer.sol
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;


contract CustomFeeAuthorizer {
event feeSetterAdded(address indexed feeSetter);
event feeSetterRemoved(address indexed feeSetter);

mapping(address => bool) private isCustomFeeSetter;
bool public isCustomFeeEnabled = false ;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Safe to say solvers will always want this to be true. If we could we make this immutable, does the code become more efficient?

Copy link
Owner

Choose a reason for hiding this comment

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

If we want to make isCustomFeeEnabled as immutable then we will have to initialize this in the constructor. But if we pass 1 more argument in the constructor of WeightedPool it gives stack to deep error.

address public solver;

function canSetCustomFee(address _setterAddress) public view returns(bool _isAuth){
if(isCustomFeeEnabled){
_isAuth = (isCustomFeeSetter[_setterAddress] || solver == _setterAddress) ;
}else{
_isAuth = false;
}
}

function addCustomFeeSetter(address _toAdd) onlySolver public {
require(_toAdd != address(0));
require(isCustomFeeEnabled,"Custom Fee Not Enabled");
isCustomFeeSetter[_toAdd] = true;
emit feeSetterAdded(_toAdd);
}

function removeCustomFeeSetter(address _toRemove) onlySolver public {
require(_toRemove != msg.sender);
isCustomFeeSetter[_toRemove] = false;
emit feeSetterRemoved(_toRemove);
}
Comment on lines +21 to +32
Copy link

Choose a reason for hiding this comment

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

@markusbkoch do we want governance to have power over this, or just the solver?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just the solver


function enableCustomFee() onlySolver() internal {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This needs to be public?

Copy link
Owner

Choose a reason for hiding this comment

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

If we want a toggle function for isCustomFeeEnabled then we can mark this function as public.

But if we can mark isCustomFeeEnabled as immutable then we will have to pass the value of isCustomFeeEnabled from WeightedPool constructor so we can remove this function.

require(!isCustomFeeEnabled, "Already Enabled");
isCustomFeeEnabled = true;
}

function _setSolverAddress(address _solver) internal {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Shouldn't this be public, onlySolver?

Copy link
Owner

Choose a reason for hiding this comment

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

if we can mark isCustomFeeEnabled as immutable then we can call this function in the constructor based on the value of isCustomFeeEnabled.
But if not then initially 0 address will be marked as solver so we cannot use onlySolver here. But we can have a logic where initially owner() can call this function and after that onlySolver can call that

Copy link

Choose a reason for hiding this comment

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

@markusbkoch who should set the solver? Governance, or is it fixed at pool creation time? Can the existing solver pass the torch to another address?

Copy link

Choose a reason for hiding this comment

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

I don't see any references to _setSolverAddress, so this needs to be worked out anyways.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@markusbkoch who should set the solver? Governance, or is it fixed at pool creation time? Can the existing solver pass the torch to another address?

Governance should not be able to set it. Fixed would be good enough, solver being able to pass it on would be a nice to have

require(_solver != address(0));
solver = _solver;
}

modifier onlySolver() {
require(solver == msg.sender,'CALLER_IS_NOT_SOLVER');
_;
}


}
51 changes: 50 additions & 1 deletion pkg/pool-weighted/contracts/WeightedPool.sol
Expand Up @@ -17,12 +17,14 @@ pragma experimental ABIEncoderV2;

import "./BaseWeightedPool.sol";
import "./WeightedPoolProtocolFees.sol";
import "./CustomFeeAuthorizer.sol";

/**
* @dev Basic Weighted Pool with immutable weights.
*/
contract WeightedPool is BaseWeightedPool, WeightedPoolProtocolFees {
contract WeightedPool is BaseWeightedPool, WeightedPoolProtocolFees, CustomFeeAuthorizer {
using FixedPoint for uint256;
using WeightedPoolUserData for bytes;

uint256 private constant _MAX_TOKENS = 8;

Expand Down Expand Up @@ -401,4 +403,51 @@ contract WeightedPool is BaseWeightedPool, WeightedPoolProtocolFees {
{
return super._isOwnerOnlyAction(actionId);
}

function getSwapFeePercentage(bytes memory userData,
OperationType _operation) public view virtual override returns (uint256 _fee) {
Comment on lines +407 to +408
Copy link

Choose a reason for hiding this comment

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

Alright, this doesn't seem to be a bad pattern. Assuming isCustomFeeEnabled is always true, then only those who can register the custom fee go through a different codepath when compared to the actual weighted pool.

// using tx.origin insted of msg.sender as these functions are called
// during join/exit/swap via vault
if(isCustomFeeEnabled && canSetCustomFee(tx.origin)){
if(_operation == OperationType.JOIN ){
WeightedPoolUserData.JoinKind kind = userData.joinKind();
if(kind == WeightedPoolUserData.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT){
_fee = userData.exactTokensInForBptOutCustomFee();
} else {
if(kind == WeightedPoolUserData.JoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT){
_fee = userData.tokenInForExactBptOutCustomFee();
}
}
}else{
if(_operation == OperationType.EXIT){
WeightedPoolUserData.ExitKind kind = userData.exitKind();
if(kind == WeightedPoolUserData.ExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT){
_fee = userData.exactBptInForTokenOutCustomFee();
} else {
if(kind == WeightedPoolUserData.ExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT){
_fee = userData.bptInForExactTokensOutCustomFee();
}
}
} else{
if(_operation == OperationType.SWAP){
_fee = userData.swapCustomFee();
}
}
}
}else{
_fee = getSwapFeePercentage();
}
}

function setSwapFeePercentage(uint256 swapFeePercentage) public virtual override whenNotPaused {
// here msg.sender is used as this function directlly be called by the admin
// not via any other contract
if (isCustomFeeEnabled) {
require(solver == msg.sender,'CALLER_IS_NOT_SOLVER');
_setSwapFeePercentage(swapFeePercentage);
} else {
super.setSwapFeePercentage(swapFeePercentage);
Copy link

Choose a reason for hiding this comment

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

If isCustomFeeEnabled is going to be immutable / true, that would break the capability of Governance of setting fees to any pools created from here.

Should Governance always be capable of setting the fee anyways? I'd consider using something like authenticateFor.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, governance should not be able to set the fess. The solver has certain expectations regarding this pool's fee, governance being able to set the fee could potentially rug the solver

}
}
// used if else so that it should not break the existing flow
}
13 changes: 12 additions & 1 deletion pkg/pool-weighted/hardhat.config.ts
Expand Up @@ -18,7 +18,18 @@ export default {
},
},
solidity: {
compilers: hardhatBaseConfig.compilers,
// compilers: hardhatBaseConfig.compilers,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is probably something we should leave as was to avoid introducing issues to the monorepo

compilers: [
{
version: '0.7.1',
settings: {
optimizer: {
enabled: true,
runs: 10,
},
},
},
],
overrides: { ...hardhatBaseConfig.overrides(name) },
},
warnings: hardhatBaseConfig.warnings,
Expand Down
Expand Up @@ -32,4 +32,4 @@ contract MockWeightedPool is WeightedPool {
function isOwnerOnlyAction(bytes32 actionId) external view returns (bool) {
return _isOwnerOnlyAction(actionId);
}
}
}