BalancerV3: SwapAdapter and Substreams (#126)
* feat: add balancer swapAdapter and Substreams * fix: undo tycho-substreams logs, ignore abi on rustmft * ci: prevent warnings from failing CI * ci: skip size check on CI * chore: forge fmt * feat: vault balance from storage Vault contract tokenBalance message are set according to the vault storage changes in the `_reserveOf` storage variable VaultStorage.sol contract This was the culprit that caused the failure in simulation since balancer enforces the invariant that `token.balanceOf(vault_addr) == _reservesOf[token]` * ci: warnings * fix: avoid duplicated balance changes * fix: order by ordinal * chore: format * feat: extract new contracts before extracting balance changes * feat: skip unnecessary steps if no balance change is found * refactor: filter out account balances for tokens that aren't part of any protocol components. On the indexer side, when we receive an account balance, we need to know about the token. This commit ensure that the token was introduced before we emit any account balance with it. * refactor: don't index liquidity buffers. Liquidity buffers rely on rate providers. Therefore we need DCI (feature to be able to index previously created contract) to deal with them. * refactor: cleanup tests and add docstrings * chore: lock tycho-substreams version * ci: set Foundry workflow to use stable foundry * feat(DCI): Add DCI Entrypoints to BalancerV3 components (#218) * refactor: fix typo in weighted_pool_factory_contract name * feat: add rate_providers static attributes * feat: add DCI entrypoints to BalancerV3 components * fix: set default trade price to Fraction(0, 1) * feat: remove buffers as components Buffers are to be used internally by Boosted pools (stable/weighted pools that use ERC4626 tokens). They are not to be treated as a separate swap component. * test: update test blocks Extend tests some tests block range to ensure liquidity was added to the pool and can be simulated on * feat: remove buffers as components Remove balance updates for buffer components * feat: listen for pool pause/unpause events * chore: formating * fix: encoding call data * test: update Balancer V3 tests to use DCI * test: set indexer log level to info * docs: add comment on support of boosted pools * feat: update balancer v3 package version --------- Co-authored-by: Thales <thales@datarevenue.com> Co-authored-by: zizou <111426680+flopell@users.noreply.github.com> Co-authored-by: Louise Poole <louise@datarevenue.com> Co-authored-by: Louise Poole <louisecarmenpoole@gmail.com>
This commit is contained in:
160
evm/src/balancer-v3/BalancerV3SwapAdapter.sol
Normal file
160
evm/src/balancer-v3/BalancerV3SwapAdapter.sol
Normal file
@@ -0,0 +1,160 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "./lib/BalancerSwapHelpers.sol";
|
||||
|
||||
/**
|
||||
* @title Balancer V3 Swap Adapter
|
||||
* @dev Supports:
|
||||
* Direct Swaps:
|
||||
* - ETH<->ERC20
|
||||
* - ERC20<->ERC20
|
||||
* - ERC4626<->ERC4626
|
||||
* - ERC4626<->ERC20
|
||||
*
|
||||
* 2 steps:
|
||||
* - (ERC20->ERC20)->ERC4626: swap, wrap_0
|
||||
* - (ERC4626->ERC20)->ERC4626: swap, wrap_1
|
||||
*
|
||||
* - (ERC4626->ERC4626)->ERC20: swap, unwrap_0
|
||||
* - (ERC20->ERC4626)->ERC20; swap, unwrap_1
|
||||
*
|
||||
* - ERC20->(ERC4626->ERC4626): wrap, swap_0
|
||||
* - ERC20->(ERC4626->ERC20); wrap, swap_1
|
||||
*
|
||||
* - ERC4626->(ERC20->ERC20): unwrap, swap_0
|
||||
* - ERC4626->(ERC20->ERC4626): unwrap, swap_1
|
||||
*
|
||||
* 3 steps:
|
||||
* - ERC20->(ERC4626->ERC4626)->ERC20
|
||||
* - ERC4626->(ERC20->ERC20)->ERC4626
|
||||
*/
|
||||
contract BalancerV3SwapAdapter is BalancerSwapHelpers {
|
||||
constructor(
|
||||
address payable vault_,
|
||||
address _router,
|
||||
address _permit2,
|
||||
address _WETH_ADDRESS
|
||||
) {
|
||||
vault = IVault(vault_);
|
||||
router = IBatchRouter(_router);
|
||||
permit2 = _permit2;
|
||||
WETH_ADDRESS = _WETH_ADDRESS;
|
||||
}
|
||||
|
||||
/// @dev Enable ETH receiving
|
||||
receive() external payable {}
|
||||
|
||||
/// @inheritdoc ISwapAdapter
|
||||
function price(
|
||||
bytes32 _poolId,
|
||||
address _sellToken,
|
||||
address _buyToken,
|
||||
uint256[] memory _specifiedAmounts
|
||||
) external override returns (Fraction[] memory _prices) {
|
||||
_prices = new Fraction[](_specifiedAmounts.length);
|
||||
|
||||
for (uint256 i = 0; i < _specifiedAmounts.length; i++) {
|
||||
_prices[i] =
|
||||
getPriceAt(_poolId, _sellToken, _buyToken, _specifiedAmounts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// @inheritdoc ISwapAdapter
|
||||
function swap(
|
||||
bytes32 poolId,
|
||||
address sellToken,
|
||||
address buyToken,
|
||||
OrderSide side,
|
||||
uint256 specifiedAmount
|
||||
) external override returns (Trade memory trade) {
|
||||
if (specifiedAmount == 0) {
|
||||
// Price defaults to Fraction(0, 0) which breaks simulation. We need
|
||||
// to explicitly set it.
|
||||
trade.price = Fraction(0, 1);
|
||||
return trade;
|
||||
}
|
||||
|
||||
uint256 gasBefore = gasleft();
|
||||
|
||||
// perform swap (forward to middleware)
|
||||
trade.calculatedAmount =
|
||||
swapMiddleware(poolId, sellToken, buyToken, side, specifiedAmount);
|
||||
|
||||
trade.gasUsed = gasBefore - gasleft();
|
||||
|
||||
// as post-trade price cannot be calculated in an external call, we
|
||||
// return the trade price here
|
||||
trade.price = Fraction(trade.calculatedAmount, specifiedAmount);
|
||||
}
|
||||
|
||||
/// @inheritdoc ISwapAdapter
|
||||
function getLimits(bytes32 poolId, address sellToken, address buyToken)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (uint256[] memory limits)
|
||||
{
|
||||
limits = getLimitsMiddleware(poolId, sellToken, buyToken);
|
||||
}
|
||||
|
||||
/// @inheritdoc ISwapAdapter
|
||||
function getCapabilities(bytes32, address, address)
|
||||
external
|
||||
pure
|
||||
override
|
||||
returns (Capability[] memory capabilities)
|
||||
{
|
||||
capabilities = new Capability[](3);
|
||||
capabilities[0] = Capability.SellOrder;
|
||||
capabilities[1] = Capability.BuyOrder;
|
||||
capabilities[2] = Capability.HardLimits;
|
||||
}
|
||||
|
||||
/// @inheritdoc ISwapAdapter
|
||||
function getTokens(bytes32 poolId)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (address[] memory tokens)
|
||||
{
|
||||
address poolAddress = address(bytes20(poolId));
|
||||
// Is accessing to vault to get the tokens of a pool / Here could be
|
||||
// where it was reverting the test
|
||||
IERC20[] memory tokens_ = vault.getPoolTokens(poolAddress);
|
||||
tokens = new address[](tokens_.length);
|
||||
|
||||
for (uint256 i = 0; i < tokens_.length; i++) {
|
||||
tokens[i] = address(tokens_[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function getPoolIds(uint256, uint256)
|
||||
external
|
||||
pure
|
||||
override
|
||||
returns (bytes32[] memory)
|
||||
{
|
||||
revert NotImplemented("BalancerV3SwapAdapter.getPoolIds");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the price of the swap
|
||||
* @dev The price is not scaled by the token decimals
|
||||
* @param pool The ID of the trading pool.
|
||||
* @param sellToken The token being sold.
|
||||
* @param buyToken The token being bought.
|
||||
* @param specifiedAmount The amount to be traded.
|
||||
*/
|
||||
function getPriceAt(
|
||||
bytes32 pool,
|
||||
address sellToken,
|
||||
address buyToken,
|
||||
uint256 specifiedAmount
|
||||
) internal returns (Fraction memory calculatedPrice) {
|
||||
calculatedPrice = Fraction(
|
||||
getAmountOutMiddleware(pool, sellToken, buyToken, specifiedAmount),
|
||||
specifiedAmount
|
||||
);
|
||||
}
|
||||
}
|
||||
511
evm/src/balancer-v3/lib/BalancerCustomWrapHelpers.sol
Normal file
511
evm/src/balancer-v3/lib/BalancerCustomWrapHelpers.sol
Normal file
@@ -0,0 +1,511 @@
|
||||
//SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "./BalancerERC20Helpers.sol";
|
||||
|
||||
abstract contract BalancerCustomWrapHelpers is BalancerERC20Helpers {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
function isERC4626(address token) internal view returns (bool) {
|
||||
if (token == WETH_ADDRESS) {
|
||||
return false;
|
||||
}
|
||||
try IERC4626(token).asset() {
|
||||
try IERC4626(token).maxRedeem(msg.sender) {
|
||||
return true;
|
||||
} catch {
|
||||
// Proceed to the next try-catch
|
||||
}
|
||||
} catch {
|
||||
// return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getCustomWrap(address sellToken, address buyToken, address pool)
|
||||
internal
|
||||
view
|
||||
returns (
|
||||
CUSTOM_WRAP_KIND kind,
|
||||
address sellTokenOutput,
|
||||
address buyTokenOutput
|
||||
)
|
||||
{
|
||||
IERC20[] memory tokens = vault.getPoolTokens(pool);
|
||||
|
||||
if (isERC4626(sellToken) && isERC4626(buyToken)) {
|
||||
// 4626-(20-20)-4626
|
||||
address sellTokenAsset = IERC4626(sellToken).asset();
|
||||
address buyTokenAsset = IERC4626(buyToken).asset();
|
||||
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
if (
|
||||
sellTokenOutput != address(0)
|
||||
&& buyTokenOutput != address(0)
|
||||
) {
|
||||
// prevent other findings, use the firsts as default
|
||||
break;
|
||||
}
|
||||
|
||||
if (token == sellTokenAsset) {
|
||||
sellTokenOutput = token; // asset
|
||||
}
|
||||
if (token == buyTokenAsset) {
|
||||
buyTokenOutput = token; // asset
|
||||
}
|
||||
}
|
||||
|
||||
require(
|
||||
sellTokenOutput != address(0) && buyTokenOutput != address(0),
|
||||
"CUSTOM_WRAP(4626-4626): Invalid Pool"
|
||||
);
|
||||
kind = CUSTOM_WRAP_KIND.ERC4626_TO_ERC4626;
|
||||
} else if (!isERC4626(sellToken) && !isERC4626(buyToken)) {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
if (isERC4626(token)) {
|
||||
if (
|
||||
sellTokenOutput != address(0)
|
||||
&& buyTokenOutput != address(0)
|
||||
) {
|
||||
// prevent other findings, use the firsts as default
|
||||
break;
|
||||
}
|
||||
|
||||
if (IERC4626(token).asset() == sellToken) {
|
||||
sellTokenOutput = token; // share
|
||||
}
|
||||
if (IERC4626(token).asset() == buyToken) {
|
||||
buyTokenOutput = token; // share
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require(
|
||||
sellTokenOutput != address(0) && buyTokenOutput != address(0),
|
||||
"CUSTOM_WRAP(4626-4626): Invalid Pool"
|
||||
);
|
||||
kind = CUSTOM_WRAP_KIND.ERC20_TO_ERC20;
|
||||
} else {
|
||||
revert("CUSTOM_WRAP: Invalid tokens");
|
||||
}
|
||||
}
|
||||
|
||||
function prepareSellCustomWrap(
|
||||
address pool,
|
||||
address _sellToken,
|
||||
address buyToken,
|
||||
uint256 specifiedAmount,
|
||||
CUSTOM_WRAP_KIND kind,
|
||||
address sellTokenOutput,
|
||||
address buyTokenOutput
|
||||
)
|
||||
internal
|
||||
view
|
||||
returns (IBatchRouter.SwapPathExactAmountIn[] memory paths)
|
||||
{
|
||||
IBatchRouter.SwapPathStep[] memory steps =
|
||||
new IBatchRouter.SwapPathStep[](3);
|
||||
|
||||
if (kind == CUSTOM_WRAP_KIND.ERC20_TO_ERC20) {
|
||||
// Step 1: sellToken.asset() -> sellToken.shares()
|
||||
(,, IBatchRouter.SwapPathStep memory step0) = createWrapOrUnwrapPath(
|
||||
sellTokenOutput,
|
||||
specifiedAmount,
|
||||
IVault.WrappingDirection.WRAP,
|
||||
false
|
||||
);
|
||||
steps[0] = step0;
|
||||
|
||||
// Step 2: sellToken.shares() -> buyToken.shares()
|
||||
(,, IBatchRouter.SwapPathStep memory step1) = createERC20Path(
|
||||
pool,
|
||||
IERC20(sellTokenOutput),
|
||||
IERC20(buyTokenOutput),
|
||||
specifiedAmount,
|
||||
false,
|
||||
false
|
||||
);
|
||||
steps[1] = step1;
|
||||
|
||||
// Step 3: buyToken.shares() -> buyToken.asset()
|
||||
(,, IBatchRouter.SwapPathStep memory step2) = createWrapOrUnwrapPath(
|
||||
buyTokenOutput,
|
||||
specifiedAmount,
|
||||
IVault.WrappingDirection.UNWRAP,
|
||||
false
|
||||
);
|
||||
steps[2] = step2;
|
||||
|
||||
paths = new IBatchRouter.SwapPathExactAmountIn[](1);
|
||||
paths[0] = IBatchRouter.SwapPathExactAmountIn({
|
||||
tokenIn: IERC20(_sellToken),
|
||||
steps: steps,
|
||||
exactAmountIn: specifiedAmount,
|
||||
minAmountOut: 1
|
||||
});
|
||||
} else {
|
||||
// ERC4626_TO_ERC4626
|
||||
// Step 1: sellToken.shares() -> sellToken.asset()
|
||||
(,, IBatchRouter.SwapPathStep memory step0) = createWrapOrUnwrapPath(
|
||||
_sellToken,
|
||||
specifiedAmount,
|
||||
IVault.WrappingDirection.UNWRAP,
|
||||
false
|
||||
);
|
||||
steps[0] = step0;
|
||||
|
||||
// Step 2: sellToken.asset() -> buyToken.asset()
|
||||
(,, IBatchRouter.SwapPathStep memory step1) = createERC20Path(
|
||||
pool,
|
||||
IERC20(sellTokenOutput),
|
||||
IERC20(buyTokenOutput),
|
||||
specifiedAmount,
|
||||
false,
|
||||
false
|
||||
);
|
||||
steps[1] = step1;
|
||||
|
||||
// Step 3: buyToken.asset() -> buyToken.shares()
|
||||
(,, IBatchRouter.SwapPathStep memory step2) = createWrapOrUnwrapPath(
|
||||
buyToken, specifiedAmount, IVault.WrappingDirection.WRAP, false
|
||||
);
|
||||
steps[2] = step2;
|
||||
|
||||
paths = new IBatchRouter.SwapPathExactAmountIn[](1);
|
||||
paths[0] = IBatchRouter.SwapPathExactAmountIn({
|
||||
tokenIn: IERC20(_sellToken),
|
||||
steps: steps,
|
||||
exactAmountIn: specifiedAmount,
|
||||
minAmountOut: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get amount out for custom wrap
|
||||
*/
|
||||
function getAmountOutCustomWrap(
|
||||
address pool,
|
||||
address _sellToken,
|
||||
address buyToken,
|
||||
uint256 specifiedAmount,
|
||||
CUSTOM_WRAP_KIND kind,
|
||||
address sellTokenOutput,
|
||||
address buyTokenOutput
|
||||
) internal returns (uint256 calculatedAmount) {
|
||||
IBatchRouter.SwapPathExactAmountIn[] memory paths =
|
||||
prepareSellCustomWrap(
|
||||
pool,
|
||||
_sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
kind,
|
||||
sellTokenOutput,
|
||||
buyTokenOutput
|
||||
);
|
||||
|
||||
(,, uint256[] memory amountsOut) =
|
||||
router.querySwapExactIn(paths, address(0), bytes(""));
|
||||
|
||||
calculatedAmount = amountsOut[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Perform a custom sell with wrap/unwrap
|
||||
* @dev
|
||||
* - Does not support ETH(gas), use wrapped ETH instead
|
||||
* @param pool the ERC4626 pool containing sellToken.share() and
|
||||
* buyToken.share(), or the ERC20 pool containing sellToken.asset() and
|
||||
* buyToken.asset(), depending on the kind
|
||||
* @param _sellToken ERC20 token being sold if kind == ERC20_TO_ERC20,
|
||||
* ERC4626 else
|
||||
* @param _buyToken ERC20 token being bought if kind == ERC20_TO_ERC20,
|
||||
* ERC4626 else
|
||||
* @param kind The Custom wrap kind
|
||||
* @param sellTokenOutput sellToken.share() if sellToken is kind ==
|
||||
* ERC20_TO_ERC20, sellToken.asset() else
|
||||
* @param buyTokenOutput buyToken.share() if sellToken is kind ==
|
||||
* ERC20_TO_ERC20, buyToken.asset() else
|
||||
* @param specifiedAmount The amount of _buyToken bought
|
||||
*/
|
||||
function sellCustomWrap(
|
||||
address pool,
|
||||
address _sellToken,
|
||||
address _buyToken,
|
||||
uint256 specifiedAmount,
|
||||
CUSTOM_WRAP_KIND kind,
|
||||
address sellTokenOutput,
|
||||
address buyTokenOutput
|
||||
) internal returns (uint256 calculatedAmount) {
|
||||
IERC20 sellToken = IERC20(_sellToken);
|
||||
|
||||
// approve and transfer
|
||||
IERC20(sellToken).safeTransferFrom(
|
||||
msg.sender, address(this), specifiedAmount
|
||||
);
|
||||
sellToken.safeIncreaseAllowance(permit2, specifiedAmount);
|
||||
IPermit2(permit2).approve(
|
||||
address(sellToken),
|
||||
address(router),
|
||||
type(uint160).max,
|
||||
type(uint48).max
|
||||
);
|
||||
|
||||
IBatchRouter.SwapPathExactAmountIn[] memory paths =
|
||||
prepareSellCustomWrap(
|
||||
pool,
|
||||
_sellToken,
|
||||
_buyToken,
|
||||
specifiedAmount,
|
||||
kind,
|
||||
sellTokenOutput,
|
||||
buyTokenOutput
|
||||
);
|
||||
|
||||
(,, uint256[] memory amountsOut) =
|
||||
router.swapExactIn(paths, type(uint256).max, false, bytes(""));
|
||||
|
||||
calculatedAmount = amountsOut[0];
|
||||
|
||||
IERC20(_buyToken).safeTransfer(msg.sender, calculatedAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Perform a custom sell with wrap/unwrap
|
||||
* @param specifiedAmount The amount of buyToken to buy
|
||||
* @return calculatedAmount The amount of sellToken spent
|
||||
*/
|
||||
function buyCustomWrap(
|
||||
address pool,
|
||||
address _sellToken,
|
||||
address _buyToken,
|
||||
uint256 specifiedAmount,
|
||||
CUSTOM_WRAP_KIND kind,
|
||||
address sellTokenOutput,
|
||||
address buyTokenOutput
|
||||
) internal returns (uint256 calculatedAmount) {
|
||||
IBatchRouter.SwapPathStep[] memory steps =
|
||||
new IBatchRouter.SwapPathStep[](3);
|
||||
IERC20 sellToken = IERC20(_sellToken);
|
||||
|
||||
// get balance of sender
|
||||
uint256 initialSenderBalance = IERC20(sellToken).balanceOf(msg.sender);
|
||||
|
||||
// approve and transfer
|
||||
IERC20(sellToken).safeTransferFrom(
|
||||
msg.sender, address(this), initialSenderBalance
|
||||
);
|
||||
sellToken.safeIncreaseAllowance(permit2, type(uint256).max);
|
||||
IPermit2(permit2).approve(
|
||||
address(sellToken),
|
||||
address(router),
|
||||
type(uint160).max,
|
||||
type(uint48).max
|
||||
);
|
||||
|
||||
if (kind == CUSTOM_WRAP_KIND.ERC20_TO_ERC20) {
|
||||
// Step 1: sellToken.asset() -> sellToken.shares()
|
||||
(,, IBatchRouter.SwapPathStep memory step0) = createWrapOrUnwrapPath(
|
||||
sellTokenOutput,
|
||||
specifiedAmount,
|
||||
IVault.WrappingDirection.WRAP,
|
||||
false
|
||||
);
|
||||
steps[0] = step0;
|
||||
|
||||
// Step 2: sellToken.shares() -> buyToken.shares()
|
||||
(,, IBatchRouter.SwapPathStep memory step1) = createERC20Path(
|
||||
pool,
|
||||
IERC4626(sellTokenOutput),
|
||||
IERC4626(buyTokenOutput),
|
||||
specifiedAmount,
|
||||
true,
|
||||
false
|
||||
);
|
||||
steps[1] = step1;
|
||||
|
||||
// Step 3: buyToken.shares() -> buyToken.asset()
|
||||
(,, IBatchRouter.SwapPathStep memory step2) = createWrapOrUnwrapPath(
|
||||
buyTokenOutput,
|
||||
specifiedAmount,
|
||||
IVault.WrappingDirection.UNWRAP,
|
||||
true
|
||||
);
|
||||
steps[2] = step2;
|
||||
|
||||
IBatchRouter.SwapPathExactAmountOut[] memory paths =
|
||||
new IBatchRouter.SwapPathExactAmountOut[](1);
|
||||
paths[0] = IBatchRouter.SwapPathExactAmountOut({
|
||||
tokenIn: IERC20(_sellToken),
|
||||
steps: steps,
|
||||
maxAmountIn: initialSenderBalance,
|
||||
exactAmountOut: specifiedAmount
|
||||
});
|
||||
|
||||
(,, uint256[] memory amountsIn) =
|
||||
router.swapExactOut(paths, type(uint256).max, false, bytes(""));
|
||||
|
||||
calculatedAmount = amountsIn[0];
|
||||
|
||||
IERC20(_buyToken).safeTransfer(msg.sender, specifiedAmount);
|
||||
} else {
|
||||
// ERC4626_TO_ERC4626
|
||||
// Step 1: sellToken.shares() -> sellToken.asset()
|
||||
(,, IBatchRouter.SwapPathStep memory step0) = createWrapOrUnwrapPath(
|
||||
_sellToken,
|
||||
specifiedAmount,
|
||||
IVault.WrappingDirection.UNWRAP,
|
||||
true
|
||||
);
|
||||
steps[0] = step0;
|
||||
|
||||
// Step 2: sellToken.asset() -> buyToken.asset()
|
||||
(,, IBatchRouter.SwapPathStep memory step1) = createERC20Path(
|
||||
pool,
|
||||
IERC20(sellTokenOutput),
|
||||
IERC20(buyTokenOutput),
|
||||
specifiedAmount,
|
||||
true,
|
||||
false
|
||||
);
|
||||
steps[1] = step1;
|
||||
|
||||
// Step 3: buyToken.asset() -> buyToken.shares()
|
||||
(,, IBatchRouter.SwapPathStep memory step2) = createWrapOrUnwrapPath(
|
||||
_buyToken, specifiedAmount, IVault.WrappingDirection.WRAP, false
|
||||
);
|
||||
steps[2] = step2;
|
||||
|
||||
IBatchRouter.SwapPathExactAmountOut[] memory paths =
|
||||
new IBatchRouter.SwapPathExactAmountOut[](1);
|
||||
paths[0] = IBatchRouter.SwapPathExactAmountOut({
|
||||
tokenIn: IERC20(_sellToken),
|
||||
steps: steps,
|
||||
maxAmountIn: initialSenderBalance,
|
||||
exactAmountOut: specifiedAmount
|
||||
});
|
||||
|
||||
(,, uint256[] memory amountsIn) =
|
||||
router.swapExactOut(paths, type(uint256).max, false, bytes(""));
|
||||
|
||||
calculatedAmount = amountsIn[0];
|
||||
|
||||
IERC20(_buyToken).safeTransfer(msg.sender, specifiedAmount);
|
||||
}
|
||||
|
||||
// transfer back sellToken to sender
|
||||
IERC20(sellToken).safeTransferFrom(
|
||||
address(this), msg.sender, initialSenderBalance - calculatedAmount
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Create a wrap or unwrap path in BalancerV3 router using buffer
|
||||
* pools
|
||||
* @param token (ERC4626) token to Wrap or Unwrap
|
||||
* @param amount Amount to buy if isBuy, amount to sell else
|
||||
* @param direction Wrap or Unwrap
|
||||
* @param isBuy True if buy, false if sell
|
||||
*/
|
||||
function createWrapOrUnwrapPath(
|
||||
address token,
|
||||
uint256 amount,
|
||||
IVault.WrappingDirection direction,
|
||||
bool isBuy
|
||||
)
|
||||
internal
|
||||
view
|
||||
returns (
|
||||
IBatchRouter.SwapPathExactAmountIn memory sellPath,
|
||||
IBatchRouter.SwapPathExactAmountOut memory buyPath,
|
||||
IBatchRouter.SwapPathStep memory step
|
||||
)
|
||||
{
|
||||
step = IBatchRouter.SwapPathStep({
|
||||
pool: token,
|
||||
tokenOut: direction == IVault.WrappingDirection.UNWRAP
|
||||
? IERC20(IERC4626(token).asset())
|
||||
: IERC20(token),
|
||||
isBuffer: true
|
||||
});
|
||||
IBatchRouter.SwapPathStep[] memory steps =
|
||||
new IBatchRouter.SwapPathStep[](1);
|
||||
steps[0] = step;
|
||||
|
||||
if (isBuy) {
|
||||
buyPath = IBatchRouter.SwapPathExactAmountOut({
|
||||
tokenIn: direction == IVault.WrappingDirection.UNWRAP
|
||||
? IERC20(token)
|
||||
: IERC20(IERC4626(token).asset()),
|
||||
steps: steps,
|
||||
maxAmountIn: direction == IVault.WrappingDirection.UNWRAP
|
||||
? IERC20(token).balanceOf(address(this))
|
||||
: IERC20(IERC4626(token).asset()).balanceOf(address(this)),
|
||||
exactAmountOut: amount
|
||||
});
|
||||
} else {
|
||||
sellPath = IBatchRouter.SwapPathExactAmountIn({
|
||||
tokenIn: direction == IVault.WrappingDirection.UNWRAP
|
||||
? IERC20(token)
|
||||
: IERC20(IERC4626(token).asset()),
|
||||
steps: steps,
|
||||
exactAmountIn: amount,
|
||||
minAmountOut: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getLimitsCustomWrap(
|
||||
bytes32 poolId,
|
||||
address sellToken,
|
||||
address buyToken,
|
||||
CUSTOM_WRAP_KIND kind,
|
||||
address sellTokenOutput,
|
||||
address buyTokenOutput
|
||||
) internal view returns (uint256[] memory limits) {
|
||||
limits = new uint256[](2);
|
||||
address pool = address(bytes20(poolId));
|
||||
|
||||
(IERC20[] memory tokens,, uint256[] memory balancesRaw,) =
|
||||
vault.getPoolTokenInfo(pool);
|
||||
|
||||
if (kind == CUSTOM_WRAP_KIND.ERC20_TO_ERC20) {
|
||||
// pool contains sellToken.share() and buyToken.share()
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
|
||||
if (token == sellTokenOutput) {
|
||||
limits[0] = IERC4626(sellTokenOutput).previewRedeem(
|
||||
(balancesRaw[i] * RESERVE_LIMIT_FACTOR) / 10
|
||||
);
|
||||
} else if (token == buyTokenOutput) {
|
||||
limits[1] = IERC4626(buyTokenOutput).previewRedeem(
|
||||
(balancesRaw[i] * RESERVE_LIMIT_FACTOR) / 10
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return limits;
|
||||
}
|
||||
|
||||
// pool contains sellToken.asset() and buyToken.asset()
|
||||
IERC20 underlyingSellToken = IERC20(IERC4626(sellToken).asset());
|
||||
IERC20 underlyingBuyToken = IERC20(IERC4626(buyToken).asset());
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
if (tokens[i] == underlyingSellToken) {
|
||||
limits[0] = IERC4626(sellToken).previewDeposit(
|
||||
(balancesRaw[i] * RESERVE_LIMIT_FACTOR) / 10
|
||||
);
|
||||
}
|
||||
if (tokens[i] == underlyingBuyToken) {
|
||||
limits[1] = IERC4626(buyToken).previewDeposit(
|
||||
(balancesRaw[i] * RESERVE_LIMIT_FACTOR) / 10
|
||||
);
|
||||
}
|
||||
}
|
||||
return limits;
|
||||
}
|
||||
}
|
||||
314
evm/src/balancer-v3/lib/BalancerERC20Helpers.sol
Normal file
314
evm/src/balancer-v3/lib/BalancerERC20Helpers.sol
Normal file
@@ -0,0 +1,314 @@
|
||||
//SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "./BalancerStorage.sol";
|
||||
|
||||
abstract contract BalancerERC20Helpers is BalancerStorage {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
/**
|
||||
* @dev Returns the amount of sellToken tokens to spend for a trade
|
||||
* @param path The path to get amountIn for
|
||||
* @return amountIn The amount of tokens to spend.
|
||||
*/
|
||||
function getAmountIn(IBatchRouter.SwapPathExactAmountOut memory path)
|
||||
internal
|
||||
returns (uint256 amountIn)
|
||||
{
|
||||
bytes memory userData; // empty bytes
|
||||
|
||||
IBatchRouter.SwapPathExactAmountOut[] memory paths =
|
||||
new IBatchRouter.SwapPathExactAmountOut[](1);
|
||||
paths[0] = path;
|
||||
|
||||
(,, uint256[] memory amountsIn) =
|
||||
router.querySwapExactOut(paths, address(0), userData);
|
||||
|
||||
// return
|
||||
amountIn = amountsIn[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the amount of buyToken tokens received from a trade
|
||||
* @param path The path of the trade.
|
||||
* @return amountOut The amount of tokens to receive.
|
||||
*/
|
||||
function getAmountOut(IBatchRouter.SwapPathExactAmountIn memory path)
|
||||
internal
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
bytes memory userData; // empty bytes
|
||||
|
||||
IBatchRouter.SwapPathExactAmountIn[] memory paths =
|
||||
new IBatchRouter.SwapPathExactAmountIn[](1);
|
||||
paths[0] = path;
|
||||
|
||||
(,, uint256[] memory amountsOut) =
|
||||
router.querySwapExactIn(paths, address(this), userData);
|
||||
|
||||
amountOut = amountsOut[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Perform a sell order for ERC20 tokens
|
||||
* @param pool The address of the pool to trade in.
|
||||
* @param sellToken The token being sold.
|
||||
* @param buyToken The token being bought.
|
||||
* @param specifiedAmount The amount to be traded.
|
||||
* @param performTransfer Whether to perform a transfer to msg.sender or
|
||||
* not(keeping tokens in the contract)
|
||||
* @return calculatedAmount The amount of tokens received.
|
||||
*/
|
||||
function sellERC20ForERC20(
|
||||
address pool,
|
||||
IERC20 sellToken,
|
||||
IERC20 buyToken,
|
||||
uint256 specifiedAmount,
|
||||
bool performTransfer
|
||||
) internal returns (uint256 calculatedAmount) {
|
||||
// prepare constants
|
||||
bytes memory userData;
|
||||
bool isETHSell = address(sellToken) == address(0);
|
||||
bool isETHBuy = address(buyToken) == address(0);
|
||||
|
||||
// prepare path
|
||||
(IBatchRouter.SwapPathExactAmountIn memory sellPath,,) = createERC20Path(
|
||||
pool,
|
||||
sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
false,
|
||||
isETHSell || isETHBuy
|
||||
);
|
||||
IBatchRouter.SwapPathExactAmountIn[] memory paths =
|
||||
new IBatchRouter.SwapPathExactAmountIn[](1);
|
||||
paths[0] = sellPath;
|
||||
|
||||
// prepare swap
|
||||
uint256[] memory amountsOut;
|
||||
if (isETHSell) {
|
||||
paths[0].tokenIn = IERC20(WETH_ADDRESS);
|
||||
} else {
|
||||
if (isETHBuy) {
|
||||
// adjust parameters for ETH buy
|
||||
paths[0].steps[0].tokenOut = IERC20(WETH_ADDRESS);
|
||||
}
|
||||
// Approve and Transfer ERC20 token
|
||||
sellToken.safeTransferFrom(
|
||||
msg.sender, address(this), specifiedAmount
|
||||
);
|
||||
|
||||
sellToken.safeIncreaseAllowance(permit2, specifiedAmount);
|
||||
IPermit2(permit2).approve(
|
||||
address(sellToken),
|
||||
address(router),
|
||||
type(uint160).max,
|
||||
type(uint48).max
|
||||
);
|
||||
}
|
||||
|
||||
// Swap (incl. WETH)
|
||||
if (isETHSell) {
|
||||
(,, amountsOut) = router.swapExactIn{value: specifiedAmount}(
|
||||
paths, type(uint256).max, isETHSell || isETHBuy, userData
|
||||
);
|
||||
} else {
|
||||
(,, amountsOut) = router.swapExactIn(
|
||||
paths, type(uint256).max, isETHSell || isETHBuy, userData
|
||||
);
|
||||
}
|
||||
|
||||
// transfer if required
|
||||
if (performTransfer) {
|
||||
if (isETHBuy) {
|
||||
(bool sent,) =
|
||||
payable(msg.sender).call{value: amountsOut[0]}("");
|
||||
require(sent, "Failed to transfer ETH");
|
||||
} else {
|
||||
buyToken.safeTransfer(msg.sender, amountsOut[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// return amount
|
||||
calculatedAmount = amountsOut[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Perform a buy order for ERC20 tokens
|
||||
* @param pool The address of the pool to trade in.
|
||||
* @param sellToken The token being sold.
|
||||
* @param buyToken The token being bought.
|
||||
* @param specifiedAmount The amount to be traded.
|
||||
* @param performTransfer Whether to perform a transfer to msg.sender or
|
||||
* not(keeping tokens in the contract)
|
||||
* @return calculatedAmount The amount of tokens received.
|
||||
*/
|
||||
function buyERC20WithERC20(
|
||||
address pool,
|
||||
IERC20 sellToken,
|
||||
IERC20 buyToken,
|
||||
uint256 specifiedAmount,
|
||||
bool performTransfer
|
||||
) internal returns (uint256 calculatedAmount) {
|
||||
// prepare constants
|
||||
bytes memory userData;
|
||||
bool isETHSell = address(sellToken) == address(0);
|
||||
bool isETHBuy = address(buyToken) == address(0);
|
||||
uint256 msgSenderBalance =
|
||||
isETHSell ? address(this).balance : sellToken.balanceOf(msg.sender);
|
||||
|
||||
// prepare path
|
||||
(, IBatchRouter.SwapPathExactAmountOut memory buyPath,) =
|
||||
createERC20Path(
|
||||
pool,
|
||||
sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
true,
|
||||
isETHSell || isETHBuy
|
||||
);
|
||||
IBatchRouter.SwapPathExactAmountOut[] memory paths =
|
||||
new IBatchRouter.SwapPathExactAmountOut[](1);
|
||||
paths[0] = buyPath;
|
||||
|
||||
// prepare swap
|
||||
uint256[] memory amountsIn;
|
||||
if (isETHSell) {
|
||||
// Set token in as WETH
|
||||
paths[0].tokenIn = IERC20(WETH_ADDRESS);
|
||||
} else {
|
||||
if (isETHBuy) {
|
||||
// adjust parameters for ETH buy
|
||||
paths[0].steps[0].tokenOut = IERC20(WETH_ADDRESS);
|
||||
}
|
||||
|
||||
// Approve and Transfer ERC20 token
|
||||
sellToken.safeTransferFrom(
|
||||
msg.sender, address(this), msgSenderBalance
|
||||
);
|
||||
sellToken.safeIncreaseAllowance(address(router), type(uint256).max);
|
||||
sellToken.safeIncreaseAllowance(permit2, type(uint256).max);
|
||||
IPermit2(permit2).approve(
|
||||
address(sellToken),
|
||||
address(router),
|
||||
type(uint160).max,
|
||||
type(uint48).max
|
||||
);
|
||||
}
|
||||
|
||||
// perform swap
|
||||
if (isETHSell) {
|
||||
(,, amountsIn) = router.swapExactOut{value: msgSenderBalance}(
|
||||
paths, type(uint256).max, isETHSell || isETHBuy, userData
|
||||
);
|
||||
} else {
|
||||
(,, amountsIn) = router.swapExactOut(
|
||||
paths, type(uint256).max, isETHSell || isETHBuy, userData
|
||||
);
|
||||
}
|
||||
|
||||
// transfer if required
|
||||
if (performTransfer) {
|
||||
if (isETHBuy) {
|
||||
(bool sent,) =
|
||||
payable(msg.sender).call{value: specifiedAmount}("");
|
||||
require(sent, "Failed to transfer ETH");
|
||||
} else {
|
||||
buyToken.safeTransfer(msg.sender, specifiedAmount);
|
||||
}
|
||||
}
|
||||
|
||||
// return amount
|
||||
calculatedAmount = amountsIn[0];
|
||||
|
||||
// re-transfer back funds to msg.sender
|
||||
if (isETHSell) {
|
||||
(bool sent2,) = payable(msg.sender).call{
|
||||
value: msgSenderBalance - calculatedAmount
|
||||
}("");
|
||||
require(sent2, "Failed to transfer ETH(2)");
|
||||
} else {
|
||||
sellToken.safeTransfer(
|
||||
msg.sender, msgSenderBalance - calculatedAmount
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Create a ERC20 swap path in BalancerV3 router
|
||||
* @param sellToken (ERC20) token to Sell
|
||||
* @param buyToken (ERC20) token to Buy
|
||||
* @param specifiedAmount Amount to buy if isBuy, amount to sell else
|
||||
* @param isBuy True if buy, false if sell
|
||||
*/
|
||||
function createERC20Path(
|
||||
address pool,
|
||||
IERC20 sellToken,
|
||||
IERC20 buyToken,
|
||||
uint256 specifiedAmount,
|
||||
bool isBuy,
|
||||
bool isETH
|
||||
)
|
||||
internal
|
||||
view
|
||||
returns (
|
||||
IBatchRouter.SwapPathExactAmountIn memory sellPath,
|
||||
IBatchRouter.SwapPathExactAmountOut memory buyPath,
|
||||
IBatchRouter.SwapPathStep memory step
|
||||
)
|
||||
{
|
||||
uint256 maxAmountIn_ = address(this).balance;
|
||||
if (!isETH) {
|
||||
maxAmountIn_ = IERC20(sellToken).balanceOf(msg.sender);
|
||||
}
|
||||
|
||||
// prepare steps
|
||||
step = IBatchRouter.SwapPathStep({
|
||||
pool: pool,
|
||||
tokenOut: buyToken,
|
||||
isBuffer: false
|
||||
});
|
||||
IBatchRouter.SwapPathStep[] memory steps =
|
||||
new IBatchRouter.SwapPathStep[](1);
|
||||
steps[0] = step;
|
||||
|
||||
if (isBuy) {
|
||||
buyPath = IBatchRouter.SwapPathExactAmountOut({
|
||||
tokenIn: sellToken,
|
||||
steps: steps,
|
||||
maxAmountIn: maxAmountIn_,
|
||||
exactAmountOut: specifiedAmount
|
||||
});
|
||||
} else {
|
||||
sellPath = IBatchRouter.SwapPathExactAmountIn({
|
||||
tokenIn: sellToken,
|
||||
steps: steps,
|
||||
exactAmountIn: specifiedAmount,
|
||||
minAmountOut: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getLimitsERC20(bytes32 poolId, address sellToken, address buyToken)
|
||||
internal
|
||||
view
|
||||
returns (uint256[] memory limits)
|
||||
{
|
||||
limits = new uint256[](2);
|
||||
address pool = address(bytes20(poolId));
|
||||
|
||||
(IERC20[] memory tokens,, uint256[] memory balancesRaw,) =
|
||||
vault.getPoolTokenInfo(pool);
|
||||
(IERC20 sellTokenERC, IERC20 buyTokenERC) =
|
||||
(IERC20(sellToken), IERC20(buyToken));
|
||||
// ERC4626-ERC4626, ERC20-ERC20
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
if (tokens[i] == sellTokenERC) {
|
||||
limits[0] = (balancesRaw[i] * RESERVE_LIMIT_FACTOR) / 10;
|
||||
}
|
||||
if (tokens[i] == buyTokenERC) {
|
||||
limits[1] = (balancesRaw[i] * RESERVE_LIMIT_FACTOR) / 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
393
evm/src/balancer-v3/lib/BalancerERC4626Helpers.sol
Normal file
393
evm/src/balancer-v3/lib/BalancerERC4626Helpers.sol
Normal file
@@ -0,0 +1,393 @@
|
||||
//SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "./BalancerCustomWrapHelpers.sol";
|
||||
|
||||
abstract contract BalancerERC4626Helpers is BalancerCustomWrapHelpers {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
function getERC4626PathType(
|
||||
address pool,
|
||||
address sellToken,
|
||||
address buyToken,
|
||||
bool sellTokenFound
|
||||
) internal view returns (ERC4626_SWAP_TYPE kind, address outputAddress) {
|
||||
IERC20[] memory tokens = vault.getPoolTokens(pool);
|
||||
|
||||
if (sellTokenFound) {
|
||||
// SWAP_WRAP and SWAP_UNWRAP
|
||||
bool isERC4626BuyToken = isERC4626(buyToken);
|
||||
|
||||
if (isERC4626BuyToken) {
|
||||
kind = ERC4626_SWAP_TYPE.SWAP_WRAP;
|
||||
} else {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
if (isERC4626(token) && IERC4626(token).asset() == buyToken)
|
||||
{
|
||||
outputAddress = token; // buyToken share
|
||||
break;
|
||||
}
|
||||
}
|
||||
require(outputAddress != address(0), "Token not found in pool");
|
||||
kind = ERC4626_SWAP_TYPE.SWAP_UNWRAP;
|
||||
}
|
||||
} else {
|
||||
bool isERC4626SellToken = isERC4626(sellToken);
|
||||
|
||||
if (isERC4626SellToken) {
|
||||
kind = ERC4626_SWAP_TYPE.UNWRAP_SWAP;
|
||||
} else {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
if (
|
||||
isERC4626(token) && IERC4626(token).asset() == sellToken
|
||||
) {
|
||||
outputAddress = token; // sellToken share
|
||||
break;
|
||||
}
|
||||
}
|
||||
require(outputAddress != address(0), "Token not found in pool");
|
||||
kind = ERC4626_SWAP_TYPE.WRAP_SWAP;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prepareERC4626SellOrBuy(
|
||||
address pool,
|
||||
address _sellToken,
|
||||
address _buyToken,
|
||||
uint256 specifiedAmount,
|
||||
ERC4626_SWAP_TYPE kind,
|
||||
address outputAddress,
|
||||
bool isBuy
|
||||
)
|
||||
internal
|
||||
view
|
||||
returns (
|
||||
IBatchRouter.SwapPathExactAmountIn[] memory sellPath,
|
||||
IBatchRouter.SwapPathExactAmountOut[] memory buyPath
|
||||
)
|
||||
{
|
||||
IBatchRouter.SwapPathStep[] memory steps;
|
||||
|
||||
address sellToken = _sellToken == address(0) ? WETH_ADDRESS : _sellToken;
|
||||
address buyToken = _buyToken == address(0) ? WETH_ADDRESS : _buyToken;
|
||||
|
||||
if (kind == ERC4626_SWAP_TYPE.SWAP_WRAP) {
|
||||
// !isERC4626(sellToken) && isERC4626(buyToken) and
|
||||
// isERC4626(buyToken) && isERC4626(sellToken)
|
||||
steps = new IBatchRouter.SwapPathStep[](2);
|
||||
|
||||
// swap: sellToken -> buyToken.asset()
|
||||
(,, steps[0]) = createERC20Path(
|
||||
pool,
|
||||
IERC20(sellToken),
|
||||
IERC20(IERC4626(buyToken).asset()),
|
||||
specifiedAmount,
|
||||
false,
|
||||
_sellToken == address(0)
|
||||
);
|
||||
|
||||
// wrap: buyToken.asset() -> buyToken.shares()
|
||||
(,, steps[1]) = createWrapOrUnwrapPath(
|
||||
buyToken, specifiedAmount, IVault.WrappingDirection.WRAP, false
|
||||
);
|
||||
} else if (kind == ERC4626_SWAP_TYPE.SWAP_UNWRAP) {
|
||||
// isERC4626(sellToken) && !isERC4626(buyToken) and
|
||||
// !isERC4626(buyToken) && !isERC4626(sellToken)
|
||||
steps = new IBatchRouter.SwapPathStep[](2);
|
||||
|
||||
// swap: sellToken -> buyToken.shares()
|
||||
(,, steps[0]) = createERC20Path(
|
||||
pool,
|
||||
IERC20(sellToken),
|
||||
IERC20(outputAddress),
|
||||
specifiedAmount,
|
||||
false,
|
||||
_sellToken == address(0)
|
||||
);
|
||||
|
||||
// unwrap: buyToken.shares() -> buyToken.asset()
|
||||
(,, steps[1]) = createWrapOrUnwrapPath(
|
||||
outputAddress,
|
||||
specifiedAmount,
|
||||
IVault.WrappingDirection.UNWRAP,
|
||||
false
|
||||
);
|
||||
} else if (kind == ERC4626_SWAP_TYPE.WRAP_SWAP) {
|
||||
// input is ERC20, output is ERC4626
|
||||
steps = new IBatchRouter.SwapPathStep[](2);
|
||||
|
||||
// wrap: sellToken.shares() -> sellToken.asset()
|
||||
(,, steps[0]) = createWrapOrUnwrapPath(
|
||||
outputAddress,
|
||||
specifiedAmount,
|
||||
IVault.WrappingDirection.WRAP,
|
||||
false
|
||||
);
|
||||
// swap: sellToken.asset() -> buyToken
|
||||
(,, steps[1]) = createERC20Path(
|
||||
pool,
|
||||
IERC20(outputAddress),
|
||||
IERC20(buyToken),
|
||||
specifiedAmount,
|
||||
false,
|
||||
_buyToken == address(0)
|
||||
);
|
||||
} else if (kind == ERC4626_SWAP_TYPE.UNWRAP_SWAP) {
|
||||
steps = new IBatchRouter.SwapPathStep[](2);
|
||||
|
||||
// unwrap: sellToken.shares() -> sellToken.asset()
|
||||
(,, steps[0]) = createWrapOrUnwrapPath(
|
||||
sellToken,
|
||||
specifiedAmount,
|
||||
IVault.WrappingDirection.UNWRAP,
|
||||
false
|
||||
);
|
||||
|
||||
// swap: sellToken.asset() -> buyToken
|
||||
(,, steps[1]) = createERC20Path(
|
||||
pool,
|
||||
IERC20(sellToken),
|
||||
IERC20(buyToken),
|
||||
specifiedAmount,
|
||||
false,
|
||||
_buyToken == address(0)
|
||||
);
|
||||
}
|
||||
|
||||
if (isBuy) {
|
||||
buyPath = new IBatchRouter.SwapPathExactAmountOut[](1);
|
||||
buyPath[0] = IBatchRouter.SwapPathExactAmountOut({
|
||||
tokenIn: IERC20(sellToken),
|
||||
steps: steps,
|
||||
maxAmountIn: IERC20(sellToken).balanceOf(address(this)),
|
||||
exactAmountOut: specifiedAmount
|
||||
});
|
||||
} else {
|
||||
sellPath = new IBatchRouter.SwapPathExactAmountIn[](1);
|
||||
sellPath[0] = IBatchRouter.SwapPathExactAmountIn({
|
||||
tokenIn: IERC20(sellToken),
|
||||
steps: steps,
|
||||
exactAmountIn: specifiedAmount,
|
||||
minAmountOut: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function swapERC4626AndERC20(
|
||||
address pool,
|
||||
address _sellToken,
|
||||
address _buyToken,
|
||||
uint256 specifiedAmount,
|
||||
ERC4626_SWAP_TYPE kind,
|
||||
address outputAddress,
|
||||
bool isBuy
|
||||
) internal returns (uint256 calculatedAmount) {
|
||||
// approve
|
||||
uint256 approvalAmount = specifiedAmount;
|
||||
|
||||
address sellToken = _sellToken == address(0) ? WETH_ADDRESS : _sellToken;
|
||||
address buyToken = _buyToken == address(0) ? WETH_ADDRESS : _buyToken;
|
||||
|
||||
if (_sellToken != address(0)) {
|
||||
if (isBuy) {
|
||||
approvalAmount = IERC20(sellToken).balanceOf(msg.sender);
|
||||
}
|
||||
IERC20(sellToken).safeIncreaseAllowance(permit2, approvalAmount);
|
||||
IPermit2(permit2).approve(
|
||||
address(sellToken),
|
||||
address(router),
|
||||
type(uint160).max,
|
||||
type(uint48).max
|
||||
);
|
||||
} else {
|
||||
if (isBuy) {
|
||||
approvalAmount = address(this).balance;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBuy) {
|
||||
if (_sellToken != address(0)) {
|
||||
IERC20(sellToken).safeTransferFrom(
|
||||
msg.sender, address(this), approvalAmount
|
||||
);
|
||||
}
|
||||
|
||||
(IBatchRouter.SwapPathExactAmountIn[] memory sellPath,) =
|
||||
prepareERC4626SellOrBuy(
|
||||
pool,
|
||||
sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
kind,
|
||||
outputAddress,
|
||||
isBuy
|
||||
);
|
||||
|
||||
uint256[] memory amountsOut;
|
||||
if (_sellToken == address(0)) {
|
||||
(,, amountsOut) = router.swapExactIn{value: specifiedAmount}(
|
||||
sellPath, type(uint256).max, true, bytes("")
|
||||
);
|
||||
} else {
|
||||
(,, amountsOut) = router.swapExactIn(
|
||||
sellPath, type(uint256).max, false, bytes("")
|
||||
);
|
||||
}
|
||||
|
||||
calculatedAmount = amountsOut[0];
|
||||
|
||||
if (_buyToken != address(0)) {
|
||||
IERC20(buyToken).safeTransfer(msg.sender, calculatedAmount);
|
||||
} else {
|
||||
(bool sent,) =
|
||||
payable(msg.sender).call{value: calculatedAmount}("");
|
||||
require(sent, "Failed to transfer ETH");
|
||||
}
|
||||
} else {
|
||||
uint256 initialSenderBalance = address(this).balance;
|
||||
if (_sellToken != address(0)) {
|
||||
initialSenderBalance = IERC20(sellToken).balanceOf(msg.sender);
|
||||
IERC20(sellToken).safeTransferFrom(
|
||||
msg.sender, address(this), approvalAmount
|
||||
);
|
||||
}
|
||||
|
||||
(, IBatchRouter.SwapPathExactAmountOut[] memory buyPath) =
|
||||
prepareERC4626SellOrBuy(
|
||||
pool,
|
||||
sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
kind,
|
||||
outputAddress,
|
||||
true
|
||||
);
|
||||
|
||||
uint256[] memory amountsIn;
|
||||
if (_sellToken == address(0)) {
|
||||
(,, amountsIn) = router.swapExactOut{value: approvalAmount}(
|
||||
buyPath, type(uint256).max, false, bytes("")
|
||||
);
|
||||
} else {
|
||||
(,, amountsIn) = router.swapExactOut(
|
||||
buyPath, type(uint256).max, false, bytes("")
|
||||
);
|
||||
}
|
||||
|
||||
calculatedAmount = amountsIn[0];
|
||||
|
||||
if (_buyToken != address(0)) {
|
||||
IERC20(buyToken).safeTransfer(msg.sender, specifiedAmount);
|
||||
} else {
|
||||
(bool sent,) =
|
||||
payable(msg.sender).call{value: specifiedAmount}("");
|
||||
require(sent, "Failed to transfer ETH");
|
||||
}
|
||||
|
||||
// transfer back sellToken to sender
|
||||
if (_sellToken != address(0)) {
|
||||
IERC20(sellToken).safeTransfer(
|
||||
msg.sender, initialSenderBalance - calculatedAmount
|
||||
);
|
||||
} else {
|
||||
(bool sent,) = payable(msg.sender).call{
|
||||
value: initialSenderBalance - calculatedAmount
|
||||
}("");
|
||||
require(sent, "Failed to transfer ETH");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAmountOutERC4626AndERC20(
|
||||
address pool,
|
||||
address sellToken,
|
||||
address buyToken,
|
||||
uint256 specifiedAmount,
|
||||
ERC4626_SWAP_TYPE kind,
|
||||
address outputAddress
|
||||
) internal returns (uint256 calculatedAmount) {
|
||||
(IBatchRouter.SwapPathExactAmountIn[] memory paths,) =
|
||||
prepareERC4626SellOrBuy(
|
||||
pool,
|
||||
sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
kind,
|
||||
outputAddress,
|
||||
false
|
||||
);
|
||||
(,, uint256[] memory amountsOut) =
|
||||
router.querySwapExactIn(paths, address(0), bytes(""));
|
||||
calculatedAmount = amountsOut[0];
|
||||
}
|
||||
|
||||
function getLimitsERC4626AndERC20(
|
||||
bytes32 poolId,
|
||||
address sellToken,
|
||||
address buyToken,
|
||||
ERC4626_SWAP_TYPE kind,
|
||||
address outputAddress
|
||||
) internal view returns (uint256[] memory limits) {
|
||||
limits = new uint256[](2);
|
||||
address pool = address(bytes20(poolId));
|
||||
(IERC20[] memory tokens,, uint256[] memory balancesRaw,) =
|
||||
vault.getPoolTokenInfo(pool);
|
||||
|
||||
uint256 tokenLimit;
|
||||
|
||||
if (kind == ERC4626_SWAP_TYPE.SWAP_WRAP) {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
if (token == sellToken) {
|
||||
limits[0] = balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10;
|
||||
}
|
||||
|
||||
if (token == IERC4626(buyToken).asset()) {
|
||||
tokenLimit = balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10;
|
||||
}
|
||||
}
|
||||
limits[1] = IERC4626(buyToken).previewDeposit(tokenLimit);
|
||||
} else if (kind == ERC4626_SWAP_TYPE.SWAP_UNWRAP) {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
if (token == sellToken) {
|
||||
limits[0] = balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10;
|
||||
} else if (token == outputAddress) {
|
||||
tokenLimit = balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10;
|
||||
}
|
||||
}
|
||||
limits[1] = IERC4626(outputAddress).previewRedeem(tokenLimit);
|
||||
} else if (kind == ERC4626_SWAP_TYPE.WRAP_SWAP) {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
|
||||
if (token == outputAddress) {
|
||||
limits[0] = IERC4626(outputAddress).previewRedeem(
|
||||
balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10
|
||||
);
|
||||
}
|
||||
|
||||
if (token == buyToken) {
|
||||
limits[1] = balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10;
|
||||
}
|
||||
}
|
||||
} else if (kind == ERC4626_SWAP_TYPE.UNWRAP_SWAP) {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
|
||||
if (token == buyToken) {
|
||||
limits[1] = balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10;
|
||||
}
|
||||
|
||||
if (token == IERC4626(sellToken).asset()) {
|
||||
limits[0] = IERC4626(sellToken).previewDeposit(
|
||||
balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
206
evm/src/balancer-v3/lib/BalancerInterfaces.sol
Normal file
206
evm/src/balancer-v3/lib/BalancerInterfaces.sol
Normal file
@@ -0,0 +1,206 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import {ISwapAdapter} from "../../interfaces/ISwapAdapter.sol";
|
||||
import {CustomBytesAppend} from "../../libraries/CustomBytesAppend.sol";
|
||||
import {
|
||||
IERC20,
|
||||
SafeERC20
|
||||
} from
|
||||
"../../../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {IERC4626} from
|
||||
"../../../lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol";
|
||||
|
||||
interface IVault {
|
||||
type PoolConfigBits is bytes32;
|
||||
|
||||
enum SwapKind {
|
||||
EXACT_IN,
|
||||
EXACT_OUT
|
||||
}
|
||||
|
||||
enum TokenType {
|
||||
STANDARD,
|
||||
WITH_RATE
|
||||
}
|
||||
|
||||
enum WrappingDirection {
|
||||
WRAP,
|
||||
UNWRAP
|
||||
}
|
||||
|
||||
struct VaultSwapParams {
|
||||
SwapKind kind;
|
||||
address pool;
|
||||
IERC20 tokenIn;
|
||||
IERC20 tokenOut;
|
||||
uint256 amountGivenRaw;
|
||||
uint256 limitRaw;
|
||||
bytes userData;
|
||||
}
|
||||
|
||||
struct BufferWrapOrUnwrapParams {
|
||||
SwapKind kind;
|
||||
WrappingDirection direction;
|
||||
IERC4626 wrappedToken;
|
||||
uint256 amountGivenRaw;
|
||||
uint256 limitRaw;
|
||||
}
|
||||
|
||||
struct PoolData {
|
||||
PoolConfigBits poolConfigBits;
|
||||
IERC20[] tokens;
|
||||
TokenInfo[] tokenInfo;
|
||||
uint256[] balancesRaw;
|
||||
uint256[] balancesLiveScaled18;
|
||||
uint256[] tokenRates;
|
||||
uint256[] decimalScalingFactors;
|
||||
}
|
||||
|
||||
struct TokenInfo {
|
||||
TokenType tokenType;
|
||||
IRateProvider rateProvider;
|
||||
bool paysYieldFees;
|
||||
}
|
||||
|
||||
function swap(VaultSwapParams memory vaultSwapParams)
|
||||
external
|
||||
returns (
|
||||
uint256 amountCalculatedRaw,
|
||||
uint256 amountInRaw,
|
||||
uint256 amountOutRaw
|
||||
);
|
||||
|
||||
function getPoolTokenCountAndIndexOfToken(address pool, IERC20 token)
|
||||
external
|
||||
view
|
||||
returns (uint256 tokenCount, uint256 index);
|
||||
|
||||
function erc4626BufferWrapOrUnwrap(BufferWrapOrUnwrapParams memory params)
|
||||
external
|
||||
returns (
|
||||
uint256 amountCalculatedRaw,
|
||||
uint256 amountInRaw,
|
||||
uint256 amountOutRaw
|
||||
);
|
||||
|
||||
function getPoolData(address pool)
|
||||
external
|
||||
view
|
||||
returns (PoolData memory);
|
||||
|
||||
function getPoolTokenInfo(address pool)
|
||||
external
|
||||
view
|
||||
returns (
|
||||
IERC20[] memory tokens,
|
||||
TokenInfo[] memory tokenInfo,
|
||||
uint256[] memory balancesRaw,
|
||||
uint256[] memory lastBalancesLiveScaled18
|
||||
);
|
||||
|
||||
function getPoolTokens(address pool)
|
||||
external
|
||||
view
|
||||
returns (IERC20[] memory tokens);
|
||||
}
|
||||
|
||||
interface IRateProvider {
|
||||
/**
|
||||
* @dev Returns an 18 decimal fixed point number that is the exchange rate
|
||||
* of the token to some other underlying
|
||||
* token. The meaning of this rate depends on the context.
|
||||
*/
|
||||
function getRate() external view returns (uint256);
|
||||
}
|
||||
|
||||
interface IBatchRouter {
|
||||
struct SwapPathStep {
|
||||
address pool;
|
||||
IERC20 tokenOut;
|
||||
// If true, the "pool" is an ERC4626 Buffer. Used to wrap/unwrap tokens
|
||||
// if pool doesn't have enough liquidity.
|
||||
bool isBuffer;
|
||||
}
|
||||
|
||||
struct SwapPathExactAmountIn {
|
||||
IERC20 tokenIn;
|
||||
// For each step:
|
||||
// If tokenIn == pool, use removeLiquidity SINGLE_TOKEN_EXACT_IN.
|
||||
// If tokenOut == pool, use addLiquidity UNBALANCED.
|
||||
SwapPathStep[] steps;
|
||||
uint256 exactAmountIn;
|
||||
uint256 minAmountOut;
|
||||
}
|
||||
|
||||
struct SwapPathExactAmountOut {
|
||||
IERC20 tokenIn;
|
||||
// for each step:
|
||||
// If tokenIn == pool, use removeLiquidity SINGLE_TOKEN_EXACT_OUT.
|
||||
// If tokenOut == pool, use addLiquidity SINGLE_TOKEN_EXACT_OUT.
|
||||
SwapPathStep[] steps;
|
||||
uint256 maxAmountIn;
|
||||
uint256 exactAmountOut;
|
||||
}
|
||||
|
||||
function querySwapExactIn(
|
||||
SwapPathExactAmountIn[] memory paths,
|
||||
address sender,
|
||||
bytes calldata userData
|
||||
)
|
||||
external
|
||||
returns (
|
||||
uint256[] memory pathAmountsOut,
|
||||
address[] memory tokensOut,
|
||||
uint256[] memory amountsOut
|
||||
);
|
||||
|
||||
function querySwapExactOut(
|
||||
SwapPathExactAmountOut[] memory paths,
|
||||
address sender,
|
||||
bytes calldata userData
|
||||
)
|
||||
external
|
||||
returns (
|
||||
uint256[] memory pathAmountsIn,
|
||||
address[] memory tokensIn,
|
||||
uint256[] memory amountsIn
|
||||
);
|
||||
|
||||
function swapExactIn(
|
||||
SwapPathExactAmountIn[] memory paths,
|
||||
uint256 deadline,
|
||||
bool wethIsEth,
|
||||
bytes calldata userData
|
||||
)
|
||||
external
|
||||
payable
|
||||
returns (
|
||||
uint256[] memory pathAmountsOut,
|
||||
address[] memory tokensOut,
|
||||
uint256[] memory amountsOut
|
||||
);
|
||||
|
||||
function swapExactOut(
|
||||
SwapPathExactAmountOut[] memory paths,
|
||||
uint256 deadline,
|
||||
bool wethIsEth,
|
||||
bytes calldata userData
|
||||
)
|
||||
external
|
||||
payable
|
||||
returns (
|
||||
uint256[] memory pathAmountsIn,
|
||||
address[] memory tokensIn,
|
||||
uint256[] memory amountsIn
|
||||
);
|
||||
}
|
||||
|
||||
interface IPermit2 {
|
||||
function approve(
|
||||
address token,
|
||||
address spender,
|
||||
uint160 amount,
|
||||
uint48 expiration
|
||||
) external;
|
||||
}
|
||||
42
evm/src/balancer-v3/lib/BalancerStorage.sol
Normal file
42
evm/src/balancer-v3/lib/BalancerStorage.sol
Normal file
@@ -0,0 +1,42 @@
|
||||
//SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "./BalancerInterfaces.sol";
|
||||
|
||||
/**
|
||||
* @title Balancer V3 Storage
|
||||
*/
|
||||
abstract contract BalancerStorage {
|
||||
// Balancer V3 constants
|
||||
uint256 constant RESERVE_LIMIT_FACTOR = 3; // 30% as being divided by 10
|
||||
uint256 constant SWAP_DEADLINE_SEC = 1000;
|
||||
|
||||
// Balancer V3 contracts
|
||||
IVault immutable vault;
|
||||
IBatchRouter immutable router;
|
||||
|
||||
// ETH and Wrapped ETH addresses, using ETH as address(0)
|
||||
address immutable WETH_ADDRESS;
|
||||
address constant ETH_ADDRESS = address(0);
|
||||
|
||||
// permit2 address
|
||||
address immutable permit2;
|
||||
|
||||
enum CUSTOM_WRAP_KIND {
|
||||
NONE,
|
||||
ERC20_TO_ERC20, // swap ERC20 to ERC20, passing through a ERC4626_4626
|
||||
// pool
|
||||
// pool
|
||||
ERC4626_TO_ERC4626 // swap ERC4626 to ERC4626, passing through a
|
||||
// ERC20_20_20 pool
|
||||
|
||||
}
|
||||
|
||||
enum ERC4626_SWAP_TYPE {
|
||||
NONE,
|
||||
SWAP_WRAP,
|
||||
SWAP_UNWRAP,
|
||||
WRAP_SWAP,
|
||||
UNWRAP_SWAP
|
||||
}
|
||||
}
|
||||
247
evm/src/balancer-v3/lib/BalancerSwapHelpers.sol
Normal file
247
evm/src/balancer-v3/lib/BalancerSwapHelpers.sol
Normal file
@@ -0,0 +1,247 @@
|
||||
//SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "./BalancerERC4626Helpers.sol";
|
||||
|
||||
/**
|
||||
* @title Balancer V3 Swap Helpers
|
||||
* @dev A wrapped library containing swap functions, helpers and storage for the
|
||||
* Balancer V3 Swap Adapter contract
|
||||
*/
|
||||
abstract contract BalancerSwapHelpers is
|
||||
BalancerERC4626Helpers,
|
||||
ISwapAdapter
|
||||
{
|
||||
function getAmountOutMiddleware(
|
||||
bytes32 pool,
|
||||
address sellToken,
|
||||
address buyToken,
|
||||
uint256 specifiedAmount
|
||||
) internal returns (uint256 amountOut) {
|
||||
address poolAddress = address(bytes20(pool));
|
||||
|
||||
// getTokens() -> [token0, token1] -> if([sellToken,buyToken) in
|
||||
// [token0, token1]) -> direct
|
||||
IERC20[] memory tokens = vault.getPoolTokens(poolAddress);
|
||||
|
||||
bool sellTokenFound;
|
||||
bool buyTokenFound;
|
||||
if (sellToken == address(0) || buyToken == address(0)) {
|
||||
sellTokenFound = true;
|
||||
buyTokenFound = true;
|
||||
} else {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
if (token == sellToken) {
|
||||
sellTokenFound = true;
|
||||
} else if (token == buyToken) {
|
||||
buyTokenFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sellTokenFound && buyTokenFound) {
|
||||
// Direct Swap
|
||||
(IBatchRouter.SwapPathExactAmountIn memory sellPath,,) =
|
||||
createERC20Path(
|
||||
poolAddress,
|
||||
IERC20(sellToken),
|
||||
IERC20(buyToken),
|
||||
specifiedAmount,
|
||||
false,
|
||||
sellToken == address(0) || buyToken == address(0)
|
||||
);
|
||||
return getAmountOut(sellPath);
|
||||
} else if (!sellTokenFound && !buyTokenFound) {
|
||||
// 3 step (4 tokens)
|
||||
(
|
||||
CUSTOM_WRAP_KIND kindWrap,
|
||||
address sellTokenOutput,
|
||||
address buyTokenOutput
|
||||
) = getCustomWrap(sellToken, buyToken, poolAddress);
|
||||
return getAmountOutCustomWrap(
|
||||
poolAddress,
|
||||
sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
kindWrap,
|
||||
sellTokenOutput,
|
||||
buyTokenOutput
|
||||
);
|
||||
} else {
|
||||
// 2 step (3 tokens)
|
||||
(ERC4626_SWAP_TYPE kind, address outputAddress) = getERC4626PathType(
|
||||
poolAddress, sellToken, buyToken, sellTokenFound
|
||||
);
|
||||
|
||||
if (kind != ERC4626_SWAP_TYPE.NONE) {
|
||||
return getAmountOutERC4626AndERC20(
|
||||
poolAddress,
|
||||
sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
kind,
|
||||
outputAddress
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Middleware for swaps
|
||||
*/
|
||||
function swapMiddleware(
|
||||
bytes32 pool,
|
||||
address sellToken,
|
||||
address buyToken,
|
||||
OrderSide side,
|
||||
uint256 specifiedAmount
|
||||
) internal returns (uint256) {
|
||||
address poolAddress = address(bytes20(pool));
|
||||
|
||||
// getTokens() -> [token0, token1] -> if([sellToken,buyToken) in
|
||||
// [token0, token1]) -> direct
|
||||
IERC20[] memory tokens = vault.getPoolTokens(poolAddress);
|
||||
|
||||
bool sellTokenFound;
|
||||
bool buyTokenFound;
|
||||
if (sellToken == address(0) || buyToken == address(0)) {
|
||||
sellTokenFound = true;
|
||||
buyTokenFound = true;
|
||||
} else {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
if (token == sellToken) {
|
||||
sellTokenFound = true;
|
||||
} else if (token == buyToken) {
|
||||
buyTokenFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sellTokenFound && buyTokenFound) {
|
||||
// Direct Swap
|
||||
// Fallback (used for ERC20<->ERC20 and ERC4626<->ERC4626 as
|
||||
// inherits
|
||||
// IERC20 logic)
|
||||
if (side == OrderSide.Buy) {
|
||||
return buyERC20WithERC20(
|
||||
poolAddress,
|
||||
IERC20(sellToken),
|
||||
IERC20(buyToken),
|
||||
specifiedAmount,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
return sellERC20ForERC20(
|
||||
poolAddress,
|
||||
IERC20(sellToken),
|
||||
IERC20(buyToken),
|
||||
specifiedAmount,
|
||||
true
|
||||
);
|
||||
}
|
||||
} else if (!sellTokenFound && !buyTokenFound) {
|
||||
// 3 step (4 tokens)
|
||||
(
|
||||
CUSTOM_WRAP_KIND kindWrap,
|
||||
address sellTokenOutput,
|
||||
address buyTokenOutput
|
||||
) = getCustomWrap(sellToken, buyToken, poolAddress);
|
||||
|
||||
if (side == OrderSide.Sell) {
|
||||
return sellCustomWrap(
|
||||
poolAddress,
|
||||
sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
kindWrap,
|
||||
sellTokenOutput,
|
||||
buyTokenOutput
|
||||
);
|
||||
} else {
|
||||
return buyCustomWrap(
|
||||
poolAddress,
|
||||
sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
kindWrap,
|
||||
sellTokenOutput,
|
||||
buyTokenOutput
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 2 step (3 tokens)
|
||||
(ERC4626_SWAP_TYPE kind, address outputAddress) = getERC4626PathType(
|
||||
poolAddress, sellToken, buyToken, sellTokenFound
|
||||
);
|
||||
|
||||
return swapERC4626AndERC20(
|
||||
poolAddress,
|
||||
sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
kind,
|
||||
outputAddress,
|
||||
side == OrderSide.Buy
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getLimitsMiddleware(
|
||||
bytes32 poolId,
|
||||
address sellToken,
|
||||
address buyToken
|
||||
) internal view returns (uint256[] memory limits) {
|
||||
address poolAddress = address(bytes20(poolId));
|
||||
|
||||
// getTokens() -> [token0, token1] -> if([sellToken,buyToken) in
|
||||
// [token0, token1]) -> direct
|
||||
IERC20[] memory tokens = vault.getPoolTokens(poolAddress);
|
||||
|
||||
bool sellTokenFound;
|
||||
bool buyTokenFound;
|
||||
if (sellToken == address(0) || buyToken == address(0)) {
|
||||
sellTokenFound = true;
|
||||
buyTokenFound = true;
|
||||
} else {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
if (token == sellToken) {
|
||||
sellTokenFound = true;
|
||||
} else if (token == buyToken) {
|
||||
buyTokenFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sellTokenFound && buyTokenFound) {
|
||||
// Direct Swap
|
||||
return getLimitsERC20(poolId, sellToken, buyToken);
|
||||
} else if (!sellTokenFound && !buyTokenFound) {
|
||||
// 3 step (4 tokens)
|
||||
(
|
||||
CUSTOM_WRAP_KIND kindWrap,
|
||||
address sellTokenOutput,
|
||||
address buyTokenOutput
|
||||
) = getCustomWrap(sellToken, buyToken, poolAddress);
|
||||
|
||||
return getLimitsCustomWrap(
|
||||
poolId,
|
||||
sellToken,
|
||||
buyToken,
|
||||
kindWrap,
|
||||
sellTokenOutput,
|
||||
buyTokenOutput
|
||||
);
|
||||
} else {
|
||||
// 2 step (3 tokens)
|
||||
(ERC4626_SWAP_TYPE kind, address outputAddress) = getERC4626PathType(
|
||||
poolAddress, sellToken, buyToken, sellTokenFound
|
||||
);
|
||||
return getLimitsERC4626AndERC20(
|
||||
poolId, sellToken, buyToken, kind, outputAddress
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
661
evm/src/balancer-v3/lib/BalancerV3Errors.sol
Normal file
661
evm/src/balancer-v3/lib/BalancerV3Errors.sol
Normal file
@@ -0,0 +1,661 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import {IERC20, IERC4626} from "src/balancer-v3/BalancerV3SwapAdapter.sol";
|
||||
|
||||
interface BalancerV3Errors {
|
||||
/**
|
||||
* @notice A pool has already been registered. `registerPool` may only be
|
||||
* called once.
|
||||
* @param pool The already registered pool
|
||||
*/
|
||||
error PoolAlreadyRegistered(address pool);
|
||||
|
||||
/**
|
||||
* @notice A pool has already been initialized. `initialize` may only be
|
||||
* called once.
|
||||
* @param pool The already initialized pool
|
||||
*/
|
||||
error PoolAlreadyInitialized(address pool);
|
||||
|
||||
/**
|
||||
* @notice A pool has not been registered.
|
||||
* @param pool The unregistered pool
|
||||
*/
|
||||
error PoolNotRegistered(address pool);
|
||||
|
||||
/**
|
||||
* @notice A referenced pool has not been initialized.
|
||||
* @param pool The uninitialized pool
|
||||
*/
|
||||
error PoolNotInitialized(address pool);
|
||||
|
||||
/**
|
||||
* @notice A hook contract rejected a pool on registration.
|
||||
* @param poolHooksContract Address of the hook contract that rejected the
|
||||
* pool registration
|
||||
* @param pool Address of the rejected pool
|
||||
* @param poolFactory Address of the pool factory
|
||||
*/
|
||||
error HookRegistrationFailed(
|
||||
address poolHooksContract, address pool, address poolFactory
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice A token was already registered (i.e., it is a duplicate in the
|
||||
* pool).
|
||||
* @param token The duplicate token
|
||||
*/
|
||||
error TokenAlreadyRegistered(IERC20 token);
|
||||
|
||||
/// @notice The token count is below the minimum allowed.
|
||||
error MinTokens();
|
||||
|
||||
/// @notice The token count is above the maximum allowed.
|
||||
error MaxTokens();
|
||||
|
||||
/// @notice Invalid tokens (e.g., zero) cannot be registered.
|
||||
error InvalidToken();
|
||||
|
||||
/// @notice The token type given in a TokenConfig during pool registration
|
||||
/// is invalid.
|
||||
error InvalidTokenType();
|
||||
|
||||
/// @notice The data in a TokenConfig struct is inconsistent or unsupported.
|
||||
error InvalidTokenConfiguration();
|
||||
|
||||
/// @notice Tokens with more than 18 decimals are not supported.
|
||||
error InvalidTokenDecimals();
|
||||
|
||||
/**
|
||||
* @notice The token list passed into an operation does not match the pool
|
||||
* tokens in the pool.
|
||||
* @param pool Address of the pool
|
||||
* @param expectedToken The correct token at a given index in the pool
|
||||
* @param actualToken The actual token found at that index
|
||||
*/
|
||||
error TokensMismatch(
|
||||
address pool, address expectedToken, address actualToken
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* Transient Accounting
|
||||
*
|
||||
*/
|
||||
|
||||
/// @notice A transient accounting operation completed with outstanding
|
||||
/// token deltas.
|
||||
error BalanceNotSettled();
|
||||
|
||||
/// @notice A user called a Vault function (swap, add/remove liquidity)
|
||||
/// outside the lock context.
|
||||
error VaultIsNotUnlocked();
|
||||
|
||||
/// @notice The pool has returned false to the beforeSwap hook, indicating
|
||||
/// the transaction should revert.
|
||||
error DynamicSwapFeeHookFailed();
|
||||
|
||||
/// @notice The pool has returned false to the beforeSwap hook, indicating
|
||||
/// the transaction should revert.
|
||||
error BeforeSwapHookFailed();
|
||||
|
||||
/// @notice The pool has returned false to the afterSwap hook, indicating
|
||||
/// the transaction should revert.
|
||||
error AfterSwapHookFailed();
|
||||
|
||||
/// @notice The pool has returned false to the beforeInitialize hook,
|
||||
/// indicating the transaction should revert.
|
||||
error BeforeInitializeHookFailed();
|
||||
|
||||
/// @notice The pool has returned false to the afterInitialize hook,
|
||||
/// indicating the transaction should revert.
|
||||
error AfterInitializeHookFailed();
|
||||
|
||||
/// @notice The pool has returned false to the beforeAddLiquidity hook,
|
||||
/// indicating the transaction should revert.
|
||||
error BeforeAddLiquidityHookFailed();
|
||||
|
||||
/// @notice The pool has returned false to the afterAddLiquidity hook,
|
||||
/// indicating the transaction should revert.
|
||||
error AfterAddLiquidityHookFailed();
|
||||
|
||||
/// @notice The pool has returned false to the beforeRemoveLiquidity hook,
|
||||
/// indicating the transaction should revert.
|
||||
error BeforeRemoveLiquidityHookFailed();
|
||||
|
||||
/// @notice The pool has returned false to the afterRemoveLiquidity hook,
|
||||
/// indicating the transaction should revert.
|
||||
error AfterRemoveLiquidityHookFailed();
|
||||
|
||||
/// @notice An unauthorized Router tried to call a permissioned function
|
||||
/// (i.e., using the Vault's token allowance).
|
||||
error RouterNotTrusted();
|
||||
|
||||
/**
|
||||
*
|
||||
* Swaps
|
||||
*
|
||||
*/
|
||||
|
||||
/// @notice The user tried to swap zero tokens.
|
||||
error AmountGivenZero();
|
||||
|
||||
/// @notice The user attempted to swap a token for itself.
|
||||
error CannotSwapSameToken();
|
||||
|
||||
/**
|
||||
* @notice The user attempted to operate with a token that is not in the
|
||||
* pool.
|
||||
* @param token The unregistered token
|
||||
*/
|
||||
error TokenNotRegistered(IERC20 token);
|
||||
|
||||
/**
|
||||
* @notice An amount in or out has exceeded the limit specified in the swap
|
||||
* request.
|
||||
* @param amount The total amount in or out
|
||||
* @param limit The amount of the limit that has been exceeded
|
||||
*/
|
||||
error SwapLimit(uint256 amount, uint256 limit);
|
||||
|
||||
/**
|
||||
* @notice A hook adjusted amount in or out has exceeded the limit specified
|
||||
* in the swap request.
|
||||
* @param amount The total amount in or out
|
||||
* @param limit The amount of the limit that has been exceeded
|
||||
*/
|
||||
error HookAdjustedSwapLimit(uint256 amount, uint256 limit);
|
||||
|
||||
/// @notice The amount given or calculated for an operation is below the
|
||||
/// minimum limit.
|
||||
error TradeAmountTooSmall();
|
||||
|
||||
/**
|
||||
*
|
||||
* Add Liquidity
|
||||
*
|
||||
*/
|
||||
|
||||
/// @notice Add liquidity kind not supported.
|
||||
error InvalidAddLiquidityKind();
|
||||
|
||||
/**
|
||||
* @notice A required amountIn exceeds the maximum limit specified for the
|
||||
* operation.
|
||||
* @param tokenIn The incoming token
|
||||
* @param amountIn The total token amount in
|
||||
* @param maxAmountIn The amount of the limit that has been exceeded
|
||||
*/
|
||||
error AmountInAboveMax(
|
||||
IERC20 tokenIn, uint256 amountIn, uint256 maxAmountIn
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice A hook adjusted amountIn exceeds the maximum limit specified for
|
||||
* the operation.
|
||||
* @param tokenIn The incoming token
|
||||
* @param amountIn The total token amount in
|
||||
* @param maxAmountIn The amount of the limit that has been exceeded
|
||||
*/
|
||||
error HookAdjustedAmountInAboveMax(
|
||||
IERC20 tokenIn, uint256 amountIn, uint256 maxAmountIn
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice The BPT amount received from adding liquidity is below the
|
||||
* minimum specified for the operation.
|
||||
* @param amountOut The total BPT amount out
|
||||
* @param minAmountOut The amount of the limit that has been exceeded
|
||||
*/
|
||||
error BptAmountOutBelowMin(uint256 amountOut, uint256 minAmountOut);
|
||||
|
||||
/// @notice Pool does not support adding liquidity with a customized input.
|
||||
error DoesNotSupportAddLiquidityCustom();
|
||||
|
||||
/// @notice Pool does not support adding liquidity through donation.
|
||||
error DoesNotSupportDonation();
|
||||
|
||||
/**
|
||||
*
|
||||
* Remove Liquidity
|
||||
*
|
||||
*/
|
||||
|
||||
/// @notice Remove liquidity kind not supported.
|
||||
error InvalidRemoveLiquidityKind();
|
||||
|
||||
/**
|
||||
* @notice The actual amount out is below the minimum limit specified for
|
||||
* the operation.
|
||||
* @param tokenOut The outgoing token
|
||||
* @param amountOut The total BPT amount out
|
||||
* @param minAmountOut The amount of the limit that has been exceeded
|
||||
*/
|
||||
error AmountOutBelowMin(
|
||||
IERC20 tokenOut, uint256 amountOut, uint256 minAmountOut
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice The hook adjusted amount out is below the minimum limit specified
|
||||
* for the operation.
|
||||
* @param tokenOut The outgoing token
|
||||
* @param amountOut The total BPT amount out
|
||||
* @param minAmountOut The amount of the limit that has been exceeded
|
||||
*/
|
||||
error HookAdjustedAmountOutBelowMin(
|
||||
IERC20 tokenOut, uint256 amountOut, uint256 minAmountOut
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice The required BPT amount in exceeds the maximum limit specified
|
||||
* for the operation.
|
||||
* @param amountIn The total BPT amount in
|
||||
* @param maxAmountIn The amount of the limit that has been exceeded
|
||||
*/
|
||||
error BptAmountInAboveMax(uint256 amountIn, uint256 maxAmountIn);
|
||||
|
||||
/// @notice Pool does not support removing liquidity with a customized
|
||||
/// input.
|
||||
error DoesNotSupportRemoveLiquidityCustom();
|
||||
|
||||
/**
|
||||
*
|
||||
* Fees
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @notice Error raised when there is an overflow in the fee calculation.
|
||||
* @dev This occurs when the sum of the parts (aggregate swap or yield fee)
|
||||
* is greater than the whole
|
||||
* (total swap or yield fee). Also validated when the protocol fee
|
||||
* controller updates aggregate fee
|
||||
* percentages in the Vault.
|
||||
*/
|
||||
error ProtocolFeesExceedTotalCollected();
|
||||
|
||||
/**
|
||||
* @notice Error raised when the swap fee percentage is less than the
|
||||
* minimum allowed value.
|
||||
* @dev The Vault itself does not impose a universal minimum. Rather, it
|
||||
* validates against the
|
||||
* range specified by the `ISwapFeePercentageBounds` interface. and reverts
|
||||
* with this error
|
||||
* if it is below the minimum value returned by the pool.
|
||||
*
|
||||
* Pools with dynamic fees do not check these limits.
|
||||
*/
|
||||
error SwapFeePercentageTooLow();
|
||||
|
||||
/**
|
||||
* @notice Error raised when the swap fee percentage is greater than the
|
||||
* maximum allowed value.
|
||||
* @dev The Vault itself does not impose a universal minimum. Rather, it
|
||||
* validates against the
|
||||
* range specified by the `ISwapFeePercentageBounds` interface. and reverts
|
||||
* with this error
|
||||
* if it is above the maximum value returned by the pool.
|
||||
*
|
||||
* Pools with dynamic fees do not check these limits.
|
||||
*/
|
||||
error SwapFeePercentageTooHigh();
|
||||
|
||||
/**
|
||||
* @notice Primary fee percentages result in an aggregate fee that cannot be
|
||||
* stored with the required precision.
|
||||
* @dev Primary fee percentages are 18-decimal values, stored here in 64
|
||||
* bits, and calculated with full 256-bit
|
||||
* precision. However, the resulting aggregate fees are stored in the Vault
|
||||
* with 24-bit precision, which
|
||||
* corresponds to 0.00001% resolution (i.e., a fee can be 1%, 1.00001%,
|
||||
* 1.00002%, but not 1.000005%).
|
||||
* Disallow setting fees such that there would be precision loss in the
|
||||
* Vault, leading to a discrepancy between
|
||||
* the aggregate fee calculated here and that stored in the Vault.
|
||||
*/
|
||||
error FeePrecisionTooHigh();
|
||||
|
||||
/// @notice A given percentage is above the maximum (usually a value close
|
||||
/// to FixedPoint.ONE, or 1e18 wei).
|
||||
error PercentageAboveMax();
|
||||
|
||||
/**
|
||||
*
|
||||
* Queries
|
||||
*
|
||||
*/
|
||||
|
||||
/// @notice A user tried to execute a query operation when they were
|
||||
/// disabled.
|
||||
error QueriesDisabled();
|
||||
|
||||
/// @notice An admin tried to re-enable queries, but they were disabled
|
||||
/// permanently.
|
||||
error QueriesDisabledPermanently();
|
||||
|
||||
/**
|
||||
*
|
||||
* Recovery Mode
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @notice Cannot enable recovery mode when already enabled.
|
||||
* @param pool The pool
|
||||
*/
|
||||
error PoolInRecoveryMode(address pool);
|
||||
|
||||
/**
|
||||
* @notice Cannot disable recovery mode when not enabled.
|
||||
* @param pool The pool
|
||||
*/
|
||||
error PoolNotInRecoveryMode(address pool);
|
||||
|
||||
/**
|
||||
*
|
||||
* Authentication
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @notice Error indicating the sender is not the Vault (e.g., someone is
|
||||
* trying to call a permissioned function).
|
||||
* @param sender The account attempting to call a permissioned function
|
||||
*/
|
||||
error SenderIsNotVault(address sender);
|
||||
|
||||
/**
|
||||
*
|
||||
* Pausing
|
||||
*
|
||||
*/
|
||||
|
||||
/// @notice The caller specified a pause window period longer than the
|
||||
/// maximum.
|
||||
error VaultPauseWindowDurationTooLarge();
|
||||
|
||||
/// @notice The caller specified a buffer period longer than the maximum.
|
||||
error PauseBufferPeriodDurationTooLarge();
|
||||
|
||||
/// @notice A user tried to perform an operation while the Vault was paused.
|
||||
error VaultPaused();
|
||||
|
||||
/// @notice Governance tried to unpause the Vault when it was not paused.
|
||||
error VaultNotPaused();
|
||||
|
||||
/// @notice Governance tried to pause the Vault after the pause period
|
||||
/// expired.
|
||||
error VaultPauseWindowExpired();
|
||||
|
||||
/**
|
||||
* @notice A user tried to perform an operation involving a paused Pool.
|
||||
* @param pool The paused pool
|
||||
*/
|
||||
error PoolPaused(address pool);
|
||||
|
||||
/**
|
||||
* @notice Governance tried to unpause the Pool when it was not paused.
|
||||
* @param pool The unpaused pool
|
||||
*/
|
||||
error PoolNotPaused(address pool);
|
||||
|
||||
/**
|
||||
* @notice Governance tried to pause a Pool after the pause period expired.
|
||||
* @param pool The pool
|
||||
*/
|
||||
error PoolPauseWindowExpired(address pool);
|
||||
|
||||
/**
|
||||
*
|
||||
* ERC4626 token buffers
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @notice The buffer for the given wrapped token was already initialized.
|
||||
* @param wrappedToken The wrapped token corresponding to the buffer
|
||||
*/
|
||||
error BufferAlreadyInitialized(IERC4626 wrappedToken);
|
||||
|
||||
/**
|
||||
* @notice The buffer for the given wrapped token was not initialized.
|
||||
* @param wrappedToken The wrapped token corresponding to the buffer
|
||||
*/
|
||||
error BufferNotInitialized(IERC4626 wrappedToken);
|
||||
|
||||
/// @notice The user is trying to remove more than their allocated shares
|
||||
/// from the buffer.
|
||||
error NotEnoughBufferShares();
|
||||
|
||||
/**
|
||||
* @notice The wrapped token asset does not match the underlying token.
|
||||
* @dev This should never happen, but a malicious wrapper contract might not
|
||||
* return the correct address.
|
||||
* Legitimate wrapper contracts should make the asset a constant or
|
||||
* immutable value.
|
||||
*
|
||||
* @param wrappedToken The wrapped token corresponding to the buffer
|
||||
* @param underlyingToken The underlying token returned by `asset`
|
||||
*/
|
||||
error WrongUnderlyingToken(IERC4626 wrappedToken, address underlyingToken);
|
||||
|
||||
/**
|
||||
* @notice A wrapped token reported the zero address as its underlying token
|
||||
* asset.
|
||||
* @dev This should never happen, but a malicious wrapper contract might do
|
||||
* this (e.g., in an attempt to
|
||||
* re-initialize the buffer).
|
||||
*
|
||||
* @param wrappedToken The wrapped token corresponding to the buffer
|
||||
*/
|
||||
error InvalidUnderlyingToken(IERC4626 wrappedToken);
|
||||
|
||||
/**
|
||||
* @notice The amount given to wrap/unwrap was too small, which can
|
||||
* introduce rounding issues.
|
||||
* @param wrappedToken The wrapped token corresponding to the buffer
|
||||
*/
|
||||
error WrapAmountTooSmall(IERC4626 wrappedToken);
|
||||
|
||||
/// @notice Buffer operation attempted while vault buffers are paused.
|
||||
error VaultBuffersArePaused();
|
||||
|
||||
/// @notice Buffer shares were minted to the zero address.
|
||||
error BufferSharesInvalidReceiver();
|
||||
|
||||
/// @notice Buffer shares were burned from the zero address.
|
||||
error BufferSharesInvalidOwner();
|
||||
|
||||
/**
|
||||
* @notice The total supply of a buffer can't be lower than the absolute
|
||||
* minimum.
|
||||
* @param totalSupply The total supply value that was below the minimum
|
||||
*/
|
||||
error BufferTotalSupplyTooLow(uint256 totalSupply);
|
||||
|
||||
/// @dev A wrap/unwrap operation consumed more or returned less underlying
|
||||
/// tokens than it should.
|
||||
error NotEnoughUnderlying(
|
||||
IERC4626 wrappedToken,
|
||||
uint256 expectedUnderlyingAmount,
|
||||
uint256 actualUnderlyingAmount
|
||||
);
|
||||
|
||||
/// @dev A wrap/unwrap operation consumed more or returned less wrapped
|
||||
/// tokens than it should.
|
||||
error NotEnoughWrapped(
|
||||
IERC4626 wrappedToken,
|
||||
uint256 expectedWrappedAmount,
|
||||
uint256 actualWrappedAmount
|
||||
);
|
||||
|
||||
/// @dev Shares issued during initialization are below the requested amount.
|
||||
error IssuedSharesBelowMin(uint256 issuedShares, uint256 minIssuedShares);
|
||||
|
||||
/**
|
||||
*
|
||||
* Miscellaneous
|
||||
*
|
||||
*/
|
||||
|
||||
/// @notice Pool does not support adding / removing liquidity with an
|
||||
/// unbalanced input.
|
||||
error DoesNotSupportUnbalancedLiquidity();
|
||||
|
||||
/// @notice The contract should not receive ETH.
|
||||
error CannotReceiveEth();
|
||||
|
||||
/**
|
||||
* @notice The `VaultExtension` contract was called by an account directly.
|
||||
* @dev It can only be called by the Vault via delegatecall.
|
||||
*/
|
||||
error NotVaultDelegateCall();
|
||||
|
||||
/// @notice The `VaultExtension` contract was configured with an incorrect
|
||||
/// Vault address.
|
||||
error WrongVaultExtensionDeployment();
|
||||
|
||||
/// @notice The `ProtocolFeeController` contract was configured with an
|
||||
/// incorrect Vault address.
|
||||
error WrongProtocolFeeControllerDeployment();
|
||||
|
||||
/// @notice The `VaultAdmin` contract was configured with an incorrect Vault
|
||||
/// address.
|
||||
error WrongVaultAdminDeployment();
|
||||
|
||||
/// @notice Quote reverted with a reserved error code.
|
||||
error QuoteResultSpoofed();
|
||||
|
||||
/// @notice Thrown when the number of tokens permissioned to a spender does
|
||||
/// not match the number of tokens being transferred
|
||||
/// @dev If the spender does not need to transfer the number of tokens
|
||||
/// permitted, the spender can request amount 0 to be transferred
|
||||
error LengthMismatch();
|
||||
|
||||
/// @notice Emits an event when the owner successfully invalidates an
|
||||
/// unordered nonce.
|
||||
event UnorderedNonceInvalidation(
|
||||
address indexed owner, uint256 word, uint256 mask
|
||||
);
|
||||
/// @notice Thrown when an allowance on a token has expired.
|
||||
/// @param deadline The timestamp at which the allowed amount is no longer
|
||||
/// valid
|
||||
|
||||
error AllowanceExpired(uint256 deadline);
|
||||
|
||||
/// @notice Thrown when an allowance on a token has been depleted.
|
||||
/// @param amount The maximum amount allowed
|
||||
error InsufficientAllowance(uint256 amount);
|
||||
|
||||
error NotStaticCall();
|
||||
error VaultQueriesDisabled();
|
||||
error SwapDeadline();
|
||||
error InsufficientEth();
|
||||
error InvalidAmount(uint256 maxAmount);
|
||||
|
||||
/**
|
||||
* @notice Error raised when the protocol swap fee percentage exceeds the
|
||||
* maximum allowed value.
|
||||
* @dev Note that this is checked for both the global and pool-specific
|
||||
* protocol swap fee percentages.
|
||||
*/
|
||||
error ProtocolSwapFeePercentageTooHigh();
|
||||
|
||||
/**
|
||||
* @notice Error raised when the protocol yield fee percentage exceeds the
|
||||
* maximum allowed value.
|
||||
* @dev Note that this is checked for both the global and pool-specific
|
||||
* protocol yield fee percentages.
|
||||
*/
|
||||
error ProtocolYieldFeePercentageTooHigh();
|
||||
|
||||
/**
|
||||
* @notice Error raised if there is no pool creator on a withdrawal attempt
|
||||
* from the given pool.
|
||||
* @param pool The pool with no creator
|
||||
*/
|
||||
error PoolCreatorNotRegistered(address pool);
|
||||
|
||||
/**
|
||||
* @notice Error raised if the wrong account attempts to withdraw pool
|
||||
* creator fees.
|
||||
* @param caller The account attempting to withdraw pool creator fees
|
||||
* @param pool The pool the caller tried to withdraw from
|
||||
*/
|
||||
error CallerIsNotPoolCreator(address caller, address pool);
|
||||
|
||||
/// @notice Error raised when the pool creator swap or yield fee percentage
|
||||
/// exceeds the maximum allowed value.
|
||||
error PoolCreatorFeePercentageTooHigh();
|
||||
|
||||
error AssetBoundsExceeded();
|
||||
|
||||
/// @notice The amplification factor is below the minimum of the range (1 -
|
||||
/// 5000).
|
||||
error AmplificationFactorTooLow();
|
||||
|
||||
/// @notice The amplification factor is above the maximum of the range (1 -
|
||||
/// 5000).
|
||||
error AmplificationFactorTooHigh();
|
||||
|
||||
/// @notice The amplification change duration is too short.
|
||||
error AmpUpdateDurationTooShort();
|
||||
|
||||
/// @notice The amplification change rate is too fast.
|
||||
error AmpUpdateRateTooFast();
|
||||
|
||||
/// @notice Amplification update operations must be done one at a time.
|
||||
error AmpUpdateAlreadyStarted();
|
||||
error StandardPoolWithCreator();
|
||||
|
||||
/// @notice Indicates that one of the pool tokens' weight is below the
|
||||
/// minimum allowed.
|
||||
error MinWeight();
|
||||
|
||||
/// @notice Indicates that the sum of the pool tokens' weights is not
|
||||
/// FixedPoint.ONE.
|
||||
error NormalizedWeightInvariant();
|
||||
error WeightedPoolBptRateUnsupported();
|
||||
/// @notice Arrays passed to a function and intended to be parallel have
|
||||
/// different lengths.
|
||||
error InputLengthMismatch();
|
||||
|
||||
/**
|
||||
* @notice More than one non-zero value was given for a single token
|
||||
* operation.
|
||||
* @dev Input arrays for single token add/remove liquidity operations are
|
||||
* expected to have only one non-zero value,
|
||||
* corresponding to the token being added or removed. This error results if
|
||||
* there are multiple non-zero entries.
|
||||
*/
|
||||
error MultipleNonZeroInputs();
|
||||
|
||||
/**
|
||||
* @notice No valid input was given for a single token operation.
|
||||
* @dev Input arrays for single token add/remove liquidity operations are
|
||||
* expected to have one non-zero value,
|
||||
* corresponding to the token being added or removed. This error results if
|
||||
* all entries are zero.
|
||||
*/
|
||||
error AllZeroInputs();
|
||||
|
||||
/**
|
||||
* @notice The tokens supplied to an array argument were not sorted in
|
||||
* numerical order.
|
||||
* @dev Tokens are not sorted by address on registration. This is an
|
||||
* optimization so that off-chain processes can
|
||||
* predict the token order without having to query the Vault. (It is also
|
||||
* legacy v2 behavior.)
|
||||
*/
|
||||
error TokensNotSorted();
|
||||
error ExcessiveInvalidation();
|
||||
|
||||
error PoolAddressMismatch(address pool);
|
||||
|
||||
error StaticATokenInvalidZeroShares();
|
||||
|
||||
error OnlyPauseGuardian(address caller);
|
||||
|
||||
error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);
|
||||
}
|
||||
36
evm/src/balancer-v3/manifest.yaml
Normal file
36
evm/src/balancer-v3/manifest.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
# information about the author helps us reach out in case of issues.
|
||||
author:
|
||||
name: Shadowy Creators
|
||||
email: hello@shadowycoders.dev
|
||||
|
||||
# Protocol Constants
|
||||
constants:
|
||||
protocol_gas: 30000
|
||||
# minimum capabilities we can expect, individual pools may extend these
|
||||
capabilities:
|
||||
- SellSide
|
||||
- BuySide
|
||||
- HardLimits
|
||||
|
||||
# The file containing the adapter contract
|
||||
contract: BalancerV3SwapAdapter.sol
|
||||
|
||||
# Deployment instances used to generate chain specific bytecode.
|
||||
instances:
|
||||
- chain:
|
||||
name: mainnet
|
||||
id: 1
|
||||
arguments:
|
||||
- "0xBA12222222228d8Ba445958a75a0704d566BF2C8"
|
||||
|
||||
# Specify some automatic test cases in case getPoolIds and
|
||||
# getTokens are not implemented.
|
||||
tests:
|
||||
instances:
|
||||
- pool_id: "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014"
|
||||
sell_token: "0xba100000625a3754423978a60c9317c58a424e3D"
|
||||
buy_token: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||
block: 17000000
|
||||
chain:
|
||||
name: mainnet
|
||||
id: 1
|
||||
108
evm/src/libraries/CustomBytesAppend.sol
Normal file
108
evm/src/libraries/CustomBytesAppend.sol
Normal file
@@ -0,0 +1,108 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
library CustomBytesAppend {
|
||||
// Constants for the custom prefix used in the bytes32 format
|
||||
string private constant CUSTOM = "_CUSTOM_";
|
||||
|
||||
/**
|
||||
* @dev Extracts an address from a bytes32 input, assuming it is either
|
||||
* prepended or appended with `_CUSTOM_`.
|
||||
* @param input The bytes32 input containing the address and custom
|
||||
* prefix/suffix.
|
||||
* @return extractedAddress The extracted address.
|
||||
*/
|
||||
function extractAddress(bytes32 input)
|
||||
public
|
||||
pure
|
||||
returns (address extractedAddress)
|
||||
{
|
||||
// Convert the bytes32 input into a dynamic bytes array for manipulation
|
||||
bytes memory inputBytes = abi.encodePacked(input);
|
||||
|
||||
// Check if the bytes contain the custom prefix
|
||||
if (hasPrefix(inputBytes)) {
|
||||
// If prefixed, extract the 20 bytes after the prefix as the address
|
||||
extractedAddress =
|
||||
bytesToAddress(slice(inputBytes, bytes(CUSTOM).length, 20));
|
||||
}
|
||||
// Check if the bytes contain the custom suffix
|
||||
else if (hasSuffix(inputBytes)) {
|
||||
// If suffixed, extract the first 20 bytes as the address
|
||||
extractedAddress = bytesToAddress(slice(inputBytes, 0, 20));
|
||||
} else {
|
||||
// Revert if neither prefix nor suffix is found
|
||||
revert("Invalid input format");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Checks if the bytes data has the custom prefix.
|
||||
* @param data The bytes array to check.
|
||||
* @return True if the prefix matches, false otherwise.
|
||||
*/
|
||||
function hasPrefix(bytes memory data) internal pure returns (bool) {
|
||||
// Compare the first bytes of the input with the prefix using keccak256
|
||||
// for hashing
|
||||
return keccak256(slice(data, 0, bytes(CUSTOM).length))
|
||||
== keccak256(bytes(CUSTOM));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Checks if the bytes data has the custom suffix.
|
||||
* @param data The bytes array to check.
|
||||
* @return True if the suffix matches, false otherwise.
|
||||
*/
|
||||
function hasSuffix(bytes memory data) internal pure returns (bool) {
|
||||
// Compare the last bytes of the input with the suffix using keccak256
|
||||
// for hashing
|
||||
return keccak256(
|
||||
slice(
|
||||
data, data.length - bytes(CUSTOM).length, bytes(CUSTOM).length
|
||||
)
|
||||
) == keccak256(bytes(CUSTOM));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Slices a bytes array.
|
||||
* @param data The bytes array to slice.
|
||||
* @param start The starting index of the slice.
|
||||
* @param length The length of the slice.
|
||||
* @return The sliced bytes array.
|
||||
*/
|
||||
function slice(bytes memory data, uint256 start, uint256 length)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
// Ensure the slice operation does not exceed the bounds of the array
|
||||
require(data.length >= start + length, "Invalid slice");
|
||||
|
||||
// Create a new bytes array to hold the sliced data
|
||||
bytes memory result = new bytes(length);
|
||||
for (uint256 i = 0; i < length; i++) {
|
||||
result[i] = data[start + i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Converts a bytes array of length 20 into an address.
|
||||
* @param data The bytes array (must be 20 bytes long).
|
||||
* @return addr The converted address.
|
||||
*/
|
||||
function bytesToAddress(bytes memory data)
|
||||
internal
|
||||
pure
|
||||
returns (address addr)
|
||||
{
|
||||
// Ensure the input length is exactly 20 bytes (size of an Ethereum
|
||||
// address)
|
||||
require(data.length == 20, "Invalid address length");
|
||||
|
||||
// Use inline assembly to efficiently convert the bytes to an address
|
||||
assembly {
|
||||
addr := mload(add(data, 20))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user