LoopFi / Debt and Collateral Scale

[WP-H1] _onWithdraw returns scaledCollateral which is already scaled, while _withdraw scales it again

The amount returned by ICDPVault#withdraw() is scaled at _onWithdraw() implementations. Then it gets scaled again at PositionAction.sol#L627-628.

function _withdraw(
    address vault,
    address position,
    CollateralParams calldata collateralParams
) internal returns (uint256) {
    uint256 collateral = _onWithdraw(vault, position, collateralParams.targetToken, collateralParams.amount, collateralParams.minAmountOut);
    uint256 scaledCollateral = wmul(collateral, ICDPVault(vault).tokenScale());

    // perform swap from collateral to arbitrary token
    if (collateralParams.auxSwap.assetIn != address(0)) {
        SwapParams memory auxSwap = collateralParams.auxSwap;
        if (auxSwap.swapType == SwapType.EXACT_IN) {
            auxSwap.amount = scaledCollateral;
        }
        
        _delegateCall(
            address(swapAction),
            abi.encodeWithSelector(swapAction.swap.selector, auxSwap)
        );
    } else {
        // otherwise just send the collateral to `collateralizer`
        IERC20(collateralParams.targetToken).safeTransfer(collateralParams.collateralizer, scaledCollateral);
    }
    return scaledCollateral;
}
    /// @notice Hook to withdraw collateral from CDPVault, handles any CDP specific actions
    /// @param vault The CDP Vault
    /// @param position The CDP Vault position
    /// @param dst Token the caller expects to receive
    /// @param amount The amount of collateral to deposit [wad]
    /// @param minAmountOut The minimum amount out for the aux swap
    /// @return Amount of collateral (or dst) withdrawn [CDPVault.tokenScale()]
    function _onWithdraw(
        address vault,
        address position,
        address dst,
        uint256 amount,
        uint256 minAmountOut
    ) internal virtual returns (uint256);
@@ 52,55 @@ /// @notice Withdraw collateral from the vault /// @param vault Address of the vault /// @param position Address of the position /// @param amount Amount of collateral to withdraw [wad]
/// @return Amount of collateral withdrawn [CDPVault.tokenScale()] function _onWithdraw( address vault, address position, address /*dst*/, uint256 amount, uint256 /*minAmountOut*/ ) internal override returns (uint256) { // Convert from wad to tokenScale using wdiv uint256 withdrawAmount = ICDPVault(vault).withdraw(position, amount); uint256 scaledAmount = wmul(withdrawAmount, ICDPVault(vault).tokenScale()); return scaledAmount; }
function _withdraw(
    address vault,
    address position,
    CollateralParams calldata collateralParams
) internal returns (uint256) {
    uint256 collateral = _onWithdraw(vault, position, collateralParams.targetToken, collateralParams.amount, collateralParams.minAmountOut);
    uint256 scaledCollateral = wmul(collateral, ICDPVault(vault).tokenScale());

    // perform swap from collateral to arbitrary token
    if (collateralParams.auxSwap.assetIn != address(0)) {
        SwapParams memory auxSwap = collateralParams.auxSwap;
        if (auxSwap.swapType == SwapType.EXACT_IN) {
            auxSwap.amount = scaledCollateral;
        }
        
        _delegateCall(
            address(swapAction),
            abi.encodeWithSelector(swapAction.swap.selector, auxSwap)
        );
    } else {
        // otherwise just send the collateral to `collateralizer`
        IERC20(collateralParams.targetToken).safeTransfer(collateralParams.collateralizer, scaledCollateral);
    }
    return scaledCollateral;
}
    /// @notice Hook to withdraw collateral from CDPVault, handles any CDP specific actions
    /// @param vault The CDP Vault
    /// @param position The CDP Vault position
    /// @param dst Token the caller expects to receive
    /// @param amount The amount of collateral to deposit [wad]
    /// @param minAmountOut The minimum amount out for the aux swap
    /// @return Amount of collateral (or dst) withdrawn [CDPVault.tokenScale()]
    function _onWithdraw(
        address vault,
        address position,
        address dst,
        uint256 amount,
        uint256 minAmountOut
    ) internal virtual returns (uint256);
