Skip to content

Add Liquidity Unbalanced Via Swap#747

Merged
brunoguerios merged 55 commits intomainfrom
unbalanced-add-Router
Feb 3, 2026
Merged

Add Liquidity Unbalanced Via Swap#747
brunoguerios merged 55 commits intomainfrom
unbalanced-add-Router

Conversation

@mkflow27
Copy link
Collaborator

@mkflow27 mkflow27 commented Dec 4, 2025

Closes #796

This PR proposes a solution for adding liquidity single sided to 2 token pools using the AddLiquidityUnbalancedViaSwapRouter.

The main problem to solve is that we want to allow users to simply input amounts in, but the router operation also expects an exactBptAmountOut.

In order to solve that, we'll need to calculate/estimate the exactBptAmountOut corresponding to the provided amounts in.

The implemented approach relies on an initial guess which is then adjusted/corrected iteratively based on how far they are from on-chain query results. A full description of each step can be found documented both in the codebase and also in this Notion doc (with illustrative example).

Integration tests have shown the approach is able to calculate/estimate BPT amounts that approximate the amounts in provided within a tolerance of 0.01% even for add liquidity amounts up to 60% of the balance of the pool.

// target from it.

const maxAjustableTokenAmount = amountsIn[adjustableTokenIndex];

Copy link
Member

Choose a reason for hiding this comment

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

This is where the core solution starts. I tried adding comments explaining how each step works, but I'll try to give more details here (and later write a more complete doc in Notion).

The whole goal is to find a bptAmount that will result in a maxAdjustableAmount close enough to the one provided by the user. Ideally approximating from below, so we favor leaving some dust instead of risking a revert transaction by requiring amounts bigger than what the user provided.

  1. start with an initial reference amount to calculate a proportional BPT. This initial guess is 50% of the maxAdjustableAmountIn provided by the user. The assumption is that ReClamms behave as a 50/50 Weighted pool and in those cases, the proportionalBptAmount calculated with 50% of the maxAdjustableAmountIn should result in a bptAmount pretty close to adding single-sided 100% of the maxAdustableAmountIn provided by the user.
    1.1 the problem is that ReClamms only behave as 50/50 Weigthed when centeredness = 1. The further the current price is from the center, the bigger the error we get on the approximation above. That error has to be compensated somehow. One important thing to note is that the calculated bptAmount will result in a smaller maxAdjustableAmountIn when centeredness decreases.
  2. we then query the add unbalanced with that bptAmount as input and get a maxAdjustableAmount as a result from it.
  3. we can then apply the ratio between maxAdjustableAmount and maxAdjustableAmountIn (in the opposite direction) to approximate the desired bptAmount (this is what I call correction factor)
  4. this actually overcorrects, resulting in a bptAmount that corresponds to a maxAdjustableAmount slightly greater that what the user provided
  5. we can then repeat the process to overcorrect in the opposite direction, resulting in a bptAmount that corresponds to a maxAdjustableAmount slightly smaller than what the user provided and that is much closer to the actual amount because the error decreases significantly on each step
  6. the existing tests already result in 0.004% error for an amountIn that is 60% of the pool balance, so 2 iterations seem to be fine (we'll need more tests to validate this)

I hope this makes it clear, but I'll spend more time polishing this next week ;)

Comment on lines +122 to +128
if (calculatedVsProvidedRatio > WAD) {
throw new SDKError(
'Error',
'Add Liquidity Unbalanced Via Swap',
'Exact BPT out calculation failed. Please add a smaller or less unbalanced liquidity amount.',
);
}
Copy link
Member

@brunoguerios brunoguerios Jan 28, 2026

Choose a reason for hiding this comment

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

This extra check should prevent scenarios where the approximation fails. I was able to force it to fail by adding liquidity above 60% the pool balance. I'd say it's safer to throw in such cases and ask the user to provide a smaller/less unbalanced amount in.

const WETH = TOKENS[chainId].WETH;

// Toggle to control whether test results should be logged to files
const ENABLE_LOGGING = false;
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if this logging is useful to keep?

Copy link
Member

Choose a reason for hiding this comment

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

It was super useful for debugging, but I agree it might to be useful long term. I'll remove that 👍

addLiquidityUnbalancedViaSwap.buildCall(buildCallInput);

expect(result).toHaveProperty('callData');
expect(result).toHaveProperty('to');
Copy link
Member

Choose a reason for hiding this comment

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

Should be able to test this is known contract address? Possible same with some other values?

Copy link
Member

@brunoguerios brunoguerios Feb 2, 2026

Choose a reason for hiding this comment

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

I refactored a bit this part of the test. Properties were already implicitly verified on all other tests, so I removed this section.

Contract address was indeed incorrect btw 👀
Apparently the attribute to from queryOutput is not used anywhere 😅
I think it's useful to keep it anyway, since it's relevant information to whoever is consuming that functionality from the SDK.

Copy link
Member

@johngrantuk johngrantuk left a comment

Choose a reason for hiding this comment

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

Few small comments otherwise looking good.

@@ -0,0 +1,212 @@
import { encodeFunctionData, maxUint256, parseEther, zeroAddress } from 'viem';
Copy link
Member

Choose a reason for hiding this comment

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

Noticed theres quite a few unused imports that don't seem to be picked up.

const output: AddLiquidityUnbalancedViaSwapQueryOutput = {
pool: poolState.address,
bptOut,
exactAmountIn: TokenAmount.fromRawAmount(
Copy link
Member

Choose a reason for hiding this comment

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

Does this need to be returned? Might be confusing to user. Instead we can just hardcode 0 amount in buildCall?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, the problem here is that buildCall requires exactToken address information and I don't have it available otherwise because buildCall doesn't expect poolState as input.
I believe the best way to provide that info is by returning exactTokenAmount in the queryOutput.
Let me know if you think there's a better approach.

balanceDeltas[expectedAdjustableToken.index === 0 ? 1 : 0];
const bptDelta = balanceDeltas[2];

// TODO: check value is within range for adjustableAmountIn provided
Copy link
Member

Choose a reason for hiding this comment

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

Still to do?

Copy link
Member

Choose a reason for hiding this comment

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

It was, but I just pushed the fix - thanks 🙏

Copy link
Member

@johngrantuk johngrantuk left a comment

Choose a reason for hiding this comment

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

Couple of small nits but otherwise LGTM. Thanks for getting it over the line!

@brunoguerios brunoguerios merged commit 8a88d09 into main Feb 3, 2026
5 checks passed
@brunoguerios brunoguerios deleted the unbalanced-add-Router branch February 3, 2026 17:52
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.

SDK - Add support for Add Liquidity Unbalanced Via Swap

3 participants