Skip to content

add swap fee minimizer and factory with tests#1577

Open
gerrrg wants to merge 4 commits intobalancer:mainfrom
gerrrg:swap-fee-minimize-contract
Open

add swap fee minimizer and factory with tests#1577
gerrrg wants to merge 4 commits intobalancer:mainfrom
gerrrg:swap-fee-minimize-contract

Conversation

@gerrrg
Copy link
Copy Markdown
Contributor

@gerrrg gerrrg commented Nov 2, 2025

Description

This PR adds a new contract, SwapFeeMinimizer (with accompanying factory), that allows the owner to swap out one specific outputToken with as low of a swap fee as possible, regardless of the nominal swap fee of the pool. All other operations with the pool are unchanged, and anyone (owner included) can swap with the nominal swap fees in the pool via a typical router. There are a few potential use cases for this functionality, though one notable use case is the ability for a DAO to perform token buybacks on a primary liquidity pool w/o needing to pay the typical swap fee.

Details

  • A new SwapFeeMinimizer is deployed atomically alongside a new WeightedPool
  • Swaps through a given SwapFeeMinimizer are constrained to its specific pool and outputToken
  • When an owner performs a swap, the contract stores the pool's current swap fee, then pool swap fees are set to _minimalSwapFee, the swap occurs, and then the fee is returned to its original level
  • Interfaces for swaps are identical to those of the router
  • A SwapFeeManager is registered as the pool's PoolRoleAccounts.swapFeeManager
  • The SwapFeeManager contract has a passthrough function for vault.setStaticSwapFeePercentage(...), though there is an argument to be made for removing this
  • SwapFeeMinimizer is Ownable2Step

Architectural Context

The initial plan was for this contract itself to be a router. Changing the architecture from an "is a" context to a "has a" context had significant improvements for simplicity and security, but they come with a few small concessions. With this setup, the SwapFeeMinimizer itself has to transfer tokens from the owner and be an intermediary with the router, which introduces some allowance handling and minor token accounting. It would have been nice to be able to inherit small parts of the broader router contract, but short of fundamentally rearchitecting the core Balancer router, this was impractical, and the realistic options here came down to:

  1. inherit the entire router and expand the scope and bytecode of this contract
  2. copy and paste the external swap functions and internal helpers I needed
  3. have this contract itself call the router's swap functions and handle token transfers from the owner

I went with Option 3 to keep the contract as small as practical, offering simplicity and minimized attack surface. The gas for a fee-minimized swap is a little higher, most notably due to the increased token transfers.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Dependency changes
  • Code refactor / cleanup
  • Optimization: [ ] gas / [ ] bytecode
  • Documentation or wording changes
  • Other

Checklist:

  • The diff is legible and has no extraneous changes
  • Complex code has been commented, including external interfaces
  • Tests have 100% code coverage
  • The base branch is either main, or there's a description of how to merge

Issue Resolution

cc: grants committee @mendesfabio

@joaobrunoah
Copy link
Copy Markdown
Collaborator

joaobrunoah commented Dec 2, 2025

Hey @gerrrg , one question about this PR: why did we opt for a router instead of a hook?
Looks like this feature could be implemented more easily with a hook that implements onComputeDynamicSwapFeePercentage (and it'd probably save some gas, because it wouldn't need to set the swap fee in the vault, so it's two less storage writes)

The idea would be to fetch the sender of a transaction from the router, which is passed through PoolSwapParams, and compare with the owner. If it's equal and the token out is the discounted token out, we return the minimized swap fee. Else, return the nominal swap fee.

This would allow the DAO, or the owner of the hook, to use any existing rpouter, including in batch swaps, to trade with the discounted swap fee in the pool. Does it make sense?

I know it's some rework, but the code size will be much shorter and easy to maintain, the gas costs of swaps will be smaller and it'll integrate better with all existing routers, which also seems like a better UX, so I didn't see a drawback. But, I may be missing a requirement or another detail, so let me know if it doesn't make sense.

@gerrrg
Copy link
Copy Markdown
Contributor Author

gerrrg commented Dec 2, 2025

Thanks for checking in @joaobrunoah! I completely agree that the intuitive implementation of this is for it to be a hook! In fact, that was what I suggested at first (and yes, implementing this as a hook would've been MUCH easier). The reason that the grants committee requested this be at the router level (feel free to correct me if I'm wrong @mendesfabio) was to avoid typical swaps by normal users going through this code path. This feature is intended to be used far less frequently than normal swaps are expected to be made through this pool, so the thinking was that it would be better to keep the "normal" swap path streamlined and gas minimized by not needing to make an external call to a hook contract on every transaction. Instead, the pool owner/DAO absorbs the slightly higher gas cost while gaining the benefits of the near-zero fees on their swap. The expectation is that the pool owner will use this only occasionally, and as such, they will likely be quite large relative to typical swap size, so the fee minimization will be quite meaningful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants