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

Conversation

markusbkoch
Copy link
Collaborator

Overrides getSwapFeePercentage so as to be able to pass userdata as an argument and dynamically set fees without having to write to storage before/after every swap.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Could we move all these files that have been renamed without changes back to their original locations so as not to clutter the PR?

@@ -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


import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/Ownable.sol";

contract CustomFeeAuthorizer is Ownable {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I worry this might conflict with Balancer's own access control mechanisms. I think it would make more sense instead to introduce the notion of address public solver; solver must be set on pool creation and can't be edited; solver can edit isCustomFeeSetter and call setSwapFeePercentage

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 && solver == msg.sender){
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We don't want governance to be able to set fees in this pool, so this needs to be something like

if (isCustomFeeEnabled) {
  require(solver == msg.sender,'CALLER_IS_NOT_SOLVER');
  _setSwapFeePercentage(swapFeePercentage);
} else {
  super.setSwapFeePercentage(swapFeePercentage);
}

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

emit feeSetterRemoved(_toRemove);
}

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.

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.

Copy link

@jubeira jubeira left a comment

Choose a reason for hiding this comment

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

First pass done!
I've left some minor comments, and I have some questions around how the solver is set / how governance would work. Also, the pool layers make it a bit difficult to reason just by seeing the diff, but is this feature intended to be used just for weighted pools, or for every pool type?

On top of that, it'd be good to see tests to cover the new functionality

Comment on lines 177 to 184
function getSwapFeePercentage(bytes memory userData,
OperationType _operation) public view virtual returns (uint256) {
// this code is just to avoid un-used variable warning.
// this will have no impact on the execution
// will remove this code in future
if(_operation == OperationType.SWAP){
userData = hex"00";
}
Copy link

Choose a reason for hiding this comment

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

This can be solved by removing the variable name from the declaration.

Copy link

Choose a reason for hiding this comment

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

Anyways I'd consider removing this from BasePool if this is a feature that will only affect WeightedPool.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Anyways I'd consider removing this from BasePool if this is a feature that will only affect WeightedPool.

We'd like this feature in other pools too

@@ -36,6 +36,8 @@ abstract contract BaseMinimalSwapInfoPool is IMinimalSwapInfoPool, BasePool {
) public override onlyVault(request.poolId) returns (uint256) {
_beforeSwapJoinExit();

emit SwapFeePercentageChanged(getSwapFeePercentage(request.userData, OperationType.SWAP));
Copy link

Choose a reason for hiding this comment

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

We're doing all the work here to get the right fee and emit the event, and doing it all over again inside _substractSwapFeeAmount. I'd save the value and pass it as an argument.

Comment on lines +228 to +229
// _doJoin performs actions specific to type of join
// but it's a view function so can not emit event
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.

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)

isCustomFeeEnabled = true;
}

function _setSolverAddress(address _solver) internal {
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?

isCustomFeeEnabled = true;
}

function _setSolverAddress(address _solver) internal {
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.

Comment on lines +407 to +408
function getSwapFeePercentage(bytes memory userData,
OperationType _operation) public view virtual override returns (uint256 _fee) {
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.

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

@@ -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

@srv-smn
Copy link
Owner

srv-smn commented Jan 7, 2024

Hey @jubeira,
If I pass any extra parameter in the constructor of WeightedPool contract it gives "stack too deep" error. so making isCustomFeeEnabled immutable will not be possible and can not call some internal function of CustomFeeAuthorizer inside the constructor based on the value of isCustomFeeEnabled. Rather we will have to make the some internal function public in CustomFeeAuthorizer.

@srv-smn
Copy link
Owner

srv-smn commented Jan 9, 2024

While running the test cases I was getting some some issues. Most of them is resolved. But 1 is remaining.
WeightedPoolFactory constructor arguments sets swap fee: TypeError: pool.getSwapFeePercentage is not a function
I think this is because I had overloaded getSwapFeePercentage function.
Any suggestion how can I resolve this ?

@jubeira
Copy link

jubeira commented Mar 5, 2024

@srv-smn I've addressed most of the comments on top of this branch and opened a PR here: balancer#2558.

Pending:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants