Inside Balancer Contracts — A composable symphony (2)

0xSkly
Balancer Protocol
Published in
5 min readMay 4, 2023

I gotta admit, the first article of this series was maybe not the most action packed. We cruised through some parent contracts without going into much detail, looked at some interfaces and read some comments. It might be a bit boring, but it gives us some context, and more importantly some sense of progress. For my brain, that’s very important. It hates being confronted by too much complexity it cannot comprehend. That makes it feel stupid, and brains don’t like that, do they? So you gotta feed it complexity in pieces and give it some low hanging fruit in between for a sense of accomplishment. But no more talk about brains, thats Josey’s specialty.

Let’s find an entry point to dive in. We’ll look for something interesting but not so complex that it burns us out. But honestly, that might be impossible. I see some simpler flows like a regular join but we’ll leave that for later. So at the risk of going mad right away, let’s look into how we join via swapping with _swapGivenIn. This function is called by the onSwap function of the parent BaseGeneralPool.

First the _beforeSwapJoinExit() hook is called which is implemented by the ComposableStablePool

Here we call back up into the BasePool which is the parent contract of the BaseGeneralPool. So we call down to the child which then calls up into the grandparent, or parent of the initial calling contract. The BasePool just checks if the pool has not been paused (and if so, that it is not in recovery mode) and then we update the cached token rates because they influence the scaling factors. Hold on, caching influences scaling factors? That sounds like something we shouldn’t gloss over. So let’s open the ComposableStablePoolRates contract and look into this _cacheTokenRatesIfNecessary. Interestingly there is not too much there, we just iterate over each token and call this thing

We call into each tokens’ TokenRateProvider contract to update the rate. Not sure how this effects the scaling factors but I have a feeling that we’ll find out soon. We head back to the onSwap function where we’ve now checked that the pool is not paused and have updated any stale token rates. Next we validate that the token indexes are actually in range and get the scaling factors by calling _scalingFactors. It took me a bit to find out where this thing is implemented. Guess what, it’s also in the ComposableStablePoolRates contract. Not where I really expected it. But I mean, the comments told us they are affected by those token rates. Now let’s resolve this mystery.

Now things are starting to make sense. The scaling factor is adjusted by the underlying token rate. This basically means we are now dealing, technically speaking, with the underlying token. Good thing we didn’t skip this! With that out of the way, we return to the onSwap function where we call down to _swapGivenIn or _swapGivenOut depending on the swap request.

Now, after all the detours, we can start looking into what we’re here for, how do we join via swapping.

We start with a routing function which checks if the tokenIn is the BPT of this pool. If that’s the case (in our case it is), we call into _swapWithBpt or otherwise into the regular swap flow which doesn’t concern us for the moment. But, before we can move on, we should pay attention to the comment provided with this function.

/**
* If this is a swap involving BPT, call `_swapWithBpt`, which computes
* the amountOut using the swapFeePercentage and charges protocol fees,
* in the same manner as single token join/exits.
* Otherwise, perform the default processing for a regular swap.
*/

So, we pay swap fees on a join? I mean technically we are swapping, but conceptually we are joining. Sounds a bit strange to me but we’ll see. Let’s keep it in the back of our heads while we move on to the end boss of this article.

Like with most end boss battles, this one has multiple stages and I’m not sure we can survive them all in one go. But, let’s see how far we can make it on the first try.

We want to join with a given token value, so isGivenIn is true. Now we upscale the registeredBalances, which are the total token balances in this pool which reside in the vault, with the scaling factors. This means we normalize for the same decimals and also apply the token rates. Then we do the same with the provided swap amount, or in our case the amount we want to join with. So far we’re cruisin’, but here comes the first attack, _beforeJoinExit. The return values give a hint on what lays ahead of us. It doesn’t look pretty.

I can already hear my brain complaining, whining about those crazy words it never heard of. “Seriously, oldAmpPreJoinExitInvariant? What is this madness? Can’t we think about some easy stuff? How about a new color theme for our editor?”

At this point I know we are in dangerous territory to burn out here and it’s time to take some rest. It’s gonna be a tough ride, but eventually we will prevail!

Website | Twitter | Discord

This article is for informational and educational purposes only. It should not be construed as investment or trading advice or a solicitation or recommendation to buy, sell, or hold any digital assets. Transactions on the blockchain are speculative. Carefully consider and accept all risks before taking action.

--

--