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:
mrBovo
2025-06-26 12:19:39 +02:00
committed by GitHub
parent dfa87f662d
commit fc0fb1e540
43 changed files with 25137 additions and 14 deletions

View File

@@ -1,4 +1,4 @@
name: test & check evm
name: Foundry CI
on:
pull_request:
@@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: true
name: Foundry project
name: Foundry Lint & Test
runs-on: ubuntu-latest
defaults:
run:
@@ -26,7 +26,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: stable
- name: Run Forge build
run: |

View File

@@ -3,11 +3,17 @@ src = "src"
out = "out"
libs = ["lib"]
via_ir = true
optimizer = true
optimizer_runs = 200
solc_version = "0.8.27" # 0.8.25 and 0.8.26 are broken with via-ir (see https://github.com/ethereum/solidity/issues/15004)
evm_version = 'cancun'
[profile.ci]
size_check = false
[rpc_endpoints]
mainnet = "${ETH_RPC_URL}"
sepolia = "${SEPOLIA_RPC_URL}"
[fmt]
line_length = 80
@@ -20,3 +26,4 @@ mainnet = { key = "${ETHERSCAN_MAINNET_KEY}" }
[[profile.default.fs_permissions]]
access = "read-write"
path = "out"

View 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
);
}
}

View 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;
}
}

View 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;
}
}
}
}

View 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
);
}
}
}
}
}

View 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;
}

View 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
}
}

View 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
);
}
}
}

View 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);
}

View 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

View 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))
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.26;
import {IERC4626} from
"openzeppelin-contracts/contracts/interfaces/IERC4626.sol";
interface IBufferRouter {
function initializeBuffer(
IERC4626 wrappedToken,
uint256 exactAmountUnderlyingIn,
uint256 exactAmountWrappedIn,
uint256 minIssuedShares
) external returns (uint256 issuedShares);
}

View File

@@ -0,0 +1,37 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.26;
import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC4626.sol";
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract MockSETHx is ERC4626 {
bool public isBufferInitialized;
constructor(IERC20 _asset) ERC4626(_asset) ERC20("Staked ETHx", "sETHx") {}
function _decimals() internal pure returns (uint8) {
return 18;
}
// Override convertToShares to implement 1:1 conversion for testing
function convertToShares(uint256 assets)
public
view
virtual
override
returns (uint256)
{
return assets;
}
// Override convertToAssets to implement 1:1 conversion for testing
function convertToAssets(uint256 shares)
public
view
virtual
override
returns (uint256)
{
return shares;
}
}

View File

@@ -0,0 +1,40 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.26;
import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC4626.sol";
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract MockSGOETH is ERC4626 {
bool public isBufferInitialized;
constructor(IERC20 _asset)
ERC4626(_asset)
ERC20("Staked GOETH", "sGOETH")
{}
function _decimals() internal pure returns (uint8) {
return 18;
}
// Override convertToShares to implement 1:1 conversion for testing
function convertToShares(uint256 assets)
public
view
virtual
override
returns (uint256)
{
return assets;
}
// Override convertToAssets to implement 1:1 conversion for testing
function convertToAssets(uint256 shares)
public
view
virtual
override
returns (uint256)
{
return shares;
}
}

View File

@@ -0,0 +1,37 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.26;
import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC4626.sol";
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract MockSUSDC is ERC4626 {
bool public isBufferInitialized;
constructor(IERC20 _asset) ERC4626(_asset) ERC20("Staked USDC", "sUSDC") {}
function _decimals() internal pure returns (uint8) {
return 6; // Same as USDC
}
// Override convertToShares to implement 1:1 conversion for testing
function convertToShares(uint256 assets)
public
view
virtual
override
returns (uint256)
{
return assets;
}
// Override convertToAssets to implement 1:1 conversion for testing
function convertToAssets(uint256 shares)
public
view
virtual
override
returns (uint256)
{
return shares;
}
}

67
substreams/Cargo.lock generated
View File

