diff --git a/pkg/interfaces/contracts/vault/ICompositeLiquidityRouter.sol b/pkg/interfaces/contracts/vault/ICompositeLiquidityRouter.sol index 1c2f6cd7e..54e889a4c 100644 --- a/pkg/interfaces/contracts/vault/ICompositeLiquidityRouter.sol +++ b/pkg/interfaces/contracts/vault/ICompositeLiquidityRouter.sol @@ -30,7 +30,9 @@ interface ICompositeLiquidityRouter { * the "parent" pool, and also make sure limits are set properly. * * @param pool Address of the liquidity pool - * @param exactUnderlyingAmountsIn Exact amounts of underlying tokens in, sorted in token registration order of + * @param useAsStandardToken An array indicating whether to use the token as standard or wrap it, + * sorted in token registration order of wrapped tokens in the pool + * @param exactAmountsIn Exact amounts of underlying/wrapped tokens in, sorted in token registration order * wrapped tokens in the pool * @param minBptAmountOut Minimum amount of pool tokens to be received * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH @@ -39,7 +41,8 @@ interface ICompositeLiquidityRouter { */ function addLiquidityUnbalancedToERC4626Pool( address pool, - uint256[] memory exactUnderlyingAmountsIn, + bool[] memory useAsStandardToken, + uint256[] memory exactAmountsIn, uint256 minBptAmountOut, bool wethIsEth, bytes memory userData @@ -52,47 +55,55 @@ interface ICompositeLiquidityRouter { * the "parent" pool, and also make sure limits are set properly. * * @param pool Address of the liquidity pool - * @param maxUnderlyingAmountsIn Maximum amounts of underlying tokens in, sorted in token registration order of + * @param useAsStandardToken An array indicating whether to use the token as standard or wrap it, + * sorted in token registration order of wrapped tokens in the pool + * @param maxAmountsIn Maximum amounts of underlying/wrapped tokens in, sorted in token registration order * wrapped tokens in the pool * @param exactBptAmountOut Exact amount of pool tokens to be received * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH * @param userData Additional (optional) data required for adding liquidity - * @return underlyingAmountsIn Actual amounts of tokens added, sorted in token registration order of wrapped tokens - * in the pool + * @return tokensIn Actual tokens added in the pool + * @return amountsIn Actual amounts of tokens added in the pool */ function addLiquidityProportionalToERC4626Pool( address pool, - uint256[] memory maxUnderlyingAmountsIn, + bool[] memory useAsStandardToken, + uint256[] memory maxAmountsIn, uint256 exactBptAmountOut, bool wethIsEth, bytes memory userData - ) external payable returns (uint256[] memory underlyingAmountsIn); + ) external payable returns (address[] memory tokensIn, uint256[] memory amountsIn); /** * @notice Remove proportional amounts of underlying from an ERC4626 pool, burning an exact pool token amount. * @dev An "ERC4626 pool" contains IERC4626 yield-bearing tokens (e.g., waDAI). * @param pool Address of the liquidity pool + * @param useAsStandardToken An array indicating whether to use the token as standard or unwrap it, + * sorted in token registration order of wrapped tokens in the pool * @param exactBptAmountIn Exact amount of pool tokens provided - * @param minUnderlyingAmountsOut Minimum amounts of underlying tokens out, sorted in token registration order of + * @param minAmountsOut Minimum amounts of each token, sorted according to tokensIn array * wrapped tokens in the pool * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH * @param userData Additional (optional) data required for removing liquidity - * @return underlyingAmountsOut Actual amounts of tokens received, sorted in token registration order of wrapped - * tokens in the pool + * @return tokensOut Actual tokens received + * @return amountsOut Actual amounts of tokens received */ function removeLiquidityProportionalFromERC4626Pool( address pool, + bool[] memory useAsStandardToken, uint256 exactBptAmountIn, - uint256[] memory minUnderlyingAmountsOut, + uint256[] memory minAmountsOut, bool wethIsEth, bytes memory userData - ) external payable returns (uint256[] memory underlyingAmountsOut); + ) external payable returns (address[] memory tokensOut, uint256[] memory amountsOut); /** * @notice Queries an `addLiquidityUnbalancedToERC4626Pool` operation without actually executing it. * @dev An "ERC4626 pool" contains IERC4626 yield-bearing tokens (e.g., waDAI). * @param pool Address of the liquidity pool - * @param exactUnderlyingAmountsIn Exact amounts of underlying tokens in, sorted in token registration order of + * @param useAsStandardToken An array indicating whether to use the token as standard or wrap it, + * sorted in token registration order of wrapped tokens in the pool + * @param exactAmountsIn Exact amounts of underlying/wrapped tokens in, sorted in token registration order * wrapped tokens in the pool * @param sender The sender passed to the operation. It can influence results (e.g., with user-dependent hooks) * @param userData Additional (optional) data required for the query @@ -100,7 +111,8 @@ interface ICompositeLiquidityRouter { */ function queryAddLiquidityUnbalancedToERC4626Pool( address pool, - uint256[] memory exactUnderlyingAmountsIn, + bool[] memory useAsStandardToken, + uint256[] memory exactAmountsIn, address sender, bytes memory userData ) external returns (uint256 bptAmountOut); @@ -109,35 +121,41 @@ interface ICompositeLiquidityRouter { * @notice Queries an `addLiquidityProportionalToERC4626Pool` operation without actually executing it. * @dev An "ERC4626 pool" contains IERC4626 yield-bearing tokens (e.g., waDAI). * @param pool Address of the liquidity pool + * @param useAsStandardToken An array indicating whether to use the token as standard or wrap/unwrap it, + * sorted in token registration order of wrapped tokens in the pool * @param exactBptAmountOut Exact amount of pool tokens to be received * @param sender The sender passed to the operation. It can influence results (e.g., with user-dependent hooks) * @param userData Additional (optional) data required for the query - * @return underlyingAmountsIn Expected amounts of tokens to add, sorted in token registration order of wrapped - * tokens in the pool + * @return tokensIn Expected tokens added in the pool + * @return amountsIn Expected amounts of tokens added in the pool */ function queryAddLiquidityProportionalToERC4626Pool( address pool, + bool[] memory useAsStandardToken, uint256 exactBptAmountOut, address sender, bytes memory userData - ) external returns (uint256[] memory underlyingAmountsIn); + ) external returns (address[] memory tokensIn, uint256[] memory amountsIn); /** * @notice Queries a `removeLiquidityProportionalFromERC4626Pool` operation without actually executing it. * @dev An "ERC4626 pool" contains IERC4626 yield-bearing tokens (e.g., waDAI). * @param pool Address of the liquidity pool + * @param useAsStandardToken An array indicating whether to use the token as standard or unwrap it, + * sorted in token registration order of wrapped tokens in the pool * @param exactBptAmountIn Exact amount of pool tokens provided for the query * @param sender The sender passed to the operation. It can influence results (e.g., with user-dependent hooks) * @param userData Additional (optional) data required for the query - * @return underlyingAmountsOut Expected amounts of tokens to receive, sorted in token registration order of - * wrapped tokens in the pool + * @return tokensOut Expected tokens to receive + * @return amountsOut Expected amounts of tokens to receive */ function queryRemoveLiquidityProportionalFromERC4626Pool( address pool, + bool[] memory useAsStandardToken, uint256 exactBptAmountIn, address sender, bytes memory userData - ) external returns (uint256[] memory underlyingAmountsOut); + ) external returns (address[] memory tokensOut, uint256[] memory amountsOut); /*************************************************************************** Nested pools @@ -152,7 +170,7 @@ interface ICompositeLiquidityRouter { * @param parentPool Address of the highest level pool (which contains BPTs of other pools) * @param tokensIn Input token addresses, sorted by user preference. `tokensIn` array must have all tokens from * child pools and all tokens that are not BPTs from the nested pool (parent pool). - * @param exactAmountsIn Amount of each underlying token in, sorted according to tokensIn array + * @param exactAmountsIn Amount of each token in, sorted according to tokensIn array * @param minBptAmountOut Expected minimum amount of parent pool tokens to receive * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH * @param userData Additional (optional) data required for the operation @@ -172,7 +190,7 @@ interface ICompositeLiquidityRouter { * @param parentPool Address of the highest level pool (which contains BPTs of other pools) * @param tokensIn Input token addresses, sorted by user preference. `tokensIn` array must have all tokens from * child pools and all tokens that are not BPTs from the nested pool (parent pool). - * @param exactAmountsIn Amount of each underlying token in, sorted according to tokensIn array + * @param exactAmountsIn Amount of each token in, sorted according to tokensIn array * @param sender The sender passed to the operation. It can influence results (e.g., with user-dependent hooks) * @param userData Additional (optional) data required for the operation * @return bptAmountOut Expected amount of parent pool tokens to receive @@ -196,7 +214,7 @@ interface ICompositeLiquidityRouter { * @param tokensOut Output token addresses, sorted by user preference. `tokensOut` array must have all tokens from * child pools and all tokens that are not BPTs from the nested pool (parent pool). If not all tokens are informed, * balances are not settled and the operation reverts. Tokens that repeat must be informed only once. - * @param minAmountsOut Minimum amounts of each outgoing underlying token, sorted according to tokensIn array + * @param minAmountsOut Minimum amounts of each token, sorted according to tokensIn array * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH * @param userData Additional (optional) data required for the operation * @return amountsOut Actual amounts of tokens received, parallel to `tokensOut` diff --git a/pkg/vault/contracts/CompositeLiquidityRouter.sol b/pkg/vault/contracts/CompositeLiquidityRouter.sol index ec7bc6104..31d96751e 100644 --- a/pkg/vault/contracts/CompositeLiquidityRouter.sol +++ b/pkg/vault/contracts/CompositeLiquidityRouter.sol @@ -48,7 +48,8 @@ contract CompositeLiquidityRouter is ICompositeLiquidityRouter, BatchRouterCommo /// @inheritdoc ICompositeLiquidityRouter function addLiquidityUnbalancedToERC4626Pool( address pool, - uint256[] memory exactUnderlyingAmountsIn, + bool[] memory useAsStandardToken, + uint256[] memory exactAmountsIn, uint256 minBptAmountOut, bool wethIsEth, bytes memory userData @@ -57,15 +58,18 @@ contract CompositeLiquidityRouter is ICompositeLiquidityRouter, BatchRouterCommo _vault.unlock( abi.encodeCall( CompositeLiquidityRouter.addLiquidityERC4626PoolUnbalancedHook, - AddLiquidityHookParams({ - sender: msg.sender, - pool: pool, - maxAmountsIn: exactUnderlyingAmountsIn, - minBptAmountOut: minBptAmountOut, - kind: AddLiquidityKind.UNBALANCED, - wethIsEth: wethIsEth, - userData: userData - }) + ( + AddLiquidityHookParams({ + sender: msg.sender, + pool: pool, + maxAmountsIn: exactAmountsIn, + minBptAmountOut: minBptAmountOut, + kind: AddLiquidityKind.UNBALANCED, + wethIsEth: wethIsEth, + userData: userData + }), + useAsStandardToken + ) ) ), (uint256) @@ -75,61 +79,70 @@ contract CompositeLiquidityRouter is ICompositeLiquidityRouter, BatchRouterCommo /// @inheritdoc ICompositeLiquidityRouter function addLiquidityProportionalToERC4626Pool( address pool, - uint256[] memory maxUnderlyingAmountsIn, + bool[] memory useAsStandardToken, + uint256[] memory maxAmountsIn, uint256 exactBptAmountOut, bool wethIsEth, bytes memory userData - ) external payable saveSender(msg.sender) returns (uint256[] memory underlyingAmountsIn) { - underlyingAmountsIn = abi.decode( + ) external payable saveSender(msg.sender) returns (address[] memory tokensIn, uint256[] memory amountsIn) { + (tokensIn, amountsIn) = abi.decode( _vault.unlock( abi.encodeCall( CompositeLiquidityRouter.addLiquidityERC4626PoolProportionalHook, - AddLiquidityHookParams({ - sender: msg.sender, - pool: pool, - maxAmountsIn: maxUnderlyingAmountsIn, - minBptAmountOut: exactBptAmountOut, - kind: AddLiquidityKind.PROPORTIONAL, - wethIsEth: wethIsEth, - userData: userData - }) + ( + AddLiquidityHookParams({ + sender: msg.sender, + pool: pool, + maxAmountsIn: maxAmountsIn, + minBptAmountOut: exactBptAmountOut, + kind: AddLiquidityKind.PROPORTIONAL, + wethIsEth: wethIsEth, + userData: userData + }), + useAsStandardToken + ) ) ), - (uint256[]) + (address[], uint256[]) ); } /// @inheritdoc ICompositeLiquidityRouter function removeLiquidityProportionalFromERC4626Pool( address pool, + bool[] memory useAsStandardToken, uint256 exactBptAmountIn, - uint256[] memory minUnderlyingAmountsOut, + uint256[] memory minAmountsOut, bool wethIsEth, bytes memory userData - ) external payable saveSender(msg.sender) returns (uint256[] memory underlyingAmountsOut) { - underlyingAmountsOut = abi.decode( + ) external payable saveSender(msg.sender) returns (address[] memory tokensOut, uint256[] memory amountsOut) { + (tokensOut, amountsOut) = abi.decode( _vault.unlock( abi.encodeCall( CompositeLiquidityRouter.removeLiquidityERC4626PoolProportionalHook, - RemoveLiquidityHookParams({ - sender: msg.sender, - pool: pool, - minAmountsOut: minUnderlyingAmountsOut, - maxBptAmountIn: exactBptAmountIn, - kind: RemoveLiquidityKind.PROPORTIONAL, - wethIsEth: wethIsEth, - userData: userData - }) + ( + RemoveLiquidityHookParams({ + sender: msg.sender, + pool: pool, + minAmountsOut: minAmountsOut, + maxBptAmountIn: exactBptAmountIn, + kind: RemoveLiquidityKind.PROPORTIONAL, + wethIsEth: wethIsEth, + userData: userData + }), + useAsStandardToken + ) ) ), - (uint256[]) + (address[], uint256[]) ); } /// @inheritdoc ICompositeLiquidityRouter function queryAddLiquidityUnbalancedToERC4626Pool( address pool, - uint256[] memory exactUnderlyingAmountsIn, + bool[] memory useAsStandardToken, + uint256[] memory exactAmountsIn, address sender, bytes memory userData ) external saveSender(sender) returns (uint256 bptAmountOut) { @@ -137,15 +150,18 @@ contract CompositeLiquidityRouter is ICompositeLiquidityRouter, BatchRouterCommo _vault.quote( abi.encodeCall( CompositeLiquidityRouter.addLiquidityERC4626PoolUnbalancedHook, - AddLiquidityHookParams({ - sender: address(this), - pool: pool, - maxAmountsIn: exactUnderlyingAmountsIn, - minBptAmountOut: 0, - kind: AddLiquidityKind.UNBALANCED, - wethIsEth: false, - userData: userData - }) + ( + AddLiquidityHookParams({ + sender: address(this), + pool: pool, + maxAmountsIn: exactAmountsIn, + minBptAmountOut: 0, + kind: AddLiquidityKind.UNBALANCED, + wethIsEth: false, + userData: userData + }), + useAsStandardToken + ) ) ), (uint256) @@ -155,71 +171,80 @@ contract CompositeLiquidityRouter is ICompositeLiquidityRouter, BatchRouterCommo /// @inheritdoc ICompositeLiquidityRouter function queryAddLiquidityProportionalToERC4626Pool( address pool, + bool[] memory useAsStandardToken, uint256 exactBptAmountOut, address sender, bytes memory userData - ) external saveSender(sender) returns (uint256[] memory underlyingAmountsIn) { - underlyingAmountsIn = abi.decode( + ) external saveSender(sender) returns (address[] memory tokensIn, uint256[] memory amountsIn) { + (tokensIn, amountsIn) = abi.decode( _vault.quote( abi.encodeCall( CompositeLiquidityRouter.addLiquidityERC4626PoolProportionalHook, - AddLiquidityHookParams({ - sender: address(this), - pool: pool, - maxAmountsIn: _maxTokenLimits(pool), - minBptAmountOut: exactBptAmountOut, - kind: AddLiquidityKind.PROPORTIONAL, - wethIsEth: false, - userData: userData - }) + ( + AddLiquidityHookParams({ + sender: address(this), + pool: pool, + maxAmountsIn: _maxTokenLimits(pool), + minBptAmountOut: exactBptAmountOut, + kind: AddLiquidityKind.PROPORTIONAL, + wethIsEth: false, + userData: userData + }), + useAsStandardToken + ) ) ), - (uint256[]) + (address[], uint256[]) ); } /// @inheritdoc ICompositeLiquidityRouter function queryRemoveLiquidityProportionalFromERC4626Pool( address pool, + bool[] memory useAsStandardToken, uint256 exactBptAmountIn, address sender, bytes memory userData - ) external saveSender(sender) returns (uint256[] memory underlyingAmountsOut) { + ) external saveSender(sender) returns (address[] memory tokensOut, uint256[] memory amountsOut) { IERC20[] memory erc4626PoolTokens = _vault.getPoolTokens(pool); - underlyingAmountsOut = abi.decode( + (tokensOut, amountsOut) = abi.decode( _vault.quote( abi.encodeCall( CompositeLiquidityRouter.removeLiquidityERC4626PoolProportionalHook, - RemoveLiquidityHookParams({ - sender: address(this), - pool: pool, - minAmountsOut: new uint256[](erc4626PoolTokens.length), - maxBptAmountIn: exactBptAmountIn, - kind: RemoveLiquidityKind.PROPORTIONAL, - wethIsEth: false, - userData: userData - }) + ( + RemoveLiquidityHookParams({ + sender: address(this), + pool: pool, + minAmountsOut: new uint256[](erc4626PoolTokens.length), + maxBptAmountIn: exactBptAmountIn, + kind: RemoveLiquidityKind.PROPORTIONAL, + wethIsEth: false, + userData: userData + }), + useAsStandardToken + ) ) ), - (uint256[]) + (address[], uint256[]) ); } function addLiquidityERC4626PoolUnbalancedHook( - AddLiquidityHookParams calldata params + AddLiquidityHookParams calldata params, + bool[] calldata useAsStandardToken ) external nonReentrant onlyVault returns (uint256 bptAmountOut) { IERC20[] memory erc4626PoolTokens = _vault.getPoolTokens(params.pool); uint256 poolTokensLength = erc4626PoolTokens.length; - // Revert if tokensIn length does not match with maxAmountsIn length. - InputHelpers.ensureInputLengthMatch(poolTokensLength, params.maxAmountsIn.length); + // Revert if `poolTokens` length does not match `maxAmountsIn` and `useAsStandardToken`. + InputHelpers.ensureInputLengthMatch(poolTokensLength, params.maxAmountsIn.length, useAsStandardToken.length); - (, uint256[] memory wrappedAmountsIn) = _wrapTokens( - params, + uint256[] memory amountsIn = _wrapTokensExactInIfRequired( + params.sender, + useAsStandardToken, erc4626PoolTokens, params.maxAmountsIn, - SwapKind.EXACT_IN, - new uint256[](poolTokensLength) + params.wethIsEth ); // Add wrapped amounts to the ERC4626 pool. @@ -227,7 +252,7 @@ contract CompositeLiquidityRouter is ICompositeLiquidityRouter, BatchRouterCommo AddLiquidityParams({ pool: params.pool, to: params.sender, - maxAmountsIn: wrappedAmountsIn, + maxAmountsIn: amountsIn, minBptAmountOut: params.minBptAmountOut, kind: params.kind, userData: params.userData @@ -236,11 +261,15 @@ contract CompositeLiquidityRouter is ICompositeLiquidityRouter, BatchRouterCommo } function addLiquidityERC4626PoolProportionalHook( - AddLiquidityHookParams calldata params - ) external nonReentrant onlyVault returns (uint256[] memory underlyingAmountsIn) { + AddLiquidityHookParams calldata params, + bool[] calldata useAsStandardToken + ) external nonReentrant onlyVault returns (address[] memory tokensIn, uint256[] memory amountsIn) { IERC20[] memory erc4626PoolTokens = _vault.getPoolTokens(params.pool); uint256 poolTokensLength = erc4626PoolTokens.length; + // Revert if `poolTokens` length does not match `maxAmountsIn` and `useAsStandardToken`. + InputHelpers.ensureInputLengthMatch(poolTokensLength, params.maxAmountsIn.length, useAsStandardToken.length); + uint256[] memory maxAmounts = new uint256[](poolTokensLength); for (uint256 i = 0; i < poolTokensLength; ++i) { maxAmounts[i] = _MAX_AMOUNT; @@ -258,21 +287,27 @@ contract CompositeLiquidityRouter is ICompositeLiquidityRouter, BatchRouterCommo }) ); - (underlyingAmountsIn, ) = _wrapTokens( - params, + (tokensIn, amountsIn) = _wrapTokensExactOutIfRequired( + params.sender, + useAsStandardToken, erc4626PoolTokens, wrappedAmountsIn, - SwapKind.EXACT_OUT, - params.maxAmountsIn + params.maxAmountsIn, + params.wethIsEth ); } function removeLiquidityERC4626PoolProportionalHook( - RemoveLiquidityHookParams calldata params - ) external nonReentrant onlyVault returns (uint256[] memory underlyingAmountsOut) { + RemoveLiquidityHookParams calldata params, + bool[] calldata useAsStandardToken + ) external nonReentrant onlyVault returns (address[] memory tokensOut, uint256[] memory amountsOut) { IERC20[] memory erc4626PoolTokens = _vault.getPoolTokens(params.pool); uint256 poolTokensLength = erc4626PoolTokens.length; - underlyingAmountsOut = new uint256[](poolTokensLength); + + // Revert if `poolTokens` length does not match `minAmountsOut` and `useAsStandardToken`. + InputHelpers.ensureInputLengthMatch(poolTokensLength, params.minAmountsOut.length, useAsStandardToken.length); + + amountsOut = new uint256[](poolTokensLength); (, uint256[] memory wrappedAmountsOut, ) = _vault.removeLiquidity( RemoveLiquidityParams({ @@ -287,120 +322,187 @@ contract CompositeLiquidityRouter is ICompositeLiquidityRouter, BatchRouterCommo bool isStaticCall = EVMCallModeHelpers.isStaticCall(); + tokensOut = new address[](poolTokensLength); for (uint256 i = 0; i < poolTokensLength; ++i) { IERC4626 wrappedToken = IERC4626(address(erc4626PoolTokens[i])); IERC20 underlyingToken = IERC20(_vault.getBufferAsset(wrappedToken)); - // If the Vault returns address 0 as underlying, it means that the ERC4626 token buffer was not - // initialized. Thus, the Router treats it as a non-ERC4626 token. - if (address(underlyingToken) == address(0)) { - if (wrappedAmountsOut[i] < params.minAmountsOut[i]) { - revert IVaultErrors.AmountOutBelowMin( - erc4626PoolTokens[i], - wrappedAmountsOut[i], - params.minAmountsOut[i] - ); + if (useAsStandardToken[i]) { + amountsOut[i] = wrappedAmountsOut[i]; + tokensOut[i] = address(wrappedToken); + + if (amountsOut[i] < params.minAmountsOut[i]) { + revert IVaultErrors.AmountOutBelowMin(erc4626PoolTokens[i], amountsOut[i], params.minAmountsOut[i]); } - underlyingAmountsOut[i] = wrappedAmountsOut[i]; if (isStaticCall == false) { - _sendTokenOut(params.sender, erc4626PoolTokens[i], underlyingAmountsOut[i], params.wethIsEth); + _sendTokenOut(params.sender, erc4626PoolTokens[i], amountsOut[i], params.wethIsEth); + } + } else { + if (address(underlyingToken) == address(0)) { + revert IVaultErrors.BufferNotInitialized(wrappedToken); } - continue; - } - // `erc4626BufferWrapOrUnwrap` will fail if the wrappedToken is not ERC4626-conforming. - (, , underlyingAmountsOut[i]) = _vault.erc4626BufferWrapOrUnwrap( - BufferWrapOrUnwrapParams({ - kind: SwapKind.EXACT_IN, - direction: WrappingDirection.UNWRAP, - wrappedToken: wrappedToken, - amountGivenRaw: wrappedAmountsOut[i], - limitRaw: params.minAmountsOut[i] - }) - ); + // `erc4626BufferWrapOrUnwrap` will fail if the wrappedToken is not ERC4626-conforming. + (, , amountsOut[i]) = _vault.erc4626BufferWrapOrUnwrap( + BufferWrapOrUnwrapParams({ + kind: SwapKind.EXACT_IN, + direction: WrappingDirection.UNWRAP, + wrappedToken: wrappedToken, + amountGivenRaw: wrappedAmountsOut[i], + limitRaw: params.minAmountsOut[i] + }) + ); + tokensOut[i] = address(underlyingToken); + + if (amountsOut[i] < params.minAmountsOut[i]) { + revert IVaultErrors.AmountOutBelowMin(underlyingToken, amountsOut[i], params.minAmountsOut[i]); + } - if (isStaticCall == false) { - _sendTokenOut(params.sender, underlyingToken, underlyingAmountsOut[i], params.wethIsEth); + if (isStaticCall == false) { + _sendTokenOut(params.sender, underlyingToken, amountsOut[i], params.wethIsEth); + } } } } /// @dev Assumes array lengths have been checked externally. - function _wrapTokens( - AddLiquidityHookParams calldata params, + function _wrapTokensExactInIfRequired( + address sender, + bool[] memory useAsStandardToken, IERC20[] memory erc4626PoolTokens, uint256[] memory amountsIn, - SwapKind kind, - uint256[] memory limits - ) private returns (uint256[] memory underlyingAmounts, uint256[] memory wrappedAmounts) { + bool wethIsEth + ) private returns (uint256[] memory wrappedAmountsIn) { uint256 poolTokensLength = erc4626PoolTokens.length; - underlyingAmounts = new uint256[](poolTokensLength); - wrappedAmounts = new uint256[](poolTokensLength); + wrappedAmountsIn = new uint256[](poolTokensLength); bool isStaticCall = EVMCallModeHelpers.isStaticCall(); - // Wrap given underlying tokens for wrapped tokens. for (uint256 i = 0; i < poolTokensLength; ++i) { // Treat all ERC4626 pool tokens as wrapped. The next step will verify if we can use the wrappedToken as - // a valid ERC4626. + // a valid ERC4626. Note that if `useWrappedTokens[i]` is false, we will treat it as a standard token. IERC4626 wrappedToken = IERC4626(address(erc4626PoolTokens[i])); IERC20 underlyingToken = IERC20(_vault.getBufferAsset(wrappedToken)); - // If the Vault returns address 0 as underlying, it means that the ERC4626 token buffer was not - // initialized. Thus, the Router treats it as a non-ERC4626 token. - if (address(underlyingToken) == address(0)) { - if (amountsIn[i] > params.maxAmountsIn[i]) { - revert IVaultErrors.AmountInAboveMax(erc4626PoolTokens[i], amountsIn[i], params.maxAmountsIn[i]); + // Check whether the caller wants to use the token as an ERC4626 (i.e., wrap/unwrap it), or just use it as + // a standard token. + if (useAsStandardToken[i]) { + wrappedAmountsIn[i] = amountsIn[i]; + + if (isStaticCall == false) { + _takeTokenIn(sender, wrappedToken, wrappedAmountsIn[i], wethIsEth); + } + } else { + if (address(underlyingToken) == address(0)) { + revert IVaultErrors.BufferNotInitialized(wrappedToken); } - underlyingAmounts[i] = amountsIn[i]; - wrappedAmounts[i] = amountsIn[i]; + uint256 wrappedAmount; + if (amountsIn[i] > 0) { + if (isStaticCall == false) { + // Take the exact amount in from the sender. + _takeTokenIn(sender, underlyingToken, amountsIn[i], wethIsEth); + } - if (isStaticCall == false) { - _takeTokenIn(params.sender, erc4626PoolTokens[i], amountsIn[i], params.wethIsEth); + // `erc4626BufferWrapOrUnwrap` will fail if the wrappedToken isn't ERC4626-conforming. + (, , wrappedAmount) = _vault.erc4626BufferWrapOrUnwrap( + BufferWrapOrUnwrapParams({ + kind: SwapKind.EXACT_IN, + direction: WrappingDirection.WRAP, + wrappedToken: wrappedToken, + amountGivenRaw: amountsIn[i], + limitRaw: 0 + }) + ); } - continue; + wrappedAmountsIn[i] = wrappedAmount; } + } + + // If there's a leftover of eth, send it back to the sender. The router should not keep ETH. + _returnEth(sender); + } + + /// @dev Assumes array lengths have been checked externally. + function _wrapTokensExactOutIfRequired( + address sender, + bool[] memory useAsStandardToken, + IERC20[] memory erc4626PoolTokens, + uint256[] memory wrappedAmountsIn, + uint256[] memory maxAmountsIn, + bool wethIsEth + ) private returns (address[] memory tokensIn, uint256[] memory amountsIn) { + uint256 poolTokensLength = erc4626PoolTokens.length; + amountsIn = new uint256[](poolTokensLength); + + bool isStaticCall = EVMCallModeHelpers.isStaticCall(); + + tokensIn = new address[](poolTokensLength); + + for (uint256 i = 0; i < poolTokensLength; ++i) { + // Treat all ERC4626 pool tokens as wrapped. The next step will verify if we can use the wrappedToken as + // a valid ERC4626. Note that if `useAsStandardToken[i]` is false, we will treat it as a standard token. + IERC4626 wrappedToken = IERC4626(address(erc4626PoolTokens[i])); + IERC20 underlyingToken = IERC20(_vault.getBufferAsset(wrappedToken)); - if (isStaticCall == false) { - if (kind == SwapKind.EXACT_IN) { - // If the SwapKind is EXACT_IN, take the exact amount in from the sender. - _takeTokenIn(params.sender, underlyingToken, amountsIn[i], params.wethIsEth); - } else { - // If the SwapKind is EXACT_OUT, the exact amount in is not known, because amountsIn is the - // amount of wrapped tokens. Therefore, take the limit. After the wrap operation, the difference - // between the limit and the actual underlying amount is returned to the sender. - _takeTokenIn(params.sender, underlyingToken, limits[i], params.wethIsEth); + // Check whether the caller wants to use the token as an ERC4626 (i.e., wrap/unwrap it), or just use it as + // a standard token. + if (useAsStandardToken[i]) { + if (wrappedAmountsIn[i] > maxAmountsIn[i]) { + revert IVaultErrors.AmountInAboveMax(wrappedToken, wrappedAmountsIn[i], maxAmountsIn[i]); } - } - if (amountsIn[i] > 0) { - // `erc4626BufferWrapOrUnwrap` will fail if the wrappedToken isn't ERC4626-conforming. - (, underlyingAmounts[i], wrappedAmounts[i]) = _vault.erc4626BufferWrapOrUnwrap( - BufferWrapOrUnwrapParams({ - kind: kind, - direction: WrappingDirection.WRAP, - wrappedToken: wrappedToken, - amountGivenRaw: amountsIn[i], - limitRaw: limits[i] - }) - ); + if (isStaticCall == false) { + _takeTokenIn(sender, wrappedToken, wrappedAmountsIn[i], wethIsEth); + } + + amountsIn[i] = wrappedAmountsIn[i]; + tokensIn[i] = address(wrappedToken); } else { - underlyingAmounts[i] = 0; - wrappedAmounts[i] = 0; - } + if (address(underlyingToken) == address(0)) { + revert IVaultErrors.BufferNotInitialized(wrappedToken); + } + + uint256 underlyingAmount; + if (wrappedAmountsIn[i] > 0) { + if (isStaticCall == false) { + // The exact amount in is not known, because we have only + // wrappedAmountsIn. Therefore, take the maxAmountsIn. After the wrap operation, the difference + // between the maxAmountsIn and the actual underlying amount is returned to the sender. + _takeTokenIn(sender, underlyingToken, maxAmountsIn[i], wethIsEth); + } + + // `erc4626BufferWrapOrUnwrap` will fail if the wrappedToken isn't ERC4626-conforming. + (, underlyingAmount, ) = _vault.erc4626BufferWrapOrUnwrap( + BufferWrapOrUnwrapParams({ + kind: SwapKind.EXACT_OUT, + direction: WrappingDirection.WRAP, + wrappedToken: wrappedToken, + amountGivenRaw: wrappedAmountsIn[i], + limitRaw: maxAmountsIn[i] + }) + ); + } + + if (underlyingAmount > maxAmountsIn[i]) { + revert IVaultErrors.AmountInAboveMax(underlyingToken, underlyingAmount, maxAmountsIn[i]); + } + + if (isStaticCall == false) { + // The maxAmountsIn of underlying tokens was taken from the user, so the + // difference between maxAmountsIn and exact underlying amount needs to be returned to the sender. + _sendTokenOut(sender, underlyingToken, maxAmountsIn[i] - underlyingAmount, wethIsEth); + } - if (isStaticCall == false && kind == SwapKind.EXACT_OUT) { - // If the SwapKind is EXACT_OUT, the limit of underlying tokens was taken from the user, so the - // difference between limit and exact underlying amount needs to be returned to the sender. - _sendTokenOut(params.sender, underlyingToken, limits[i] - underlyingAmounts[i], params.wethIsEth); + amountsIn[i] = underlyingAmount; + tokensIn[i] = address(underlyingToken); } } // If there's a leftover of eth, send it back to the sender. The router should not keep ETH. - _returnEth(params.sender); + _returnEth(sender); } /*************************************************************************** diff --git a/pkg/vault/test/.contract-sizes/CompositeLiquidityRouter b/pkg/vault/test/.contract-sizes/CompositeLiquidityRouter index c5c2d49d6..1ac900f58 100644 --- a/pkg/vault/test/.contract-sizes/CompositeLiquidityRouter +++ b/pkg/vault/test/.contract-sizes/CompositeLiquidityRouter @@ -1,2 +1,2 @@ -Bytecode 21.554 -InitCode 23.372 \ No newline at end of file +Bytecode 22.513 +InitCode 24.338 \ No newline at end of file diff --git a/pkg/vault/test/foundry/CompositeLiquidityRouterERC4626Pool.t.sol b/pkg/vault/test/foundry/CompositeLiquidityRouterERC4626Pool.t.sol index d09da8588..1641816a0 100644 --- a/pkg/vault/test/foundry/CompositeLiquidityRouterERC4626Pool.t.sol +++ b/pkg/vault/test/foundry/CompositeLiquidityRouterERC4626Pool.t.sol @@ -112,6 +112,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vm.prank(alice); uint256 bptOut = compositeLiquidityRouter.addLiquidityUnbalancedToERC4626Pool( pool, + new bool[](exactUnderlyingAmountsIn.length), exactUnderlyingAmountsIn, 1, false, @@ -127,7 +128,55 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vars.wrappedWethPoolDelta = exactWrappedAmountsIn[waWethIdx]; vars.isPartialERC4626Pool = false; - _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars); + _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars, false); + + assertEq(bptOut, expectBPTOut, "BPT operationAmount should match expected"); + assertEq(IERC20(pool).balanceOf(alice), bptOut, "Alice: wrong BPT balance"); + } + + function testAddLiquidityUnbalancedToERC4626PoolWithWrappedToken__Fuzz(uint256 rawOperationAmount) public { + uint256 operationAmount = bound(rawOperationAmount, MIN_AMOUNT, bufferInitialAmount / 2); + uint256[] memory exactUnderlyingAmountsIn = [operationAmount, operationAmount].toMemoryArray(); + + uint256[] memory exactWrappedAmountsIn = new uint256[](2); + exactWrappedAmountsIn[waDaiIdx] = operationAmount; + exactWrappedAmountsIn[waWethIdx] = _vaultPreviewDeposit(waWETH, operationAmount); + + bool[] memory useAsStandardToken = new bool[](exactUnderlyingAmountsIn.length); + useAsStandardToken[waDaiIdx] = true; + + uint256 snapshot = vm.snapshot(); + _prankStaticCall(); + uint256 expectBPTOut = router.queryAddLiquidityUnbalanced( + pool, + exactWrappedAmountsIn, + address(this), + bytes("") + ); + vm.revertTo(snapshot); + + TestBalances memory balancesBefore = _getTestBalances(alice); + + vm.prank(alice); + uint256 bptOut = compositeLiquidityRouter.addLiquidityUnbalancedToERC4626Pool( + pool, + useAsStandardToken, + exactUnderlyingAmountsIn, + 1, + false, + bytes("") + ); + + TestBalances memory balancesAfter = _getTestBalances(alice); + + TestLocals memory vars; + vars.underlyingDaiAmountDelta = 0; + vars.underlyingWethAmountDelta = exactUnderlyingAmountsIn[waWethIdx]; + vars.wrappedDaiPoolDelta = exactWrappedAmountsIn[waDaiIdx]; + vars.wrappedWethPoolDelta = exactWrappedAmountsIn[waWethIdx]; + vars.isPartialERC4626Pool = false; + + _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars, true); assertEq(bptOut, expectBPTOut, "BPT operationAmount should match expected"); assertEq(IERC20(pool).balanceOf(alice), bptOut, "Alice: wrong BPT balance"); @@ -159,7 +208,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vm.prank(alice); uint256 bptOut = compositeLiquidityRouter.addLiquidityUnbalancedToERC4626Pool{ value: operationAmount + (forceEthLeftover ? 1e18 : 0) - }(pool, exactUnderlyingAmountsIn, 1, true, bytes("")); + }(pool, new bool[](exactUnderlyingAmountsIn.length), exactUnderlyingAmountsIn, 1, true, bytes("")); TestBalances memory balancesAfter = _getTestBalances(alice); @@ -171,7 +220,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vars.isPartialERC4626Pool = false; vars.wethIsEth = true; - _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars); + _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars, false); assertEq(bptOut, expectBPTOut, "BPT operationAmount should match expected"); assertEq(IERC20(pool).balanceOf(alice), bptOut, "Alice: wrong BPT balance"); @@ -203,6 +252,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vm.prank(alice); uint256 bptOut = compositeLiquidityRouter.addLiquidityUnbalancedToERC4626Pool( pool, + new bool[](exactUnderlyingAmountsIn.length), exactUnderlyingAmountsIn, 1, false, @@ -218,7 +268,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vars.wrappedWethPoolDelta = exactWrappedAmountsIn[waWethIdx]; vars.isPartialERC4626Pool = false; - _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars); + _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars, false); assertEq(bptOut, expectBPTOut, "BPT operationAmount should match expected"); assertEq(IERC20(pool).balanceOf(alice), bptOut, "Alice: wrong BPT balance"); @@ -244,9 +294,13 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { TestBalances memory balancesBefore = _getTestBalances(alice); + bool[] memory useAsStandardToken = new bool[](exactUnderlyingAmountsIn.length); + useAsStandardToken[partialWethIdx] = true; + vm.prank(alice); uint256 bptOut = compositeLiquidityRouter.addLiquidityUnbalancedToERC4626Pool( partialErc4626Pool, + useAsStandardToken, exactUnderlyingAmountsIn, 0, false, @@ -261,7 +315,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vars.wrappedDaiPoolDelta = exactWrappedAmountsIn[partialWaDaiIdx]; vars.isPartialERC4626Pool = true; - _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars); + _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars, false); assertEq(bptOut, expectBPTOut, "Wrong BPT out"); assertEq(IERC20(address(partialErc4626Pool)).balanceOf(alice), bptOut, "Alice: wrong BPT balance"); @@ -290,10 +344,13 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { TestBalances memory balancesBefore = _getTestBalances(alice); + bool[] memory useAsStandardToken = new bool[](exactUnderlyingAmountsIn.length); + useAsStandardToken[partialWethIdx] = true; + vm.prank(alice); uint256 bptOut = compositeLiquidityRouter.addLiquidityUnbalancedToERC4626Pool{ value: operationAmount + (forceEthLeftover ? 1e18 : 0) - }(partialErc4626Pool, exactUnderlyingAmountsIn, 0, true, bytes("")); + }(partialErc4626Pool, useAsStandardToken, exactUnderlyingAmountsIn, 0, true, bytes("")); TestBalances memory balancesAfter = _getTestBalances(alice); @@ -304,7 +361,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vars.isPartialERC4626Pool = true; vars.wethIsEth = true; - _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars); + _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars, false); assertEq(bptOut, expectBPTOut, "Wrong BPT out"); assertEq(IERC20(address(partialErc4626Pool)).balanceOf(alice), bptOut, "Alice: wrong BPT balance"); @@ -317,6 +374,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { _prankStaticCall(); compositeLiquidityRouter.queryAddLiquidityUnbalancedToERC4626Pool( pool, + new bool[](exactUnderlyingAmountsIn.length), exactUnderlyingAmountsIn, address(this), bytes("") @@ -331,6 +389,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { _prankStaticCall(); uint256 queryBptAmountOut = compositeLiquidityRouter.queryAddLiquidityUnbalancedToERC4626Pool( pool, + new bool[](exactUnderlyingAmountsIn.length), exactUnderlyingAmountsIn, address(this), bytes("") @@ -340,6 +399,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vm.prank(alice); uint256 actualBptAmountOut = compositeLiquidityRouter.addLiquidityUnbalancedToERC4626Pool( pool, + new bool[](exactUnderlyingAmountsIn.length), exactUnderlyingAmountsIn, 1, false, @@ -357,6 +417,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { _prankStaticCall(); uint256 queryBptAmountOut = compositeLiquidityRouter.queryAddLiquidityUnbalancedToERC4626Pool( pool, + new bool[](exactUnderlyingAmountsIn.length), exactUnderlyingAmountsIn, address(this), bytes("") @@ -366,6 +427,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vm.prank(alice); uint256 actualBptAmountOut = compositeLiquidityRouter.addLiquidityUnbalancedToERC4626Pool( pool, + new bool[](exactUnderlyingAmountsIn.length), exactUnderlyingAmountsIn, 1, false, @@ -379,10 +441,14 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { uint256 operationAmount = bufferInitialAmount / 2; uint256[] memory exactUnderlyingAmountsIn = [operationAmount, operationAmount].toMemoryArray(); + bool[] memory useAsStandardToken = new bool[](exactUnderlyingAmountsIn.length); + useAsStandardToken[partialWethIdx] = true; + uint256 snapshotId = vm.snapshot(); _prankStaticCall(); uint256 queryBptAmountOut = compositeLiquidityRouter.queryAddLiquidityUnbalancedToERC4626Pool( partialErc4626Pool, + useAsStandardToken, exactUnderlyingAmountsIn, address(this), bytes("") @@ -392,6 +458,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vm.prank(alice); uint256 actualBptAmountOut = compositeLiquidityRouter.addLiquidityUnbalancedToERC4626Pool( partialErc4626Pool, + useAsStandardToken, exactUnderlyingAmountsIn, 1, false, @@ -420,8 +487,9 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { TestBalances memory balancesBefore = _getTestBalances(alice); vm.prank(alice); - uint256[] memory actualUnderlyingAmountsIn = compositeLiquidityRouter.addLiquidityProportionalToERC4626Pool( + (, uint256[] memory actualUnderlyingAmountsIn) = compositeLiquidityRouter.addLiquidityProportionalToERC4626Pool( pool, + new bool[](maxAmountsIn.length), maxAmountsIn, exactBptAmountOut, false, @@ -437,7 +505,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vars.wrappedWethPoolDelta = expectedWrappedAmountsIn[waWethIdx]; vars.isPartialERC4626Pool = false; - _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars); + _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars, false); assertEq( actualUnderlyingAmountsIn[waDaiIdx], @@ -475,9 +543,9 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { TestBalances memory balancesBefore = _getTestBalances(alice); vm.prank(alice); - uint256[] memory actualUnderlyingAmountsIn = compositeLiquidityRouter.addLiquidityProportionalToERC4626Pool{ + (, uint256[] memory actualUnderlyingAmountsIn) = compositeLiquidityRouter.addLiquidityProportionalToERC4626Pool{ value: operationAmount + (forceEthLeftover ? 1e18 : 0) - }(pool, maxAmountsIn, exactBptAmountOut, true, bytes("")); + }(pool, new bool[](maxAmountsIn.length), maxAmountsIn, exactBptAmountOut, true, bytes("")); TestBalances memory balancesAfter = _getTestBalances(alice); @@ -489,7 +557,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vars.isPartialERC4626Pool = false; vars.wethIsEth = true; - _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars); + _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars, false); assertEq( actualUnderlyingAmountsIn[waDaiIdx], @@ -531,9 +599,13 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { TestBalances memory balancesBefore = _getTestBalances(alice); + bool[] memory useAsStandardToken = new bool[](maxAmountsIn.length); + useAsStandardToken[partialWethIdx] = true; + vm.prank(alice); - uint256[] memory actualUnderlyingAmountsIn = compositeLiquidityRouter.addLiquidityProportionalToERC4626Pool( + (, uint256[] memory actualUnderlyingAmountsIn) = compositeLiquidityRouter.addLiquidityProportionalToERC4626Pool( partialErc4626Pool, + useAsStandardToken, maxAmountsIn, exactBptAmountOut, false, @@ -548,7 +620,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vars.wrappedDaiPoolDelta = expectedWrappedAmountsIn[partialWaDaiIdx]; vars.isPartialERC4626Pool = true; - _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars); + _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars, false); assertEq( actualUnderlyingAmountsIn[partialWaDaiIdx], @@ -593,10 +665,13 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { TestBalances memory balancesBefore = _getTestBalances(alice); + bool[] memory useAsStandardToken = new bool[](maxAmountsIn.length); + useAsStandardToken[partialWethIdx] = true; + vm.prank(alice); - uint256[] memory actualUnderlyingAmountsIn = compositeLiquidityRouter.addLiquidityProportionalToERC4626Pool{ + (, uint256[] memory actualUnderlyingAmountsIn) = compositeLiquidityRouter.addLiquidityProportionalToERC4626Pool{ value: operationAmount + (forceEthLeftover ? 1e18 : 0) - }(partialErc4626Pool, maxAmountsIn, exactBptAmountOut, true, bytes("")); + }(partialErc4626Pool, useAsStandardToken, maxAmountsIn, exactBptAmountOut, true, bytes("")); TestBalances memory balancesAfter = _getTestBalances(alice); @@ -607,7 +682,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vars.isPartialERC4626Pool = true; vars.wethIsEth = true; - _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars); + _checkBalancesAfterAddLiquidity(balancesBefore, balancesAfter, vars, false); assertEq( actualUnderlyingAmountsIn[partialWaDaiIdx], @@ -629,6 +704,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { _prankStaticCall(); compositeLiquidityRouter.queryAddLiquidityProportionalToERC4626Pool( pool, + new bool[](2), operationAmount, address(this), bytes("") @@ -656,6 +732,9 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { maxAmountsIn[partialWaDaiIdx] = operationAmount; maxAmountsIn[partialWethIdx] = expectedWrappedAmountsIn[partialWethIdx] - 1; + bool[] memory useAsStandardToken = new bool[](maxAmountsIn.length); + useAsStandardToken[partialWethIdx] = true; + vm.expectRevert( abi.encodeWithSelector( IVaultErrors.AmountInAboveMax.selector, @@ -667,6 +746,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vm.prank(alice); compositeLiquidityRouter.addLiquidityProportionalToERC4626Pool( partialErc4626Pool, + useAsStandardToken, maxAmountsIn, exactBptAmountOut, false, @@ -681,17 +761,20 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { uint256 snapshotId = vm.snapshot(); _prankStaticCall(); - uint256[] memory queryUnderlyingAmountsIn = compositeLiquidityRouter.queryAddLiquidityProportionalToERC4626Pool( - pool, - exactBptAmountOut, - address(this), - bytes("") - ); + (, uint256[] memory queryUnderlyingAmountsIn) = compositeLiquidityRouter + .queryAddLiquidityProportionalToERC4626Pool( + pool, + new bool[](maxAmountsIn.length), + exactBptAmountOut, + address(this), + bytes("") + ); vm.revertTo(snapshotId); vm.prank(alice); - uint256[] memory actualUnderlyingAmountsIn = compositeLiquidityRouter.addLiquidityProportionalToERC4626Pool( + (, uint256[] memory actualUnderlyingAmountsIn) = compositeLiquidityRouter.addLiquidityProportionalToERC4626Pool( pool, + new bool[](maxAmountsIn.length), maxAmountsIn, exactBptAmountOut, false, @@ -712,19 +795,25 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { uint256[] memory maxAmountsIn = [operationAmount, operationAmount].toMemoryArray(); uint256 exactBptAmountOut = operationAmount; + bool[] memory useAsStandardToken = new bool[](maxAmountsIn.length); + useAsStandardToken[partialWethIdx] = true; + uint256 snapshotId = vm.snapshot(); _prankStaticCall(); - uint256[] memory queryUnderlyingAmountsIn = compositeLiquidityRouter.queryAddLiquidityProportionalToERC4626Pool( - partialErc4626Pool, - exactBptAmountOut, - address(this), - bytes("") - ); + (, uint256[] memory queryUnderlyingAmountsIn) = compositeLiquidityRouter + .queryAddLiquidityProportionalToERC4626Pool( + partialErc4626Pool, + useAsStandardToken, + exactBptAmountOut, + address(this), + bytes("") + ); vm.revertTo(snapshotId); vm.prank(alice); - uint256[] memory actualUnderlyingAmountsIn = compositeLiquidityRouter.addLiquidityProportionalToERC4626Pool( + (, uint256[] memory actualUnderlyingAmountsIn) = compositeLiquidityRouter.addLiquidityProportionalToERC4626Pool( partialErc4626Pool, + useAsStandardToken, maxAmountsIn, exactBptAmountOut, false, @@ -766,8 +855,15 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { TestBalances memory balancesBefore = _getTestBalances(bob); vm.prank(bob); - uint256[] memory actualUnderlyingAmountsOut = compositeLiquidityRouter - .removeLiquidityProportionalFromERC4626Pool(pool, exactBptAmountIn, minAmountsOut, false, bytes("")); + (, uint256[] memory actualUnderlyingAmountsOut) = compositeLiquidityRouter + .removeLiquidityProportionalFromERC4626Pool( + pool, + new bool[](minAmountsOut.length), + exactBptAmountIn, + minAmountsOut, + false, + bytes("") + ); TestBalances memory balancesAfter = _getTestBalances(bob); @@ -778,7 +874,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vars.wrappedWethPoolDelta = expectedWrappedAmountsOut[waWethIdx]; vars.isPartialERC4626Pool = false; - _checkBalancesAfterRemoveLiquidity(balancesBefore, balancesAfter, vars); + _checkBalancesAfterRemoveLiquidity(balancesBefore, balancesAfter, vars, false); assertEq( actualUnderlyingAmountsOut[waDaiIdx], @@ -795,6 +891,67 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { assertEq(afterBPTBalance, beforeBPTBalance - exactBptAmountIn, "Bob: wrong BPT balance"); } + function testRemoveLiquidityProportionalFromERC4626PoolWithWrappedToken__Fuzz(uint256 rawOperationAmount) public { + uint256 exactBptAmountIn = bound(rawOperationAmount, MIN_AMOUNT, bufferInitialAmount / 2); + + uint256 snapshot = vm.snapshot(); + _prankStaticCall(); + uint256[] memory expectedWrappedAmountsOut = router.queryRemoveLiquidityProportional( + pool, + exactBptAmountIn, + address(this), + bytes("") + ); + vm.revertTo(snapshot); + + uint256 beforeBPTBalance = IERC20(pool).balanceOf(bob); + + uint256[] memory minAmountsOut = new uint256[](2); + minAmountsOut[waWethIdx] = _vaultPreviewRedeem(waWETH, expectedWrappedAmountsOut[waWethIdx]); + minAmountsOut[waDaiIdx] = expectedWrappedAmountsOut[waDaiIdx]; + + bool[] memory useAsStandardToken = new bool[](2); + useAsStandardToken[waDaiIdx] = true; + + TestBalances memory balancesBefore = _getTestBalances(bob); + + vm.prank(bob); + (, uint256[] memory actualUnderlyingAmountsOut) = compositeLiquidityRouter + .removeLiquidityProportionalFromERC4626Pool( + pool, + useAsStandardToken, + exactBptAmountIn, + minAmountsOut, + false, + bytes("") + ); + + TestBalances memory balancesAfter = _getTestBalances(bob); + + TestLocals memory vars; + vars.underlyingDaiAmountDelta = 0; + vars.underlyingWethAmountDelta = actualUnderlyingAmountsOut[waWethIdx]; + vars.wrappedDaiPoolDelta = expectedWrappedAmountsOut[waDaiIdx]; + vars.wrappedWethPoolDelta = expectedWrappedAmountsOut[waWethIdx]; + vars.isPartialERC4626Pool = false; + + _checkBalancesAfterRemoveLiquidity(balancesBefore, balancesAfter, vars, true); + + assertEq( + actualUnderlyingAmountsOut[waDaiIdx], + expectedWrappedAmountsOut[waDaiIdx], + "DAI actualUnderlyingAmountsOut should match expected" + ); + assertEq( + actualUnderlyingAmountsOut[waWethIdx], + _vaultPreviewRedeem(waWETH, expectedWrappedAmountsOut[waWethIdx]), + "WETH actualUnderlyingAmountsOut should match expected" + ); + + uint256 afterBPTBalance = IERC20(pool).balanceOf(bob); + assertEq(afterBPTBalance, beforeBPTBalance - exactBptAmountIn, "Bob: wrong BPT balance"); + } + function testRemoveLiquidityProportionalFromERC4626PoolWithEth__Fuzz(uint256 rawOperationAmount) public { uint256 exactBptAmountIn = bound(rawOperationAmount, MIN_AMOUNT, bufferInitialAmount / 2); @@ -817,8 +974,15 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { TestBalances memory balancesBefore = _getTestBalances(bob); vm.prank(bob); - uint256[] memory actualUnderlyingAmountsOut = compositeLiquidityRouter - .removeLiquidityProportionalFromERC4626Pool(pool, exactBptAmountIn, minAmountsOut, true, bytes("")); + (, uint256[] memory actualUnderlyingAmountsOut) = compositeLiquidityRouter + .removeLiquidityProportionalFromERC4626Pool( + pool, + new bool[](minAmountsOut.length), + exactBptAmountIn, + minAmountsOut, + true, + bytes("") + ); TestBalances memory balancesAfter = _getTestBalances(bob); @@ -830,7 +994,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vars.isPartialERC4626Pool = false; vars.wethIsEth = true; - _checkBalancesAfterRemoveLiquidity(balancesBefore, balancesAfter, vars); + _checkBalancesAfterRemoveLiquidity(balancesBefore, balancesAfter, vars, false); assertEq( actualUnderlyingAmountsOut[waDaiIdx], @@ -868,10 +1032,14 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { TestBalances memory balancesBefore = _getTestBalances(bob); + bool[] memory useAsStandardToken = new bool[](minAmountsOut.length); + useAsStandardToken[partialWethIdx] = true; + vm.prank(bob); - uint256[] memory actualUnderlyingAmountsOut = compositeLiquidityRouter + (, uint256[] memory actualUnderlyingAmountsOut) = compositeLiquidityRouter .removeLiquidityProportionalFromERC4626Pool( partialErc4626Pool, + useAsStandardToken, exactBptAmountIn, minAmountsOut, false, @@ -886,7 +1054,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vars.wrappedDaiPoolDelta = expectedWrappedAmountsOut[partialWaDaiIdx]; vars.isPartialERC4626Pool = true; - _checkBalancesAfterRemoveLiquidity(balancesBefore, balancesAfter, vars); + _checkBalancesAfterRemoveLiquidity(balancesBefore, balancesAfter, vars, false); assertEq( actualUnderlyingAmountsOut[partialWaDaiIdx], @@ -925,10 +1093,14 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { TestBalances memory balancesBefore = _getTestBalances(bob); + bool[] memory useAsStandardToken = new bool[](minAmountsOut.length); + useAsStandardToken[partialWethIdx] = true; + vm.prank(bob); - uint256[] memory actualUnderlyingAmountsOut = compositeLiquidityRouter + (, uint256[] memory actualUnderlyingAmountsOut) = compositeLiquidityRouter .removeLiquidityProportionalFromERC4626Pool( partialErc4626Pool, + useAsStandardToken, exactBptAmountIn, minAmountsOut, true, @@ -944,7 +1116,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vars.isPartialERC4626Pool = true; vars.wethIsEth = true; - _checkBalancesAfterRemoveLiquidity(balancesBefore, balancesAfter, vars); + _checkBalancesAfterRemoveLiquidity(balancesBefore, balancesAfter, vars, false); assertEq( actualUnderlyingAmountsOut[partialWaDaiIdx], @@ -968,6 +1140,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { _prankStaticCall(); compositeLiquidityRouter.queryRemoveLiquidityProportionalFromERC4626Pool( pool, + new bool[](2), exactBptAmountIn, address(this), bytes("") @@ -992,6 +1165,9 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { minAmountsOut[partialWethIdx] = expectedWrappedAmountsOut[partialWethIdx] + 1; minAmountsOut[partialWaDaiIdx] = _vaultPreviewRedeem(waDAI, expectedWrappedAmountsOut[partialWaDaiIdx]); + bool[] memory useAsStandardToken = new bool[](minAmountsOut.length); + useAsStandardToken[partialWethIdx] = true; + vm.expectRevert( abi.encodeWithSelector( IVaultErrors.AmountOutBelowMin.selector, @@ -1003,6 +1179,7 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vm.prank(bob); compositeLiquidityRouter.removeLiquidityProportionalFromERC4626Pool( partialErc4626Pool, + useAsStandardToken, exactBptAmountIn, minAmountsOut, false, @@ -1029,14 +1206,21 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { uint256 snapshotId = vm.snapshot(); _prankStaticCall(); - uint256[] memory queryUnderlyingAmountsOut = compositeLiquidityRouter - .queryRemoveLiquidityProportionalFromERC4626Pool(pool, exactBptAmountIn, address(this), bytes("")); + (, uint256[] memory queryUnderlyingAmountsOut) = compositeLiquidityRouter + .queryRemoveLiquidityProportionalFromERC4626Pool( + pool, + new bool[](minUnderlyingAmountsOut.length), + exactBptAmountIn, + address(this), + bytes("") + ); vm.revertTo(snapshotId); vm.prank(bob); - uint256[] memory actualUnderlyingAmountsOut = compositeLiquidityRouter + (, uint256[] memory actualUnderlyingAmountsOut) = compositeLiquidityRouter .removeLiquidityProportionalFromERC4626Pool( pool, + new bool[](minUnderlyingAmountsOut.length), exactBptAmountIn, minUnderlyingAmountsOut, false, @@ -1072,11 +1256,15 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { expectedWrappedAmountsOut[partialWaDaiIdx] ); + bool[] memory useAsStandardToken = new bool[](minUnderlyingAmountsOut.length); + useAsStandardToken[partialWethIdx] = true; + uint256 snapshotId = vm.snapshot(); _prankStaticCall(); - uint256[] memory queryUnderlyingAmountsOut = compositeLiquidityRouter + (, uint256[] memory queryUnderlyingAmountsOut) = compositeLiquidityRouter .queryRemoveLiquidityProportionalFromERC4626Pool( partialErc4626Pool, + useAsStandardToken, exactBptAmountIn, address(this), bytes("") @@ -1084,9 +1272,10 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { vm.revertTo(snapshotId); vm.prank(bob); - uint256[] memory actualUnderlyingAmountsOut = compositeLiquidityRouter + (, uint256[] memory actualUnderlyingAmountsOut) = compositeLiquidityRouter .removeLiquidityProportionalFromERC4626Pool( partialErc4626Pool, + useAsStandardToken, exactBptAmountIn, minUnderlyingAmountsOut, false, @@ -1133,7 +1322,8 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { function _checkBalancesAfterAddLiquidity( TestBalances memory balancesBefore, TestBalances memory balancesAfter, - TestLocals memory vars + TestLocals memory vars, + bool useWrappedDai ) private { address ybPool = vars.isPartialERC4626Pool ? partialErc4626Pool : pool; uint256 ybDaiIdx = vars.isPartialERC4626Pool ? partialWaDaiIdx : waDaiIdx; @@ -1162,23 +1352,44 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { ); } - assertEq( - balancesAfter.balances.aliceTokens[balancesAfter.daiIdx], - balancesBefore.balances.aliceTokens[balancesBefore.daiIdx] - vars.underlyingDaiAmountDelta, - "Alice: wrong DAI balance" - ); + if (useWrappedDai == false) { + assertEq( + balancesAfter.balances.aliceTokens[balancesAfter.daiIdx], + balancesBefore.balances.aliceTokens[balancesBefore.daiIdx] - vars.underlyingDaiAmountDelta, + "Alice: wrong DAI balance" + ); - // The underlying tokens are wrapped in the buffer, so the buffer gains underlying and loses wrapped tokens. - assertEq( - balancesAfter.waDAIBuffer.underlying, - balancesBefore.waDAIBuffer.underlying + vars.underlyingDaiAmountDelta, - "Vault: wrong waDAI underlying buffer balance" - ); - assertEq( - balancesAfter.waDAIBuffer.wrapped, - balancesBefore.waDAIBuffer.wrapped - vars.wrappedDaiPoolDelta, - "Vault: wrong waDAI wrapped buffer balance" - ); + // The underlying tokens are wrapped in the buffer, so the buffer gains underlying and loses wrapped tokens. + assertEq( + balancesAfter.waDAIBuffer.underlying, + balancesBefore.waDAIBuffer.underlying + vars.underlyingDaiAmountDelta, + "Vault: wrong waDAI underlying buffer balance" + ); + + assertEq( + balancesAfter.waDAIBuffer.wrapped, + balancesBefore.waDAIBuffer.wrapped - vars.wrappedDaiPoolDelta, + "Vault: wrong waDAI wrapped buffer balance" + ); + } else { + assertEq( + balancesAfter.balances.aliceTokens[balancesAfter.waDaiIdx], + balancesBefore.balances.aliceTokens[balancesBefore.waDaiIdx] - vars.wrappedDaiPoolDelta, + "Alice: wrong DAI balance" + ); + + assertEq( + balancesAfter.waDAIBuffer.underlying, + balancesBefore.waDAIBuffer.underlying, + "Vault: wrong waDAI underlying buffer balance" + ); + + assertEq( + balancesAfter.waDAIBuffer.wrapped, + balancesBefore.waDAIBuffer.wrapped, + "Vault: wrong waDAI wrapped buffer balance" + ); + } // The pool gains the wrapped tokens from the buffer and mints BPT to the user. assertApproxEqAbs( @@ -1228,7 +1439,8 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { function _checkBalancesAfterRemoveLiquidity( TestBalances memory balancesBefore, TestBalances memory balancesAfter, - TestLocals memory vars + TestLocals memory vars, + bool useWrappedDai ) private { address ybPool = vars.isPartialERC4626Pool ? partialErc4626Pool : pool; uint256 ybDaiIdx = vars.isPartialERC4626Pool ? partialWaDaiIdx : waDaiIdx; @@ -1243,18 +1455,34 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { 2, "ERC4626 Pool: wrong waDAI balance" ); - // The wrapped tokens removed from the pool are unwrapped in the buffer, so the user will receive underlying - // tokens. The buffer loses underlying and gains the wrapped tokens. - assertEq( - balancesAfter.waDAIBuffer.wrapped, - balancesBefore.waDAIBuffer.wrapped + vars.wrappedDaiPoolDelta, - "Vault: wrong waDAI wrapped buffer balance" - ); - assertEq( - balancesAfter.waDAIBuffer.underlying, - balancesBefore.waDAIBuffer.underlying - vars.underlyingDaiAmountDelta, - "Vault: wrong waDAI underlying buffer balance" - ); + + if (useWrappedDai == false) { + // The wrapped tokens removed from the pool are unwrapped in the buffer, so the user will receive underlying + // tokens. The buffer loses underlying and gains the wrapped tokens. + assertEq( + balancesAfter.waDAIBuffer.wrapped, + balancesBefore.waDAIBuffer.wrapped + vars.wrappedDaiPoolDelta, + "Vault: wrong waDAI wrapped buffer balance" + ); + + assertEq( + balancesAfter.waDAIBuffer.underlying, + balancesBefore.waDAIBuffer.underlying - vars.underlyingDaiAmountDelta, + "Vault: wrong waDAI underlying buffer balance" + ); + } else { + assertEq( + balancesAfter.waDAIBuffer.wrapped, + balancesBefore.waDAIBuffer.wrapped, + "Vault: wrong waDAI wrapped buffer balance" + ); + + assertEq( + balancesAfter.waDAIBuffer.underlying, + balancesBefore.waDAIBuffer.underlying, + "Vault: wrong waDAI underlying buffer balance" + ); + } if (vars.isPartialERC4626Pool == false) { // The yield-bearing pool holds yield-bearing tokens, so in a remove liquidity event we remove @@ -1287,12 +1515,21 @@ contract CompositeLiquidityRouterERC4626PoolTest is BaseERC4626BufferTest { ); } - // When removing liquidity, Bob gets underlying tokens. - assertEq( - balancesAfter.balances.bobTokens[balancesAfter.daiIdx], - balancesBefore.balances.bobTokens[balancesBefore.daiIdx] + vars.underlyingDaiAmountDelta, - "Bob: wrong DAI balance" - ); + if (useWrappedDai == false) { + // When removing liquidity, Bob gets underlying tokens. + assertEq( + balancesAfter.balances.bobTokens[balancesAfter.daiIdx], + balancesBefore.balances.bobTokens[balancesBefore.daiIdx] + vars.underlyingDaiAmountDelta, + "Bob: wrong DAI balance" + ); + } else { + // When removing liquidity, Bob gets wrapped tokens. + assertEq( + balancesAfter.balances.bobTokens[balancesAfter.waDaiIdx], + balancesBefore.balances.bobTokens[balancesBefore.waDaiIdx] + vars.wrappedDaiPoolDelta, + "Bob: wrong DAI balance" + ); + } if (vars.wethIsEth) { assertEq(