@@ 52,55 @@ /// @notice Withdraw collateral from the vault /// @param vault Address of the vault /// @param position Address of the position /// @param amount Amount of collateral to withdraw [wad]
/// @return Amount of collateral withdrawn [CDPVault.tokenScale()] function _onWithdraw( address vault, address position, address /*dst*/, uint256 amount, uint256 /*minAmountOut*/ ) internal override returns (uint256) { // Convert from wad to tokenScale using wdiv uint256 withdrawAmount = ICDPVault(vault).withdraw(position, amount); uint256 scaledAmount = wmul(withdrawAmount, ICDPVault(vault).tokenScale()); return scaledAmount; }

[WP-H2] Incorrect Deposit Amount Due to Scale Mismatch

PositionAction4626.sol#L50 already represents the face value of ERC4626's shares (returned by IERC4626(collateral).deposit(amount, address(this))), which is then multiplied by math

This calculation leads to incorrect amount (PositionAction4626.sol#L54-55) values when tokenScale is not equal to 1e18.

    /// @notice Deposit collateral into the vault
    /// @param vault Address of the vault
    /// @param src Token passed in by the caller
    /// @param amount Amount of collateral to deposit [CDPVault.tokenScale()]
    /// @return Amount of collateral deposited [wad]
    function _onDeposit(address vault, address position, address src, uint256 amount) internal override returns (uint256) {
        address collateral = address(ICDPVault(vault).token());

        // if the src is not the collateralToken, we need to deposit the underlying into the ERC4626 vault
        if (src != collateral) {
            address underlying = IERC4626(collateral).asset();
            IERC20(underlying).forceApprove(collateral, amount);
            uint256 depositAmount = IERC4626(collateral).deposit(amount, address(this));
            amount = wmul(depositAmount, ICDPVault(vault).tokenScale());
        }

        IERC20(collateral).forceApprove(vault, amount);
        return ICDPVault(vault).deposit(position, amount);
    }
    /// @notice Deposit collateral into the vault
    /// @param vault Address of the vault
    /// @param src Token passed in by the caller
    /// @param amount Amount of collateral to deposit [CDPVault.tokenScale()]
    /// @return Amount of collateral deposited [wad]
    function _onDeposit(address vault, address position, address src, uint256 amount) internal override returns (uint256) {
        address collateral = address(ICDPVault(vault).token());

        // if the src is not the collateralToken, we need to deposit the underlying into the ERC4626 vault
        if (src != collateral) {
            address underlying = IERC4626(collateral).asset();
            IERC20(underlying).forceApprove(collateral, amount);
            uint256 depositAmount = IERC4626(collateral).deposit(amount, address(this));
            amount = wmul(depositAmount, ICDPVault(vault).tokenScale());
        }

        IERC20(collateral).forceApprove(vault, amount);
        return ICDPVault(vault).deposit(position, amount);
    }

[WP-H3] PositionAction4626._onWithdraw() should return the amount of IERC4626(collateral).asset() tokens when dst == IERC4626(collateral).asset().

This allows the _onWithdraw() consumer to process the correct available IERC4626(collateral).asset() token amount.

Note: In the previous version, L81 was return collateralWithdrawn;

    /// @notice Hook to withdraw collateral from CDPVault, handles any CDP specific actions
    /// @param vault The CDP Vault
    /// @param position The CDP Vault position
    /// @param dst Token the caller expects to receive
    /// @param amount The amount of collateral to deposit [wad]
    /// @param minAmountOut The minimum amount out for the aux swap
    /// @return Amount of collateral (or dst) withdrawn [CDPVault.tokenScale()]
    function _onWithdraw(
        address vault,
        address position,
        address dst,
        uint256 amount,
        uint256 minAmountOut
    ) internal virtual returns (uint256);
    /// @notice Withdraw collateral from the vault
    /// @param vault Address of the vault
    /// @param position Address of the position
    /// @param dst Token the caller expects to receive
    /// @param amount Amount of collateral to withdraw [wad]
    /// @param /*minAmountOut*/ The minimum amount out for the aux swap
    /// @return Amount of collateral withdrawn [CDPVault.tokenScale()]
    function _onWithdraw(
        address vault,
        address position,
        address dst,
        uint256 amount,
        uint256 /*minAmountOut*/
    ) internal override returns (uint256) {
        uint256 collateralWithdrawn = ICDPVault(vault).withdraw(position, amount);
        uint256 scaledAmount = wmul(collateralWithdrawn, ICDPVault(vault).tokenScale());

        // if collateral is not the dst token, we need to withdraw the underlying from the ERC4626 vault
        address collateral = address(ICDPVault(vault).token());
        if (dst != collateral) {
            collateralWithdrawn = IERC4626(collateral).redeem(scaledAmount, address(this), address(this));
        }

        return scaledAmount;
    }
    /// @notice Withdraws collateral from CDPVault (optionally swaps collateral to an arbitrary token)
    /// @param vault The CDP Vault
    /// @param collateralParams The collateral parameters
    /// @return The amount of collateral withdrawn [token.decimals()]
    function _withdraw(
        address vault,
        address position,
        CollateralParams calldata collateralParams
    ) internal returns (uint256) {
        uint256 collateral = _onWithdraw(vault, position, collateralParams.targetToken, collateralParams.amount, collateralParams.minAmountOut);
        uint256 scaledCollateral = wmul(collateral, ICDPVault(vault).tokenScale());

        // perform swap from collateral to arbitrary token
        if (collateralParams.auxSwap.assetIn != address(0)) {
            SwapParams memory auxSwap = collateralParams.auxSwap;
            if (auxSwap.swapType == SwapType.EXACT_IN) {
               auxSwap.amount = scaledCollateral;
            }
            
            _delegateCall(
                address(swapAction),
                abi.encodeWithSelector(swapAction.swap.selector, auxSwap)
            );
        } else {
            // otherwise just send the collateral to `collateralizer`
            IERC20(collateralParams.targetToken).safeTransfer(collateralParams.collateralizer, scaledCollateral);
        }
        return scaledCollateral;
    }

Recommendation

Consider changing to:

https://github.com/LoopFi/loop-contracts/blob/4b5625ca126c9084023e22449982f58374373d61/src/proxy/PositionAction4626.sol#L58-L82

    /// @notice Withdraw collateral from the vault
    /// @param vault Address of the vault
    /// @param position Address of the position
    /// @param dst Token the caller expects to receive
    /// @param amount Amount of collateral to withdraw [wad]
    /// @param /*minAmountOut*/ The minimum amount out for the aux swap
    /// @return Amount of dst token withdrawn [10 ** dst.decimals()]
    function _onWithdraw(
        address vault,
        address position,
        address dst,
        uint256 amount,
        uint256 /*minAmountOut*/
    ) internal override returns (uint256) {
        uint256 collateralWithdrawn = ICDPVault(vault).withdraw(position, amount);
        uint256 scaledAmount = wmul(collateralWithdrawn, ICDPVault(vault).tokenScale());

        // if collateral is not the dst token, we need to withdraw the underlying from the ERC4626 vault
        address collateral = address(ICDPVault(vault).token());
        if (dst == collateral) {
            return scaledAmount;
        } else {
            return IERC4626(collateral).redeem(scaledAmount, address(this), address(this));
        }
    }

https://github.com/LoopFi/loop-contracts/blob/4b5625ca126c9084023e22449982f58374373d61/src/proxy/PositionAction.sol#L181-L194

    /// @notice Hook to withdraw collateral from CDPVault, handles any CDP specific actions
    /// @param vault The CDP Vault
    /// @param position The CDP Vault position
    /// @param dst Token the caller expects to receive
    /// @param amount The amount of collateral to deposit [wad]
    /// @param minAmountOut The minimum amount out for the aux swap
    /// @return Amount of dst token withdrawn [10 ** dst.decimals()]
    function _onWithdraw(
        address vault,
        address position,
        address dst,
        uint256 amount,
        uint256 minAmountOut
    ) internal virtual returns (uint256);

And update the @return NatSpec documentation for other _onWithdraw() implementations.

For consistency and to avoid confusion, it is recommended to rename all instances of "scaled" to follow the convention where scaled*Amount refers to the normalized amount in [wad] units, rather than calling the raw token amount (in [token ** token.decimals()] base) as scaled*Amount.

    /// @notice Hook to withdraw collateral from CDPVault, handles any CDP specific actions
    /// @param vault The CDP Vault
    /// @param position The CDP Vault position
    /// @param dst Token the caller expects to receive
    /// @param amount The amount of collateral to deposit [wad]
    /// @param minAmountOut The minimum amount out for the aux swap
    /// @return Amount of collateral (or dst) withdrawn [CDPVault.tokenScale()]
    function _onWithdraw(
        address vault,
        address position,
        address dst,
        uint256 amount,
        uint256 minAmountOut
    ) internal virtual returns (uint256);
    /// @notice Withdraw collateral from the vault
    /// @param vault Address of the vault
    /// @param position Address of the position
    /// @param dst Token the caller expects to receive
    /// @param amount Amount of collateral to withdraw [wad]
    /// @param /*minAmountOut*/ The minimum amount out for the aux swap
    /// @return Amount of collateral withdrawn [CDPVault.tokenScale()]
    function _onWithdraw(
        address vault,
        address position,
        address dst,
        uint256 amount,
        uint256 /*minAmountOut*/
    ) internal override returns (uint256) {
        uint256 collateralWithdrawn = ICDPVault(vault).withdraw(position, amount);
        uint256 scaledAmount = wmul(collateralWithdrawn, ICDPVault(vault).tokenScale());

        // if collateral is not the dst token, we need to withdraw the underlying from the ERC4626 vault
        address collateral = address(ICDPVault(vault).token());
        if (dst != collateral) {
            collateralWithdrawn = IERC4626(collateral).redeem(scaledAmount, address(this), address(this));
        }

        return scaledAmount;
    }
    /// @notice Withdraws collateral from CDPVault (optionally swaps collateral to an arbitrary token)
    /// @param vault The CDP Vault
    /// @param collateralParams The collateral parameters
    /// @return The amount of collateral withdrawn [token.decimals()]
    function _withdraw(
        address vault,
        address position,
        CollateralParams calldata collateralParams
    ) internal returns (uint256) {
        uint256 collateral = _onWithdraw(vault, position, collateralParams.targetToken, collateralParams.amount, collateralParams.minAmountOut);
        uint256 scaledCollateral = wmul(collateral, ICDPVault(vault).tokenScale());

        // perform swap from collateral to arbitrary token
        if (collateralParams.auxSwap.assetIn != address(0)) {
            SwapParams memory auxSwap = collateralParams.auxSwap;
            if (auxSwap.swapType == SwapType.EXACT_IN) {
               auxSwap.amount = scaledCollateral;
            }
            
            _delegateCall(
                address(swapAction),
                abi.encodeWithSelector(swapAction.swap.selector, auxSwap)
            );
        } else {
            // otherwise just send the collateral to `collateralizer`
            IERC20(collateralParams.targetToken).safeTransfer(collateralParams.collateralizer, scaledCollateral);
        }
        return scaledCollateral;
    }

[WP-H4] PositionActionPendle._onWithdraw() when dst == collateralToken || dst == address(0), it unexpectedly returns the amount in [wad] base instead of the expected [10 ** dst.decimals()] base.

We recommend changing the return value to the amount of dst token withdrawn in [10 ** dst.decimals()] base.

    /// @notice Withdraw collateral from the vault
    /// @param vault Address of the vault
    /// @param amount Amount of collateral to withdraw [wad]
    /// @param minAmountOut The minimum amount out for the aux swap
    /// @return Amount of collateral withdrawn [CDPVault.tokenScale()]
    function _onWithdraw(
        address vault,
        address position,
        address dst,
        uint256 amount,
        uint256 minAmountOut
    ) internal override returns (uint256) {
        uint256 collateralWithdrawn = ICDPVault(vault).withdraw(address(position), amount);
        uint256 scaledAmount = wmul(collateralWithdrawn, ICDPVault(vault).tokenScale());
        address collateralToken = address(ICDPVault(vault).token());

        if (dst != collateralToken && dst != address(0)) {
@@ 70,86 @@ PoolActionParams memory poolActionParams = PoolActionParams({ protocol: Protocol.PENDLE, minOut: minAmountOut, recipient: address(this), args: abi.encode( collateralToken, scaledAmount, dst ) }); bytes memory exitData = _delegateCall( address(poolAction), abi.encodeWithSelector(poolAction.exit.selector, poolActionParams) ); collateralWithdrawn = abi.decode(exitData, (uint256));
} return collateralWithdrawn; }
    /// @notice Withdraw collateral from the vault
    /// @param vault Address of the vault
    /// @param amount Amount of collateral to withdraw [wad]
    /// @param minAmountOut The minimum amount out for the aux swap
    /// @return Amount of collateral withdrawn [CDPVault.tokenScale()]
    function _onWithdraw(
        address vault,
        address position,
        address dst,
        uint256 amount,
        uint256 minAmountOut
    ) internal override returns (uint256) {
        uint256 collateralWithdrawn = ICDPVault(vault).withdraw(address(position), amount);
        uint256 scaledAmount = wmul(collateralWithdrawn, ICDPVault(vault).tokenScale());
        address collateralToken = address(ICDPVault(vault).token());

        if (dst != collateralToken && dst != address(0)) {
@@ 70,86 @@ PoolActionParams memory poolActionParams = PoolActionParams({ protocol: Protocol.PENDLE, minOut: minAmountOut, recipient: address(this), args: abi.encode( collateralToken, scaledAmount, dst ) }); bytes memory exitData = _delegateCall( address(poolAction), abi.encodeWithSelector(poolAction.exit.selector, poolActionParams) ); collateralWithdrawn = abi.decode(exitData, (uint256));
} return collateralWithdrawn; }

[WP-N5] The unit of return value in PositionAction20._onDeposit(), PositionActionPendle._onDeposit(), PositionActionTranchess._onDeposit() is inconsistent with NatSpec documentation and PositionAction4626._onDeposit()

  • NatSpec documentation and PositionAction4626._onDeposit() return value unit: [wad]
  • PositionAction20._onDeposit(), PositionActionPendle._onDeposit(), PositionActionTranchess._onDeposit() return value unit: [CDPVault.tokenScale()]
    /// @notice Deposit collateral into the vault
    /// @param vault Address of the vault
    /// @param amount Amount of collateral to deposit [CDPVault.tokenScale()]
    /// @return Amount of collateral deposited [wad]
    function _onDeposit(
        address vault,
        address position,
        address /*src*/,
        uint256 amount
    ) internal override returns (uint256) {
        address collateralToken = address(ICDPVault(vault).token());
        IERC20(collateralToken).forceApprove(vault, amount);
        uint256 depositAmount = ICDPVault(vault).deposit(position, amount);
        uint256 scaledAmount = wmul(depositAmount, ICDPVault(vault).tokenScale());
        return scaledAmount;
    }
    /// @notice Deposit collateral into the vault
    /// @param vault Address of the vault
    /// @param amount Amount of collateral to deposit [CDPVault.tokenScale()]
    /// @return Amount of collateral deposited [wad]
    function _onDeposit(
        address vault,
        address position,
        address /*src*/,
        uint256 amount
    ) internal override returns (uint256) {
        address collateralToken = address(ICDPVault(vault).token());
        IERC20(collateralToken).forceApprove(vault, amount);
        uint256 depositAmount = ICDPVault(vault).deposit(position, amount);
        uint256 scaledAmount = wmul(depositAmount, ICDPVault(vault).tokenScale());
        return scaledAmount;
    }
    /// @notice Deposit collateral into the vault
    /// @param vault Address of the vault
    /// @param amount Amount of collateral to deposit [CDPVault.tokenScale()]
    /// @return Amount of collateral deposited [wad]
    function _onDeposit(
        address vault,
        address position,
        address /*src*/,
        uint256 amount
    ) internal override returns (uint256) {
        address collateralToken = address(ICDPVault(vault).token());
        IERC20(collateralToken).forceApprove(vault, amount);
        uint256 depositAmount = ICDPVault(vault).deposit(position, amount);
        uint256 scaledAmount = wmul(depositAmount, ICDPVault(vault).tokenScale());
        return scaledAmount;
    }
    /// @notice Deposit collateral into the vault
    /// @param vault Address of the vault
    /// @param amount Amount of collateral to deposit [CDPVault.tokenScale()]
    /// @return Amount of collateral deposited [wad]
    function _onDeposit(
        address vault,
        address position,
        address /*src*/,
        uint256 amount
    ) internal override returns (uint256) {
        address collateralToken = address(ICDPVault(vault).token());
        IERC20(collateralToken).forceApprove(vault, amount);
        uint256 depositAmount = ICDPVault(vault).deposit(position, amount);
        uint256 scaledAmount = wmul(depositAmount, ICDPVault(vault).tokenScale());
        return scaledAmount;
    }
    /// @notice Deposit collateral into the vault
    /// @param vault Address of the vault
    /// @param amount Amount of collateral to deposit [CDPVault.tokenScale()]
    /// @return Amount of collateral deposited [wad]
    function _onDeposit(
        address vault,
        address position,
        address /*src*/,
        uint256 amount
    ) internal override returns (uint256) {
        address collateralToken = address(ICDPVault(vault).token());
        IERC20(collateralToken).forceApprove(vault, amount);
        uint256 depositAmount = ICDPVault(vault).deposit(position, amount);
        uint256 scaledAmount = wmul(depositAmount, ICDPVault(vault).tokenScale());
        return scaledAmount;
    }
    /// @notice Deposit collateral into the vault
    /// @param vault Address of the vault
    /// @param amount Amount of collateral to deposit [CDPVault.tokenScale()]
    /// @return Amount of collateral deposited [wad]
    function _onDeposit(
        address vault,
        address position,
        address /*src*/,
        uint256 amount
    ) internal override returns (uint256) {
        address collateralToken = address(ICDPVault(vault).token());
        IERC20(collateralToken).forceApprove(vault, amount);
        uint256 depositAmount = ICDPVault(vault).deposit(position, amount);
        uint256 scaledAmount = wmul(depositAmount, ICDPVault(vault).tokenScale());
        return scaledAmount;
    }

[WP-N6] Wrong comment: Convert from wad to tokenScale using wmul instead of wdiv

https://github.com/LoopFi/loop-contracts/blob/4b5625ca126c9084023e22449982f58374373d61/src/proxy/PositionAction20.sol#L52-L68

    /// @notice Withdraw collateral from the vault
    /// @param vault Address of the vault
    /// @param position Address of the position
    /// @param amount Amount of collateral to withdraw [wad]
    /// @return Amount of collateral withdrawn [CDPVault.tokenScale()]
    function _onWithdraw(
        address vault,
        address position,
        address /*dst*/,
        uint256 amount,
        uint256 /*minAmountOut*/
    ) internal override returns (uint256) {
        // Convert from wad to tokenScale using wdiv
        uint256 withdrawAmount = ICDPVault(vault).withdraw(position, amount);
        uint256 scaledAmount = wmul(withdrawAmount, ICDPVault(vault).tokenScale());
        return scaledAmount;
    }

[WP-N7] NatSpec Documentation Error: Wrong scale unit in PositionAction4626._onDecreaseLever() @return

    /// @notice Hook to decrease lever by withdrawing collateral from the CDPVault and the ERC4626 Vault
    /// @param leverParams LeverParams struct
    /// @param subCollateral Amount of collateral to withdraw in CDPVault decimals [wad]
    /// @return tokenOut Amount of underlying token withdrawn from the ERC4626 vault [CDPVault.tokenScale()]
    function _onDecreaseLever(
        LeverParams memory leverParams,
        uint256 subCollateral
    ) internal override returns (uint256 tokenOut) {
        // withdraw collateral from vault
        uint256 withdrawnCollateral = ICDPVault(leverParams.vault).withdraw(leverParams.position, subCollateral);
        withdrawnCollateral = wmul(withdrawnCollateral, ICDPVault(leverParams.vault).tokenScale());

        // withdraw collateral from the ERC4626 vault and return underlying assets
        tokenOut = IERC4626(leverParams.collateralToken).redeem(withdrawnCollateral, address(this), address(this));

        if (leverParams.auxAction.args.length != 0) {
            _delegateCall(
                address(poolAction),
                abi.encodeWithSelector(poolAction.exit.selector, leverParams.auxAction)
            );

            tokenOut = IERC20(IERC4626(leverParams.collateralToken).asset()).balanceOf(address(this));
        }
    }

Recommendation

Consider changing to:

    /// @notice Hook to decrease lever by withdrawing collateral from the CDPVault and the ERC4626 Vault
    /// @param leverParams LeverParams struct
    /// @param subCollateral Amount of collateral to withdraw in CDPVault decimals [wad]
    /// @return tokenOut Amount of underlying token withdrawn from the ERC4626 vault [10 ** IERC4626(collateralToken).asset().decimals()]
    function _onDecreaseLever(
        LeverParams memory leverParams,
        uint256 subCollateral
    ) internal override returns (uint256 tokenOut) {
        // withdraw collateral from vault
        uint256 withdrawnCollateral = ICDPVault(leverParams.vault).withdraw(leverParams.position, subCollateral);
        withdrawnCollateral = wmul(withdrawnCollateral, ICDPVault(leverParams.vault).tokenScale());

        // withdraw collateral from the ERC4626 vault and return underlying assets
        tokenOut = IERC4626(leverParams.collateralToken).redeem(withdrawnCollateral, address(this), address(this));

        if (leverParams.auxAction.args.length != 0) {
            _delegateCall(
                address(poolAction),
                abi.encodeWithSelector(poolAction.exit.selector, leverParams.auxAction)
            );

            tokenOut = IERC20(IERC4626(leverParams.collateralToken).asset()).balanceOf(address(this));
        }
    }
    /// @notice Hook to decrease lever by withdrawing collateral from the CDPVault and the ERC4626 Vault
    /// @param leverParams LeverParams struct
    /// @param subCollateral Amount of collateral to withdraw in CDPVault decimals [wad]
    /// @return tokenOut Amount of underlying token withdrawn from the ERC4626 vault [CDPVault.tokenScale()]
    function _onDecreaseLever(
        LeverParams memory leverParams,
        uint256 subCollateral
    ) internal override returns (uint256 tokenOut) {
        // withdraw collateral from vault
        uint256 withdrawnCollateral = ICDPVault(leverParams.vault).withdraw(leverParams.position, subCollateral);
        withdrawnCollateral = wmul(withdrawnCollateral, ICDPVault(leverParams.vault).tokenScale());

        // withdraw collateral from the ERC4626 vault and return underlying assets
        tokenOut = IERC4626(leverParams.collateralToken).redeem(withdrawnCollateral, address(this), address(this));

        if (leverParams.auxAction.args.length != 0) {
            _delegateCall(
                address(poolAction),
                abi.encodeWithSelector(poolAction.exit.selector, leverParams.auxAction)
            );

            tokenOut = IERC20(IERC4626(leverParams.collateralToken).asset()).balanceOf(address(this));
        }
    }