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:
393
evm/src/balancer-v3/lib/BalancerERC4626Helpers.sol
Normal file
393
evm/src/balancer-v3/lib/BalancerERC4626Helpers.sol
Normal file
@@ -0,0 +1,393 @@
|
||||
//SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "./BalancerCustomWrapHelpers.sol";
|
||||
|
||||
abstract contract BalancerERC4626Helpers is BalancerCustomWrapHelpers {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
function getERC4626PathType(
|
||||
address pool,
|
||||
address sellToken,
|
||||
address buyToken,
|
||||
bool sellTokenFound
|
||||
) internal view returns (ERC4626_SWAP_TYPE kind, address outputAddress) {
|
||||
IERC20[] memory tokens = vault.getPoolTokens(pool);
|
||||
|
||||
if (sellTokenFound) {
|
||||
// SWAP_WRAP and SWAP_UNWRAP
|
||||
bool isERC4626BuyToken = isERC4626(buyToken);
|
||||
|
||||
if (isERC4626BuyToken) {
|
||||
kind = ERC4626_SWAP_TYPE.SWAP_WRAP;
|
||||
} else {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
if (isERC4626(token) && IERC4626(token).asset() == buyToken)
|
||||
{
|
||||
outputAddress = token; // buyToken share
|
||||
break;
|
||||
}
|
||||
}
|
||||
require(outputAddress != address(0), "Token not found in pool");
|
||||
kind = ERC4626_SWAP_TYPE.SWAP_UNWRAP;
|
||||
}
|
||||
} else {
|
||||
bool isERC4626SellToken = isERC4626(sellToken);
|
||||
|
||||
if (isERC4626SellToken) {
|
||||
kind = ERC4626_SWAP_TYPE.UNWRAP_SWAP;
|
||||
} else {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
if (
|
||||
isERC4626(token) && IERC4626(token).asset() == sellToken
|
||||
) {
|
||||
outputAddress = token; // sellToken share
|
||||
break;
|
||||
}
|
||||
}
|
||||
require(outputAddress != address(0), "Token not found in pool");
|
||||
kind = ERC4626_SWAP_TYPE.WRAP_SWAP;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prepareERC4626SellOrBuy(
|
||||
address pool,
|
||||
address _sellToken,
|
||||
address _buyToken,
|
||||
uint256 specifiedAmount,
|
||||
ERC4626_SWAP_TYPE kind,
|
||||
address outputAddress,
|
||||
bool isBuy
|
||||
)
|
||||
internal
|
||||
view
|
||||
returns (
|
||||
IBatchRouter.SwapPathExactAmountIn[] memory sellPath,
|
||||
IBatchRouter.SwapPathExactAmountOut[] memory buyPath
|
||||
)
|
||||
{
|
||||
IBatchRouter.SwapPathStep[] memory steps;
|
||||
|
||||
address sellToken = _sellToken == address(0) ? WETH_ADDRESS : _sellToken;
|
||||
address buyToken = _buyToken == address(0) ? WETH_ADDRESS : _buyToken;
|
||||
|
||||
if (kind == ERC4626_SWAP_TYPE.SWAP_WRAP) {
|
||||
// !isERC4626(sellToken) && isERC4626(buyToken) and
|
||||
// isERC4626(buyToken) && isERC4626(sellToken)
|
||||
steps = new IBatchRouter.SwapPathStep[](2);
|
||||
|
||||
// swap: sellToken -> buyToken.asset()
|
||||
(,, steps[0]) = createERC20Path(
|
||||
pool,
|
||||
IERC20(sellToken),
|
||||
IERC20(IERC4626(buyToken).asset()),
|
||||
specifiedAmount,
|
||||
false,
|
||||
_sellToken == address(0)
|
||||
);
|
||||
|
||||
// wrap: buyToken.asset() -> buyToken.shares()
|
||||
(,, steps[1]) = createWrapOrUnwrapPath(
|
||||
buyToken, specifiedAmount, IVault.WrappingDirection.WRAP, false
|
||||
);
|
||||
} else if (kind == ERC4626_SWAP_TYPE.SWAP_UNWRAP) {
|
||||
// isERC4626(sellToken) && !isERC4626(buyToken) and
|
||||
// !isERC4626(buyToken) && !isERC4626(sellToken)
|
||||
steps = new IBatchRouter.SwapPathStep[](2);
|
||||
|
||||
// swap: sellToken -> buyToken.shares()
|
||||
(,, steps[0]) = createERC20Path(
|
||||
pool,
|
||||
IERC20(sellToken),
|
||||
IERC20(outputAddress),
|
||||
specifiedAmount,
|
||||
false,
|
||||
_sellToken == address(0)
|
||||
);
|
||||
|
||||
// unwrap: buyToken.shares() -> buyToken.asset()
|
||||
(,, steps[1]) = createWrapOrUnwrapPath(
|
||||
outputAddress,
|
||||
specifiedAmount,
|
||||
IVault.WrappingDirection.UNWRAP,
|
||||
false
|
||||
);
|
||||
} else if (kind == ERC4626_SWAP_TYPE.WRAP_SWAP) {
|
||||
// input is ERC20, output is ERC4626
|
||||
steps = new IBatchRouter.SwapPathStep[](2);
|
||||
|
||||
// wrap: sellToken.shares() -> sellToken.asset()
|
||||
(,, steps[0]) = createWrapOrUnwrapPath(
|
||||
outputAddress,
|
||||
specifiedAmount,
|
||||
IVault.WrappingDirection.WRAP,
|
||||
false
|
||||
);
|
||||
// swap: sellToken.asset() -> buyToken
|
||||
(,, steps[1]) = createERC20Path(
|
||||
pool,
|
||||
IERC20(outputAddress),
|
||||
IERC20(buyToken),
|
||||
specifiedAmount,
|
||||
false,
|
||||
_buyToken == address(0)
|
||||
);
|
||||
} else if (kind == ERC4626_SWAP_TYPE.UNWRAP_SWAP) {
|
||||
steps = new IBatchRouter.SwapPathStep[](2);
|
||||
|
||||
// unwrap: sellToken.shares() -> sellToken.asset()
|
||||
(,, steps[0]) = createWrapOrUnwrapPath(
|
||||
sellToken,
|
||||
specifiedAmount,
|
||||
IVault.WrappingDirection.UNWRAP,
|
||||
false
|
||||
);
|
||||
|
||||
// swap: sellToken.asset() -> buyToken
|
||||
(,, steps[1]) = createERC20Path(
|
||||
pool,
|
||||
IERC20(sellToken),
|
||||
IERC20(buyToken),
|
||||
specifiedAmount,
|
||||
false,
|
||||
_buyToken == address(0)
|
||||
);
|
||||
}
|
||||
|
||||
if (isBuy) {
|
||||
buyPath = new IBatchRouter.SwapPathExactAmountOut[](1);
|
||||
buyPath[0] = IBatchRouter.SwapPathExactAmountOut({
|
||||
tokenIn: IERC20(sellToken),
|
||||
steps: steps,
|
||||
maxAmountIn: IERC20(sellToken).balanceOf(address(this)),
|
||||
exactAmountOut: specifiedAmount
|
||||
});
|
||||
} else {
|
||||
sellPath = new IBatchRouter.SwapPathExactAmountIn[](1);
|
||||
sellPath[0] = IBatchRouter.SwapPathExactAmountIn({
|
||||
tokenIn: IERC20(sellToken),
|
||||
steps: steps,
|
||||
exactAmountIn: specifiedAmount,
|
||||
minAmountOut: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function swapERC4626AndERC20(
|
||||
address pool,
|
||||
address _sellToken,
|
||||
address _buyToken,
|
||||
uint256 specifiedAmount,
|
||||
ERC4626_SWAP_TYPE kind,
|
||||
address outputAddress,
|
||||
bool isBuy
|
||||
) internal returns (uint256 calculatedAmount) {
|
||||
// approve
|
||||
uint256 approvalAmount = specifiedAmount;
|
||||
|
||||
address sellToken = _sellToken == address(0) ? WETH_ADDRESS : _sellToken;
|
||||
address buyToken = _buyToken == address(0) ? WETH_ADDRESS : _buyToken;
|
||||
|
||||
if (_sellToken != address(0)) {
|
||||
if (isBuy) {
|
||||
approvalAmount = IERC20(sellToken).balanceOf(msg.sender);
|
||||
}
|
||||
IERC20(sellToken).safeIncreaseAllowance(permit2, approvalAmount);
|
||||
IPermit2(permit2).approve(
|
||||
address(sellToken),
|
||||
address(router),
|
||||
type(uint160).max,
|
||||
type(uint48).max
|
||||
);
|
||||
} else {
|
||||
if (isBuy) {
|
||||
approvalAmount = address(this).balance;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBuy) {
|
||||
if (_sellToken != address(0)) {
|
||||
IERC20(sellToken).safeTransferFrom(
|
||||
msg.sender, address(this), approvalAmount
|
||||
);
|
||||
}
|
||||
|
||||
(IBatchRouter.SwapPathExactAmountIn[] memory sellPath,) =
|
||||
prepareERC4626SellOrBuy(
|
||||
pool,
|
||||
sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
kind,
|
||||
outputAddress,
|
||||
isBuy
|
||||
);
|
||||
|
||||
uint256[] memory amountsOut;
|
||||
if (_sellToken == address(0)) {
|
||||
(,, amountsOut) = router.swapExactIn{value: specifiedAmount}(
|
||||
sellPath, type(uint256).max, true, bytes("")
|
||||
);
|
||||
} else {
|
||||
(,, amountsOut) = router.swapExactIn(
|
||||
sellPath, type(uint256).max, false, bytes("")
|
||||
);
|
||||
}
|
||||
|
||||
calculatedAmount = amountsOut[0];
|
||||
|
||||
if (_buyToken != address(0)) {
|
||||
IERC20(buyToken).safeTransfer(msg.sender, calculatedAmount);
|
||||
} else {
|
||||
(bool sent,) =
|
||||
payable(msg.sender).call{value: calculatedAmount}("");
|
||||
require(sent, "Failed to transfer ETH");
|
||||
}
|
||||
} else {
|
||||
uint256 initialSenderBalance = address(this).balance;
|
||||
if (_sellToken != address(0)) {
|
||||
initialSenderBalance = IERC20(sellToken).balanceOf(msg.sender);
|
||||
IERC20(sellToken).safeTransferFrom(
|
||||
msg.sender, address(this), approvalAmount
|
||||
);
|
||||
}
|
||||
|
||||
(, IBatchRouter.SwapPathExactAmountOut[] memory buyPath) =
|
||||
prepareERC4626SellOrBuy(
|
||||
pool,
|
||||
sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
kind,
|
||||
outputAddress,
|
||||
true
|
||||
);
|
||||
|
||||
uint256[] memory amountsIn;
|
||||
if (_sellToken == address(0)) {
|
||||
(,, amountsIn) = router.swapExactOut{value: approvalAmount}(
|
||||
buyPath, type(uint256).max, false, bytes("")
|
||||
);
|
||||
} else {
|
||||
(,, amountsIn) = router.swapExactOut(
|
||||
buyPath, type(uint256).max, false, bytes("")
|
||||
);
|
||||
}
|
||||
|
||||
calculatedAmount = amountsIn[0];
|
||||
|
||||
if (_buyToken != address(0)) {
|
||||
IERC20(buyToken).safeTransfer(msg.sender, specifiedAmount);
|
||||
} else {
|
||||
(bool sent,) =
|
||||
payable(msg.sender).call{value: specifiedAmount}("");
|
||||
require(sent, "Failed to transfer ETH");
|
||||
}
|
||||
|
||||
// transfer back sellToken to sender
|
||||
if (_sellToken != address(0)) {
|
||||
IERC20(sellToken).safeTransfer(
|
||||
msg.sender, initialSenderBalance - calculatedAmount
|
||||
);
|
||||
} else {
|
||||
(bool sent,) = payable(msg.sender).call{
|
||||
value: initialSenderBalance - calculatedAmount
|
||||
}("");
|
||||
require(sent, "Failed to transfer ETH");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAmountOutERC4626AndERC20(
|
||||
address pool,
|
||||
address sellToken,
|
||||
address buyToken,
|
||||
uint256 specifiedAmount,
|
||||
ERC4626_SWAP_TYPE kind,
|
||||
address outputAddress
|
||||
) internal returns (uint256 calculatedAmount) {
|
||||
(IBatchRouter.SwapPathExactAmountIn[] memory paths,) =
|
||||
prepareERC4626SellOrBuy(
|
||||
pool,
|
||||
sellToken,
|
||||
buyToken,
|
||||
specifiedAmount,
|
||||
kind,
|
||||
outputAddress,
|
||||
false
|
||||
);
|
||||
(,, uint256[] memory amountsOut) =
|
||||
router.querySwapExactIn(paths, address(0), bytes(""));
|
||||
calculatedAmount = amountsOut[0];
|
||||
}
|
||||
|
||||
function getLimitsERC4626AndERC20(
|
||||
bytes32 poolId,
|
||||
address sellToken,
|
||||
address buyToken,
|
||||
ERC4626_SWAP_TYPE kind,
|
||||
address outputAddress
|
||||
) internal view returns (uint256[] memory limits) {
|
||||
limits = new uint256[](2);
|
||||
address pool = address(bytes20(poolId));
|
||||
(IERC20[] memory tokens,, uint256[] memory balancesRaw,) =
|
||||
vault.getPoolTokenInfo(pool);
|
||||
|
||||
uint256 tokenLimit;
|
||||
|
||||
if (kind == ERC4626_SWAP_TYPE.SWAP_WRAP) {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
if (token == sellToken) {
|
||||
limits[0] = balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10;
|
||||
}
|
||||
|
||||
if (token == IERC4626(buyToken).asset()) {
|
||||
tokenLimit = balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10;
|
||||
}
|
||||
}
|
||||
limits[1] = IERC4626(buyToken).previewDeposit(tokenLimit);
|
||||
} else if (kind == ERC4626_SWAP_TYPE.SWAP_UNWRAP) {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
if (token == sellToken) {
|
||||
limits[0] = balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10;
|
||||
} else if (token == outputAddress) {
|
||||
tokenLimit = balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10;
|
||||
}
|
||||
}
|
||||
limits[1] = IERC4626(outputAddress).previewRedeem(tokenLimit);
|
||||
} else if (kind == ERC4626_SWAP_TYPE.WRAP_SWAP) {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
|
||||
if (token == outputAddress) {
|
||||
limits[0] = IERC4626(outputAddress).previewRedeem(
|
||||
balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10
|
||||
);
|
||||
}
|
||||
|
||||
if (token == buyToken) {
|
||||
limits[1] = balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10;
|
||||
}
|
||||
}
|
||||
} else if (kind == ERC4626_SWAP_TYPE.UNWRAP_SWAP) {
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
address token = address(tokens[i]);
|
||||
|
||||
if (token == buyToken) {
|
||||
limits[1] = balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10;
|
||||
}
|
||||
|
||||
if (token == IERC4626(sellToken).asset()) {
|
||||
limits[0] = IERC4626(sellToken).previewDeposit(
|
||||
balancesRaw[i] * RESERVE_LIMIT_FACTOR / 10
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user