Skip to content

Commit

Permalink
:)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnquid committed Oct 20, 2024
1 parent 489d9b0 commit 280ed76
Show file tree
Hide file tree
Showing 3 changed files with 1,661 additions and 0 deletions.
247 changes: 247 additions & 0 deletions packages/foundry/contracts/MinimalRouter.sol
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);
}
}
}
Loading

0 comments on commit 280ed76

Please sign in to comment.