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

Refactoring addLiquidityUnbalancedNestedPool in CLR #1203

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

elshan-eth
Copy link
Contributor

@elshan-eth elshan-eth commented Dec 27, 2024

Description

This refactoring fixes implicit recursion by turning it into explicit recursion.

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

@elshan-eth elshan-eth requested review from joaobrunoah, EndymionJkb and jubeira and removed request for joaobrunoah and EndymionJkb December 27, 2024 19:21
Copy link
Contributor

@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. Just a few comments, otherwise makes sense.

Other than code specific comments, just a few generic comments:

  • I don't think we want to support nesting beyond 1 level. That makes me feel a bit reluctant to use recursion. The implementation here is rather simple, but we've had instances in the past where being too flexible ended up being counterproductive. I'd give it a second thought, maybe just using a function that goes down only one level instead of full blown recursion.
  • I get that the case described in the PR title is being covered. What I'd like to see is an explicit test where a pool has mixed tokens (e.g. child pool has sDAI and waUSDT) and the user is able to add liquidity using sDAI (wrapped) and USDT (underlying), which is the case we don't support today (in the current main we must add dai and usdt).

Comment on lines 562 to 564
} else if (_settledTokenAmounts().tGet(childToken) == 0) {
amountsIn[i] = _currentSwapTokenInAmounts().tGet(childToken);
_settledTokenAmounts().tSet(childToken, amountsIn[i]);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't fully get this last if clause. Could you please add some comments here? Why are we checking settled token amounts?

We are covering:

  • tokens that are pools (we keep adding recursively)
  • wrapper case: user wants to add using underlying (buffer is initialized, there is no wrapped amount specified)

I understand the missing case is user wants to add with wrapped tokens, but I think that's not necessarily the case when you get here. User could specify underlying, but if the buffer is not initialized you'll still end up here.

Sounds like the case where bufferInitialized == false and user specifies underlying should revert.

Copy link
Contributor

Choose a reason for hiding this comment

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

There are three cases being covered:

  • tokens that are pools (we keep adding recursively)
  • wrapper case: user wants to add using underlying (buffer is initialized, there is no wrapped amount specified)
  • Normal ERC20 tokens

If the buffer is not initialized, that token may or may not be ERC4626, we treat it as a normal ERC20 token.

The if is in there to make sure the token amount was not spent in a previous child pool. In the previous solution I modified the token in amounts directly, so the if was not needed, but then I needed to restore the token in amounts array at the end. This solution is better.

Copy link
Contributor

@joaobrunoah joaobrunoah left a comment

Choose a reason for hiding this comment

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

It mostly LGTM, but I'm worried too with the recursion. I mean, the Vault is safe, but the router can behave unpredictably if there's a cyclic path where the same pool appears multiple times. Maybe it reverts, maybe it adds zero tokens, but what happens with the transient accounting? I believe we'd need to check that case if we want to proceed with the unlimited recursion.

@elshan-eth
Copy link
Contributor Author

@jubeira @joaobrunoah

We use almost the same code inside these two functions (_getPoolAmountsIn and addLiquidityUnbalancedNestedPoolHook), so I think the recursion here will look more obvious. I will add recursion constraints to avoid such flexibility. What do you think about it?

@elshan-eth elshan-eth requested a review from jubeira January 6, 2025 15:03
Copy link
Collaborator

@EndymionJkb EndymionJkb left a comment

Choose a reason for hiding this comment

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

I'm not sure I have full context on all this, but tried to clarify some things in the comments.

}

IERC20[] memory parentPoolTokens = _vault.getPoolTokens(params.pool);
(uint256[] memory amountsIn, ) = _addLiquidityRecursive(params.pool, params);
Copy link
Collaborator

Choose a reason for hiding this comment

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

At first glance it looks like you could just have _addLiquidityRecursive take a single parameter, since the pool is already in the params - but you can't, because sometimes it's the child pool, different from the one in params. Would be good to document that somewhere (maybe where the function's defined).

Copy link
Collaborator

Choose a reason for hiding this comment

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

We might also note that the Vault explicitly prohibits "Linear pool-style" recursion, where a pool contains its own BPT, so that case is impossible and we don't need to handle it.

}
}

function _addLiquidityRecursive(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
function _addLiquidityRecursive(
function _addLiquidity(

Rather than overloading _addLiquidityRecursive, consider naming the "outer" one _addLiquidity, leaving the "inner" one (that is actually recursive, with the level argument) as _addLiquidityRecursive.

@@ -32,6 +32,8 @@ contract CompositeLiquidityRouter is ICompositeLiquidityRouter, BatchRouterCommo
using TransientEnumerableSet for TransientEnumerableSet.AddressSet;
using TransientStorageHelpers for *;

uint256 constant _MAX_LEVEL_IN_NESTED_OPERATIONS = 2;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
uint256 constant _MAX_LEVEL_IN_NESTED_OPERATIONS = 2;
// The level is 1-based, so the outermost pool (level 1) can contain a nested BPT, but any pool tokens in the child
// pool (level 2) will not be expanded further, but simply treated as standard tokens.
uint256 constant _MAX_LEVEL_IN_NESTED_OPERATIONS = 2;

Clarify what 2 levels means (there was some confusion off-line).

params.sender,
params.wethIsEth
if (level > _MAX_LEVEL_IN_NESTED_OPERATIONS) {
// if we have reached the maximum level of nested operations, the token will be calculated as ERC20
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// if we have reached the maximum level of nested operations, the token will be calculated as ERC20
// If we have exceeded the maximum level of nested operations, the token will be considered a standard ERC20.

_wrapAndUpdateTokenInAmounts(IERC4626(childToken), params.sender, params.wethIsEth);
amountsIn[i] = _wrapAndUpdateTokenInAmounts(IERC4626(childToken), params.sender, params.wethIsEth);
} else if (_settledTokenAmounts().tGet(childToken) == 0) {
// if this token is ERC20 and the amount was not settled in a previous operation, it should be added
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// if this token is ERC20 and the amount was not settled in a previous operation, it should be added
// Set this token's amountIn if it's a standard token that was not previously settled.

Comment on lines +628 to +629
// Remove the underlying token from the tokensIn and set the amount as zero because we took it here.
// We will take other tokens at the end of the calculation.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// Remove the underlying token from the tokensIn and set the amount as zero because we took it here.
// We will take other tokens at the end of the calculation.
// Remove the underlying token from `_currentSwapTokensIn` and zero out the amount, as these tokens were paid
// in advance and wrapped. Remaining tokens will be transferred in at the end of the calculation.

Is this correct? I wasn't sure what "taking the token" meant in the original.

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.

4 participants