@@ -174,7 +174,7 @@ dependencies = [
"serde_json",
"sha3",
"thiserror 1.0.69",
"uint",
"uint 0.9.5",
]
[[package]]
@@ -191,7 +191,7 @@ dependencies = [
"serde_json",
"sha3",
"thiserror 1.0.69",
"uint",
"uint 0.9.5",
]
[[package]]
@@ -253,6 +253,29 @@ dependencies = [
"tycho-substreams 0.2.2",
]
[[package]]
name = "ethereum-balancer-v3"
version = "0.4.0"
dependencies = [
"anyhow",
"bytes",
"ethabi 18.0.0",
"getrandom",
"hex",
"hex-literal 0.4.1",
"itertools 0.13.0",
"keccak-hash",
"num-bigint",
"prost 0.11.9",
"prost-types 0.12.6",
"regex",
"serde",
"serde_qs",
"substreams",
"substreams-ethereum",
"tycho-substreams 0.2.1 (git+https://github.com/propeller-heads/tycho-protocol-sdk.git?rev=51995f9)",
]
[[package]]
name = "ethereum-curve"
version = "0.3.3"
@@ -417,7 +440,7 @@ dependencies = [
"impl-rlp",
"impl-serde 0.3.2",
"primitive-types 0.11.1",
"uint",
"uint 0.9.5",
]
[[package]]
@@ -431,7 +454,7 @@ dependencies = [
"impl-rlp",
"impl-serde 0.4.0",
"primitive-types 0.12.2",
"uint",
"uint 0.9.5",
]
[[package]]
@@ -766,6 +789,16 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "keccak-hash"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e1b8590eb6148af2ea2d75f38e7d29f5ca970d5a4df456b3ef19b8b415d0264"
dependencies = [
"primitive-types 0.13.1",
"tiny-keccak",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -973,7 +1006,7 @@ dependencies = [
"impl-codec",
"impl-rlp",
"impl-serde 0.3.2",
"uint",
"uint 0.9.5",
]
[[package]]
@@ -986,7 +1019,17 @@ dependencies = [
"impl-codec",
"impl-rlp",
"impl-serde 0.4.0",
"uint",
"uint 0.9.5",
]
[[package]]
name = "primitive-types"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5"
dependencies = [
"fixed-hash 0.8.0",
"uint 0.10.0",
]
[[package]]
@@ -1738,6 +1781,18 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "uint"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e"
dependencies = [
"byteorder",
"crunchy",
"hex",
"static_assertions",
]
[[package]]
name = "unicode-ident"
version = "1.0.14"

View File

@@ -16,6 +16,7 @@ members = [
"ethereum-uniswap-v4",
"ethereum-ekubo-v2",
"ethereum-maverick-v2",
"ethereum-balancer-v3",
]
resolver = "2"

View File

@@ -14,9 +14,9 @@ use crate::{
models::{InterimContractChange, TransactionChanges},
prelude::TransactionChangesBuilder,
};
use substreams_ethereum::pb::{
eth,
eth::v2::{block::DetailLevel, CallType, TransactionTrace},
use substreams_ethereum::pb::eth::{
self,
v2::{block::DetailLevel, CallType, TransactionTrace},
};
/// Extracts and aggregates contract changes from a block.

1330
substreams/ethereum-balancer-v3/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
[package]
name = "ethereum-balancer-v3"
version = "0.4.0"
edition = "2021"
[lib]
name = "ethereum_balancer_v3"
crate-type = ["cdylib"]
[dependencies]
substreams = "0.5.22"
substreams-ethereum = "0.9.9"
prost = "0.11"
prost-types = "0.12.3"
hex-literal = "0.4.1"
ethabi = "18.0.0"
hex = "0.4.3"
bytes = "1.5.0"
anyhow = "1.0.75"
num-bigint = "0.4.4"
tycho-substreams = { git = "https://github.com/propeller-heads/tycho-protocol-sdk.git", rev = "51995f9" }
serde = { version = "1.0", features = ["derive"] }
serde_qs = "0.13.0"
itertools = "0.13.0"
keccak-hash = "0.11.0"
[build-dependencies]
anyhow = "1"
substreams-ethereum = "0.9"
regex = "1.10.0"
# Required so that ethabi > ethereum-types build correctly under wasm32-unknown-unknown
[target.wasm32-unknown-unknown.dependencies]
getrandom = { version = "0.2", features = ["custom"] }

View File

@@ -0,0 +1,38 @@
# balancer_v3 Substreams modules
This package was initialized via `substreams init`, using the `evm-events-calls` template.
## Usage
```bash
substreams build
substreams auth
substreams gui # Get streaming!
substreams registry login # Login to substreams.dev
substreams registry publish # Publish your Substreams to substreams.dev
```
## Modules
All of these modules produce data filtered by these contracts:
- _vault_ at **0xba1333333333a1ba1108e8412f11850a5c319ba9**
- _stable_pool_factory_ at **0xb9d01ca61b9c181da1051bfdd28e1097e920ab14**
- _weighted_pool_factory_ at **0x201efd508c8dfe9de1a13c2452863a78cb2a86cc**
- stable_pool contracts created from _stable_pool_factory_
- weighted_pool contracts created from _weighted_pool_factory_
### `map_events_calls`
This module gets you events _and_ calls
### `map_events`
This module gets you only events that matched.
### `map_calls`
This module gets you only calls that matched.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,11 @@
version: v1
plugins:
- plugin: buf.build/community/neoeinstein-prost:v0.4.0
out: src/pb
opt:
- file_descriptor_set=false
- plugin: buf.build/community/neoeinstein-prost-crate:v0.4.1
out: src/pb
opt:
- no_features

View File

@@ -0,0 +1,50 @@
#![allow(clippy::all)]
use anyhow::{Ok, Result};
use regex::Regex;
use std::fs;
use substreams_ethereum::Abigen;
fn main() -> Result<(), anyhow::Error> {
let file_names = [
"abi/vault_contract.abi.json",
"abi/stable_pool_factory_contract.abi.json",
"abi/weighted_pool_factory_contract.abi.json",
"abi/stable_pool_contract.abi.json",
"abi/weighted_pool_contract.abi.json",
];
let file_output_names = [
"src/abi/vault_contract.rs",
"src/abi/stable_pool_factory_contract.rs",
"src/abi/weighted_pool_factory_contract.rs",
"src/abi/stable_pool_contract.rs",
"src/abi/weighted_pool_contract.rs",
];
let mut i = 0;
for f in file_names {
let contents = fs::read_to_string(f).expect("Should have been able to read the file");
// sanitize fields and attributes starting with an underscore
let regex = Regex::new(r#"("\w+"\s?:\s?")_(\w+")"#).unwrap();
let sanitized_abi_file = regex.replace_all(contents.as_str(), "${1}u_${2}");
// sanitize fields and attributes with multiple consecutive underscores
let re = Regex::new(r"_+").unwrap();
let re_sanitized_abi_file =
re.replace_all(&sanitized_abi_file, |caps: &regex::Captures| {
let count = caps[0].len();
let replacement = format!("{}_", "_u".repeat(count - 1));
replacement
});
Abigen::from_bytes("Contract", re_sanitized_abi_file.as_bytes())?
.generate()?
.write_to_file(file_output_names[i])?;
i = i + 1;
}
Ok(())
}

View File

@@ -0,0 +1,110 @@
substreams_yaml_path: ./substreams.yaml
protocol_type_names:
- "balancer_v3_pool"
adapter_contract: "BalancerV3SwapAdapter"
adapter_build_signature: "constructor(address,address,address,address)"
adapter_build_args: "0xbA1333333333a1BA1108E8412f11850A5C319bA9,0x136f1EFcC3f8f88516B9E94110D56FDBfB1778d1,0x000000000022D473030F116dDEE9F6B43aC78BA3,0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
skip_balance_check: true # Skipped because this Balancer V3 uses a Vault, the current version of the testing module doesn't support this.
# vault address for v3 needed for the router contract
initialized_accounts:
- "0xbA1333333333a1BA1108E8412f11850A5C319bA9"
tests:
- name: test_stable_pool
start_block: 21374757
stop_block: 21374777
expected_components:
- id: "0xc4ce391d82d164c166df9c8336ddf84206b2f812"
tokens:
- "0x0FE906e030a44eF24CA8c7dC7B7c53A6C4F00ce9"
- "0x775F661b0bD1739349b9A2A3EF60be277c5d2D29"
static_attributes:
manual_updates: "0x01"
pool_type: "0x537461626c65506f6f6c466163746f7279"
bpt: "0xc4ce391d82d164c166df9c8336ddf84206b2f812"
fee: "0x5af3107a4000"
rate_providers: "0x5b22307866346235643163323266333561343630623931656464376633336365666536313965326661616634222c22307863646161363863653332323732386665343138356136306631303363313934663165326334376263225d"
skip_simulation: false
creation_tx: "0xecfe638ea155e78ca2698bea0bee5f86c2f77abe3e9dc93ed19ab6e674165b95"
- name: test_weighted_pool_creation
start_block: 21701410
stop_block: 21701420
expected_components:
- id: "0x4Fd081923824D6AbdaCc862d8449e124A8634b12"
tokens:
- "0x249ca82617ec3dfb2589c4c17ab7ec9765350a18"
- "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
static_attributes:
manual_updates: "0x01"
normalized_weights: "0x5b22307830623161326263326563353030303030222c22307830326336386166306262313430303030225d"
fee: "0x038d7ea4c68000"
pool_type: "0x5765696768746564506f6f6c466163746f7279"
skip_simulation: true # This test is only for creation, simulation tests are below
creation_tx: "0xaf0f8da4b7b148f156605673c6ac56657f67c47001aadd5b3b64d6cb787daad4"
- name: test_erc4626_pool_creation # stable pool
start_block: 21375196
stop_block: 21375216
expected_components:
- id: "0x89BB794097234E5E930446C0CeC0ea66b35D7570"
tokens:
- "0x7bc3485026ac48b6cf9baf0a377477fff5703af8"
- "0xd4fa2d31b7968e448877f69a96de69f5de8cd23e"
static_attributes:
manual_updates: "0x01"
pool_type: "0x537461626c65506f6f6c466163746f7279"
bpt: "0x89bb794097234e5e930446c0cec0ea66b35d7570"
fee: "0x2d79883d2000"
rate_providers: "0x5b22307865646636336363653462613730636265373430363462373638373838326537316562623065393838222c22307838663465383433396239373033363336343834323163363932646438393766623963306264316439225d"
skip_simulation: false
creation_tx: "0x536baa8f18a5f9e32b08f695593f47d996a509edc886ccab6f89b001f2d2b4e4"
- name: test_swap_g_usdc # weighted pool
start_block: 21389576
stop_block: 21389676
expected_components:
- id: "0xf91c11BA4220b7a72E1dc5E92f2b48D3fdF62726"
tokens:
- "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
- "0x440017A1b021006d556d7fc06A54c32E42Eb745B"
static_attributes:
pool_type: "0x5765696768746564506f6f6c466163746f7279"
manual_updates: "0x01"
normalized_weights: "0x5b22307830366630356235396433623230303030222c22307830366630356235396433623230303030225d"
fee: "0x0aa87bee538000"
skip_simulation: false
creation_tx: "0x50a58ca41af90029a67bc7f4305cceb1e85800246f9c559fa74d8625ddfe14ed"
- name: test_obs_weth # weighted pool
start_block: 21420490
stop_block: 21420516
expected_components:
- id: "0x4403a2721A9A9956584dc19F553720CEf0Df35b0"
tokens:
- "0xafd9268fdfebee905f7439b12c943bc18ad293c2"
- "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
static_attributes:
pool_type: "0x5765696768746564506f6f6c466163746f7279"
manual_updates: "0x01"
normalized_weights: "0x5b22307830646264326663313337613330303030222c2230783233383666323666633130303030225d"
fee: "0x038d7ea4c68000"
skip_simulation: false
creation_tx: "0x8540c2bfbc16dc2db44bc96dd32c2901eb2080d27f959b5831d96cd99903523e"
- name: test_tbc_verse
start_block: 21722567
stop_block: 21722950
expected_components:
- id: "0x82074b99f3E927658BcAFd81F9948d89192CDF91"
tokens:
- "0x18084fba666a33d37592fa2633fd49a74dd93a88"
- "0x249ca82617ec3dfb2589c4c17ab7ec9765350a18"
static_attributes:
pool_type: "0x5765696768746564506f6f6c466163746f7279"
manual_updates: "0x01"
normalized_weights: "0x5b22307830326336386166306262313430303030222c22307830623161326263326563353030303030225d"
fee: "0x038d7ea4c68000"
skip_simulation: false
creation_tx: "0x214064cfcf41f7b94b4f9cdd35644b851e19ba246822b7d8f2bdcc3d6c06074e"

View File

@@ -0,0 +1,7 @@
#![allow(clippy::all)]
pub mod stable_pool_contract;
pub mod stable_pool_factory_contract;
pub mod vault_contract;
pub mod weighted_pool_contract;
pub mod weighted_pool_factory_contract;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
mod abi;
mod modules;
mod pool_factories;

View File

@@ -0,0 +1,541 @@
use crate::{
abi::vault_contract::{
events::{LiquidityAdded, LiquidityRemoved, PoolPausedStateChanged, Swap},
functions::{Erc4626BufferWrapOrUnwrap, SendTo, Settle},
},
pool_factories,
};
use anyhow::Result;
use itertools::Itertools;
use keccak_hash::keccak;
use std::collections::HashMap;
use substreams::{
hex, log,
pb::substreams::StoreDeltas,
store::{
StoreAddBigInt, StoreGet, StoreGetInt64, StoreGetProto, StoreNew, StoreSetIfNotExists,
StoreSetIfNotExistsInt64, StoreSetIfNotExistsProto,
},
};
use substreams_ethereum::{
pb::eth::{self, v2::StorageChange},
Event, Function,
};
use tycho_substreams::{
attributes::json_deserialize_address_list, balances::aggregate_balances_changes,
block_storage::get_block_storage_changes, contract::extract_contract_changes_builder,
entrypoint::create_entrypoint, models::entry_point_params::TraceData, prelude::*,
};
pub const VAULT_ADDRESS: &[u8] = &hex!("bA1333333333a1BA1108E8412f11850A5C319bA9");
pub const VAULT_EXTENSION_ADDRESS: &[u8; 20] = &hex!("0E8B07657D719B86e06bF0806D6729e3D528C9A9");
pub const BATCH_ROUTER_ADDRESS: &[u8; 20] = &hex!("136f1efcc3f8f88516b9e94110d56fdbfb1778d1");
pub const PERMIT_2_ADDRESS: &[u8; 20] = &hex!("000000000022D473030F116dDEE9F6B43aC78BA3");
#[substreams::handlers::map]
pub fn map_components(block: eth::v2::Block) -> Result<BlockTransactionProtocolComponents> {
let mut tx_components = Vec::new();
for tx in block.transactions() {
let mut components = Vec::new();
for (log, call) in tx.logs_with_calls() {
if let Some(component) =
pool_factories::address_map(log.address.as_slice(), log, call.call)
{
components.push(component);
}
}
if !components.is_empty() {
tx_components.push(TransactionProtocolComponents { tx: Some(tx.into()), components });
}
}
Ok(BlockTransactionProtocolComponents { tx_components })
}
/// Simply stores the `ProtocolComponent`s with the pool address as the key and the pool id as value
#[substreams::handlers::store]
pub fn store_components(
map: BlockTransactionProtocolComponents,
store: StoreSetIfNotExistsProto<ProtocolComponent>,
) {
map.tx_components
.into_iter()
.for_each(|tx_pc| {
tx_pc
.components
.into_iter()
.for_each(|pc| store.set_if_not_exists(0, format!("pool:{0}", &pc.id), &pc))
});
}
/// Set of token that are used by BalancerV3. This is used to filter out account balances updates
/// for unknown tokens.
#[substreams::handlers::store]
pub fn store_token_set(map: BlockTransactionProtocolComponents, store: StoreSetIfNotExistsInt64) {
map.tx_components
.into_iter()
.for_each(|tx_pc| {
tx_pc
.components
.into_iter()
.for_each(|pc| {
pc.tokens
.into_iter()
.for_each(|token| store.set_if_not_exists(0, hex::encode(token), &1))
})
});
}
#[substreams::handlers::map]
pub fn map_relative_balances(
block: eth::v2::Block,
store: StoreGetProto<ProtocolComponent>,
) -> Result<BlockBalanceDeltas, anyhow::Error> {
let balance_deltas = block
.logs()
.filter(|log| log.address() == VAULT_ADDRESS)
.flat_map(|vault_log| {
let mut deltas = Vec::new();
if let Some(Swap { pool, token_in, token_out, amount_in, amount_out, .. }) =
Swap::match_and_decode(vault_log.log)
{
let component_id = format!("0x{}", hex::encode(pool));
log::info!(
"swap at component id: {:?} with key: {:?}",
component_id,
format!("pool:{}", &component_id)
);
if store
.get_last(format!("pool:{}", &component_id))
.is_some()
{
deltas.extend_from_slice(&[
BalanceDelta {
ord: vault_log.ordinal(),
tx: Some(vault_log.receipt.transaction.into()),
token: token_in.to_vec(),
delta: amount_in.to_signed_bytes_be(),
component_id: component_id.as_bytes().to_vec(),
},
BalanceDelta {
ord: vault_log.ordinal(),
tx: Some(vault_log.receipt.transaction.into()),
token: token_out.to_vec(),
delta: amount_out.neg().to_signed_bytes_be(),
component_id: component_id.as_bytes().to_vec(),
},
]);
}
}
if let Some(LiquidityAdded { pool, amounts_added_raw, .. }) =
LiquidityAdded::match_and_decode(vault_log.log)
{
let component_id = format!("0x{}", hex::encode(pool));
if let Some(component) = store.get_last(format!("pool:{}", &component_id)) {
if component.tokens.len() != amounts_added_raw.len() {
panic!(
"liquidity added to pool with different number of tokens than expected"
);
}
log::info!(
"liquidity added at component id: {:?} with key: {:?} with tokens: {:?}",
component_id,
format!("pool:{}", &component_id),
component.tokens
);
let deltas_from_added_liquidity = amounts_added_raw
.into_iter()
.zip(component.tokens.iter())
.map(|(amount, token)| BalanceDelta {
ord: vault_log.ordinal(),
tx: Some(vault_log.receipt.transaction.into()),
token: token.to_vec(),
delta: amount.to_signed_bytes_be(),
component_id: component_id.as_bytes().to_vec(),
})
.collect::<Vec<_>>();
deltas.extend_from_slice(&deltas_from_added_liquidity);
}
}
if let Some(LiquidityRemoved { pool, amounts_removed_raw, .. }) =
LiquidityRemoved::match_and_decode(vault_log.log)
{
let component_id = format!("0x{}", hex::encode(pool));
log::info!(
"liquidity removed at component id: {:?} with key: {:?}",
component_id,
format!("pool:{}", &component_id)
);
if let Some(component) = store.get_last(format!("pool:{}", &component_id)) {
if component.tokens.len() != amounts_removed_raw.len() {
panic!(
"liquidity removed from pool with different number of tokens than expected"
);
}
let deltas_from_removed_liquidity = amounts_removed_raw
.into_iter()
.zip(component.tokens.iter())
.map(|(amount, token)| BalanceDelta {
ord: vault_log.ordinal(),
tx: Some(vault_log.receipt.transaction.into()),
token: token.to_vec(),
delta: amount.neg().to_signed_bytes_be(),
component_id: component_id.as_bytes().to_vec(),
})
.collect::<Vec<_>>();
deltas.extend_from_slice(&deltas_from_removed_liquidity);
}
}
deltas
})
.collect::<Vec<_>>();
Ok(BlockBalanceDeltas { balance_deltas })
}
/// It's significant to include both the `pool_id` and the `token_id` for each balance delta as the
/// store key to ensure that there's a unique balance being tallied for each.
#[substreams::handlers::store]
pub fn store_balances(deltas: BlockBalanceDeltas, store: StoreAddBigInt) {
tycho_substreams::balances::store_balance_changes(deltas, store);
}
/// This is the main map that handles most of the indexing of this substream.
/// Every contract change is grouped by transaction index via the `transaction_changes`
/// map. Each block of code will extend the `TransactionChanges` struct with the
/// cooresponding changes (balance, component, contract), inserting a new one if it doesn't exist.
/// At the very end, the map can easily be sorted by index to ensure the final
/// `BlockChanges` is ordered by transactions properly.
#[substreams::handlers::map]
pub fn map_protocol_changes(
block: eth::v2::Block,
grouped_components: BlockTransactionProtocolComponents,
deltas: BlockBalanceDeltas,
components_store: StoreGetProto<ProtocolComponent>,
tokens_store: StoreGetInt64,
balance_store: StoreDeltas, // Note, this map module is using the `deltas` mode for the store.
) -> Result<BlockChanges> {
// We merge contract changes by transaction (identified by transaction index) making it easy to
// sort them at the very end.
let mut transaction_changes: HashMap<_, TransactionChangesBuilder> = HashMap::new();
// Handle pool pause state changes
block
.logs()
.filter(|log| log.address() == VAULT_ADDRESS)
.for_each(|log| {
if let Some(PoolPausedStateChanged { pool, paused }) =
PoolPausedStateChanged::match_and_decode(log)
{
let component_id = format!("0x{}", hex::encode(&pool));
let tx: Transaction = log.receipt.transaction.into();
if components_store
.get_last(format!("pool:{}", &component_id))
.is_some()
{
let builder = transaction_changes
.entry(tx.index)
.or_insert_with(|| TransactionChangesBuilder::new(&tx));
let entity_change = EntityChanges {
component_id,
attributes: vec![Attribute {
name: "paused".to_string(),
value: vec![1u8],
change: if paused {
ChangeType::Creation.into()
} else {
ChangeType::Deletion.into()
},
}],
};
builder.add_entity_change(&entity_change);
}
}
});
// `ProtocolComponents` are gathered from `map_pools_created` which just need a bit of work to
// convert into `TransactionChanges`
let default_attributes = vec![
Attribute {
// TODO: remove this and track account_balances instead
name: "balance_owner".to_string(),
value: VAULT_ADDRESS.to_vec(),
change: ChangeType::Creation.into(),
},
Attribute {
name: "stateless_contract_addr_0".into(),
value: address_to_bytes_with_0x(VAULT_EXTENSION_ADDRESS),
change: ChangeType::Creation.into(),
},
Attribute {
name: "stateless_contract_addr_1".into(),
value: address_to_bytes_with_0x(BATCH_ROUTER_ADDRESS),
change: ChangeType::Creation.into(),
},
Attribute {
name: "stateless_contract_addr_2".into(),
value: address_to_bytes_with_0x(PERMIT_2_ADDRESS),
change: ChangeType::Creation.into(),
},
Attribute {
name: "update_marker".to_string(),
value: vec![1u8],
change: ChangeType::Creation.into(),
},
];
grouped_components
.tx_components
.iter()
.for_each(|tx_component| {
// initialise builder if not yet present for this tx
let tx = tx_component.tx.as_ref().unwrap();
let builder = transaction_changes
.entry(tx.index)
.or_insert_with(|| TransactionChangesBuilder::new(tx));
// iterate over individual components created within this tx
tx_component
.components
.iter()
.for_each(|component| {
let rate_providers = component
.static_att
.iter()
.find(|att| att.name == "rate_providers")
.map(|att| json_deserialize_address_list(&att.value));
if let Some(rate_providers) = rate_providers {
for rate_provider in rate_providers {
let trace_data = TraceData::Rpc(RpcTraceData {
caller: None,
calldata: hex::decode("679aefce").unwrap(), // getRate()
});
let (entrypoint, entrypoint_params) = create_entrypoint(
rate_provider,
"getRate()".to_string(),
component.id.clone(),
trace_data,
);
builder.add_entrypoint(&entrypoint);
builder.add_entrypoint_params(&entrypoint_params);
}
}
builder.add_protocol_component(component);
let entity_change = EntityChanges {
component_id: component.id.clone(),
attributes: default_attributes.clone(),
};
builder.add_entity_change(&entity_change)
});
});
// Balance changes are gathered by the `StoreDelta` based on `PoolBalanceChanged` creating
// `BlockBalanceDeltas`. We essentially just process the changes that occurred to the `store`
// this block. Then, these balance changes are merged onto the existing map of tx contract
// changes, inserting a new one if it doesn't exist.
aggregate_balances_changes(balance_store, deltas)
.iter()
.for_each(|(_, (tx, balances))| {
let builder = transaction_changes
.entry(tx.index)
.or_insert_with(|| TransactionChangesBuilder::new(tx));
balances
.values()
.for_each(|token_bc_map| {
token_bc_map.values().for_each(|bc| {
builder.add_balance_change(bc);
})
});
});
// Extract and insert any storage changes that happened for any of the components.
extract_contract_changes_builder(
&block,
|addr| {
components_store
.get_last(format!("pool:0x{0}", hex::encode(addr)))
.is_some() ||
addr.eq(VAULT_ADDRESS)
},
&mut transaction_changes,
);
// Extract token balances for balancer v3 vault
block
.transaction_traces
.iter()
.for_each(|tx| {
let vault_balance_change_per_tx =
get_vault_reserves(tx, &components_store, &tokens_store);
if !vault_balance_change_per_tx.is_empty() {
let tycho_tx = Transaction::from(tx);
let builder = transaction_changes
.entry(tx.index.into())
.or_insert_with(|| TransactionChangesBuilder::new(&tycho_tx));
let mut vault_contract_tlv_changes =
InterimContractChange::new(VAULT_ADDRESS, false);
for (token_addr, reserve_value) in vault_balance_change_per_tx {
vault_contract_tlv_changes.upsert_token_balance(
token_addr.as_slice(),
reserve_value.value.as_slice(),
);
}
builder.add_contract_changes(&vault_contract_tlv_changes);
}
});
transaction_changes
.iter_mut()
.for_each(|(_, change)| {
// this indirection is necessary due to borrowing rules.
let addresses = change
.changed_contracts()
.map(|e| e.to_vec())
.collect::<Vec<_>>();
addresses
.into_iter()
.for_each(|address| {
if address != VAULT_ADDRESS {
// We reconstruct the component_id from the address here
let id = components_store
.get_last(format!("pool:0x{}", hex::encode(address)))
.map(|c| c.id)
.unwrap(); // Shouldn't happen because we filter by known components
// in `extract_contract_changes_builder`
change.mark_component_as_updated(&id);
}
})
});
let block_storage_changes = get_block_storage_changes(&block);
// Process all `transaction_changes` for final output in the `BlockChanges`,
// sorted by transaction index (the key).
Ok(BlockChanges {
block: Some((&block).into()),
changes: transaction_changes
.drain()
.sorted_unstable_by_key(|(index, _)| *index)
.filter_map(|(_, builder)| builder.build())
.collect::<Vec<_>>(),
storage_changes: block_storage_changes,
})
}
/// Converts address bytes into a Vec<u8> containing a leading `0x`.
fn address_to_bytes_with_0x(address: &[u8; 20]) -> Vec<u8> {
address_to_string_with_0x(address).into_bytes()
}
/// Converts address bytes into a string containing a leading `0x`.
fn address_to_string_with_0x(address: &[u8]) -> String {
format!("0x{}", hex::encode(address))
}
// function needed to match reservesOf in vault storage, which by definition
// they should always be equal to the `token.balanceOf(this)` except during unlock
fn get_vault_reserves(
transaction: &eth::v2::TransactionTrace,
store: &StoreGetProto<ProtocolComponent>,
token_store: &StoreGetInt64,
) -> HashMap<Vec<u8>, ReserveValue> {
// reservesOf mapping for the current block Address -> Balance
let mut reserves_of = HashMap::new();
transaction
.calls
.iter()
.filter(|call| !call.state_reverted)
.filter(|call| call.address == VAULT_ADDRESS)
.for_each(|call| {
if let Some(Settle { token, .. }) = Settle::match_and_decode(call) {
for change in &call.storage_changes {
add_change_if_accounted(
&mut reserves_of,
change,
token.as_slice(),
token_store,
);
}
}
if let Some(SendTo { token, .. }) = SendTo::match_and_decode(call) {
for change in &call.storage_changes {
add_change_if_accounted(
&mut reserves_of,
change,
token.as_slice(),
token_store,
);
}
}
if let Some(Erc4626BufferWrapOrUnwrap { params }) =
Erc4626BufferWrapOrUnwrap::match_and_decode(call)
{
for change in &call.storage_changes {
let wrapped_token = params.2.clone();
let component_id = format!("0x{}", hex::encode(&wrapped_token));
if let Some(component) = store.get_last(component_id) {
let underlying_token = component.tokens[1].clone();
add_change_if_accounted(
&mut reserves_of,
change,
wrapped_token.as_slice(),
token_store,
);
add_change_if_accounted(
&mut reserves_of,
change,
underlying_token.as_slice(),
token_store,
);
}
}
}
});
reserves_of
}
struct ReserveValue {
ordinal: u64,
value: Vec<u8>,
}
fn add_change_if_accounted(
reserves_of: &mut HashMap<Vec<u8>, ReserveValue>,
change: &StorageChange,
token_address: &[u8],
token_store: &StoreGetInt64,
) {
let slot_key = get_storage_key_for_token(token_address);
// record changes happening on vault contract at reserves_of storage key
if change.key == slot_key && token_store.has_last(hex::encode(token_address)) {
reserves_of
.entry(token_address.to_vec())
.and_modify(|v| {
if v.ordinal < change.ordinal {
v.value = change.new_value.clone();
v.ordinal = change.ordinal;
}
})
.or_insert(ReserveValue { value: change.new_value.clone(), ordinal: change.ordinal });
}
}
// token_addr -> keccak256(abi.encode(token_address, 8)) as 8 is the order in which reserves of are
// declared
fn get_storage_key_for_token(token_address: &[u8]) -> Vec<u8> {
let mut input = [0u8; 64];
input[12..32].copy_from_slice(token_address);
input[63] = 8u8;
let result = keccak(input.as_slice())
.as_bytes()
.to_vec();
result
}

View File

@@ -0,0 +1,119 @@
use crate::{abi, modules::VAULT_ADDRESS};
use abi::{
stable_pool_factory_contract::{
events::PoolCreated as StablePoolCreated, functions::Create as StablePoolCreate,
},
weighted_pool_factory_contract::{
events::PoolCreated as WeightedPoolCreated, functions::Create as WeightedPoolCreate,
},
};
use substreams::{hex, scalar::BigInt};
use substreams_ethereum::{
pb::eth::v2::{Call, Log},
Event, Function,
};
use tycho_substreams::{
attributes::{json_serialize_address_list, json_serialize_bigint_list},
prelude::*,
};
// Token config: (token_address, rate, rate_provider_address, is_exempt_from_yield_fees)
type TokenConfig = Vec<(Vec<u8>, substreams::scalar::BigInt, Vec<u8>, bool)>;
pub fn collect_rate_providers(tokens: &TokenConfig) -> Vec<Vec<u8>> {
tokens
.iter()
.filter(|token| token.1 == BigInt::from(1)) // WITH_RATE == 1
.map(|token| token.2.clone())
.collect::<Vec<_>>()
}
pub fn address_map(
pool_factory_address: &[u8],
log: &Log,
call: &Call,
) -> Option<ProtocolComponent> {
match *pool_factory_address {
hex!("201efd508c8DfE9DE1a13c2452863A78CB2a86Cc") => {
let WeightedPoolCreate {
tokens: token_config,
normalized_weights,
swap_fee_percentage,
..
} = WeightedPoolCreate::match_and_decode(call)?;
let WeightedPoolCreated { pool } = WeightedPoolCreated::match_and_decode(log)?;
let rate_providers = collect_rate_providers(&token_config);
// TODO: to add "buffers" support for boosted pools, we need to add the unwrapped
// version of all ERC4626 tokens to the pool tokens list. Skipped for now - we need
// to test that the adapter supports it correctly and ERC4626 overwrites are handled
// correctly in simulation.
let tokens = token_config
.into_iter()
.map(|t| t.0)
.collect::<Vec<_>>();
let normalized_weights_bytes =
json_serialize_bigint_list(normalized_weights.as_slice());
let fee_bytes = swap_fee_percentage.to_signed_bytes_be();
let rate_providers_bytes = json_serialize_address_list(rate_providers.as_slice());
let mut attributes = vec![
("pool_type", "WeightedPoolFactory".as_bytes()),
("normalized_weights", &normalized_weights_bytes),
("fee", &fee_bytes),
("manual_updates", &[1u8]),
];
if !rate_providers.is_empty() {
attributes.push(("rate_providers", &rate_providers_bytes));
}
Some(
ProtocolComponent::new(&format!("0x{}", hex::encode(&pool)))
.with_contracts(&[pool, VAULT_ADDRESS.to_vec()])
.with_tokens(tokens.as_slice())
.with_attributes(&attributes)
.as_swap_type("balancer_v3_pool", ImplementationType::Vm),
)
}
hex!("B9d01CA61b9C181dA1051bFDd28e1097e920AB14") => {
let StablePoolCreate { tokens: token_config, swap_fee_percentage, .. } =
StablePoolCreate::match_and_decode(call)?;
let StablePoolCreated { pool } = StablePoolCreated::match_and_decode(log)?;
let rate_providers = collect_rate_providers(&token_config);
// TODO: to add "buffers" support for boosted pools, we need to add the unwrapped
// version of all ERC4626 tokens to the pool tokens list. Skipped for now - we need
// to test that the adapter supports it correctly and ERC4626 overwrites are handled
// correctly in simulation.
let tokens = token_config
.into_iter()
.map(|t| t.0)
.collect::<Vec<_>>();
let fee_bytes = swap_fee_percentage.to_signed_bytes_be();
let rate_providers_bytes = json_serialize_address_list(rate_providers.as_slice());
let mut attributes = vec![
("pool_type", "StablePoolFactory".as_bytes()),
("bpt", &pool),
("fee", &fee_bytes),
("manual_updates", &[1u8]),
];
if !rate_providers.is_empty() {
attributes.push(("rate_providers", &rate_providers_bytes));
}
Some(
ProtocolComponent::new(&format!("0x{}", hex::encode(&pool)))
.with_contracts(&[pool.to_owned(), VAULT_ADDRESS.to_vec()])
.with_tokens(tokens.as_slice())
.with_attributes(&attributes)
.as_swap_type("balancer_v3_pool", ImplementationType::Vm),
)
}
_ => None,
}
}

View File

@@ -0,0 +1,78 @@
specVersion: v0.1.0
package:
name: "ethereum_balancer_v3"
version: v0.4.0
protobuf:
files:
- tycho/evm/v1/vm.proto
- tycho/evm/v1/common.proto
- tycho/evm/v1/utils.proto
importPaths:
- ../../proto
excludePaths:
- sf/substreams
- google
binaries:
default:
type: wasm/rust-v1
file: ../target/wasm32-unknown-unknown/release/ethereum_balancer_v3.wasm
modules:
- name: map_components
kind: map
initialBlock: 21332121
inputs:
- source: sf.ethereum.type.v2.Block
output:
type: proto:tycho.evm.v1.BlockTransactionProtocolComponents
- name: store_components
kind: store
initialBlock: 21332121
updatePolicy: set_if_not_exists
valueType: proto:tycho.evm.v1.ProtocolComponents
inputs:
- map: map_components
- name: store_token_set
kind: store
initialBlock: 21332121
updatePolicy: set_if_not_exists
valueType: int64
inputs:
- map: map_components
- name: map_relative_balances
kind: map
initialBlock: 21332121
inputs:
- source: sf.ethereum.type.v2.Block
- store: store_components
output:
type: proto:tycho.evm.v1.BlockBalanceDeltas
- name: store_balances
kind: store
initialBlock: 21332121
updatePolicy: add
valueType: bigint
inputs:
- map: map_relative_balances
- name: map_protocol_changes
kind: map
initialBlock: 21332121
inputs:
- source: sf.ethereum.type.v2.Block
- map: map_components
- map: map_relative_balances
- store: store_components
- store: store_token_set
- store: store_balances
mode: deltas # This is the key property that simplifies `BalanceChange` handling
output:
type: proto:tycho.evm.v1.BlockChanges
network: mainnet

View File

@@ -21,4 +21,5 @@ ignore = [
"ethereum-pancakeswap-v3/src/abi",
"ethereum-uniswap-v4/src/abi",
"ethereum-maverick-v2/src/abi",
"ethereum-balancer-v3/src/abi"
]

View File

@@ -85,6 +85,8 @@ class TychoRunner:
"--stop-block",
# +2 is to make up for the cache in the index side.
str(end_block + 2),
"--dci-plugin",
"rpc",
]
+ (
[