From 353b5a745bd112426e446b5f80740e5128d6d9fd Mon Sep 17 00:00:00 2001 From: utkarshdagoat Date: Sun, 20 Oct 2024 02:33:51 +0530 Subject: [PATCH 1/4] adds oracle --- .../hooks/rebalancer/oracle/Oracle.sol | 52 ++++++++++++++++++ .../rebalancer/oracle/interfaces/IOracle.sol | 55 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 packages/foundry/contracts/hooks/rebalancer/oracle/Oracle.sol create mode 100644 packages/foundry/contracts/hooks/rebalancer/oracle/interfaces/IOracle.sol diff --git a/packages/foundry/contracts/hooks/rebalancer/oracle/Oracle.sol b/packages/foundry/contracts/hooks/rebalancer/oracle/Oracle.sol new file mode 100644 index 00000000..3832bfa0 --- /dev/null +++ b/packages/foundry/contracts/hooks/rebalancer/oracle/Oracle.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity ^0.8.24; + +import {Ownable} from "@openzepplin/contracts/access/Ownable.sol"; +import {IOracle,position} from "./interfaces/IOracle.sol"; + +contract Oracle is IOracle,Ownable { + uint24 immutable public baseFee; + address public oracle; + + event FeeUpdate(address indexed pool, uint24 fee); + event PositionUpdate(address indexed pool, PositionData); + + error UnAuthorized(); + + mapping (address => uint24) public dynamicFee; + + mapping (address => PositionData) public position; + + error NotOracle(); + + modifier onlyOracle() { + if(msg.sender != oracle) { + revert UnAuthorized(); + } + _; + } + + constructor(uint24 _baseFee, address _oracle) Ownable(msg.sender) { + baseFee = _baseFee; + oracle = _oracle; + } + + function setFee(address pool, uint24 fee) external override onlyOracle { + dynamicFee[pool] = fee; + emit FeeUpdate(pool, fee); + } + + + function setPositionData(address pool, uint24 _lowerTick, uint24 _upperTick) external override onlyOracle { + position[pool] = PositionData(_lowerTick, _upperTick); + emit PositionUpdate(pool, PositionData(_lowerTick, _upperTick)); + } + + function getFee(address pool) external view override returns (uint24) { + return dynamicFee[pool]; + } + + function getPosition(address pool) external view override returns (PositionData memory) { + return position[pool]; + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/rebalancer/oracle/interfaces/IOracle.sol b/packages/foundry/contracts/hooks/rebalancer/oracle/interfaces/IOracle.sol new file mode 100644 index 00000000..53404011 --- /dev/null +++ b/packages/foundry/contracts/hooks/rebalancer/oracle/interfaces/IOracle.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity ^0.8.24; + + +/** + * @dev Struct for holding position for the token + */ +struct PositionData{ + uint24 tickUpper; + uint24 tickLower; +} + + +/** + * @dev Interface for the rebalancer oracle + */ + +interface IOracle { + /** + * @dev Get the current fee for the given pool + * @param pool The address of the pool + * @return The current fee + */ + function getFee(address pool) external view returns (uint24); + + /** + * @dev Get the current position for the given pool + * @param pool The address of the pool + * @return The current position + */ + function getPosition(address pool) external view returns (Position memory); + + + /** + * @dev Set the fee for the given pool + * @param pool The address of the pool + * @param fee The fee to set + */ + function setFee(address pool, uint24 fee) external; + + /** + * @dev Set the position for the given pool + * @param pool The address of the pool + * @param _lowerTick The lower tick boundry + * @param _upperTick The upper tick boundry + */ + function setPositionData(address pool, uint24 _lowerTick, uint24 _upperTick) external; + + /** + * @dev Update the oracle for the given pool + * @param pool The address of the pool + */ + function updateOracle(address pool) external; + +} \ No newline at end of file From 0b4a164903272e8d9723893144046bf11911f929 Mon Sep 17 00:00:00 2001 From: utkarshdagoat Date: Mon, 21 Oct 2024 00:48:55 +0530 Subject: [PATCH 2/4] rebalancer logic done --- .../rebalancer/MinimalRouterWithSwap.sol | 169 ++++++ .../hooks/rebalancer/{oracle => }/Oracle.sol | 32 +- .../contracts/hooks/rebalancer/Rebalancer.sol | 514 ++++++++++++++++++ .../{oracle => }/interfaces/IOracle.sol | 34 +- packages/foundry/remappings.txt | 1 + 5 files changed, 722 insertions(+), 28 deletions(-) create mode 100644 packages/foundry/contracts/hooks/rebalancer/MinimalRouterWithSwap.sol rename packages/foundry/contracts/hooks/rebalancer/{oracle => }/Oracle.sol (51%) create mode 100644 packages/foundry/contracts/hooks/rebalancer/Rebalancer.sol rename packages/foundry/contracts/hooks/rebalancer/{oracle => }/interfaces/IOracle.sol (51%) diff --git a/packages/foundry/contracts/hooks/rebalancer/MinimalRouterWithSwap.sol b/packages/foundry/contracts/hooks/rebalancer/MinimalRouterWithSwap.sol new file mode 100644 index 00000000..bb3f8b46 --- /dev/null +++ b/packages/foundry/contracts/hooks/rebalancer/MinimalRouterWithSwap.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity ^0.8.24; + +import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + + +import { MinimalRouter } from "@balancer-labs/v3-pool-hooks/contracts/MinimalRouter.sol"; +import { SwapKind , VaultSwapParams } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + + +import { IWETH } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/misc/IWETH.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + + +abstract contract MinimalRouterWithSwap is MinimalRouter { + + constructor(IVault vault, IWETH weth, IPermit2 permit2) MinimalRouter(vault, weth, permit2) { + // solhint-disable-previous-line no-empty-blocks + } + + + + /** + * @notice Data for the swap hook. + * @param sender Account initiating the swap operation + * @param kind Type of swap (exact in or exact out) + * @param pool Address of the liquidity pool + * @param tokenIn Token to be swapped from + * @param tokenOut Token to be swapped to + * @param amountGiven Amount given based on kind of the swap (e.g., tokenIn for exact in) + * @param limit Maximum or minimum amount based on the kind of swap (e.g., maxAmountIn for exact out) + * @param deadline Deadline for the swap, after which it will revert + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the swap request + */ + struct SwapSingleTokenHookParams { + address sender; + SwapKind kind; + address pool; + IERC20 tokenIn; + IERC20 tokenOut; + uint256 amountGiven; + uint256 limit; + uint256 deadline; + bool wethIsEth; + bytes userData; + } + + + /*************************************************************************** + Swaps + ***************************************************************************/ + + function swapSingleTokenExactIn( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountIn, + uint256 minAmountOut, + uint256 deadline, + bool wethIsEth, + bytes calldata userData + ) external payable saveSender returns (uint256) { + return + abi.decode( + _vault.unlock( + abi.encodeCall( + MinimalRouterWithSwap.swapSingleTokenHook, + SwapSingleTokenHookParams({ + sender: msg.sender, + kind: SwapKind.EXACT_IN, + pool: pool, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountGiven: exactAmountIn, + limit: minAmountOut, + deadline: deadline, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256) + ); + } + + function swapSingleTokenExactOut( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountOut, + uint256 maxAmountIn, + uint256 deadline, + bool wethIsEth, + bytes calldata userData + ) external payable saveSender returns (uint256) { + return + abi.decode( + _vault.unlock( + abi.encodeCall( + MinimalRouterWithSwap.swapSingleTokenHook, + SwapSingleTokenHookParams({ + sender: msg.sender, + kind: SwapKind.EXACT_OUT, + pool: pool, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountGiven: exactAmountOut, + limit: maxAmountIn, + deadline: deadline, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256) + ); + } + + /** + * @notice Hook for swaps. + * @dev Can only be called by the Vault. Also handles native ETH. + * @param params Swap parameters (see IRouter for struct definition) + * @return amountCalculated Token amount calculated by the pool math (e.g., amountOut for a exact in swap) + */ + function swapSingleTokenHook( + SwapSingleTokenHookParams calldata params + ) external nonReentrant onlyVault returns (uint256) { + (uint256 amountCalculated, uint256 amountIn, uint256 amountOut) = _swapHook(params); + + IERC20 tokenIn = params.tokenIn; + + _takeTokenIn(params.sender, tokenIn, amountIn, params.wethIsEth); + _sendTokenOut(params.sender, params.tokenOut, amountOut, params.wethIsEth); + + if (tokenIn == _weth) { + // Return the rest of ETH to sender + _returnEth(params.sender); + } + + return amountCalculated; + } + + function _swapHook( + SwapSingleTokenHookParams calldata params + ) internal returns (uint256 amountCalculated, uint256 amountIn, uint256 amountOut) { + // The deadline is timestamp-based: it should not be relied upon for sub-minute accuracy. + // solhint-disable-next-line not-rely-on-time + if (block.timestamp > params.deadline) { + revert SwapDeadline(); + } + + (amountCalculated, amountIn, amountOut) = _vault.swap( + VaultSwapParams({ + kind: params.kind, + pool: params.pool, + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + amountGivenRaw: params.amountGiven, + limitRaw: params.limit, + userData: params.userData + }) + ); + } + + +} diff --git a/packages/foundry/contracts/hooks/rebalancer/oracle/Oracle.sol b/packages/foundry/contracts/hooks/rebalancer/Oracle.sol similarity index 51% rename from packages/foundry/contracts/hooks/rebalancer/oracle/Oracle.sol rename to packages/foundry/contracts/hooks/rebalancer/Oracle.sol index 3832bfa0..b479428d 100644 --- a/packages/foundry/contracts/hooks/rebalancer/oracle/Oracle.sol +++ b/packages/foundry/contracts/hooks/rebalancer/Oracle.sol @@ -1,22 +1,20 @@ // SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity ^0.8.24; -import {Ownable} from "@openzepplin/contracts/access/Ownable.sol"; -import {IOracle,position} from "./interfaces/IOracle.sol"; +import {IOracle,TokenData} from "./interfaces/IOracle.sol"; -contract Oracle is IOracle,Ownable { +abstract contract Oracle is IOracle { uint24 immutable public baseFee; address public oracle; event FeeUpdate(address indexed pool, uint24 fee); - event PositionUpdate(address indexed pool, PositionData); + event PositionUpdate(address indexed pool, TokenData); error UnAuthorized(); mapping (address => uint24) public dynamicFee; - mapping (address => PositionData) public position; - + mapping(address => TokenData[]) public poolTokens; error NotOracle(); modifier onlyOracle() { @@ -26,7 +24,7 @@ contract Oracle is IOracle,Ownable { _; } - constructor(uint24 _baseFee, address _oracle) Ownable(msg.sender) { + constructor(uint24 _baseFee, address _oracle) { baseFee = _baseFee; oracle = _oracle; } @@ -36,17 +34,23 @@ contract Oracle is IOracle,Ownable { emit FeeUpdate(pool, fee); } - - function setPositionData(address pool, uint24 _lowerTick, uint24 _upperTick) external override onlyOracle { - position[pool] = PositionData(_lowerTick, _upperTick); - emit PositionUpdate(pool, PositionData(_lowerTick, _upperTick)); + function setPoolTokenData( + address pool, + uint i, + uint256 latestRoundPrice, + uint256 predictedPrice + ) external override onlyOracle { + TokenData[] storage tokensData = poolTokens[pool]; + tokensData[i].latestRoundPrice = latestRoundPrice; + tokensData[i].predictedPrice = predictedPrice; } - + function getFee(address pool) external view override returns (uint24) { return dynamicFee[pool]; } - function getPosition(address pool) external view override returns (PositionData memory) { - return position[pool]; + function getPoolTokensData(address pool) external view override returns (TokenData[] memory){ + return poolTokens[pool]; } + } \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/rebalancer/Rebalancer.sol b/packages/foundry/contracts/hooks/rebalancer/Rebalancer.sol new file mode 100644 index 00000000..818bc98d --- /dev/null +++ b/packages/foundry/contracts/hooks/rebalancer/Rebalancer.sol @@ -0,0 +1,514 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity ^0.8.24; + +import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +import { BaseHooks } from "@balancer-labs/v3-vault/contracts/BaseHooks.sol"; + +import { WeightedMath } from "@balancer-labs/v3-solidity-utils/contracts/math/WeightedMath.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { BasePoolMath } from "@balancer-labs/v3-vault/contracts/BasePoolMath.sol"; + +import { IRouter } from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol"; +import { + IWeightedPool, + WeightedPoolImmutableData, + WeightedPoolDynamicData +} from "@balancer-labs/v3-interfaces/contracts/pool-weighted/IWeightedPool.sol"; +import { IBasePoolFactory } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePoolFactory.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { IWETH } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/misc/IWETH.sol"; +import { IHooks } from "@balancer-labs/v3-interfaces/contracts/vault/IHooks.sol"; +import { + TokenConfig, + LiquidityManagement, + HookFlags, + AddLiquidityKind, + RemoveLiquidityKind, + AddLiquidityParams, + PoolRoleAccounts, + AfterSwapParams, + SwapKind +} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { MinimalRouterWithSwap } from "./MinimalRouterWithSwap.sol"; +import { IOracle, TokenData } from "./interfaces/IOracle.sol"; + +contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { + using FixedPoint for uint256; + + ///@dev price feeds give the data in 1e8 precision + uint256 constant PRICE_PRECISION = 1e8; + + /** + * + * @param minRatio is the min price ration for rebalance required for token this is in 1e18 precision. + * Thus for 1% change it is 1e16 + * @param rebalanceRequired is the flag that is enabled to indicate rebalancing + */ + struct RebalanceData { + uint256 minRatio; + bool rebalanceRequired; + } + ///@dev A map to the address of pool and the rebalancing data for tokens of that pool + mapping(address => RebalanceData[]) rebalanceData; + + ///@dev I could not find a way to get all the LPs of the pool + ///@dev I used many variable to cut down the for loops + mapping(address => address[]) liqudityProviders; + mapping(address => mapping(address => uint256[])) amountTokens; + mapping(address => mapping(address => bool)) isStillLp; + + ///@dev This is private as only owner of the hook contract can change the address + address private weightedPoolFactory; + + ///@dev address of the oracle + address private oracle; + /** + * Emmited when the hook is registered successfully for the given pool + * @param hooksContract The address of this contract + * @param pool The address of the pool that the hook is used for + */ + event RebalancerHookRegistered(address indexed hooksContract, address indexed pool); + + /** + * emitted whenever liquidity is added to the pool + * @param pool address of the pool + * @param lp the address of liquidity provider + */ + event LiquidtyProviderAdded(address indexed pool, address indexed lp); + + /** + * Emmited when the rebalance is triggered for the pool + * @param pool The address of the pool + */ + event RebalanceStarted(address indexed pool); + + /** + * @notice The hook only supports weighted pools + * @dev The math is written for weighted pools only though it can be extended to other pools + * but the math will be simpler for CPP or CSP. + */ + error OnlyWeightedPoolsAllowed(); + + /** + * @notice only pool creator can change rebalancing metrics for pools + */ + error OnlyCreatorCanChangeRebalanceData(address pool); + + /** + * @notice The rebalance data is not set for the pool which triggers the rebalancing + */ + error RebalanceDataNotSet(address pool); + + /** + * @notice The error given anywhere we encounter dissimilar array lengths + * @dev The param indicates various places the error has accoured + * example: "PW_N_PT" is Pool weights do not match pool tokens + */ + error InvalidArrayLenghts(string message); + + /** + * @notice Hooks functions called from an external router. + * @dev This contract inherits both `MinimalRouter` and `BaseHooks`, and functions as is its own router. + * @param router The address of the Router + */ + error CannotUseExternalRouter(address router); + + constructor( + IVault vault, + IPermit2 permi2, + IWETH weth, + address _weightedPoolFactory, + address _oracle + ) MinimalRouterWithSwap(vault, weth, permi2) Ownable(msg.sender) { + weightedPoolFactory = _weightedPoolFactory; + oracle = _oracle; + } + + modifier onlySelfRouter(address router) { + _ensureSelfRouter(router); + _; + } + + /*************************************************************************** + Router Functions + ***************************************************************************/ + function addLiquidityProportional( + address pool, + uint256[] memory maxAmountsIn, + uint256 exactBptAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable saveSender returns (uint256[] memory amountsIn) { + // Do addLiquidity operation - BPT is minted to this contract. + amountsIn = _addLiquidityProportional( + pool, + msg.sender, + address(this), + maxAmountsIn, + exactBptAmountOut, + wethIsEth, + userData + ); + address[] storage lps = liqudityProviders[pool]; + lps.push(msg.sender); + uint256[] storage amounts = amountTokens[pool][msg.sender]; + for (uint256 i = 0; i < amounts.length; i++) { + amounts[i] += amountsIn[i]; + } + isStillLp[pool][msg.sender] = true; + emit LiquidtyProviderAdded(pool, msg.sender); + } + + function removeLiquidityProportional( + uint256 tokenId, + address pool, + uint256 exactBptAmountIn, + uint256[] memory minAmountsOut, + bool wethIsEth + ) external payable saveSender returns (uint256[] memory amountsOut) { + // Do removeLiquidity operation - tokens sent to msg.sender. + amountsOut = _removeLiquidityProportional( + pool, + msg.sender, + msg.sender, + exactBptAmountIn, + minAmountsOut, + wethIsEth, + "" + ); + uint256[] memory lpBptAmount = amountTokens[pool][msg.sender]; + + uint256 length = amountsOut.length; + for (uint256 i = 0; i < length; i++) { + lpBptAmount[i] -= amountsOut[i]; + } + + bool hasRemovedAll = true; + + for (uint256 i = 0; i < length; i++) { + if (lpBptAmount[i] != 0) { + hasRemovedAll = false; + } + } + + isStillLp[pool][msg.sender] = hasRemovedAll; + } + + /*************************************************************************** + Hook Functions + ***************************************************************************/ + ///@inheritdoc IHooks + function onRegister( + address, + address pool, + TokenConfig[] memory, + LiquidityManagement calldata + ) public override onlyVault returns (bool) { + if (IBasePoolFactory(weightedPoolFactory).isPoolFromFactory(pool)) { + revert OnlyWeightedPoolsAllowed(); + } + + emit RebalancerHookRegistered(address(this), pool); + + return true; + } + + /// @inheritdoc IHooks + function getHookFlags() public pure override returns (HookFlags memory) { + HookFlags memory hookFlags; + hookFlags.enableHookAdjustedAmounts = true; + hookFlags.shouldCallBeforeAddLiquidity = true; + hookFlags.shouldCallAfterRemoveLiquidity = true; + return hookFlags; + } + + /// @inheritdoc BaseHooks + function onBeforeAddLiquidity( + address router, + address, + AddLiquidityKind, + uint256[] memory, + uint256, + uint256[] memory, + bytes memory + ) public view override onlySelfRouter(router) returns (bool) { + // We only allow addLiquidity via the Router/Hook itself + return true; + } + + /// @inheritdoc BaseHooks + function onAfterRemoveLiquidity( + address router, + address pool, + RemoveLiquidityKind, + uint256, + uint256[] memory, + uint256[] memory amountsOutRaw, + uint256[] memory, + bytes memory userData + ) public override onlySelfRouter(router) returns (bool, uint256[] memory hookAdjustedAmountsOutRaw) { + return (true, amountsOutRaw); + } + + /// @inheritdoc IHooks + function onAfterSwap( + AfterSwapParams calldata params + ) public override onlyVault returns (bool success, uint256 hookAdjustedAmountCalculatedRaw) { + address pool = params.pool; + if (rebalanceData[pool].length == 0) { + revert RebalanceDataNotSet(pool); + } + + (bool rebalanceRequired, uint256[] memory priceActionRatio) = isRebalanceRequired(pool); + if (rebalanceRequired) { + emit RebalanceStarted(pool); + rebalance(pool, priceActionRatio); + } + + return (true, params.amountCalculatedRaw); + } + + /*************************************************************************** + Setter Functions + ***************************************************************************/ + function setWeightedPoolFactoryAddress(address newWeightedPoolFactory) external onlyOwner { + ///@dev I am a little confused that whether to check if the factory is disable or not would love + /// reason to check why cause I am thinking the pool can be created from a disabled factory + /// and still work fine? Let's keep it like this for now while I study about balancer governance + /// more + weightedPoolFactory = newWeightedPoolFactory; + } + + function setRebalanceData(address pool, RebalanceData[] memory _rebalanceData) external { + PoolRoleAccounts memory roleAccounts = _vault.getPoolRoleAccounts(pool); + + if (msg.sender != roleAccounts.poolCreator) { + revert OnlyCreatorCanChangeRebalanceData(pool); + } + + rebalanceData[pool] = _rebalanceData; + } + + function setOracle(address newOracle) external onlyOwner { + oracle = newOracle; + } + + /*************************************************************************** + internal Functions + ***************************************************************************/ + function rebalance(address pool, uint256[] memory priceActionRatio) internal { + RebalanceData[] memory poolRebalanceData = rebalanceData[pool]; + WeightedPoolDynamicData memory poolData = IWeightedPool(pool).getWeightedPoolDynamicData(); + uint256[] memory currentBalances = poolData.balancesLiveScaled18; + uint256[] memory normalizedWeights = IWeightedPool(pool).getNormalizedWeights(); + (uint256[] memory tokenDeltas, bool[] memory remove) = _calculateAmounts( + poolRebalanceData, + currentBalances, + priceActionRatio, + normalizedWeights + ); + _changeLiqudity(pool, tokenDeltas, remove, currentBalances, true); + } + + function isRebalanceRequired(address pool) internal view returns (bool, uint256[] memory) { + TokenData[] memory tokenData = IOracle(oracle).getPoolTokensData(pool); + RebalanceData[] memory poolRebalanceData = rebalanceData[pool]; + bool didPriceChange = false; + uint256[] memory priceActionRatioArr = new uint256[](tokenData.length); + for (uint i = 0; i < tokenData.length; i++) { + if (tokenData[i].predictedPrice != 0) { + uint256 priceActionRatio = tokenData[i].predictedPrice.divUp(tokenData[i].latestRoundPrice); + uint256 priceActionRatioScaled = priceActionRatio.mulUp(PRICE_PRECISION); + if (priceActionRatio > poolRebalanceData[i].minRatio) { + didPriceChange = true; + priceActionRatioArr[i] = priceActionRatioScaled; + } else { + priceActionRatioArr[i] = FixedPoint.ONE; + } + } + } + return (didPriceChange, priceActionRatioArr); + } + + /** + * @param pool The address of pool + * @param tokenDeltas The token balance changes + * @param remove The array whether to remove or add liquidity + */ + function _changeLiqudity( + address pool, + uint256[] memory tokenDeltas, + bool[] memory remove, + uint256[] memory currentBalances, + bool wethIsEth + ) internal { + uint256 length = tokenDeltas.length; + uint256[] memory addTokens; + uint256[] memory removeTokens; + address[] memory activeLiquidityProviders = _getActiveLiquidityProviders(pool); + + ///@dev This is not for production but I take an assumption that every Lp has enough funds to handle + /// distribute between top 2 for dev + uint256 numberOfLps = activeLiquidityProviders.length >= 2 ? 2 : activeLiquidityProviders.length; + /** + * @dev So here What I do is to use directly the _addLiquidity and _removeLiquidity programs. I spilt the two + * arrays into the addTokens for which i increase the current tokenbalance and if the token balance decrease + * I do not change in the addTokens. Thus the result by calling _distributeFunds on the addTokens should leave + * removeTokens unchanged + */ + for (uint256 tokenIndex = 0; tokenIndex < length; tokenIndex++) { + if (tokenDeltas[tokenIndex] > 0) { + if (remove[tokenIndex]) { + removeTokens[tokenIndex] = currentBalances[tokenIndex] - tokenDeltas[tokenIndex]; + addTokens[tokenIndex] = currentBalances[tokenIndex]; + } else { + addTokens[tokenIndex] = currentBalances[tokenIndex] + tokenDeltas[tokenIndex]; + removeTokens[tokenIndex] = currentBalances[tokenIndex]; + } + } + } + if (numberOfLps == 1) { + _distributeLiquidity(pool, activeLiquidityProviders[0], addTokens, false, wethIsEth); + _distributeLiquidity(pool, activeLiquidityProviders[0], removeTokens, true, wethIsEth); + } else { + (uint256[][] memory lpOneTokens, uint256[][] memory lpTwoTokens) = _getTwoLpsTokens( + addTokens, + removeTokens + ); + _distributeLiquidity(pool, activeLiquidityProviders[0], lpOneTokens[0], false, wethIsEth); + _distributeLiquidity(pool, activeLiquidityProviders[0], lpOneTokens[1], false, wethIsEth); + _distributeLiquidity(pool, activeLiquidityProviders[1], lpTwoTokens[0], false, wethIsEth); + _distributeLiquidity(pool, activeLiquidityProviders[1], lpOneTokens[1], false, wethIsEth); + } + } + + /// @dev The following is a hack as the computaion is toon if I also calculate the exacptBptAmont + uint256 constant MIN_BPT_AMOUNT_OUT = 0; + uint256 constant MAX_BPT_AMOUNT_IN = type(uint256).max; + + /** + * @param pool address of the pool + * @param liquidityProvider The liquidity Providers for which we have to distribute the liquidity + * @param tokenAmounts The amount of tokens out / in + * @param remove The flag to remove or add liquidity for the particular token + */ + function _distributeLiquidity( + address pool, + address liquidityProvider, + uint256[] memory tokenAmounts, + bool remove, + bool wethIsEth + ) internal { + if (!remove) { + _addLiquidityProportional( + pool, + liquidityProvider, + liquidityProvider, + tokenAmounts, + MIN_BPT_AMOUNT_OUT, + wethIsEth, + "" + ); + } else { + _removeLiquidityProportional( + pool, + liquidityProvider, + liquidityProvider, + MAX_BPT_AMOUNT_IN, + tokenAmounts, + wethIsEth, + "" + ); + } + } + + /** + * This function calculates the newTokenBalances based on the currentTokenBalances and priceRations + * @param poolRebalanceData The rebalancing config for pool + * @param currentLiveTokenBalancesScaled18 are the live token balances from + * @param priceActionRatio The price action is just the ratio of updatePrice/lastPrice the precision is 1e18 so 1% change is 1e16 + * @param normalizedWeights The noramlized weights of toknes + * + * @return tokenDeltas Is the delta for token change + * @return remove this is true if we have to remove the liqudity + */ + function _calculateAmounts( + RebalanceData[] memory poolRebalanceData, + uint256[] memory currentLiveTokenBalancesScaled18, + uint256[] memory priceActionRatio, + uint256[] memory normalizedWeights + ) internal pure returns (uint256[] memory tokenDeltas, bool[] memory remove) { + uint256 length = poolRebalanceData.length; + + for (uint256 i = 0; i < length; i++) { + if (poolRebalanceData[i].rebalanceRequired) { + uint256 innerProduct = FixedPoint.ONE; + for (uint256 j = 0; j < length; j++) { + uint256 exponent = normalizedWeights[j].divUp(length.mulUp(FixedPoint.ONE)); // This definetly less than 1 + uint256 power = priceActionRatio[j].powUp(exponent); + innerProduct = innerProduct.mulUp(power); // This is in 1e18 + } + // This is now not in scale + // As 5e18/e16 => 5e2 + uint256 balanceFactor = currentLiveTokenBalancesScaled18[i].divUp(priceActionRatio[i]); + // This is again in scale + uint256 newBalance = balanceFactor.mulUp(innerProduct); + remove[i] = newBalance < currentLiveTokenBalancesScaled18[i] ? true : false; + tokenDeltas[i] = _getDiff(newBalance, currentLiveTokenBalancesScaled18[i]); + } else { + tokenDeltas[i] = 0; + remove[i] = false; + } + } + } + + function _ensureSelfRouter(address router) private view { + if (router != address(this)) { + revert CannotUseExternalRouter(router); + } + } + + ///@dev always get the positive diff + function _getDiff(uint256 a, uint256 b) internal view returns (uint256) { + return a > b ? a - b : b - a; + } + + /** + * This returns the active lp address + * @param pool The address of the pool + * @return The list of active liquidity providers for the pool + */ + function _getActiveLiquidityProviders(address pool) internal view returns (address[] memory) { + address[] memory poolLiquidityProviders = liqudityProviders[pool]; + uint256 length = 0; + address[] memory activeLp; + for (uint256 i = 0; i < poolLiquidityProviders.length; i++) { + if (isStillLp[pool][poolLiquidityProviders[i]]) { + activeLp[length] = poolLiquidityProviders[i]; + length++; + } + } + return activeLp; + } + + function _getTwoLpsTokens( + uint256[] memory addTokens, + uint256[] memory removeTokens + ) internal pure returns (uint256[][] memory, uint256[][] memory) { + uint256[][][] memory tokenMatrix; + uint256 length = addTokens.length; + for (uint256 lpIndex; lpIndex < 2; lpIndex++) { + for (uint256 tokenIndex = 0; tokenIndex < length; tokenIndex++) { + tokenMatrix[lpIndex][0][tokenIndex] = addTokens[tokenIndex].divUp(2); + } + for (uint256 tokenIndex = 0; tokenIndex < length; tokenIndex++) { + tokenMatrix[lpIndex][1][tokenIndex] = removeTokens[tokenIndex].divUp(2); + } + } + + return (tokenMatrix[0], tokenMatrix[1]); + } +} diff --git a/packages/foundry/contracts/hooks/rebalancer/oracle/interfaces/IOracle.sol b/packages/foundry/contracts/hooks/rebalancer/interfaces/IOracle.sol similarity index 51% rename from packages/foundry/contracts/hooks/rebalancer/oracle/interfaces/IOracle.sol rename to packages/foundry/contracts/hooks/rebalancer/interfaces/IOracle.sol index 53404011..91e90a74 100644 --- a/packages/foundry/contracts/hooks/rebalancer/oracle/interfaces/IOracle.sol +++ b/packages/foundry/contracts/hooks/rebalancer/interfaces/IOracle.sol @@ -1,23 +1,24 @@ // SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity ^0.8.24; - /** - * @dev Struct for holding position for the token + * @param latestRoundPrice It is the latest pice from the Oracle + * @param predicitedPrice It is the Prediction Price from forward event oracle + * @param rebalanceRequired It is true if the current price is changed above the + * minChangeForBalance in which case the pool runs the _rebalance function */ -struct PositionData{ - uint24 tickUpper; - uint24 tickLower; +struct TokenData { + uint256 latestRoundPrice; + uint256 predictedPrice; } - /** * @dev Interface for the rebalancer oracle */ interface IOracle { /** - * @dev Get the current fee for the given pool + * @dev Get the current fee for the given pool * @param pool The address of the pool * @return The current fee */ @@ -28,8 +29,7 @@ interface IOracle { * @param pool The address of the pool * @return The current position */ - function getPosition(address pool) external view returns (Position memory); - + function getPoolTokensData(address pool) external view returns (TokenData[] memory); /** * @dev Set the fee for the given pool @@ -41,15 +41,21 @@ interface IOracle { /** * @dev Set the position for the given pool * @param pool The address of the pool - * @param _lowerTick The lower tick boundry - * @param _upperTick The upper tick boundry + * @param i The index of the token to set for the pool + * @param latestRoundPrice The Latest Round price from Price Aggregator + * @param predictedPrice The predict price based on forward events */ - function setPositionData(address pool, uint24 _lowerTick, uint24 _upperTick) external; + function setPoolTokenData( + address pool, + uint i, + uint256 latestRoundPrice, + uint256 predictedPrice + ) external; + /** * @dev Update the oracle for the given pool * @param pool The address of the pool */ function updateOracle(address pool) external; - -} \ No newline at end of file +} diff --git a/packages/foundry/remappings.txt b/packages/foundry/remappings.txt index 8abd1432..2d53cb43 100644 --- a/packages/foundry/remappings.txt +++ b/packages/foundry/remappings.txt @@ -4,6 +4,7 @@ @balancer-labs/v3-interfaces/=lib/balancer-v3-monorepo/pkg/interfaces/ @balancer-labs/v3-pool-weighted/=lib/balancer-v3-monorepo/pkg/pool-weighted/ @balancer-labs/v3-vault/=lib/balancer-v3-monorepo/pkg/vault/ +@balancer-labs/v3-pool-hooks=lib/balancer-v3-monorepo/pkg/pool-hooks/ permit2/=lib/permit2/ forge-gas-snapshot/=node_modules/forge-gas-snapshot/src/ @openzeppelin/=lib/openzeppelin-contracts/ \ No newline at end of file From f073d96f8375b350801ba07fa07bd81eafac5381 Mon Sep 17 00:00:00 2001 From: utkarshdagoat Date: Mon, 21 Oct 2024 01:05:24 +0530 Subject: [PATCH 3/4] minor changes --- README.md | 301 +----------------- .../contracts/hooks/rebalancer/Rebalancer.sol | 23 +- 2 files changed, 20 insertions(+), 304 deletions(-) diff --git a/README.md b/README.md index 1397b2da..6e5e30d5 100644 --- a/README.md +++ b/README.md @@ -1,301 +1,2 @@ -# πŸ—οΈŽ Scaffold Balancer v3 +# ReBalancer -A starter kit for building on top of Balancer v3. Accelerate the process of creating custom pools and hooks contracts. Concentrate on mastering the core concepts within a swift and responsive environment augmented by a local fork and a frontend pool operations playground. - -[![intro-to-scaffold-balancer](https://github.com/user-attachments/assets/f862091d-2fe9-4b4b-8d70-cb2fdc667384)](https://www.youtube.com/watch?v=m6q5M34ZdXw) - -### πŸ” Development Life Cycle - -1. Learn the core concepts for building on top of Balancer v3 -2. Configure and deploy factories, pools, and hooks contracts to a local anvil fork of Sepolia -3. Interact with pools via a frontend that runs at [localhost:3000](http://localhost:3000/) - -### πŸͺ§ Table Of Contents - -- [πŸ§‘β€πŸ’» Environment Setup](#-environment-setup) -- [πŸ‘©β€πŸ« Learn Core Concepts](#-learn-core-concepts) -- [πŸ•΅οΈ Explore the Examples](#-explore-the-examples) -- [🌊 Create a Custom Pool](#-create-a-custom-pool) -- [🏭 Create a Pool Factory](#-create-a-pool-factory) -- [πŸͺ Create a Pool Hook](#-create-a-pool-hook) -- [🚒 Deploy the Contracts](#-deploy-the-contracts) -- [πŸ§ͺ Test the Contracts](#-test-the-contracts) - -## πŸ§‘β€πŸ’» Environment Setup - -### 1. Requirements πŸ“œ - -- [Node (>= v18.17)](https://nodejs.org/en/download/) -- Yarn ([v1](https://classic.yarnpkg.com/en/docs/install/) or [v2+](https://yarnpkg.com/getting-started/install)) -- [Git](https://git-scm.com/downloads) -- [Foundry](https://book.getfoundry.sh/getting-started/installation) (>= v0.2.0) - -### 2. Quickstart πŸƒ - -1. Ensure you have the latest version of foundry installed - -``` -foundryup -``` - -2. Clone this repo & install dependencies - -```bash -git clone https://github.com/balancer/scaffold-balancer-v3.git -cd scaffold-balancer-v3 -yarn install -``` - -3. Set a `SEPOLIA_RPC_URL` in the `packages/foundry/.env` file - -``` -SEPOLIA_RPC_URL=... -``` - -4. Start a local anvil fork of the Sepolia testnet - -```bash -yarn fork -``` - -5. Deploy the mock tokens, pool factories, pool hooks, and custom pools contracts - > By default, the anvil account #0 will be the deployer and recieve the mock tokens and BPT from pool initialization - -```bash -yarn deploy -``` - -6. Start the nextjs frontend - -```bash -yarn start -``` - -7. Explore the frontend - -- Navigate to http://localhost:3000 to see the home page -- Visit the [Pools Page](http://localhost:3000/pools) to search by address or select using the pool buttons -- Vist the [Debug Page](http://localhost:3000/debug) to see the mock tokens, factory, and hooks contracts - -8. Run the Foundry tests - -``` -yarn test -``` - -### 3. Scaffold ETH 2 Tips πŸ—οΈ - -SE-2 offers a variety of configuration options for connecting an account, choosing networks, and deploying contracts - -
πŸ”₯ Burner Wallet - -If you do not have an active wallet extension connected to your web browser, then scaffold eth will automatically connect to a "burner wallet" that is randomly generated on the frontend and saved to the browser's local storage. When using the burner wallet, transactions will be instantly signed, which is convenient for quick iterative development. - -To force the use of burner wallet, disable your browsers wallet extensions and refresh the page. Note that the burner wallet comes with 0 ETH to pay for gas so you will need to click the faucet button in top right corner. Also the mock tokens for the pool are minted to your deployer account set in `.env` so you will want to navigate to the "Debug Contracts" page to mint your burner wallet some mock tokens to use with the pool. - -![Burner Wallet](https://github.com/Dev-Rel-as-a-Service/scaffold-balancer-v3/assets/73561520/0a1f3456-f22a-46b5-9e05-0ef5cd17cce7) - -![Debug Tab Mint](https://github.com/Dev-Rel-as-a-Service/scaffold-balancer-v3/assets/73561520/fbb53772-8f6d-454d-a153-0e7a2925ef9f) - -
- -
πŸ‘› Browser Extension Wallet - -- To use your preferred browser extension wallet, ensure that the account you are using matches the PK you previously provided in the `foundry/.env` file -- You may need to add a local development network with rpc url `http://127.0.0.1:8545/` and chain id `31337`. Also, you may need to reset the nonce data for your wallet exension if it gets out of sync. - -
- -
πŸ› Debug Contracts Page - -The [Debug Contracts Page](http://localhost:3000/debug) can be useful for viewing and interacting with all of the externally avaiable read and write functions of a contract. The page will automatically hot reload with contracts that are deployed via the `01_DeployConstantSumFactory.s.sol` script. We use this handy setup to mint `mockERC20` tokens to any connected wallet - -
- -
🌐 Changing The Frontend Network Connection - -- The network the frontend points at is set via `targetNetworks` in the `scaffold.config.ts` file using `chains` from viem. -- By default, the frontend runs on a local node at `http://127.0.0.1:8545` - -```typescript -const scaffoldConfig = { - targetNetworks: [chains.foundry], -``` - -
- -
🍴 Changing The Forked Network - -- By default, the `yarn fork` command points at sepolia, but any of the network aliases from the `[rpc_endpoints]` of `foundry.toml` can be used to modify the `"fork"` alias in the `packages/foundry/package.json` file - -```json - "fork": "anvil --fork-url ${0:-sepolia} --chain-id 31337 --config-out localhost.json", -``` - -- To point the frontend at a different forked network, change the `targetFork` in `scaffold.config.ts` - -```typescript -const scaffoldConfig = { - // The networks the frontend can connect to - targetNetworks: [chains.foundry], - - // If using chains.foundry as your targetNetwork, you must specify a network to fork - targetFork: chains.sepolia, -``` - -
- -## πŸ‘©β€πŸ« Learn Core Concepts - -- [Contract Architecture](https://docs-v3.balancer.fi/concepts/core-concepts/architecture.html) -- [Balancer Pool Tokens](https://docs-v3.balancer.fi/concepts/core-concepts/balancer-pool-tokens.html) -- [Balancer Pool Types](https://docs-v3.balancer.fi/concepts/explore-available-balancer-pools/) -- [Building Custom AMMs](https://docs-v3.balancer.fi/build-a-custom-amm/) -- [Exploring Hooks and Custom Routers](https://pitchandrolls.com/2024/08/30/unlocking-the-power-of-balancer-v3-exploring-hooks-and-custom-routers/) -- [Hook Development Tips](https://medium.com/@johngrant/unlocking-the-power-of-balancer-v3-hook-development-made-simple-831391a68296) - -![v3-components](https://github.com/user-attachments/assets/ccda9323-790f-4276-b092-c867fd80bf9e) - -## πŸ•΅οΈ Explore the Examples - -Each of the following examples have turn key deploy scripts that can be found in the [foundry/script/](https://github.com/balancer/scaffold-balancer-v3/tree/main/packages/foundry/script) directory - -### 1. Constant Sum Pool with Dynamic Swap Fee Hook - -The swap fee percentage is altered by the hook contract before the pool calculates the amount for the swap - -![dynamic-fee-hook](https://github.com/user-attachments/assets/5ba69ea3-6894-4eeb-befa-ed87cfeb6b13) - -### 2. Constant Product Pool with Lottery Hook - -An after swap hook makes a request to an oracle contract for a random number - -![after-swap-hook](https://github.com/user-attachments/assets/594ce1ac-2edc-4d16-9631-14feb2d085f8) - -### 3. Weighted Pool with Exit Fee Hook - -An after remove liquidity hook adjusts the amounts before the vault transfers tokens to the user - -![after-remove-liquidity-hook](https://github.com/user-attachments/assets/2e8f4a5c-f168-4021-b316-28a79472c8d1) - -## 🌊 Create a Custom Pool - -[![custom-amm-video](https://github.com/user-attachments/assets/e6069a51-f1b5-4f98-a2a9-3a2098696f96)](https://www.youtube.com/watch?v=kXynS3jAu0M) - -### 1. Review the Docs πŸ“– - -- [Create a custom AMM with a novel invariant](https://docs-v3.balancer.fi/build-a-custom-amm/build-an-amm/create-custom-amm-with-novel-invariant.html) - -### 2. Recall the Key Requirements πŸ”‘ - -- Must inherit from `IBasePool` and `BalancerPoolToken` -- Must implement `onSwap`, `computeInvariant`, and `computeBalance` -- Must implement `getMaximumSwapFeePercentage` and `getMinimumSwapFeePercentage` - -### 3. Write a Custom Pool Contract πŸ“ - -- To get started, edit the`ConstantSumPool.sol` contract directly or make a copy - -## 🏭 Create a Pool Factory - -After designing a pool contract, the next step is to prepare a factory contract because Balancer's off-chain infrastructure uses the factory address as a means to identify the type of pool, which is important for integration into the UI, SDK, and external aggregators - -### 1. Review the Docs πŸ“– - -- [Deploy a Custom AMM Using a Factory](https://docs-v3.balancer.fi/build-a-custom-amm/build-an-amm/deploy-custom-amm-using-factory.html) - -### 2. Recall the Key Requirements πŸ”‘ - -- A pool factory contract must inherit from [BasePoolFactory](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/vault/contracts/factories/BasePoolFactory.sol) -- Use the internal `_create` function to deploy a new pool -- Use the internal `_registerPoolWithVault` fuction to register a pool immediately after creation - -### 3. Write a Factory Contract πŸ“ - -- To get started, edit the`ConstantSumFactory.sol` contract directly or make a copy - -## πŸͺ Create a Pool Hook - -[![hook-video](https://github.com/user-attachments/assets/96e12c29-53c2-4a52-9437-e477f6d992d1)](https://www.youtube.com/watch?v=kaz6duliRPA) - -### 1. Review the Docs πŸ“– - -- [Extend an Existing Pool Type Using Hooks](https://docs-v3.balancer.fi/build-a-custom-amm/build-an-amm/extend-existing-pool-type-using-hooks.html) - -### 2. Recall the Key Requirements πŸ”‘ - -- A hooks contract must inherit from [BasePoolHooks.sol](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/vault/contracts/BaseHooks.sol) -- A hooks contract should also inherit from [VaultGuard.sol](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/vault/contracts/VaultGuard.sol) -- Must implement `onRegister` to determine if a pool is allowed to use the hook contract -- Must implement `getHookFlags` to define which hooks are supported -- The `onlyVault` modifier should be applied to all hooks functions (i.e. `onRegister`, `onBeforeSwap`, `onAfterSwap` ect.) - -### 3. Write a Hook Contract πŸ“ - -- To get started, edit the `VeBALFeeDiscountHook.sol` contract directly or make a copy - -## 🚒 Deploy the Contracts - -The deploy scripts are located in the [foundry/script/](https://github.com/balancer/scaffold-balancer-v3/tree/main/packages/foundry/script) directory. To better understand the lifecycle of deploying a pool that uses a hooks contract, see the diagram below - -![pool-deploy-scripts](https://github.com/user-attachments/assets/bb906080-8f42-46c0-af90-ba01ba1754fc) - -### 1. Modifying the Deploy Scripts πŸ› οΈ - -For all the scaffold integrations to work properly, each deploy script must be imported into `Deploy.s.sol` and inherited by the `DeployScript` contract in `Deploy.s.sol` - -### 2. Broadcast the Transactions πŸ“‘ - -#### Deploy to local fork - -1. Run the following command - -```bash -yarn deploy -``` - -#### Deploy to a live network - -1. Add a `DEPLOYER_PRIVATE_KEY` to the `packages/foundry/.env` file - -``` -DEPLOYER_PRIVATE_KEY=0x... -SEPOLIA_RPC_URL=... -``` - -> The `DEPLOYER_PRIVATE_KEY` must start with `0x` and must hold enough Sepolia ETH to deploy the contracts. This account will receive the BPT from pool initialization - -2. Run the following command - -``` -yarn deploy --network sepolia -``` - -## πŸ§ͺ Test the Contracts - -The [balancer-v3-monorepo](https://github.com/balancer/balancer-v3-monorepo) provides testing utility contracts like [BasePoolTest](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/vault/test/foundry/utils/BasePoolTest.sol) and [BaseVaultTest](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/vault/test/foundry/utils/BaseVaultTest.sol). Therefore, the best way to begin writing tests for custom factories, pools, and hooks contracts is to leverage the examples established by the source code. - -### 1. Testing Factories πŸ‘¨β€πŸ”¬ - -The `ConstantSumFactoryTest` roughly mirrors the [WeightedPool8020FactoryTest -](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/pool-weighted/test/foundry/WeightedPool8020Factory.t.sol) - -``` -yarn test --match-contract ConstantSumFactoryTest -``` - -### 2. Testing Pools 🏊 - -The `ConstantSumPoolTest` roughly mirrors the [WeightedPoolTest](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/pool-weighted/test/foundry/WeightedPool.t.sol) - -``` -yarn test --match-contract ConstantSumPoolTest -``` - -### 3. Testing Hooks 🎣 - -The `VeBALFeeDiscountHookExampleTest` mirrors the [VeBALFeeDiscountHookExampleTest](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/pool-hooks/test/foundry/VeBALFeeDiscountHookExample.t.sol) - -``` -yarn test --match-contract VeBALFeeDiscountHookExampleTest -``` diff --git a/packages/foundry/contracts/hooks/rebalancer/Rebalancer.sol b/packages/foundry/contracts/hooks/rebalancer/Rebalancer.sol index 818bc98d..f603a96b 100644 --- a/packages/foundry/contracts/hooks/rebalancer/Rebalancer.sol +++ b/packages/foundry/contracts/hooks/rebalancer/Rebalancer.sol @@ -29,7 +29,8 @@ import { AddLiquidityParams, PoolRoleAccounts, AfterSwapParams, - SwapKind + SwapKind, + PoolSwapParams } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; import { MinimalRouterWithSwap } from "./MinimalRouterWithSwap.sol"; @@ -222,6 +223,8 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { hookFlags.enableHookAdjustedAmounts = true; hookFlags.shouldCallBeforeAddLiquidity = true; hookFlags.shouldCallAfterRemoveLiquidity = true; + hookFlags.shouldCallComputeDynamicSwapFee = true; + hookFlags.shouldCallAfterSwap = true; return hookFlags; } @@ -260,17 +263,28 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { address pool = params.pool; if (rebalanceData[pool].length == 0) { revert RebalanceDataNotSet(pool); - } - + } + (bool rebalanceRequired, uint256[] memory priceActionRatio) = isRebalanceRequired(pool); if (rebalanceRequired) { - emit RebalanceStarted(pool); + emit RebalanceStarted(pool); rebalance(pool, priceActionRatio); } return (true, params.amountCalculatedRaw); } + /// @inheritdoc IHooks + function onComputeDynamicSwapFeePercentage( + PoolSwapParams calldata params, + address pool, + uint256 staticSwapFeePercentage + ) public view override onlyVault returns (bool, uint256) { + uint256 dynamicFee = IOracle(oracle).getFee(pool); + + return (true, dynamicFee); + } + /*************************************************************************** Setter Functions ***************************************************************************/ @@ -299,6 +313,7 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { /*************************************************************************** internal Functions ***************************************************************************/ + function rebalance(address pool, uint256[] memory priceActionRatio) internal { RebalanceData[] memory poolRebalanceData = rebalanceData[pool]; WeightedPoolDynamicData memory poolData = IWeightedPool(pool).getWeightedPoolDynamicData(); From 4919481fbcf49c75be1c299402e87bc5c6540b13 Mon Sep 17 00:00:00 2001 From: utkarshdagoat Date: Mon, 21 Oct 2024 04:30:30 +0100 Subject: [PATCH 4/4] readme: math pending --- README.md | 104 ++++++- images/balance.png | Bin 0 -> 145587 bytes .../contracts/hooks/rebalancer/Oracle.sol | 35 ++- .../contracts/hooks/rebalancer/Rebalancer.sol | 230 ++++++++++---- .../hooks/rebalancer/interfaces/IOracle.sol | 20 +- packages/foundry/test/ReBalancerE2E.t.sol | 285 ++++++++++++++++++ 6 files changed, 580 insertions(+), 94 deletions(-) create mode 100644 images/balance.png create mode 100644 packages/foundry/test/ReBalancerE2E.t.sol diff --git a/README.md b/README.md index 6e5e30d5..610eb0b5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,104 @@ -# ReBalancer +
+ + Logo + +

ReBalancer

+

+ReBalancer is a balancer v3 hook that dynamically rebalances lp positions and fees based on real-time events and market implied volatility. +
+ The PitchDeck Β» +
+
+ View Demo +

+
+ + ## The Motivation + Volatility spikes are common during key real-world asset (RWA) events like central bank interest rate decisions, inflation reports (e.g., CPI), corporate earnings releases, bond coupon payments, and dividend announcements. Additionally, off-market hours in traditional finance and major geopolitical developments can drive price fluctuations. + RWAs that generate income, when these predictable price changes occur (like a bond's coupon payment), the value lost due to this change is permanent. That’s because the LP has effectively lost part of the asset’s value as it was transferred in the form of a coupon or dividend to the holder. + As the value of RWA tokens is expecteed to cross $16 Trillion by 2030 said by [bcg](https://web-assets.bcg.com/1e/a2/5b5f2b7e42dfad2cb3113a291222/on-chain-asset-tokenization.pdf). Solving such a problem with balancer hooks can attaract LPs that provide such assets. + + ## The Problem + Arbitrageurs capture all expected price and volatility changes at the expense of LPs. These predictable arbitrages harm liquidity, lead to MEV leaks, and deter swappers due to poor liquidity + Let's look at an example + Scenario: $1000 face value bond paying 5% semi-annual coupon + Traditional AMM: + - Bond token drops from $1000 to $950 after $50 coupon payment + - LPs lose $50 per $1000 of liquidity + - Arbitrageurs capture the entire coupon value + + ReBalancer Response: + - Adjusts position bounds pre-coupon payment + - Increases fees leading up to event + - LPs capture 60-80% of would-be arbitrage profits + ## Solution + A hook that dynamically optimizes LP fees and positions by leveraging forward-looking volatility for flexible fee adjustments, redirecting value from arbitrageurs to LPs, and using anticipated price movements to rebalance LP positions in advance. + + *ReBalancer* solves the problems by + - Positions itself smartly in case of future events that leads to voltality of these assets. + - Improve LP returns by dynamic rebalancing and fees. + - Minimizes losses to arbitrages + + ## Implmentation + As *ReBalancer* works on modifying and removing the liquidity it is both a *Hook* + *Router*. + + ### Architecture/Flow + ![Rebalancer flow](images/balance.png) + + ### Math + The math on a basic ideology is that we should keep the value functios same throught the price changes + Let $V$ be the current value of the value function for are weighted pool. We know that it is calculate by + $$ + \prod_{t} (B_{t})^{W_t} + $$ + Where $B_{t}$ is the balance of token and $W_{t}$ is the normalized weight of the token. + Let $P_t$ be the price of the token and $L$ is the liqudity of the pool. Then, + $$ + B_t = \frac{L*W_t}{P_t} + $$ + Now Due to a event the Price of each asset changes to $P_t'$. Resulting balance, liquidity and value function be given be $B_t'$ , $L'$ , $V'$. + Now as + $$V = V'$$ + $$\prod_{t} (B_{t})^{W_t} = \prod_{t} (B_{t}')^{W_t}$$ + $$\prod_{t} (\frac{L*W_t}{P_t})^{W_t} = \prod_{t} (\frac{L'*W_t'}{P_t'})^{W_t}$$ + which simplifies to assuming K tokens it the pool; + $$ + L' = L \times (\prod_{t} \frac{P_t'}{P_t})^{W_t})^{\frac{1}{k}} + $$ + Let $\frac{P_t'}{P_t}=r_t$ where $r_t$ represents the price ratio. So + $$ + L' = L \times (\prod_{t} (r_t)^{\frac{W_t}{k}}) + $$ + Thus the balance of new tokens can be calculate as + $$ + B_t' = \frac{L*W_t}{P_t'}\times (\prod_{t} (r_t)^{\frac{W_t}{k}}) + $$ + which is simplfied to: + $$ + B_t' = \frac{B_t}{r_t}\times (\prod_{t} (r_t)^{\frac{W_t}{k}}) + $$ + This is the simple mathematics implemented in `_calculateAmount` Function + +## Running locally + + +1. Ensure you have the latest version of foundry installed + +``` +foundryup +``` + +2. Clone this repo & install dependencies + +```bash +git clone https://github.com/utkarshdagoat/ReBalancer +cd ReBalancer +yarn install +``` + +3. Run the end2end test: +```bash +cd packages/foundry +forge test --match-path test/ReBalancerE2E.t.sol -vvvv +``` diff --git a/images/balance.png b/images/balance.png new file mode 100644 index 0000000000000000000000000000000000000000..395459b32e8a50e9a1b2cc4b1d33eec87c0f6edf GIT binary patch literal 145587 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

RGK;1f9BH!X0JMGVq- znu#unpKv*A378)+f(<)@j!oedHXB4S2>=rRmZku9h@p&SI1Ms5))nd?#>+SoI5;12 zfQhWfhCY&{A$`M^I@8`e(m*vEad-5CXgRJvisnjWpiX02DN&W^iMCLr6Jax1KOTud z*FSxxesd){zH{MLWajMs2A7p`7ziXYJmvR1du8|p>z|31A+>jjbyI-|YHX-*sI|}v zEZS$|V%!rxf>l9z)sI+qXlqcYCHh9tveE8@X?J2UVz9#@?t{%MM}hXX6|7s;5A<$G zX|St`-faoQep35XW-hJ9h8tVlTLT|TXFRsT0p6aI)yXjgMA+ZBi%0B%vX7a{^$ufZ z=dAI7M?klgvg+c;Md*$EzpNFbxa^Mf!D)G=l@0YGe2EIEb4`umNPY z6)=sm9$ByaZ)oNG$bE`jHa7G}QfC-xD;7vfj`B>)2 z`cR34MnszBA+%zyyqK93+>+D-6uoz4zcaA4P=zhCR#)o*t)gBJU=6v4#Xyy=6`Y-W)>nn}NR57Pd;y}AaJ5q2;k z@v0WSbt|6WDLd@>jIV5@2u_mZS)sL-p$kDco?fG6GH>1^550M|XD-1-nmgCc43D+a z4bGwKcRY4=vH^k;;#euM9J9u`-kWGjn}soa5HjthUu>L8F)eZrUSQ~~F10vd$ETKvpP`mx^!GTRS$zY<0`?-oVT&n#t* zMzYG67QSi{++hwUDq1|->#eTR?LV0C+k8XV9c|# z6=-8yicZC3%3fr4LWrzz$=;U%1Qne$B)16HF3qx7BBzJ*<>!dK=g9&sX;Ux~Y=!VD z7zGkIUoS$w`yKZ`2@K39fxyvpx{1hn2RfEAg_8V#m(HJP&0i@MLtKCT^nEUv)4*~-bZrkafs`84*>)Da{h39XsHo>GFh^l3^3ScZ&@~ZObh0a7+f8o zRgv`oCpx6t^tjsJuRhal@?;ONAlsQvkw3weG9~i+6xkl6BJNfwLt{yp>24=H<)+U> zg2Lne>qo2%7*bXGpzqjumn<@qGXKb#SOR1rdZ)lV3I}XihA4O{BdK1K#K?#gzD0;@ z6R~#45TUz2jG^vHpNW1ShGcs{mFku~HJ%0WOCQL|1`ppQRv@l*Iw=NAJVROrl)I{z zOi?va_VmqM7+(V6X}haUq1U2Bxr~Aow&IE>xC7qK`c!hSJ6WnDt>)869)r7@gydIi ztay?pI^9T*Dp-Dtokgf8g0z zEXL1l0%#kJv@nGq3QJQ8=kSFN{2EDEQ;?7^EBL{6xu=Y*KnTTCU3LP8)h;ES)e?*^L%U8W> z*qdQl+(%X2$W%Vd?Wafd9qS#GZS-e}7rIa@C#XgPf&wAoeoyS1Fy}%HL-lJ@Fa0-? z`~s)nh&CiF?C`aV;45wh_0ks_&w%TvEmQfLB!I}FBNRgD860VjXPFon`ysqn&6NHI z?2$+T4OKRZUZa4JycFdD(Dg!5*li$_nH-83lvyHQRwdlRdZ+kMLV5&zV)FK}OFobG zKMaQOxnio=7gi#)8FZF80xQot<3iPd2AT^6T}r6ncsVe|5g?#YEb=YBj?&V@f!>Ko z@48Bxs5Y&w|Dg22A^X0l8q!7fOhaUl43dl z!oo;LxUiv>WdeBtCBmE+1ZoPQ!0B%SRIsf2(yj$a>nKz5G_wq`rk@09_CVJE)nUj~ zj!|$OhQR)5*ghC5LX*=YuGAmlq#$9ekk=rMx;7pLO+nAJD2c3UAosN8XBYMwdPMj> z%!m2Bf_Ml|O50A>EQ9zG4Pb$8(PJ2Q56sr>#(sCS8(t%6ZAlgWcJZssu_=1}LD>7F zgn6dAiXiIPaid_io_v9SL12Tc^Jidp=w ziY~$Xn%R)pv)-(j2KhG{5nE+w3=i!MgKJwCqQNx+dWAL;KGUc5C_B3>rl%_8SBCUG zqYKw6;7c6%b+*OIR`JUyyBqtK>J1~NADgh6+}{S)tNPj4)fT@>u^8wW&LMLAE|9k^ zY^y2y2B&3#T-RAO2#jm;BIK`_K#mq?p4u8k>va?&?Rdf<$m5!DM%qCTLb(QpI63wB zXe}7tjBt+pJh^j6hcqsjhG@EnI)t~8W(OhonIW^B8`fncOa$6ZMF|L@RK*XSfKgJB zkU6{rnAlZI2uK{;6bJ#tuE21I3onrcaM|aEmNg+YDM=_tB0lUX`wNMu1%o)JjnCV>^uN-weXl!f@1~f~& z?%oU!Ik3Uw4DgCck7VTIEBOSekSTX_kSwLe8E4v7`h-0*7Q7%y=9?~VP&{|RtT=+( z^b%!9XO3yLINQ`h)l2g4i7yCx-1XEKgty8vyDA45LgJ8|4hjQ9_3j=wazpP{iH=YN zvrU!JzQKYzOIcI!vqXMRE4|Z2n#jtFqq0p;^bcE*Bv)*N`h4u8UH6@|5ZbD`rZ72Q zEM|?a=rHZl(;=; zG^404F#4`}jL!0yktq`C^%v0IvwyCe*P1Ne2T`8sXy0 zZG@SZj}{La(^4p^RPsV#>Wwd09#G|;1bRNyd7hF;N?A%#sdw37RY z?-B!aiKE4tl~!rFs0@^WmwW<&>r(owp8SNvM{ja!yHgVADs^{uezgsSuDwWCDnt?# zW`t6UeKni#TBR$fS$(RKdTucY;~45Ay4~1{h=&8?W2>9A;nMx}>IO)lL6Z!4F7h{X zB;gtXthl%xsjWU?V`)O9G!m*Z3R_7Qd6Vnep!@+rf?_9x)rB@lTvB2Sq;Ui&pOflA zTNKYEY29hG3wrVdy>67E5tzeAne~Ve5s}4*BL|1rzI&vPdlWAvbP`Ik31o3LSqBEg zM418~y3h#3u8ZbL!zC8|TzFMpzd&YFsh>C9Kg4ii12O^a<~wOX@lwl|1_?-!pnv07kyrq>BQpx zf#Os$@Oyeusj*6>R)Dw)+-oU>K0@UA^Ba-A zE%2WAPqi`xMS$3AFfCZ?051i1uxtOrR;as_SG$wwuO{y05Xh4Oc_o zTPn?2A~zT9Uk9d86zdt?DC_|A`zcjFk$2xpyu?!wTd4B2kNC+mpOX|;MiB?H#@52> zzF!-5(lES$-|92Zn2e<}xGt)0>*6k_j_a;xTiggz9#OTrSgE7B+6p5XGI)c$6uDC9 z*;Z)+V0GK#F%ud82(Vm?u-NGS)j9!;S*g-;%dG4`oOo_AjzRh9extfPxQXyjj;*Re z}r>X-;=Az67!Mh5vrP_mMlAnqvwNOz)S zC4faz{6+#^%0ol^ zb7oZr9K^hc;YW`V9zfVtT4(8BRSg3565`RyscRIcAc*@D;A{F6Lr>ygq~;1 zdu_U{JY;tsM8$Aa=hH-CQb6CO?8tYgS==ZqwqTSOt&;bzAVWMo68)FWWh{cqoGEy(9yH1i7spvB)IL= z6vPl!>q7^9RNs(=VtN%^PcES^eu5YrERr224;_#wS4!l)j8GGY&UVk_eOlp=l3CKb z_|g}YF?W>rwmbCd$mASlaDVm-j-t%HjmBM0y7-f4aAXoRi=d+i5!6w%8IZ!e!hkeN zQ$0T;gj6*<8pRyP(&Qd0N(-Cl65O4LmvNy$4fi5e!|FxZ9SA;4PY`%d<^DVfFeG94-b3gV z(0UxGA_&lAUB;2>77kF0Em=}0RxP}cCq%*ZP^l)gg(3QWB>n=*2Qn{$f5Ffh!m312 zfY(4$K?ZCP`023$As_h}#7iiAmpO8Ce_;PyUY>X-Dqucj@z(Q#KD;(XN@33-&O>=4 zOv4YhwFKIcIWl`rtU1!8isN#3F+ySVpw~<)dB@>!V_ayfg4$?0sC1t{GACCEGpR{% z4YBMf@uwas(f{e58$; z!8)+PQ%~{~`g+3$8Bed{C82&}kVy#&O;74J;e5O&eaoR45J!%Z>twNxg;*|x6cN!(c2jQdM8VCzG*QMmL%YgxYufAkEg#;r&gD_qJN zF^BT>23M_chOD!x`*p{~fbE2RPO5GS&nqXjdG9I3Ydsd(hI8W9_Yv5(O9-Oy@a(Rf zMm5Or>xGZvaAXYXa+4&81U1S|`YpQjfD0pf(l{>$cDX}Z*AbCIDUKm23fVoV&W48z zxxn==Kp>OCDtRH1sY+&6A8f>(oKiZ2Efi*@&?PtWxC85f|Bs^!2#LES~S; zdjwEh7`?B6+U_J&e=J2+veo_takaf@$E8<+ZGZXx!soVy;sViI@Ud;Qs@tKJX|yQ= zZ!ZEHFd{otYYbJp70zUYf*AlOo*6Ld4A>T#0sWF+6kqv}-`n0HE|Ly`c7X>!t^+V3 zm6=>5>3FiCSojpTX9m9$WD&is}eN#=y~5&j*#_6A--PQen8x z+kWcMX@X*igW01}#xo^eD?~R^H@e|Mkc=#n=TqI)1?nfbI#Q*-Tb^uk56Y&Wz^R~6 z(2hjOsYO)<;wdB`kALG-tWEm5RpEK+?|FHgwbr0=`C~CJw&Oz6*|$-`pFGEw&YL$~ zQBQP;%MIi6``uXGL-`*rz?7O^*voA^Npw_dzavX&-~8^DP*K?BOTI#pUdinXa}YV^ ziFr{bdjkHHF)(4DoGG@lLy!1vG)4K!<*IDM7DTW{w#mHe`sCH(TXw!RHuU7hFz8 zlzlFM{=`n^>^YX92s%5|baol;ByFhax&4*K{+)0BOln53ngm(m7il=0B=mz(*aFLVBA14W1LaAyS4M)vp28oI`vgRi{dH(d=yk!A z*9ZYYj9@c46*^(>ql67)c{ctRj^0G#lgL9$2ibB>+D9MS1w>nvLT7i)GXn&EIEh|%UFAceLX ziRaDXk1ppwLxj<7{L75swQ9vurH~lNB1yYRUC5ym^7{HyWDh{MQ)Pxr$scv0t_P;9 zL||5ew0Q|~{qt;}lp(m5_)(PIFVHjS!WB1I7tB{+mO0nk_}%vwv4@5@-HSph;KJ0C z_cFUF#2_0?P)>!3E$Q;&4>cW-Ik&h4ALtNfcwEe=n?tJI=)bO7nJO9<6E5x6oL@TH zc*&?YuvpRba~+{jdN;FY!ko%V<^Ai0i)q3pW=R93fKbv}By$o3F<2><_9OFLf<<9= zUDXod2F6+GoZ```oQp8iTO7vAEwbw236L;Gm+fYBkOgM2J<9m zL0VZ+!Bpszz?(tDh)XQZN2wQCo=Aonsqd#CxlNa*AFXX{poyaTu2?$Nf${wmQZ3x5 zfMp8P!=`uecS{>Y?4y0qODmoKH0=jN;4B)|7hi6Y5)teqH0u9uzE`8L$u0+yZkM`CA zq%d@w=~zOPfMTI?0afBzl2-EGz`y4;?;W;#`Tsb3@31EC_760;a3LyMtSC4Tw5V7I zpo}<+ib}1oQdgoyr3w;|A^}2z6LCdFr3jK%tG=z(Xptq5fGklVZBYRkfiNO6k`N#} z&$;g>Xy4yD=eo`xI3U22@4Uxn%yYfj=qONnYU=pva57y{T7&3>rP0i*btXMg#Zmc5 zhStD&;;eaSr-jSxJFeif*hTkTB41RB_7?JpzOG!okN8!X4!_Knad{PnAlgsO?xVUR zm`~cW4LuC6W4_v4dSfNEt&gk&P}uM98Arw@l)9>q`^H$UKVzrM7Pgjhx*3`{$GXD) zw<%bB3PdzkPP|ouJ;60#c`56G- z5F|w6pB|9#p>U|jsbyf8_tVvtb54PE6La(GA$DLD@xq%`9k&on3C9%}*3Z#LXkWn2 zG~TW|s_XJ-o#WdY%H??6@4Er|w46QJyCA^vyjm4?p{fxixKUs`^09UFT*Q`$U%C+1 zyE)}!EPKz_I`{nOO?w&Ep?e|ays%N8b&g)d(?<%Q!y`*$TKQq6^@-}=xtziFW>6~T zHL)>gY`)p>;m+(?%wXuU(6xQSVwL_mZ!na3^kW-7T%eDR#d9BQg^Z&qUfpTzi#&rs z)mmLKC3b@{kE)fj8vKltEqm3D;+Lb26o$LkF;st_mG)1i%I|g3b|`jo~t4-b{ zXH9iSq>QT`hou=oN1XBeg*By)K$fQfNG;Ne8`F~R(Y+4Au}7R_YBO$g3hzK|D1=qEdkt?I%)-TW!em96wrnRJHn-M@i)0iyms&(`5*asz2)a`_}o^V+$ zv|U@kxoWy4du(lOt==0l5+na<_l>Ekp`~jeow&#;JHhZOQ?U&0%6^$HuP&lHmTu^B zRA3HOCmTiW+Q?K{8di)MV{X**WsM?2J0_L4ogOKUh#BhDc7mROHH5aWL|j%!j?&An zZzjYGQY=LW>wXos9itiVFeDuU`C+C$LDm{W{oq};Sgi8r6j-x9B+Kg#Sv-BX;Nk_U z=4CCEU0c+Tm*OeT(pnP>Va2Bj_3~=;Mx=VK<*T0=xK(-XFo_nRJduGr-7nznHDY z2iS%0L0j=D05kysapGv9oBWEiQG#{cU142Ic13ihqtfKnZ`30D5wL>$aN~-ynrC2H zQu3Fj@U%6wSCHU-C3*Q)+4Q_G7A$~CCN~v(3oQ!wW2|uZdc~4f7hH1mmRJBg7EZ?Q80Ka-M=ka77ud{B_J`uWPp*1_WDYv5ZcxkKw5ma4v*MyU(w8iRwk)z9-@=#4YqKuwe2 zuOCvenp6iyO>~266rx%wf-Z32M)YZ`_(M9}g{0N40Ai{$mH^vpU&Dc()2QnCwsaX#2+4i z`grw6c_|9%$@!Zcw8J*wjQi=H)8sQf!>q6?s5D8sDB8`e!w8~dGk*EBO zRqSfOw@K@QlZDLkp7M<$a5m^!&!dH3D7Tkd$CNW3*-qwYG9!9^=I9bAy}PF$zB?x} z*xC4FhZ{SnfXk|l2KWt5nzCx)2$E2?9#T4-Ig&sttg((UN)@stJds8Ny)qCQynOOY zfJFDZFtp=y%aa8|h{-bLe15)-pns-DD+5Qo57tC#>!BfgCfQj2ps!AW65}#j`+i2# ziG~hzWWb30)FaS9CpneEkt1qN^ipU&V$SIRc)b8u8t1&Tw%4_F5!Gm+PJ^L_=xnZe z+~9L-TJLsnmDS$6Ut+9BJ~?Q$U0+8+Z~hBn5r0pTJ(!^qVdo-C0|hTuMV9+T&i2bg z`Zdya7G6#=L6HDSVS@P~sVe-bhd@*am-fS5L$0#ELF@K?dLJaD03#_Ji@Rq`{2k7*ljP+O zQT3pjCmT$tXcJL*Kq)E!G!Nm)fbKuOqUfLT39=#?rb)md#RLx!tDoi{*ek<>W|~6v zVBG3%dm|*e&^h~htTs@u;$B4N4rOt{S^nwU|KCg#b?8xpGDUa}pH+5lR zFOWUBoKNZ$1D0=g^4rPjB*1XbggE3t{O;Aw{npO0F&vCNv& z#rPvrDA_5LaWy=UBUZUZG+nYYBng{-BfPGJOeVQX(4d5$n zZ{%w~Ff_ZnJ)`rvM4ec1d{^!dMSmR`f)@fY$9{e^1k3>05`qRRIib#%R=@lOscGbb zKWNeuf_WKcZxluSYMo;i+qdA$(N-dEc^dX`;kkOtHc}9l^H3|>cODu=_@3_5rX?dh z2}5893Wqau2HC{~9VFUbEB6dQgW(6Q zdN%=28O}!2y8Ae2L)wT7-yD}pCEbwatJ|#fzF`L$#S1p<>Vxu`KTaUJ-_s;h z9op4r=QF5w-wiwfVSG=0X<{4zaKI1MA$x)clWw z4;K1)_xXbm-Ktp%tC?y%7eE2*0cVtDFNP{2?celPXJgwK+9mAv1Pc08>YsUfCxLnq zbhV-{c!vmKz}3(AMhkY2wK3Hi08?klEfK(Ow^N@{ah7;V4D_2)Xd?9-WlJr2FGPk9 z%~?+}CG66pZMlY>x%Pa+Z%-+faKdIfZ;e*Jei{DVd$rJXES&Jl5zcNyk)PMozCCm>yymV z%&0kD-K%Kl0yi;WYuk<({pl@UHPLc-pX0v%bk?68p0B4rv32n3U&Mhbb!$5~cvgxO z&phjic<;Wt_f^dXvEf>l_+@U<1v1+!WkL89&(kUO9~e|XOe=&fn=Pq@(Sqie zO8r{-TSkwB_1atj-ngyyeuk<~XUc-gh0wXeJ7|Q7rSwZP;x84b3>iX$xAx#$G)Ml41<7*9XD5)$4k@d|2fr@pA1YT^rzrj z_!#mmS~H~PJa{Q1KB7Bi(S zd^i@du;LXX?2J1o9v_;jLS!|AD|WlJSQ4GvAeXj%rBs%36r;(K#qHL$C)Iz8qQU^0 zswsd2rduTe1ir84-&M_N!cM=WyDgs7&Nxj!KgyieeJ2?3%)7?_b1D7!>k>8rU=h5~ z55)M=ozfoOzJE>7@jmd5Y}tX`rN7HYj97OS)Yvbb8NPDHjNza{Z@+%d@Y#39f{Htk zfu@5}J&8p+br4|6Aep~mVZ!xw??BFb$IQc^hQHzO11S#M3iA(5`qmOW+V?FP{rlU% zx1jPpX;R7MHw!L(L@y02ve>bN^@T0XY)D=809FYT4iAMT5vj{$ju0r#iSxROsd5*Y zp*~lZlRLw?|9#-Ev)rPMmxR&!dgyn0r3Tc6jw8pep03)Ivh4AQ{OH`mmx>%t@8X5! zI`(UBPlHU~ll#ue>ty+bi}hq{Ge)nKc_mSa4tL7Gm#F`Mp(I20odOk?DcMp1pogu& zf+jaI(ieMm2jg*laa4{sh@-MBac4uOsklJz$;d5nqMp2OY+m9kV}BYxKkf3#?3J;{UbP$pc%1oJ z)cqe0&85wte?NTv5TFEt%sc#ZB{V^21Hc7JZfn=h07dyVV~;O~7lL;>%*+sITAvv{ zWcKmNDC{18>C87^k9FzN>eVn?!)?r_PmhPbb%q&8+}mFUfqD=&q6|xk&wfU6If#<% zzgV~o3-X_~sKCK2=_IyppBB24PYxzgez~ydU{dwn{a0Kv4uV$x^9|#l zZ`k3GwC>$Lhj~`DPc{TqE_}T4z<0^7ik82tb~wC}Y?9>02vMNPaPK2M9ZFzEeiU}3>s8&*Nh9@6}_0`e&QT1du(&y_yio6vh9{1hg8Se;>sR-+UR~;C%=LEVOtb>=4rUvLbfB^xh^@9Td2=v2}fl^R`q)LfjeKX2ge97ml%nyrQUQf+Ks9D zvLjk62zaaN-R)hqmG-ZT7)5`SKf{>5h*(GOi)0jwh!4$7tmimCcM`lR$1>{`^ZFl- z>!wmO8l{{@0~tAy#c#Ty{^qV~PcSFD@6o@xs^=YFyP%=B&#rK$ZPX2^v_tUzL1daM zbF{G6#<$U#=-vd_G_b8dfju-5OY{|AIh&?w4PG%`6^$b#7q)@eODY5u$wygi`u0Gx>aJ0BgM)R1Rz#|B) z2h=vs4hIb(nU}J;;gY4g+OGL$%Vouc{mx_%E4O)A+qB55-vamG?H&`q-;|HRMJYZzP;yu^q=d|+qm1@umk$2#zYB8YgJeEty>k46CI8oJrY*uJ zqq#w*JHjF$fdBhX zvBsnq)CMN}wF7*;KK&4wiixJFFdsX}q>r{{Nc%v=9T)lAMY#d`y6->f-#mHAbq`_Y4>T0`s@arSO>2q6<4CB z?+E<;t-1aM--w1%JuT~wW@KjfzYEOLv?Y_CUeHP2T@?y7YJOi7Bfl%6ZiQI2dI&fT zazxo-QiQni)gzrn)~s$RDFZR^z9K$qFOrFpaQVo)2QBiWJmvQ|+JN(w&n;Z*@;8D- zaa)^%du=UgkQ7pAQO9hyKvEZMeuEgUE6q+8Qb#PK->*`a*%`*24xP3rNz_(%wDvH< z9!Rp0kD$&;p2X{)lUwPY3r->f!zLVXWir2Xass)As)Jl+E`kFq#;SjQ7ik^_Zlykn z^P%tPnG`0iI1(hCX!N+VkBz!}a_bN`MdUH?*QZDNYdBu#EBR%VXnXuE+EYReFrHKh)xLyHkyU& zE|J2&6uRs8jVdj|v_Zp!%@b;r)aEtV?mI8krcs-=(wft;TOLWMV(d&a1lxqOG=z+LE&;4csy0vDgcN=- z<~B*Se44nC13R1Sk@P)VGbCwU{4*sgK9pYcypF)xg`)>cD|k}$+Ir|NlcYw=_JaZ5 z4t}<(L6-$kD=?sPcdtdmTcqRe7zqI7MFE?_fE#tBKD-L4j6p#`KxX%VeXMcOrJ=ZTL)XZq7nSr05H>0bPl^@fEHmaK>BMvOQNSv~Mk2>4>4 zbm=NmTOY!6ftgkd+MsT8Ivv^qHgSL!nz~_&KY-~FCaPD5!ri{R^w%wpxP7y_;p5{r zCfIfgP|vt8_;6odS3%EOKJS`;=~3oSbmKKTF!&6vK%F9XA$o$uJUQj+Un=+7_5H;X zMX=mIE&p>LwOp?b{{`zmQ3WekEW2DJe9Q8Zf>jV{7u5Lv{h8?QyhLD!BqPfeHo>CN zgA3h`81<0|@PRN&7L87tQzIJI%sX`k_@vD^s3cbyyqc%f*8B|RIwBl_cpiqg0rJ6O zrKiWAz65CVtmE~!=Mp-BB~Sj?u5fTnc@@VK;eBekrC|^D@I!>ox?9eYiL|~qA2U31 zXt4zbLu`JDJX52l-bNabU!_ZPp*_YFSt0fdGTW~ zZmVSET|!gRDL~4>&fYFgA&|7| z!&AJ%fj5}_fm%Vb)G`>#w{j=V@<4*a3GA*@yw!__%S^4l~rEjO^?(@{PhIQ9_u^=Xt97(%s<6zB@u( zu2EV~I&BtvqHq<+7|ex=`=jja0-)L<%F*oGxt{@aa&*;h&Q!$U!|wkMl%L)R%fT!i zb^=WTiaNTOerb$Z;Q~1nHMXWKi@YN3A0^bk1Z3KYs_2+h9(u+i>?!|$JmxM_RI(NX zjaCmK-T*cQ+0f7MQ7OQg$p#O!Y~TWb_inTsps$q3rx;BE zD`u#!0%=Z|YV%N-3dp@hJW=+CHDLhV-kZELID2U9-bv~cRQ;-pQ)`=Gsy#B5*7N0f z7v=UiORir%q}h$E^y9SWJ^xtSHQnHA0m4jJAxs>}igBL3KU1J(8=~@5g#cnM ztg1dDtm)ZM(58Sh{&8leH~7uft+PXpGjEtw%vKlX>#IPsyx z=3(iNNP;T`stfwfm$SV!YKt>Yl=B4>b)v!*4xuA90f*(PYmQZ}jPg!@TS!eB`MbN)M%j$o04tRpDy~AtH#w z)HjnR;(`vhu(rh*4FN3>u1?-vAXf6DvlFAYgJ1v_>^dAQXmFD{sfF+JK!qq$q>7{(XJwc%VM&#Y z>Zt|(LhfHSxgc-yF(CXDSCAbV+5)q&3llu9%W{d!_$>1t^ixvVmQ}SJ6}mt zJVFPp+d$&v3AwsdX$hFBG=UViY2`-OP*4U6gKY{ZPrAGZG(mz62lvbq_jdvNCztB{ z9PWF0KM?KfLJIpls2I2S8hd?7eV$Lz1!v+wXLKVOszT*xTmj(0jt!d;sPS zv~&(<)|VhA8E)#ZCdnEKP&M7%-LHOgsR+1l5T-fe2g!v4r0}PK%593mff9G(0@A$W z6T`gT4{qRDx4i^hA8515&k2E5f5A`M`|B3`{p64lUkoHa%a4=z!#v7qu*%eP-iA*_ zRx`mjDD%gFly-*p4{!9q52fF4hR)L0(Hpy zsa>%7Xp-u4it*0I@8%}@Aaoz0{ur8o^v!+-$1WBWpDI$8ZUlirpTz#Gu6}p?P!Beq z@ppH}fH$l4dUAG?wxg!Hx{-5&N3`=K7&oh5@HIQU`fqz(;arG_5vf{oz~!9G%Q4e5 zZv1%qW4930DzWYd=QYHfA8-7ioqH`?;S39S{H>f?V`pfP$(J$YUwQN$^li2gwB6Sv>Lt#G zg?7pjyyuqiy^4j9iu^N$4g*t|??Ps+QD&pBKu}HmvBRk1Yz{Z^$*?XHL`$y%q!Oe#kC5HK-5U0Yx4$lXgAU+aMUv=iXI*^(q zy}8BI?ohZJl|X@O`q?sOI4($-6=EH`#azgv>-xlv_l4aZYNT6f1&%Xj&w78zyDA)@ zF}WjnKno|^UZ&&2kTXzrqO6;3n&si(vF>Nk0bSa(&DtGkeOqY|Y&iEkxktTu&#Uhs zj9LtChjkdTVHj}ro9bF|b82*VtIXJBr_SN1%^^qypLpA%Rkl5Q5U>l!9$`hE{0zC4 zZw@4*K6+Sp=Tgch-qLcErNdz05*rQTG;oq$WvkzVJoPX`c-5Bx$l-yT zllIO(3%+t-EXI6HV_}-0Xov3-HLj4}nF1Pp_o#+;yy#jXJ;RSX_pb%=^GP)0pi7b@KixcK_s-KFVOv_b%qKVBPRd^WUOptB{F(uA5E zVRLoG7)uy`h$HQu^;PZm7OVc*JOLl499L*w$P6ey326_meY(9^r~Dc*3T-OFa)q`Z3tB)~ z$Mt`1IDjK6;k1QF8b3%u08drJ%gWULA0YX@r|jLyD|i6noSL8#*k}1Kb9;R*Cy{(7 z{FNxw0*oV0F_u|o+G*ATfy2SY#pP2K&FJ^h@YYo2gkT16<;FOGgY66!B;ki&WUHS9 z47c0bK{O2&k9>hY0?2IM3%FlMAU0GOQm}~9yOYsc23d$N`@@!{p?Wv)s4ir-K;a2a z84`CeaH|A<992_c@8lY&brBoVYR&`ErvOR!aMR&oiHD>{Dc$~HTV0H*ehQU+9&vm> zzkSht$7fe=$YqEkK?urty}6VdYJgJ&E?|i4KWzeP;fvq=*}zOcvN8=qEVK>C`_1UcO!8yS zf=L21XRC3blTIN!4&T9#cUYA4?&RSeKG1D|^ykW%vEx?EhF%o917l7CqM_c z00U{$K)JYJF9jd%1eij@8^Zs7iI&Rt`1VX+G`UA(-vyKgq0=?!QWBT~8$l?QJ8&E< zHPx3L$o;~3C?ak-x9K&bSZ@+JbBL-3Zv$yP6KP2{!BJx~J}(^?@b|mbn=ZVa{9xmT z%GH5Z%?lQ6oH@^?Icnbxhj)wN--S`-k8CHEEGb-Y<-z70=&y_i`|i!qV_84v>W(>g z7Iut;fb_xD*{{Ee-!c2nH-TTTzjO53jxX?&uV-J~5kLItjFqO5!)X~eZaDM;DIY&E zz+A)kF(V#)&CQ;}$|3u7UxJzRFw(yqlB`!nU|hcLz~4IxvHP@m34ZFA{e>`A(8}89 z0NOC{UHfjlf@e-yci=ml*te70?a5ULQVHOC4&Mvw@^1Q92L}BKCh2D1*F3sl0iT2} z;SiIpxhvK&ZGZ!7xVWq4d3*SY4%Q}5wvn`zHU1Ct-{PtrANmdv4!Yj!QK&^s%QgaRz zySiOBT7luZoO8k6R)0fQnN8XxXKkm-h zaaU*0UcLI~tH;UL{IDkIhZR?+O;|k#JejsBWnVhqJwt?&0A!&7uAkqC#~*{o!=~a! z5jj#BeNp9!oHu?-cgSK`FC5u}Ncs~oq@kdSUIsT#7l?v*#iKfOvkAmu!KX&Roupl1X<;=y~A9@gnGkCJc$qS}9ow2P^)u%VEU2SO5wPSqYbf7S; zdic84!>8x3U3q-=v9aUVt{il2=+7%x!=b-A7S84oGX|NB_b{xh_2`OEp5nee%h*SH{hs*;N}rQV2&|x{|*N1 z>nxxkijHvBx^X!#p=GRjtQ^A>HHzceMd`TN}DpLg$j6|=3&Ys~3@1=<-&7I5)n6u=Bd zTrJE$i0MHR1KHKCK7L}-uDZPE=OoDa#e~75Iips6U#PK3Rh$oy*9FIvEc$+4WKCk^ z=K=#L>A*ytlT<(Ur;y|>Nt2~ixKgYlW#8?zE9{<=N-8xxl?K$m6gR~NPnOtHKTT$Q zD0^1QYVm%Qmsiet@~TDBy#_SrT7WePqGznv-Yhr5>Ovu?__=^i0;tDz%e3=Zr4cc% zRK*r*d5*9nN6tBM8TK+v*cg-*ojee}fJTHmoDC6X!kOjMR~}n=a`o$t>j!QF5*!0j z`ihaTlK8FB&)Nad}wU1(5N#sr7KJ zt!_h+udEMi(x5CW>)f8IeSHtGBOoAF$&8C&`(X_Z#P>d{&3y^vZk~PwFW(tzV8H*@ z<>-s)VAl-_huJrw@D})RX`undE-3S9x%qeUJTdvQ2F(gkCIB@z4{{p+hH?q?j-c)w zZ)eyGjoo|73*w%`ED+&$7NVd*73+gL@)d=RH59DRp9Eca7vRcWLof!qr24dX^CE zDC;6rMN6vs!5qoo->KT`vkewUMK1jnuG^$P@BZTbhW-9NK~qOb zN@UN$mXs@7nfWNXeY_AP2@2qg)E7ph88Ow~EClN7pqs4y4MifpAT>h+*cH;AVALV3 zvF1!?D7P_+g>M{VKHNiv&t_OOwnUL5Fsv28G}$Sf$b|%rS?tFeYMci*2&piO)!-TN zMEu*r8yy+^=Iom=s3?Th3St9sKpatzBBjKMu9S7ZP9=BBMet*q2IeX8rsRSMZQjFH z-s@-T>2v3-nX+sBL#?p63GAfbG%(Lm^=}5x-~IjTce7_@DGIK|30M1`%u@BogCp&j z`Ad#EFAnA$^3U6#&2~F_>ruEz^}9J!sN3CQR)HzoGM9+s_h~;+4U4GI$&yC&fWX^L ztjeI!d`p+O|8O>_qf(eyV>p)wvY_d7!otPXMSYH{nQ0#?JaJ+$F395VDoSH|y_G0_ z0c}YGyUCgpUz4Z$bMn$5~O z?2Nn>Wr;g-`vVo{oK=;%A{}R%9kGu042i?Q>m!fcxL?V)^VVBpV#IjYzx#h*%>y& zkdPt=UUGw&i$TvZ$S#SCeB)Dovxty5n=zlCuqz^IQ+PhYHD>!>$~Ibq9SZ$sKQ@>G zWg}NtDPGp1;wWMtbRD-)fgj%+V(QM)1I^u6vhhqQ9EIJ0{bmPd1x;5bOQ|6huv6DVIhyb}cxt#+!mi)H@FN$x@XQ3RHb}e4)_jcO=8UF zC{U$1Men)85~*US;{I}Rk-x2BHY(M483(i&hBmH%b{duw5jt{%ROB&dtG(6N8S1E` zM>EW-#o_?yMoXn&4KV?$_@L;@$tQ+p#D9a8A!FL`+O=@WTbV9-qA*n-5y`Va7#&zCD%vVK2>A%~){U7p%Rb9}6*xC5&bH_9;HQ9 z5UwP&Cy}M`TQGVsg_KiGs=OEZGLsp$Bk<}s6!i`0Vz5DyJEh}c-gUm_H-6(px)FrF z=;}3I5`VAjbmJR(b019=UJb>3c5SUYDc7a>HNEFJ)Vsd{p9?^Zij27;^F}xqSRJmM zna;Yuoh|Y)byaNV>9Sd{7{@#MrZ%$a`u+4V%U8=$DTO5|<;=x}V%fU{j#6?_;(zJN zt@HvX0bntdLissXQ7LP#P=8-o(gEk>f=x4{qjesdGT$cDp^2<(<^e04_7aPEq(#Ln zx?1D$=Yorrx!jA)U^q5F`#$D#hQfvMk_^G$K=4k{D}We@f*fD@xqBJ?y>4K+bjm!g zy2w|vcs?`_F3@#L>KFi%xNyM&lqB*Bt$4lrXg|vIAf~MpzCQ;=blWm1`)$AxXw(`d zeB)WJ;iT_(3!wxmLcC)bU zj{{O+&XUK;Ukc@6deFHUjJoB6@_htF=agOIp7NBXF_UY_t{|^3n^&46tcq6pP>D4C zpIrTHPR~8g$b3?I;uKG{^!1LV%a%zHx~A#jYY%xfo}iwzGai$z8XxEFjcxSfxka!i zggr2vZWrNtB97iUO2#Vdh8+D4PfrsKC8GlO|Ev#za}#xCiPe+Em{kS8uh@z=+22e?8nS(zo& zII;VzIjL1KH>=vVP@5M;X8W-xV$_JOZ$bM`!NgdUfZdu9KlTiW;Exn*acze#%fgte;@}K2X@> zq(`q*sbL6Yz1vvxJI#@KbYFGYH3CoI69TtNlC3p z#MNmZnl~o0HGZam=+diwd9;Ki@MwaAuACIdMYAvcYp!87sr!YK|1+g6dUb&&19lD&il9!Xj8%K^s^l|r3`5Vk-TOd z1mA99+QpPBOw=n9bl(?V!G4T^s$K;>@bR%^6WvGne$>Cy2Z8J;sm}-=V*Nks$&42y zL^m7&XqmF+y!F25KrF5i)~kOtMa3(h0Vo#2ITM$T`035&oNl&5y{_D>j<=UxlBD-|@qvs^rot~pIg+1NW?}h3O|+Tjv7cc$(9pZF;YVn<&9pU! z%=q5NYfVZ6+=3Hcb?1((rC~J zr8MHP7s~VB|1fBm>G!W<>F_9QHTQJ9Ye>#bQgIm z@KX2q^S`SfQq*a?$KI=n;({xdPL(F!-(5 zs;jNJ`}QJsYpCAuVhixvG&OXHFE^Mp`9BjKcdQ#_L%RpL)OTCpf~afo>Lg+%7HrF^49GTNJNcP^*vE5->` z=gjrxcJpd#H$IG_ahXLkDJDw_lxifyAUsE*0><&)5eWM$Z#ufzVX}l$=WIhF=Q0C^ zxXnV`*cbW6FLI)neiij3xs9>on+d6dZ7hhvW{CqJR+Vq1E5q>&0+uY7Q??}F;a&!Z zmm|xLGk^NSK`zPz1MpID4Ae07O?Jcx3fc30Dd_c4l73tDuB@3Ekw1ym%T6suAQI6>jv3}Hlq9v|U^bre!Wta7me1nsYx zN1;>=g`@G~6c4`8{G9X{7$eg^O!17ksA7v$KXoj_lpJ~xfEJnGpc+j5(Gf0vllTyV z_vi%NOql<8P%-YIpj^IC1g?Hf_d%kS;ug_81)Mwc>H0TxkfqWzj4sSaz1mPzGFIjY z;#Yx7c~P0QN`k^nz1;UB}QwKxCd`gm>^(mb}1=kY1rg-9)@<` zfEy0B?*T#zHuI$?{BO89@Nih zi6jD>Oztp0Lo>6ULNSpCqW4q(NXsh>a%m>DSMi!i<>eV-C|<1zUdOPPD8w2BrI9or zX_*Q1N}6#${W__^G{ToTgRXc(F9g#h1P&XuK;{D^3G_=zIT7?^GGm@|jo!ir)3KB2 zO4x|m?&q2HUmYMNW64rJ0z0QRXm)Ep@O4oT5^VBn-a z?uZ2a^BC;oDj((mQkzxp7c+~rN%F~i?bGv6rl945O%#=<_?7nzi#yOlrpV++8Kn!( zv$O#*E_Se03u*;@JC%3ny+eQZbHaH*+$f1BbzmsAdGuW7f()XZ3((bv;4FfePEvl5 z+;c}|y%ms|=Y0k>WHP8mjT(f^>=Y_GBCA)~y}*Vubkj?5mQWE_@uRmV35*66a1h2S z^-M)H1WTG3({l*Q%7>QAv=zfvcT&pFn>_a@NF@2^>#>h;MTjcd@WB0f67vXlrNg+*v z_Tb$C8$3ST3GmSF|7?o;=evet)5Tn%$^mNzQ2GIQEWocQBnk&XH4^9kZz<3ep~cfR zkcE&ZvJ5N%Kp(vS3;ruKU;;fo&1MK?7mW7!0D}!=hZyEwuv3EYyWJ$6_&hqdHU~&+ z)P5^!0bcKylgN3{v@uFxT60K05(+n}Xl8ZYJ4q|Of4`afvYnpo1(r59ghp^w>4zZD zq!h>pLpDxP}F}~YM0{^;Yb079+?)JlU&2M z=zBMaR8EBz#9n0A_t;M4$1bk|EhNqO+)f|iVpVizL}WmQi>u}rU)>L+=wc$+&mFko zfH1fQnRbkqPAQ1xX_I(Oh|_PS_3zib=juanuYVTntT`@{lGhcu;t2q_YPOD))m#Or zxk$c>837x{A|K8Wolu&i#+1eQo@@LO=NcMcH0><<6;fgSWYw1Oco9Ykj`myn9BE#p5-bQevX#Oc<*+LTf zLJTE+K)4#UZ;O>zw?8v3GuqwQ1BD=!A=a`5WW1f252{!e#yq1wldEoGfEcQ<-G!k@ zb-y};a&gQns=fxy7jIfnyg>e?1q`W37%KJ&W;F0bf#MT0rZ*|f;TU( zf^H}mQ^o=tkAn6`6X}YH>R%w)LNxrYJ^&rFP^)r3-$x9qJnLDd?*t}{p%Wv=ag0FsRQ>Xs(qNB0zEJs~AQAqgc zI#ZLGy)VnN+Y6?qtqGZkAX|z@KbR&Rv+aBCEl;5K&(zn@^+Q1nRR~c1hT?duQb<{t z7Bs^f+O=;+618B@$8AK-cQPpl2RRNGu-50S94~@eyJ9kFk_a%mD4Kie(Q0Zncm)Mm}E!vF{Q+JuPM~v!<$gkBZgICql{; zTdsKvJ5z?wPi73UjP5+YrE2RYIz3WCxA17v*0xojP+8On`0^|_Jh7x)!m~l`!M1v^9^GZFm@!0Z8_+5N3Ya7DtWn{S6x@zFwQ$t8C#*YGo7}1ml4(w3s=Pyy0$H$srpb4br5JR_e`(_ zeN~caNt=McSd_KK5G?xx)i)5O%KCUtJ6COfWP?B{lYSDf(UJY(9us|J8}u8+C!Mu= z5poo~z<}c-)u5zG<2a2vFxz86Yj{fMETQ&x^sb04t?3O%M#XdfD%tkPW9`GI-(~N@ zdmPn(0OE|Arm1}OJDVxpON<{>+^ipdR@5Du=<1c#^93WT*hXL&gOxR3W9>v? zkH1de&ae$NHpCN_I|9Ly0B{4jwb24sQc0F!=$FYK0a+1z0x^}<5jbU)%^|kX#0`uAe<#z3z^;SY%n7K#W{c_h!UU5}LUKO18@1-)9!BSF@KIG4R_o>5-!s#>|7_U9oNUk^ zvp21rMc~^&4)v?Arr+HQV6Mw1Fqgr`V4!+j6dCJ8&Bfrar&n|JW61~u5L^_Df7^NL zFA-{POsk90J&#JtZK&=3`YbcJ=G7KsaPc|e-^Zm#d)|%A*F<}7+Pzav1QUCBT@Mad z+;kTa)`nem`bQTz58lNm+r15g^7v`Evo%TKSIVrL^wT>Pki7@FomX8_CIbc zZ!k=3&TM)tA%w5rKLXDMKftfN;(96F8m&?&4vU`9+WgQIY)Urd5ix90nM$%)EURo zKsU~_b1!zH?$C`B$pG~27339@&<=3I4#@84&H+1R{@ckUklLuMcDQxv@IS``Z3xs7 zSdf<5<3UWORoHBRh>&`MTo%}3(D3Y&Bi~Vfk1X=3h+JW-|95I(Udh&wCdttCBe3fM zrpxZ^-=Oo8S7sxu9K&TQfP%2M#gFp?m#XVpbyVbvq20^$s$F2MRhBG207Vm^ml;1m z6$grsO2B_euzgE1YA~XJWBFd_q=D62y6M)0cd%MM1w%u9S$`)$f@$~{imczXG{&l= zR$iJdHF-~7at(4zbbACSoF>}Fy28b_?@Ac$QexD#q4)R?b`7UmJFs#4gr$o&l46^1s3o6SnE`Oifpro!9Wp5nFCto` zlH3$4rjqsg2E}gi1vJzm#oBS}ac+gidJA5s#0jcF<-UOXAu`>yrsWIZq1OB4Ky?oU zTh2eY37&fU3vBY5U7U?)!@32BTdLSB9#zvfOWK`8E^NF3$tu;$yjL$nIu@lCAP*7R z*WcrrgbRnPfkM9qAP=@lCV4%MZNRPJcYpn7)M>F|J=o2om8`miZII@KcQgL{`K}luvIgu#6g;1joCEhvjgxB#1T+zY&_d z2Un63C&aF^`AhL>O$|vAiI`FTWk)EW!OZp)Dx-y^zJ^|r94?U)CQsMO^3UL|3IfpX z9{`l7NcAke=^VqCV*P0F~FvD+$plfudH6T8aQeH9L3psy|aD> zPy0EqEEC+Yl>SuB7BGgCc(dg?vTMAR$VB_mO0YRG>W6N+xeOz-id}T5pVhTkH0+HF zWPDK9Qn&YXPz55$A{fwTk7)8l2qK|jYN54UI z&Rv*XZd!^!p}rXg0X+1UD;IAj&sC!msxk%t5vz%Mt)Ym)f*5TI)>>a7U|VsQp)UbJ ze%1oeF9|p5mu5r&_2BZu4&V-zD7KtRj4mHqRsT0QZ$4@%;|SXt+#P8ju33BZ79Z%G zLax2Pf$qldJw#;C>j|G z<&I^PlbWCbGwrENS>;Mfl+i4$P!UPl4Kr>sMbuHrNKvMh%UDS`Zc;=B24p89OldMA z$N&S3iZDB2W|+g!8iCkgvN!G=G&!2a(BUm zC0!7IKyaA|t=8&uIg4`_CES$C*v67v=BIQ$DN8FHo*2HWBSd1}B6QSEnc1wNjpa)c zU&IYm#a-)GHpIq5o#u86sLS-x%q*rqkXmtFowpBX`|FQxM+;tg<2T&>9_@o}t>ag< z^ZA&2<3$HV1BXOyth+|hhwhM+%OZvCWsEW2JXM>o#5p?zk!j9XvVkLVMcr;}#I4PB6N3-QDv0FaMx#^piz8TH_VMU0xW6aq z-GBihB&`*~-;`o1TJc+oslSzS~d#6`R82LJf}Q|VCs zL1g;MSZaWrY0!G$Kw_7o=<;F__j?6Yg%!@-8&gAHWx)_(yDKrN84THie<{ z4%nNK?>tOXD_m7^#s$z1kZBJL+s~#mUmvot|2*;7ZpKAZCWxApPE?F6)Y|qY%Mw$g z+yz&^hPdL$PL*vBy{3A3b#QK^yz3-4f3U&cj6!~O_F~OH=#FSRFTbUam!A0~`Ey!U z*NJJ9T~gy`p>%C)OJcwUT*t-%aw4teBf4>eet9Cyh4L?s{8>aE`}ernf8A_ds)u9} zrbcdMAAmS;EqB+7K!oG-=r{#JKhrJf$%< z2~$fAZl@qo8%$g3AsUBd!#ZvmP&h9A+Hks#UL#GG`P@|nGk4-c`nBQF3%c^tw^v`H z)iri-nO`T(l5w}fX#QiVy;(i*p1UB`tUZ;EuE%?7AjG|VXP)+N7~J63yc~bYyirk1 z>pQq0OsPkMwTli;m3#ZN)FMLlMKvOo^CqmNNtq!jQ-YuLBGDtj4m3o{l6FQRIo*+p z{?5Z439c*YsuAV%+@vqsSOT|)|mtFcbV`_<^s3e2)qW%IzFdH9y+IG@+`RcO; z1=S{9H43x~xCEX_IJ==%hZtqqcg|k^MB|=7Z@OMyw6C0WVmDpHVF2g%->SxGQ^~IF zWOs!G43RCiPLb?(;WI0E}SIP~=Jpa?_Op{u9Cj)y#$#jZPf_qel=!(Y}n?MSm z$B);S!jCg}i{cbwQ&!r1G7K}N3X?TS%L-Gc&&-L%AgBv3FBDyDOn@_re6eCdoHfL| zUYHc99=BeSR4sD192DC3PAd?8*6~@)y0Y#5jdP;$6!pn_uHCyq%Epn{wk9!;#Y+px z69H$<(aw#Pq!!x%Jkft#rR%<3`i1Tac6H{Cc57MeFkGzbK~oVOwI&rLc5=Jp*ZJE& zNbq-lpELLf>X0o(_aW@>C`V>ubNDv~w&vTHb=)U)I_UD^oPT)8#6M5q_dkw%<92rMBeu2ye++b;D*fvADxu=Cc|CFcS&xhdYVof(ZcC>7YR}ofSpH z;x6}jKAD5*2>R6L#P!=K`vwbMw+I?YZuI1BYS%C74&VaZ{KGG7+o`&>C(pfU=22B? ze|<2B7e45Q4ruebE6e>ub(^%Sv!jyBM>W)T6iC*~wD!jE^{M77FCC!=p&tqH--jgY zLfR4hHx*`zTU|t?qQ{a+{OCd%pa2L33-crwcH#YD>*o&$+@gHNCwvQOLZ1GU>M|sf(6J72bPxlC;lrf!n6ZYTnchMfv5U(f%#x zZasmwD6AzLK90n2+Cc+APl&&FecI;4M9jswQ55iWQel1$!xJ3rd%ifb>(Qh4q~Dt_ zF7o-))HxvRA~lrs4PDa%`kxKJ(H_o*)F=cnp1?OWqZK2t zMMrP9SMB@?x*WbeTexha{ze47zOaX9!kenS=%v87yeh!*IVtw)v;*)Pd>yzE8QxI+ zMM7-FRxg!!C#3=K_Ku~LhLhZ|3-AADABBY5s#Wr+|M8B0hq4Ok%>Y&pPXcLbBFoZAi znHuCjD$1tw4KSawMXNu(@Y{E@-=fz-u8KZs!(Z`b2)kLMN~mZo!oMwm&O9vSPSO;D zTztq$uRV!AhPdi*caY}JGJCghsX0(3xe^X#a@S}2Fx!DS!57f~b47&0wyv;VQk3H` zw3iFrtElDWlVh6-B~=S!Dc&l-cwZFvrO%^^tvdL`xTUdwJwqqC9cK?yDda0XfoW0` zZNTNnU&n0;D}|dV{4l8y&oY{OXqfRa)ld|=z8K0kJ?`c2*ywet@XX&&($oAlY2U8~ zsm4~z0!Zg#O&-WsQ&Q<{Ftc7(xNg&c^Ir{DQA>c;SIQ38kJpadvG%WL2&mM5o=Xg& zqpH1f4SZ&(F_9Uq%PBIn*-Spm`nbiK4A1bh`i}F5(7m#wb!SeLD%g86 zRl|BkBZdk86%6Uiri>+8LME@)h z;-fk1Q{Ju?Pr-+7XfLd~5m3;D|En$O8WFe%!Q|~Lz0>}driH#m>dunZ&^?!$-C^@E zw=|7sNKEO+kLb``@}Be$4p$+g;ZMbs>Ng*5b>-pi&_zdQ(ksITgNH~c&B2+8I`G6U zd^ES_zfUc`YFFjKKsSQubCg|`qLpQTZn$d+sw6rf$v-<^;(Q8R2?S^j_71{^yoTt#KVM++ z-*JYjYJAh~WtlRBjvE+&*tU|`63Uo@1Qb6)0iv-A3PYf|rMItdiiw~e06!MnmcYma|$jWghAw>*Z*;T z2MwQ3NjFscxHs0}ubQ672}$Wu%TT`@PH@#7T0b4%`{l+e-?Urd6MF-*o64e<`dKkO zdqWP#VW?M2NDV&})Ei(8k>K%>k)XzT8yd?Se;~9rbvXluq?A2soj!MBe?AH%>euv1 zODRCr*#i6*HvyalK)X;9fAawoI<)ayEJR;K!}^dW=>@WL7+;|lOMn}!M!%AK*$L1i z^GNoqycSe~DK$(q2)2y;%6&N*O&WGf%Zu4xx!SnPS8lo=cc~_?q~^({!0jprM!o&I zRE)H zAR3@UfKtw2Py}HH+9*GLfx4Mhw9*44^TX=i7bQI(r4!yJnZ*XQawJ@VG?V+h{6!v& z&-?*+Xwps~o-ahmxX7)JcJ~7s1%0D@Q1SONY|!%(U5HtH0INRDxL> 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�bZ->}KT`4=M5Lp%VRn=B6 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 literal 0 HcmV?d00001 diff --git a/packages/foundry/contracts/hooks/rebalancer/Oracle.sol b/packages/foundry/contracts/hooks/rebalancer/Oracle.sol index b479428d..16a8276a 100644 --- a/packages/foundry/contracts/hooks/rebalancer/Oracle.sol +++ b/packages/foundry/contracts/hooks/rebalancer/Oracle.sol @@ -3,16 +3,16 @@ pragma solidity ^0.8.24; import {IOracle,TokenData} from "./interfaces/IOracle.sol"; -abstract contract Oracle is IOracle { - uint24 immutable public baseFee; +contract Oracle is IOracle { + uint256 immutable public baseFee; address public oracle; - event FeeUpdate(address indexed pool, uint24 fee); + event FeeUpdate(address indexed pool, uint256 fee); event PositionUpdate(address indexed pool, TokenData); error UnAuthorized(); - mapping (address => uint24) public dynamicFee; + mapping (address => uint256) public dynamicFee; mapping(address => TokenData[]) public poolTokens; error NotOracle(); @@ -24,28 +24,33 @@ abstract contract Oracle is IOracle { _; } - constructor(uint24 _baseFee, address _oracle) { + constructor(uint256 _baseFee, address _oracle) { baseFee = _baseFee; oracle = _oracle; } - function setFee(address pool, uint24 fee) external override onlyOracle { + function setFee(address pool, uint256 fee) external override { dynamicFee[pool] = fee; emit FeeUpdate(pool, fee); } - function setPoolTokenData( + function setPoolTokensData( address pool, - uint i, - uint256 latestRoundPrice, - uint256 predictedPrice - ) external override onlyOracle { - TokenData[] storage tokensData = poolTokens[pool]; - tokensData[i].latestRoundPrice = latestRoundPrice; - tokensData[i].predictedPrice = predictedPrice; + TokenData[] memory _tokensData + ) external onlyOracle { + + TokenData[] storage poolRebalanceData = poolTokens[pool]; + + if (poolRebalanceData.length > 0) { + delete poolTokens[pool]; + } + + for (uint256 i = 0; i < _tokensData.length; i++) { + poolRebalanceData.push(_tokensData[i]); + } } - function getFee(address pool) external view override returns (uint24) { + function getFee(address pool) external view override returns (uint256) { return dynamicFee[pool]; } diff --git a/packages/foundry/contracts/hooks/rebalancer/Rebalancer.sol b/packages/foundry/contracts/hooks/rebalancer/Rebalancer.sol index f603a96b..2bb8c1fa 100644 --- a/packages/foundry/contracts/hooks/rebalancer/Rebalancer.sol +++ b/packages/foundry/contracts/hooks/rebalancer/Rebalancer.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity ^0.8.24; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; @@ -27,6 +30,7 @@ import { AddLiquidityKind, RemoveLiquidityKind, AddLiquidityParams, + RemoveLiquidityParams, PoolRoleAccounts, AfterSwapParams, SwapKind, @@ -36,8 +40,10 @@ import { import { MinimalRouterWithSwap } from "./MinimalRouterWithSwap.sol"; import { IOracle, TokenData } from "./interfaces/IOracle.sol"; -contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { +contract ReBalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { using FixedPoint for uint256; + using SafeCast for *; + using Address for address payable; ///@dev price feeds give the data in 1e8 precision uint256 constant PRICE_PRECISION = 1e8; @@ -57,9 +63,9 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { ///@dev I could not find a way to get all the LPs of the pool ///@dev I used many variable to cut down the for loops - mapping(address => address[]) liqudityProviders; - mapping(address => mapping(address => uint256[])) amountTokens; - mapping(address => mapping(address => bool)) isStillLp; + mapping(address => address[]) public liquidityProviders; + mapping(address => mapping(address => uint256[])) public amountTokens; + mapping(address => mapping(address => bool)) public isStillLp; ///@dev This is private as only owner of the hook contract can change the address address private weightedPoolFactory; @@ -132,6 +138,15 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { _ensureSelfRouter(router); _; } + ///@dev prevent race conditions when rebalancing + bool private isRebalancing; + + modifier nonReentrantRebalance() { + require(!isRebalancing, "Rebalance in progress"); + isRebalancing = true; + _; + isRebalancing = false; + } /*************************************************************************** Router Functions @@ -153,7 +168,7 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { wethIsEth, userData ); - address[] storage lps = liqudityProviders[pool]; + address[] storage lps = liquidityProviders[pool]; lps.push(msg.sender); uint256[] storage amounts = amountTokens[pool][msg.sender]; for (uint256 i = 0; i < amounts.length; i++) { @@ -208,9 +223,10 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { TokenConfig[] memory, LiquidityManagement calldata ) public override onlyVault returns (bool) { - if (IBasePoolFactory(weightedPoolFactory).isPoolFromFactory(pool)) { - revert OnlyWeightedPoolsAllowed(); - } + ///@dev removing for testing + // if (!IBasePoolFactory(weightedPoolFactory).isPoolFromFactory(pool)) { + // revert OnlyWeightedPoolsAllowed(); + // } emit RebalancerHookRegistered(address(this), pool); @@ -220,7 +236,6 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { /// @inheritdoc IHooks function getHookFlags() public pure override returns (HookFlags memory) { HookFlags memory hookFlags; - hookFlags.enableHookAdjustedAmounts = true; hookFlags.shouldCallBeforeAddLiquidity = true; hookFlags.shouldCallAfterRemoveLiquidity = true; hookFlags.shouldCallComputeDynamicSwapFee = true; @@ -280,9 +295,9 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { address pool, uint256 staticSwapFeePercentage ) public view override onlyVault returns (bool, uint256) { - uint256 dynamicFee = IOracle(oracle).getFee(pool); + // uint256 dynamicFee = IOracle(oracle).getFee(pool); - return (true, dynamicFee); + return (true, staticSwapFeePercentage); } /*************************************************************************** @@ -303,7 +318,15 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { revert OnlyCreatorCanChangeRebalanceData(pool); } - rebalanceData[pool] = _rebalanceData; + RebalanceData[] storage poolRebalanceData = rebalanceData[pool]; + + if (poolRebalanceData.length > 0) { + delete rebalanceData[pool]; + } + + for (uint256 i = 0; i < _rebalanceData.length; i++) { + poolRebalanceData.push(_rebalanceData[i]); + } } function setOracle(address newOracle) external onlyOwner { @@ -313,33 +336,39 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { /*************************************************************************** internal Functions ***************************************************************************/ + event Debug(); + event tokens(uint256[] tokens); function rebalance(address pool, uint256[] memory priceActionRatio) internal { RebalanceData[] memory poolRebalanceData = rebalanceData[pool]; WeightedPoolDynamicData memory poolData = IWeightedPool(pool).getWeightedPoolDynamicData(); uint256[] memory currentBalances = poolData.balancesLiveScaled18; uint256[] memory normalizedWeights = IWeightedPool(pool).getNormalizedWeights(); + emit Debug(); (uint256[] memory tokenDeltas, bool[] memory remove) = _calculateAmounts( poolRebalanceData, currentBalances, priceActionRatio, normalizedWeights ); + emit tokens(tokenDeltas); _changeLiqudity(pool, tokenDeltas, remove, currentBalances, true); } - function isRebalanceRequired(address pool) internal view returns (bool, uint256[] memory) { + function isRebalanceRequired(address pool) internal returns (bool, uint256[] memory) { TokenData[] memory tokenData = IOracle(oracle).getPoolTokensData(pool); - RebalanceData[] memory poolRebalanceData = rebalanceData[pool]; + RebalanceData[] storage poolRebalanceData = rebalanceData[pool]; bool didPriceChange = false; uint256[] memory priceActionRatioArr = new uint256[](tokenData.length); for (uint i = 0; i < tokenData.length; i++) { if (tokenData[i].predictedPrice != 0) { - uint256 priceActionRatio = tokenData[i].predictedPrice.divUp(tokenData[i].latestRoundPrice); - uint256 priceActionRatioScaled = priceActionRatio.mulUp(PRICE_PRECISION); + uint256 priceActionRatio = (tokenData[i].predictedPrice.mulUp(FixedPoint.ONE)).divUp( + tokenData[i].latestRoundPrice + ); if (priceActionRatio > poolRebalanceData[i].minRatio) { didPriceChange = true; - priceActionRatioArr[i] = priceActionRatioScaled; + poolRebalanceData[i].rebalanceRequired = true; + priceActionRatioArr[i] = priceActionRatio; } else { priceActionRatioArr[i] = FixedPoint.ONE; } @@ -348,6 +377,7 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { return (didPriceChange, priceActionRatioArr); } + /** * @param pool The address of pool * @param tokenDeltas The token balance changes @@ -361,8 +391,8 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { bool wethIsEth ) internal { uint256 length = tokenDeltas.length; - uint256[] memory addTokens; - uint256[] memory removeTokens; + uint256[] memory addTokens = new uint256[](length); + uint256[] memory removeTokens = new uint256[](length); address[] memory activeLiquidityProviders = _getActiveLiquidityProviders(pool); ///@dev This is not for production but I take an assumption that every Lp has enough funds to handle @@ -389,14 +419,11 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { _distributeLiquidity(pool, activeLiquidityProviders[0], addTokens, false, wethIsEth); _distributeLiquidity(pool, activeLiquidityProviders[0], removeTokens, true, wethIsEth); } else { - (uint256[][] memory lpOneTokens, uint256[][] memory lpTwoTokens) = _getTwoLpsTokens( - addTokens, - removeTokens - ); - _distributeLiquidity(pool, activeLiquidityProviders[0], lpOneTokens[0], false, wethIsEth); - _distributeLiquidity(pool, activeLiquidityProviders[0], lpOneTokens[1], false, wethIsEth); - _distributeLiquidity(pool, activeLiquidityProviders[1], lpTwoTokens[0], false, wethIsEth); - _distributeLiquidity(pool, activeLiquidityProviders[1], lpOneTokens[1], false, wethIsEth); + (LPTokens memory lpOneTokens, LPTokens memory lpTwoTokens) = _getTwoLpsTokens(addTokens, removeTokens); + _distributeLiquidity(pool, activeLiquidityProviders[0], lpOneTokens.addTokens, false, wethIsEth); + _distributeLiquidity(pool, activeLiquidityProviders[0], lpOneTokens.removeTokens, false, wethIsEth); + _distributeLiquidity(pool, activeLiquidityProviders[1], lpTwoTokens.addTokens, false, wethIsEth); + _distributeLiquidity(pool, activeLiquidityProviders[1], lpOneTokens.removeTokens, false, wethIsEth); } } @@ -418,28 +445,83 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { bool wethIsEth ) internal { if (!remove) { - _addLiquidityProportional( - pool, - liquidityProvider, - liquidityProvider, - tokenAmounts, - MIN_BPT_AMOUNT_OUT, - wethIsEth, - "" + (uint256[] memory amountsIn, , ) = _vault.addLiquidity( + AddLiquidityParams({ + pool: pool, + to: liquidityProvider, + maxAmountsIn: tokenAmounts, + minBptAmountOut: MIN_BPT_AMOUNT_OUT, + kind: AddLiquidityKind.PROPORTIONAL, + userData: "" + }) ); + + // maxAmountsIn length is checked against tokens length at the vault. + IERC20[] memory tokens = _vault.getPoolTokens(pool); + + for (uint256 i = 0; i < tokens.length; ++i) { + IERC20 token = tokens[i]; + uint256 amountIn = amountsIn[i]; + + // There can be only one WETH token in the pool. + if (wethIsEth && address(token) == address(_weth)) { + if (address(this).balance < amountIn) { + revert InsufficientEth(); + } + + _weth.deposit{ value: amountIn }(); + _weth.transfer(address(_vault), amountIn); + _vault.settle(_weth, amountIn); + } else { + // Any value over MAX_UINT128 would revert above in `addLiquidity`, so this SafeCast shouldn't be + // necessary. Done out of an abundance of caution. + _permit2.transferFrom(liquidityProvider, address(_vault), amountIn.toUint160(), address(token)); + _vault.settle(token, amountIn); + } + } + + // Send remaining ETH to the user. + _returnEth(liquidityProvider); } else { - _removeLiquidityProportional( - pool, - liquidityProvider, - liquidityProvider, - MAX_BPT_AMOUNT_IN, - tokenAmounts, - wethIsEth, - "" - ); + (,uint256[] memory amountsOut, ) = _vault.removeLiquidity( + RemoveLiquidityParams({ + pool: pool, + from: liquidityProvider, + maxBptAmountIn: MAX_BPT_AMOUNT_IN, + minAmountsOut: tokenAmounts, + kind: RemoveLiquidityKind.PROPORTIONAL, + userData: "" + }) + ); + + // minAmountsOut length is checked against tokens length at the vault. + IERC20[] memory tokens = _vault.getPoolTokens(pool); + + uint256 ethAmountOut = 0; + for (uint256 i = 0; i < tokens.length; ++i) { + uint256 amountOut = amountsOut[i]; + IERC20 token = tokens[i]; + + // There can be only one WETH token in the pool. + if (wethIsEth && address(token) == address(_weth)) { + // Send WETH here and unwrap to native ETH. + _vault.sendTo(_weth, address(this), amountOut); + _weth.withdraw(amountOut); + ethAmountOut = amountOut; + } else { + // Transfer the token to the receiver (amountOut). + _vault.sendTo(token, liquidityProvider, amountOut); + } + } + + if (ethAmountOut > 0) { + // Send ETH to receiver. + payable(liquidityProvider).sendValue(ethAmountOut); + } } } + /** * This function calculates the newTokenBalances based on the currentTokenBalances and priceRations * @param poolRebalanceData The rebalancing config for pool @@ -455,20 +537,23 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { uint256[] memory currentLiveTokenBalancesScaled18, uint256[] memory priceActionRatio, uint256[] memory normalizedWeights - ) internal pure returns (uint256[] memory tokenDeltas, bool[] memory remove) { + ) internal returns (uint256[] memory, bool[] memory) { uint256 length = poolRebalanceData.length; + uint256[] memory tokenDeltas = new uint256[](length); + bool[] memory remove = new bool[](length); for (uint256 i = 0; i < length; i++) { if (poolRebalanceData[i].rebalanceRequired) { uint256 innerProduct = FixedPoint.ONE; for (uint256 j = 0; j < length; j++) { - uint256 exponent = normalizedWeights[j].divUp(length.mulUp(FixedPoint.ONE)); // This definetly less than 1 + uint256 exponent = normalizedWeights[j].divUp(length * FixedPoint.ONE); uint256 power = priceActionRatio[j].powUp(exponent); innerProduct = innerProduct.mulUp(power); // This is in 1e18 } // This is now not in scale // As 5e18/e16 => 5e2 uint256 balanceFactor = currentLiveTokenBalancesScaled18[i].divUp(priceActionRatio[i]); + // This is again in scale uint256 newBalance = balanceFactor.mulUp(innerProduct); remove[i] = newBalance < currentLiveTokenBalancesScaled18[i] ? true : false; @@ -478,6 +563,7 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { remove[i] = false; } } + return (tokenDeltas, remove); } function _ensureSelfRouter(address router) private view { @@ -487,7 +573,7 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { } ///@dev always get the positive diff - function _getDiff(uint256 a, uint256 b) internal view returns (uint256) { + function _getDiff(uint256 a, uint256 b) internal pure returns (uint256) { return a > b ? a - b : b - a; } @@ -497,33 +583,51 @@ contract RebalancerHook is Ownable, MinimalRouterWithSwap, BaseHooks { * @return The list of active liquidity providers for the pool */ function _getActiveLiquidityProviders(address pool) internal view returns (address[] memory) { - address[] memory poolLiquidityProviders = liqudityProviders[pool]; - uint256 length = 0; - address[] memory activeLp; + address[] memory poolLiquidityProviders = liquidityProviders[pool]; + uint256 activeCount = 0; + + // First, count the number of active liquidity providers for (uint256 i = 0; i < poolLiquidityProviders.length; i++) { if (isStillLp[pool][poolLiquidityProviders[i]]) { - activeLp[length] = poolLiquidityProviders[i]; - length++; + activeCount++; } } + + // Initialize the activeLp array with the correct length + address[] memory activeLp = new address[](activeCount); + uint256 index = 0; + + // Populate the activeLp array + for (uint256 i = 0; i < poolLiquidityProviders.length; i++) { + if (isStillLp[pool][poolLiquidityProviders[i]]) { + activeLp[index] = poolLiquidityProviders[i]; + index++; + } + } + return activeLp; } + struct LPTokens { + uint256[] addTokens; + uint256[] removeTokens; + } + function _getTwoLpsTokens( uint256[] memory addTokens, uint256[] memory removeTokens - ) internal pure returns (uint256[][] memory, uint256[][] memory) { - uint256[][][] memory tokenMatrix; + ) internal pure returns (LPTokens memory lp1, LPTokens memory lp2) { uint256 length = addTokens.length; - for (uint256 lpIndex; lpIndex < 2; lpIndex++) { - for (uint256 tokenIndex = 0; tokenIndex < length; tokenIndex++) { - tokenMatrix[lpIndex][0][tokenIndex] = addTokens[tokenIndex].divUp(2); - } - for (uint256 tokenIndex = 0; tokenIndex < length; tokenIndex++) { - tokenMatrix[lpIndex][1][tokenIndex] = removeTokens[tokenIndex].divUp(2); - } - } + lp1.addTokens = new uint256[](length); + lp1.removeTokens = new uint256[](length); + lp2.addTokens = new uint256[](length); + lp2.removeTokens = new uint256[](length); - return (tokenMatrix[0], tokenMatrix[1]); + for (uint256 i = 0; i < length; i++) { + lp1.addTokens[i] = addTokens[i] / 2; + lp1.removeTokens[i] = removeTokens[i] / 2; + lp2.addTokens[i] = addTokens[i] - lp1.addTokens[i]; + lp2.removeTokens[i] = removeTokens[i] - lp1.removeTokens[i]; + } } } diff --git a/packages/foundry/contracts/hooks/rebalancer/interfaces/IOracle.sol b/packages/foundry/contracts/hooks/rebalancer/interfaces/IOracle.sol index 91e90a74..59ae897e 100644 --- a/packages/foundry/contracts/hooks/rebalancer/interfaces/IOracle.sol +++ b/packages/foundry/contracts/hooks/rebalancer/interfaces/IOracle.sol @@ -22,7 +22,7 @@ interface IOracle { * @param pool The address of the pool * @return The current fee */ - function getFee(address pool) external view returns (uint24); + function getFee(address pool) external view returns (uint256); /** * @dev Get the current position for the given pool @@ -36,26 +36,16 @@ interface IOracle { * @param pool The address of the pool * @param fee The fee to set */ - function setFee(address pool, uint24 fee) external; + function setFee(address pool, uint256 fee) external; /** * @dev Set the position for the given pool * @param pool The address of the pool - * @param i The index of the token to set for the pool - * @param latestRoundPrice The Latest Round price from Price Aggregator - * @param predictedPrice The predict price based on forward events + * @param _tokensData The TokenData[] array to set */ - function setPoolTokenData( + function setPoolTokensData( address pool, - uint i, - uint256 latestRoundPrice, - uint256 predictedPrice + TokenData[] memory _tokensData ) external; - - /** - * @dev Update the oracle for the given pool - * @param pool The address of the pool - */ - function updateOracle(address pool) external; } diff --git a/packages/foundry/test/ReBalancerE2E.t.sol b/packages/foundry/test/ReBalancerE2E.t.sol new file mode 100644 index 00000000..0e1cc977 --- /dev/null +++ b/packages/foundry/test/ReBalancerE2E.t.sol @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { LiquidityManagement, PoolRoleAccounts } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { IVaultExtension } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultExtension.sol"; +import { IVaultAdmin } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultAdmin.sol"; +import { IVaultMock } from "@balancer-labs/v3-interfaces/contracts/test/IVaultMock.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; + +import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; +import { BasicAuthorizerMock } from "@balancer-labs/v3-vault/contracts/test/BasicAuthorizerMock.sol"; +import { ArrayHelpers } from "@balancer-labs/v3-solidity-utils/contracts/test/ArrayHelpers.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { BaseTest } from "@balancer-labs/v3-solidity-utils/test/foundry/utils/BaseTest.sol"; +import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol"; + +import { BatchRouterMock } from "@balancer-labs/v3-vault/contracts/test/BatchRouterMock.sol"; +import { PoolFactoryMock } from "@balancer-labs/v3-vault/contracts/test/PoolFactoryMock.sol"; +import { BalancerPoolToken } from "@balancer-labs/v3-vault/contracts/BalancerPoolToken.sol"; +import { RouterMock } from "@balancer-labs/v3-vault/contracts/test/RouterMock.sol"; +import { PoolMock } from "@balancer-labs/v3-vault/contracts/test/PoolMock.sol"; +import { WeightedPoolFactory } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPoolFactory.sol"; +import { WeightedPool } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPool.sol"; +import { VaultMockDeployer } from "@balancer-labs/v3-vault/test/foundry/utils/VaultMockDeployer.sol"; +import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; +import { ReBalancerHook, IOracle, TokenData } from "../contracts/hooks/rebalancer/Rebalancer.sol"; +import { Oracle } from "../contracts/hooks/rebalancer/Oracle.sol"; +import { SwapKind } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { BasePoolTest } from "@balancer-labs/v3-vault/test/foundry/utils/BasePoolTest.sol"; +import { PoolHooksMock } from "@balancer-labs/v3-vault/contracts/test/PoolHooksMock.sol"; + +contract ReBalancerHookE2E is BaseVaultTest { + using CastingHelpers for address[]; + using ArrayHelpers for *; + using FixedPoint for uint256; + + uint256 internal daiIdx; + uint256 internal usdcIdx; + + // Maximum exit fee of 10% + uint64 public constant MAX_EXIT_FEE_PERCENTAGE = 10e16; + + uint256 internal constant DEFAULT_AMP_FACTOR = 200; + uint256 constant DEFAULT_SWAP_FEE = 1e16; // 1% + uint256 constant TOKEN_AMOUNT = 1e3 * 1e18; + + ReBalancerHook internal rebalancer; + + uint256[] internal weights; + uint256[] internal tokenAmounts; + IERC20[] internal poolTokens; + Oracle internal oracle; + address ORACLE = 0x1f5D28a5b9a79A18eC5c1f12edaB35b7D13d1615; + + WeightedPoolFactory internal _factoryMock; + + // Overrides to include a deployment forReBalancerHook + function setUp() public virtual override { + BaseTest.setUp(); + + vault = IVaultMock(address(VaultMockDeployer.deploy())); + vm.label(address(vault), "vault"); + vaultExtension = IVaultExtension(vault.getVaultExtension()); + vm.label(address(vaultExtension), "vaultExtension"); + vaultAdmin = IVaultAdmin(vault.getVaultAdmin()); + vm.label(address(vaultAdmin), "vaultAdmin"); + authorizer = BasicAuthorizerMock(address(vault.getAuthorizer())); + vm.label(address(authorizer), "authorizer"); + factoryMock = PoolFactoryMock(address(vault.getPoolFactoryMock())); + _factoryMock = new WeightedPoolFactory(IVault(address(vault)), 365 days, "Factory v1", "Pool v1"); + vm.label(address(factoryMock), "factory"); + router = new RouterMock(IVault(address(vault)), weth, permit2); + vm.label(address(router), "router"); + batchRouter = new BatchRouterMock(IVault(address(vault)), weth, permit2); + vm.label(address(batchRouter), "batch router"); + feeController = vault.getProtocolFeeController(); + vm.label(address(feeController), "fee controller"); + oracle = new Oracle(100 * 1e20, ORACLE); + rebalancer = new ReBalancerHook(IVault(address(vault)), permit2, weth, address(factoryMock), address(oracle)); + vm.label(address(rebalancer), "rebalancer"); + + // Here the router is also the hook + poolHooksContract = address(rebalancer); + pool = createPool(); + + // Approve vault allowances + for (uint256 i = 0; i < users.length; ++i) { + address user = users[i]; + vm.startPrank(user); + approveForSender(); + vm.stopPrank(); + } + if (pool != address(0)) { + approveForPool(IERC20(pool)); + } + // Add initial liquidity + initPool(); + + (daiIdx, usdcIdx) = getSortedIndexes(address(dai), address(usdc)); + } + + // Overrides approval to include NFTRouter + function approveForSender() internal override { + for (uint256 i = 0; i < tokens.length; ++i) { + tokens[i].approve(address(permit2), type(uint256).max); + permit2.approve(address(tokens[i]), address(router), type(uint160).max, type(uint48).max); + permit2.approve(address(tokens[i]), address(batchRouter), type(uint160).max, type(uint48).max); + permit2.approve(address(tokens[i]), address(rebalancer), type(uint160).max, type(uint48).max); + } + } + + // Overrides approval to include NFTRouter + function approveForPool(IERC20 bpt) internal override { + for (uint256 i = 0; i < users.length; ++i) { + vm.startPrank(users[i]); + + bpt.approve(address(router), type(uint256).max); + bpt.approve(address(batchRouter), type(uint256).max); + bpt.approve(address(rebalancer), type(uint256).max); + + IERC20(bpt).approve(address(permit2), type(uint256).max); + permit2.approve(address(bpt), address(router), type(uint160).max, type(uint48).max); + permit2.approve(address(bpt), address(batchRouter), type(uint160).max, type(uint48).max); + permit2.approve(address(bpt), address(rebalancer), type(uint160).max, type(uint48).max); + + vm.stopPrank(); + } + } + + // Overrides pool creation to set liquidityManagement (disables unbalanced liquidity). + function createPool() internal override returns (address) { + IERC20[] memory sortedTokens = InputHelpers.sortTokens( + [address(dai), address(usdc)].toMemoryArray().asIERC20() + ); + for (uint256 i = 0; i < sortedTokens.length; i++) { + poolTokens.push(sortedTokens[i]); + tokenAmounts.push(TOKEN_AMOUNT); + } + + weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + + PoolRoleAccounts memory roleAccounts; + // Allow pools created by `factory` to use poolHooksMock hooks + // PoolHooksMock(poolHooksContract).allowFactory(address(_factoryMock)); + + WeightedPool newPool = WeightedPool( + WeightedPoolFactory(address(_factoryMock)).create( + "ERC20 Pool", + "ERC20POOL", + vault.buildTokenConfig(sortedTokens), + weights, + roleAccounts, + DEFAULT_SWAP_FEE, + poolHooksContract, + false, // Do not enable donations + false, // Do not disable unbalanced add/remove liquidity + ZERO_BYTES32 + ) + ); + return address(newPool); + } + + function testAddLiquidity() public { + BaseVaultTest.Balances memory balancesBefore = getBalances(bob); + uint256[] memory maxAmountsIn = [dai.balanceOf(bob), usdc.balanceOf(bob)].toMemoryArray(); + vm.prank(bob); + uint256[] memory amountsIn = rebalancer.addLiquidityProportional( + pool, + maxAmountsIn, + bptAmount, + false, + bytes("") + ); + vm.stopPrank(); + + BaseVaultTest.Balances memory balancesAfter = getBalances(bob); + + // bob sends correct lp tokens + assertEq( + balancesBefore.bobTokens[daiIdx] - balancesAfter.bobTokens[daiIdx], + amountsIn[daiIdx], + "bob's DAI amount is wrong" + ); + assertEq( + balancesBefore.bobTokens[usdcIdx] - balancesAfter.bobTokens[usdcIdx], + amountsIn[usdcIdx], + "bob's USDC amount is wrong" + ); + } + + function setUpRebalanceData(address pool, uint256[] memory minRatios) internal { + ReBalancerHook.RebalanceData[] memory data = new ReBalancerHook.RebalanceData[](2); + for (uint256 i = 0; i < 2; i++) { + data[i] = ReBalancerHook.RebalanceData({ minRatio: minRatios[i], rebalanceRequired: false }); + } + + vm.prank(address(0)); // Only pool creator can set rebalance data + rebalancer.setRebalanceData(pool, data); + } + + function testRebalancing() public { + // Set up initial liquidity with both Bob and Alice + uint256[] memory maxAmountsInBob = [dai.balanceOf(bob), usdc.balanceOf(bob)].toMemoryArray(); + vm.prank(bob); + rebalancer.addLiquidityProportional(pool, maxAmountsInBob, bptAmount, false, ""); + + uint256[] memory maxAmountsInAlice = [dai.balanceOf(alice), usdc.balanceOf(alice)].toMemoryArray(); + vm.prank(alice); + rebalancer.addLiquidityProportional(pool, maxAmountsInAlice, bptAmount, false, ""); + + // et up rebalance thresholds (1% change trigger) + uint256[] memory minRatios = new uint256[](2); + minRatios[0] = 1e16; // 1% for DAI + minRatios[1] = 1e16; // 1% for USDC + setUpRebalanceData(pool, minRatios); + + // Record initial balances + BaseVaultTest.Balances memory balancesBefore = getBalances(address(pool)); + + // Mock a significant price change in DAI (5% increase) + uint256 daiInitialPrice = 1e8; // $1.00 in 1e8 precision + uint256 usdcInitialPrice = 1e8; // $1.00 + uint256 daiPredictedPrice = 155e6; // $1.55 + uint256 usdcPredictedPrice = 1e8; // No change in USDC + + TokenData[] memory tokensData = new TokenData[](2); + tokensData[0] = TokenData({ latestRoundPrice: daiInitialPrice, predictedPrice: daiPredictedPrice }); + tokensData[1] = TokenData({ latestRoundPrice: usdcInitialPrice, predictedPrice: usdcPredictedPrice }); + + vm.prank(ORACLE); + oracle.setPoolTokensData(pool, tokensData); + + require(oracle.getPoolTokensData(pool)[0].predictedPrice == daiPredictedPrice, "Predicted Price is not set"); + require(oracle.getPoolTokensData(pool)[1].predictedPrice == usdcPredictedPrice, "Predicted Price is not set"); + + // Trigger rebalancing through a swap + vm.expectEmit(true, true, true, true); + emit ReBalancerHook.RebalanceStarted(pool); + + // Perform a small swap to trigger rebalancing + vm.prank(bob); + rebalancer.swapSingleTokenExactIn( + pool, + tokens[daiIdx], + tokens[usdcIdx], + 1e18, // 1 DAI + 0, + block.timestamp + 1000, + true, + "" // No min amount out + ); + + // Verify rebalancing effects + BaseVaultTest.Balances memory balancesAfter = getBalances(address(pool)); + + // Verify DAI balance increased (as it's worth more) + assertGt( + balancesAfter.poolTokens[daiIdx], + balancesBefore.poolTokens[daiIdx], + "DAI balance should increase after rebalancing" + ); + + // Verify USDC balance decreased proportionally + assertLt( + balancesAfter.poolTokens[usdcIdx], + balancesBefore.poolTokens[usdcIdx], + "USDC balance should decrease after rebalancing" + ); + + // Verify the ratios are closer to target after rebalancing + uint256 initialRatio = balancesBefore.poolTokens[daiIdx].divDown(balancesBefore.poolTokens[usdcIdx]); + uint256 finalRatio = balancesAfter.poolTokens[daiIdx].divDown(balancesAfter.poolTokens[usdcIdx]); + uint256 targetRatio = daiPredictedPrice.divDown(usdcPredictedPrice); + + uint256 initialDiff = initialRatio > targetRatio ? initialRatio - targetRatio : targetRatio - initialRatio; + uint256 finalDiff = finalRatio > targetRatio ? finalRatio - targetRatio : targetRatio - finalRatio; + + assertLt(finalDiff, initialDiff, "Rebalancing should move ratios closer to target"); + } +}