-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
1,661 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
pragma solidity ^0.8.24; | ||
|
||
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; | ||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import { Address } from "@openzeppelin/contracts/utils/Address.sol"; | ||
import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol"; | ||
|
||
import { IWETH } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/misc/IWETH.sol"; | ||
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; | ||
import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; | ||
|
||
import { | ||
ReentrancyGuardTransient | ||
} from "@balancer-labs/v3-solidity-utils/contracts/openzeppelin/ReentrancyGuardTransient.sol"; | ||
|
||
import { RouterCommon } from "@balancer-labs/v3-vault/contracts/RouterCommon.sol"; | ||
|
||
abstract contract MinimalRouter is RouterCommon, ReentrancyGuardTransient { | ||
using Address for address payable; | ||
using SafeCast for *; | ||
|
||
/** | ||
* @notice Data for the add liquidity hook. | ||
* @dev Extends AddLiquidityHookParams to include a receiver. | ||
* @param sender Account originating the add liquidity operation | ||
* @param receiver Account to receive the BPT | ||
* @param pool Address of the liquidity pool | ||
* @param maxAmountsIn Maximum amounts of tokens to be added, sorted in token registration order | ||
* @param minBptAmountOut Minimum amount of pool tokens to be received | ||
* @param kind Type of join (e.g., single or multi-token) | ||
* @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH | ||
* @param userData Additional (optional) data sent with the request to add liquidity | ||
*/ | ||
struct ExtendedAddLiquidityHookParams { | ||
address sender; | ||
address receiver; | ||
address pool; | ||
uint256[] maxAmountsIn; | ||
uint256 minBptAmountOut; | ||
AddLiquidityKind kind; | ||
bool wethIsEth; | ||
bytes userData; | ||
} | ||
|
||
/** | ||
* @notice Data for the remove liquidity hook. | ||
* @dev Extends RemoveLiquidityHookParams to include a receiver. | ||
* @param sender Account originating the remove liquidity operation | ||
* @param receiver Account to receive the tokens | ||
* @param pool Address of the liquidity pool | ||
* @param minAmountsOut Minimum amounts of tokens to be received, sorted in token registration order | ||
* @param maxBptAmountIn Maximum amount of pool tokens provided | ||
* @param kind Type of exit (e.g., single or multi-token) | ||
* @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH | ||
* @param userData Additional (optional) data sent with the request to remove liquidity | ||
*/ | ||
struct ExtendedRemoveLiquidityHookParams { | ||
address sender; | ||
address receiver; | ||
address pool; | ||
uint256[] minAmountsOut; | ||
uint256 maxBptAmountIn; | ||
RemoveLiquidityKind kind; | ||
bool wethIsEth; | ||
bytes userData; | ||
} | ||
|
||
constructor(IVault vault, IWETH weth, IPermit2 permit2) RouterCommon(vault, weth, permit2) { | ||
// solhint-disable-previous-line no-empty-blocks | ||
} | ||
|
||
/*************************************************************************** | ||
Add Liquidity | ||
***************************************************************************/ | ||
|
||
function _addLiquidityProportional( | ||
address pool, | ||
address sender, | ||
address receiver, | ||
uint256[] memory maxAmountsIn, | ||
uint256 exactBptAmountOut, | ||
bool wethIsEth, | ||
bytes memory userData | ||
) internal saveSender returns (uint256[] memory amountsIn) { | ||
(amountsIn, , ) = abi.decode( | ||
_vault.unlock( | ||
abi.encodeWithSelector( | ||
MinimalRouter.addLiquidityHook.selector, | ||
ExtendedAddLiquidityHookParams({ | ||
sender: sender, | ||
receiver: receiver, | ||
pool: pool, | ||
maxAmountsIn: maxAmountsIn, | ||
minBptAmountOut: exactBptAmountOut, | ||
kind: AddLiquidityKind.PROPORTIONAL, | ||
wethIsEth: wethIsEth, | ||
userData: userData | ||
}) | ||
) | ||
), | ||
(uint256[], uint256, bytes) | ||
); | ||
} | ||
|
||
/** | ||
* @notice Hook for adding liquidity. | ||
* @dev Can only be called by the Vault. | ||
* @param params Add liquidity parameters | ||
* @return amountsIn Actual amounts in required for the join | ||
* @return bptAmountOut BPT amount minted in exchange for the input tokens | ||
* @return returnData Arbitrary data with encoded response from the pool | ||
*/ | ||
function addLiquidityHook( | ||
ExtendedAddLiquidityHookParams calldata params | ||
) | ||
external | ||
nonReentrant | ||
onlyVault | ||
returns (uint256[] memory amountsIn, uint256 bptAmountOut, bytes memory returnData) | ||
{ | ||
(amountsIn, bptAmountOut, returnData) = _vault.addLiquidity( | ||
AddLiquidityParams({ | ||
pool: params.pool, | ||
to: params.receiver, | ||
maxAmountsIn: params.maxAmountsIn, | ||
minBptAmountOut: params.minBptAmountOut, | ||
kind: params.kind, | ||
userData: params.userData | ||
}) | ||
); | ||
|
||
// maxAmountsIn length is checked against tokens length at the vault. | ||
IERC20[] memory tokens = _vault.getPoolTokens(params.pool); | ||
|
||
for (uint256 i = 0; i < tokens.length; ++i) { | ||
IERC20 token = tokens[i]; | ||
uint256 amountIn = amountsIn[i]; | ||
|
||
// There can be only one WETH token in the pool. | ||
if (params.wethIsEth && address(token) == address(_weth)) { | ||
if (address(this).balance < amountIn) { | ||
revert InsufficientEth(); | ||
} | ||
|
||
_weth.deposit{ value: amountIn }(); | ||
_weth.transfer(address(_vault), amountIn); | ||
_vault.settle(_weth, amountIn); | ||
} else { | ||
// Any value over MAX_UINT128 would revert above in `addLiquidity`, so this SafeCast shouldn't be | ||
// necessary. Done out of an abundance of caution. | ||
_permit2.transferFrom(params.sender, address(_vault), amountIn.toUint160(), address(token)); | ||
_vault.settle(token, amountIn); | ||
} | ||
} | ||
|
||
// Send remaining ETH to the user. | ||
_returnEth(params.sender); | ||
} | ||
|
||
/*************************************************************************** | ||
Remove Liquidity | ||
***************************************************************************/ | ||
|
||
function _removeLiquidityProportional( | ||
address pool, | ||
address sender, | ||
address receiver, | ||
uint256 exactBptAmountIn, | ||
uint256[] memory minAmountsOut, | ||
bool wethIsEth, | ||
bytes memory userData | ||
) internal saveSender returns (uint256[] memory amountsOut) { | ||
(, amountsOut, ) = abi.decode( | ||
_vault.unlock( | ||
abi.encodeWithSelector( | ||
MinimalRouter.removeLiquidityHook.selector, | ||
ExtendedRemoveLiquidityHookParams({ | ||
sender: sender, | ||
receiver: receiver, | ||
pool: pool, | ||
minAmountsOut: minAmountsOut, | ||
maxBptAmountIn: exactBptAmountIn, | ||
kind: RemoveLiquidityKind.PROPORTIONAL, | ||
wethIsEth: wethIsEth, | ||
userData: userData | ||
}) | ||
) | ||
), | ||
(uint256, uint256[], bytes) | ||
); | ||
} | ||
|
||
/** | ||
* @notice Hook for removing liquidity. | ||
* @dev Can only be called by the Vault. | ||
* @param params Remove liquidity parameters | ||
* @return bptAmountIn BPT amount burned for the output tokens | ||
* @return amountsOut Actual token amounts transferred in exchange for the BPT | ||
* @return returnData Arbitrary (optional) data with an encoded response from the pool | ||
*/ | ||
function removeLiquidityHook( | ||
ExtendedRemoveLiquidityHookParams calldata params | ||
) | ||
external | ||
nonReentrant | ||
onlyVault | ||
returns (uint256 bptAmountIn, uint256[] memory amountsOut, bytes memory returnData) | ||
{ | ||
(bptAmountIn, amountsOut, returnData) = _vault.removeLiquidity( | ||
RemoveLiquidityParams({ | ||
pool: params.pool, | ||
from: params.sender, | ||
maxBptAmountIn: params.maxBptAmountIn, | ||
minAmountsOut: params.minAmountsOut, | ||
kind: params.kind, | ||
userData: params.userData | ||
}) | ||
); | ||
|
||
// minAmountsOut length is checked against tokens length at the vault. | ||
IERC20[] memory tokens = _vault.getPoolTokens(params.pool); | ||
|
||
uint256 ethAmountOut = 0; | ||
for (uint256 i = 0; i < tokens.length; ++i) { | ||
uint256 amountOut = amountsOut[i]; | ||
IERC20 token = tokens[i]; | ||
|
||
// There can be only one WETH token in the pool. | ||
if (params.wethIsEth && address(token) == address(_weth)) { | ||
// Send WETH here and unwrap to native ETH. | ||
_vault.sendTo(_weth, address(this), amountOut); | ||
_weth.withdraw(amountOut); | ||
ethAmountOut = amountOut; | ||
} else { | ||
// Transfer the token to the receiver (amountOut). | ||
_vault.sendTo(token, params.receiver, amountOut); | ||
} | ||
} | ||
|
||
if (ethAmountOut > 0) { | ||
// Send ETH to receiver. | ||
payable(params.receiver).sendValue(ethAmountOut); | ||
} | ||
} | ||
} |
Oops, something went wrong.