diff --git a/README.md b/README.md index 1397b2da..2c1cf801 100644 --- a/README.md +++ b/README.md @@ -1,301 +1,83 @@ -# πŸ—οΈŽ Scaffold Balancer v3 +# Volatility and Loyalty Hook for Balancer V3 Hackathon -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. +The Volatility and Loyalty Hook aims to stimulate trading activity in a newly launched pool on Balancer by rewarding users with discounts on swap fees when they hold project tokens for a longer duration. It simultaneously ensures stability by increasing swap fees during periods of high volatility. -[![intro-to-scaffold-balancer](https://github.com/user-attachments/assets/f862091d-2fe9-4b4b-8d70-cb2fdc667384)](https://www.youtube.com/watch?v=m6q5M34ZdXw) +## Contents +- [Hook Lifecycle Points](#hook-lifecycle-points) +- [Swap Fee Calculation](#swap-fee-calculation) + - [Swap Fee with Loyalty Discount](#swap-fee-with-loyalty-discount) + - [Volatility Fee](#volatility-fee) +- [Volatility Percentage Calculation](#volatility-percentage-calculation) +- [References](#references) -### πŸ” Development Life Cycle +## Hook Lifecycle Points -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/) +- **onAfterRemoveLiquidity(), onAfterAddLiquidity():** + Updates the new token price in the Volatility Oracle along with the timestamps. + +- **onAfterSwap():** + Updates the new token price in the Volatility Oracle along with the timestamps. + Updates the loyalty index of the user. -### πŸͺ§ Table Of Contents +- **onComputeDynamicSwapFeePercentage():** + Calculates the swap fee based on the pool's volatility and the user's loyalty index. -- [πŸ§‘β€πŸ’» 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) +![Balancer Hook Diagram](https://github.com/user-attachments/assets/6453b5b8-03ad-4108-bc66-228cc684716f) -## πŸ§‘β€πŸ’» Environment Setup +## Swap Fee Calculation -### 1. Requirements πŸ“œ +The swap fee is calculated as the sum of the **swapFeeWithLoyaltyDiscount** and the **volatilityFee**. -- [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) +### Swap Fee with Loyalty Discount -### 2. Quickstart πŸƒ +This reduces the static swap fee but maintains a **minimum fee** and a **cap on the loyalty discount** to prevent exploitation by large holders (whales). -1. Ensure you have the latest version of foundry installed +- **Minimum fee that must be paid:** 1% +- **Cap on loyalty discount:** 1% -``` -foundryup -``` +Let’s assume `MAX_LOYALTY_FEE = 1%`. The logic is as follows: -2. Clone this repo & install dependencies +- If `staticSwapFee <= 1%`: + `swapFeeWithLoyaltyDiscount = staticSwapFee` + +- If `1% < staticSwapFee < 2%` (1% + `MAX_LOYALTY_FEE`): + `swapFeeWithLoyaltyDiscount = 1% + (staticSwapFee - 1%) * (1 - loyaltyPercentage)` -```bash -git clone https://github.com/balancer/scaffold-balancer-v3.git -cd scaffold-balancer-v3 -yarn install -``` +- If `staticSwapFee == 2%` (1% + `MAX_LOYALTY_FEE`): + `swapFeeWithLoyaltyDiscount = 1% + MAX_LOYALTY_FEE * (1 - loyaltyPercentage)` -3. Set a `SEPOLIA_RPC_URL` in the `packages/foundry/.env` file +- Else: + `swapFeeWithLoyaltyDiscount = 1% + MAX_LOYALTY_FEE * (1 - loyaltyPercentage) + (staticSwapFee - 1% - MAX_LOYALTY_FEE)` -``` -SEPOLIA_RPC_URL=... -``` +#### Loyalty Percentage Calculation -4. Start a local anvil fork of the Sepolia testnet +We calculate a **loyaltyIndex** based on the time the tokens have been held, preventing **flash loan attacks**: -```bash -yarn fork -``` +`newLoyaltyIndex = previousLoyaltyIndex + (tokens held at the previous transaction) * (current timestamp - previous swap transaction timestamp)` -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 +Using this **loyaltyIndex**, we calculate the **loyaltyPercentage** through a tier-based system. -```bash -yarn deploy -``` +The loyalty index is refreshed if the previous transaction occurred more than **_LOYALTY_REFRESH_WINDOW** (30 days) ago. -6. Start the nextjs frontend +### Volatility Fee -```bash -yarn start -``` +`volatilityFee = MAX_VOLATILITY_FEE * volatilityPercentage` -7. Explore the frontend +## Volatility Percentage Calculation -- 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 +The volatility percentage is calculated using a circular buffer to maintain a history of price and timestamp objects. The buffer ensures price updates are stored at intervals of, for example, 2 minutes. If an update occurs within this interval, the previous entry is overwritten; if it occurs later, a new entry is added. -8. Run the Foundry tests +Once the prices over a span of time are captured, the price 1 hour ago (or a shorter duration for demo purposes) is extracted using **binary search** from the oracle. -``` -yarn test -``` +The formula for calculating volatility is: -### 3. Scaffold ETH 2 Tips πŸ—οΈ +Screenshot 2024-10-21 at 1 58 47β€―AM -SE-2 offers a variety of configuration options for connecting an account, choosing networks, and deploying contracts +Where: +- **tf** and **ti** are the final and initial timestamps of the samples collected from the buffer for the last **ago** seconds. -
πŸ”₯ Burner Wallet +The unit of volatility is **% price change per second**. A tier-based system is then used to calculate the **volatility fee percent**. -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 -``` +## References +- Implementation of PriceOracle in Balancer V2: + [Etherscan](https://etherscan.deth.net/address/0xA5bf2ddF098bb0Ef6d120C98217dD6B141c74EE0) diff --git a/packages/foundry/contracts/factories/ConstantProductFactoryV2.sol b/packages/foundry/contracts/factories/ConstantProductFactoryV2.sol new file mode 100644 index 00000000..5f2afaae --- /dev/null +++ b/packages/foundry/contracts/factories/ConstantProductFactoryV2.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import { + LiquidityManagement, + PoolRoleAccounts, + TokenConfig +} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { BasePoolFactory } from "@balancer-labs/v3-pool-utils/contracts/BasePoolFactory.sol"; + +import { ConstantProductPool } from "../pools/ConstantProductPool.sol"; + +/** + * @title Constant Product Factory + * @notice This custom pool factory is based on the example from the Balancer v3 docs + * https://docs-v3.balancer.fi/build-a-custom-amm/build-an-amm/deploy-custom-amm-using-factory.html + */ +contract ConstantProductFactoryV2 is BasePoolFactory { + error OnlyTwoTokenPoolsAllowed(); + + /** + * @dev The pool's creationCode is used to deploy pools via CREATE3 + * @notice The pool creationCode cannot be changed after the factory has been deployed + * @param vault The contract instance of the Vault + * @param pauseWindowDuration The period ( starting from deployment of this factory ) during which pools can be paused and unpaused + */ + constructor( + IVault vault, + uint32 pauseWindowDuration + ) BasePoolFactory(vault, pauseWindowDuration, type(ConstantProductPool).creationCode) {} + + /** + * @notice Deploys a new pool and registers it with the vault + * @param name The name of the pool + * @param symbol The symbol of the pool + * @param salt The salt value that will be passed to create3 deployment + * @param tokens An array of descriptors for the tokens the pool will manage + * @param swapFeePercentage Initial swap fee percentage + * @param protocolFeeExempt true, the pool's initial aggregate fees will be set to 0 + * @param roleAccounts Addresses the Vault will allow to change certain pool settings + * @param poolHooksContract Contract that implements the hooks for the pool + * @param liquidityManagement Liquidity management flags with implemented methods + */ + function create( + string memory name, + string memory symbol, + bytes32 salt, + TokenConfig[] memory tokens, + uint256 swapFeePercentage, + bool protocolFeeExempt, + PoolRoleAccounts memory roleAccounts, + address poolHooksContract, + LiquidityManagement memory liquidityManagement + ) external returns (address pool) { + if (tokens.length != 2) revert OnlyTwoTokenPoolsAllowed(); + + // First deploy a new pool + pool = _create(abi.encode(getVault(), name, symbol), salt); + // Then register the pool + _registerPoolWithVault( + pool, + tokens, + swapFeePercentage, + protocolFeeExempt, + roleAccounts, + poolHooksContract, + liquidityManagement + ); + } +} diff --git a/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/PoolPriceOracle.sol b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/PoolPriceOracle.sol new file mode 100644 index 00000000..67a75bfc --- /dev/null +++ b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/PoolPriceOracle.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "./library/Buffer.sol"; +import "./library/Samples.sol"; + +import "forge-std/console.sol"; + +contract PoolPriceOracle { + using Buffer for uint256; + using Samples for bytes32; + + uint256 private constant _MAX_SAMPLE_DURATION = 1 seconds; + + mapping(uint256 => bytes32) private _samples; + + struct LogPairPrices { + int256 logPairPrice; + uint256 timestamp; + } + + function findAllSamples(uint256 latestIndex, uint256 ago) public view returns (LogPairPrices[] memory) { + console.log("(findAllSamples) Here 1", latestIndex, ago); + uint256 blockTimeStamp = block.timestamp; + console.log("(findAllSamples) blockTimeStamp", blockTimeStamp); + uint256 lookUpTime = blockTimeStamp - ago; + console.log("(findAllSamples) lookUpTime", lookUpTime); + uint256 offset = latestIndex < Buffer.SIZE ? 0 : latestIndex.next(); + console.log("(findAllSamples) offset", offset); + + uint256 low = 0; + uint256 high = latestIndex < Buffer.SIZE ? latestIndex : Buffer.SIZE - 1; + uint256 mid; + + uint256 sampleTimestamp; + + while (low <= high) { + console.log("(findAllSamples) low", low); + console.log("(findAllSamples) high", high); + + uint256 midWithoutOffset = (high + low) / 2; + mid = midWithoutOffset.add(offset); + console.log("(findAllSamples) midWithoutOffset, mid", midWithoutOffset, mid); + + (, uint256 timestamp) = getSample(mid.add(offset)); + + sampleTimestamp = timestamp; + console.log("(findAllSamples) sampleTimestamp", sampleTimestamp); + + if (sampleTimestamp < lookUpTime) { + console.log("(findAllSamples) sampleTimestamp < lookUpTime is true"); + low = midWithoutOffset + 1; + } else if (sampleTimestamp > lookUpTime) { + console.log("(findAllSamples) sampleTimestamp > lookUpTime is true"); + high = midWithoutOffset - 1; + } else { + console.log("(findAllSamples) break low high mid", low, high, mid); + console.log("(findAllSamples) break midWithoutOffset", midWithoutOffset); + break; + } + } + + console.log("(findAllSamples) out low high mid", low, high, mid); + uint256 lowerBound; + uint256 upperBound; + + if (latestIndex < Buffer.SIZE) { + lowerBound = sampleTimestamp <= lookUpTime ? mid : mid > 0 ? mid - 1 : 0; + upperBound = latestIndex; + } else { + lowerBound = sampleTimestamp >= lookUpTime ? mid : mid.prev(); + upperBound = Buffer.SIZE - 1; + } + + console.log("(findAllSamples) lowerBound, upperBound, offset", lowerBound, upperBound, offset); + + LogPairPrices[] memory logPairPrices = new LogPairPrices[](upperBound - lowerBound + 1); + + for (uint256 i = lowerBound; i <= upperBound; i = i.next()) { + (int256 logPairPrice, uint256 timestamp) = getSample(i.add(offset)); + console.log("(findAllSamples) i, timestamp", i, timestamp, uint256(logPairPrice)); + logPairPrices[i - lowerBound] = (LogPairPrices(logPairPrice, timestamp)); + } + + return logPairPrices; + } + + function getSample(uint256 index) public view returns (int256 logPairPrice, uint256 timestamp) { + // add error for buffer size + + bytes32 sample = _getSample(index); + return sample.unpack(); + } + + function _processPriceData( + uint256 latestSampleCreationTimestamp, + uint256 latestIndex, + int256 logPairPrice + ) internal returns (uint256) { + bytes32 sample = _getSample(latestIndex).update(logPairPrice, block.timestamp); + + bool newSample = block.timestamp - latestSampleCreationTimestamp >= _MAX_SAMPLE_DURATION; + latestIndex = newSample ? latestIndex.next() : latestIndex; + + _samples[latestIndex] = sample; + + return latestIndex; + } + + function _getSample(uint256 index) internal view returns (bytes32) { + return _samples[index]; + } +} diff --git a/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/Buffer.sol b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/Buffer.sol new file mode 100644 index 00000000..c0a38640 --- /dev/null +++ b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/Buffer.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +library Buffer { + uint256 internal constant SIZE = 1024; + + function prev(uint256 index) internal pure returns (uint256) { + return sub(index, 1); + } + + function next(uint256 index) internal pure returns (uint256) { + return add(index, 1); + } + + function add(uint256 index, uint256 offset) internal pure returns (uint256) { + return (index + offset) % SIZE; + } + + function sub(uint256 index, uint256 offset) internal pure returns (uint256) { + return (index + SIZE - offset) % SIZE; + } +} diff --git a/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/Samples.sol b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/Samples.sol new file mode 100644 index 00000000..d3152d3d --- /dev/null +++ b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/Samples.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "../library/WordCodec.sol"; + +library Samples { + using WordCodec for int256; + using WordCodec for uint256; + using WordCodec for bytes32; + + uint256 internal constant _TIMESTAMP_OFFSET = 0; + uint256 internal constant _INST_LOG_PAIR_PRICE_OFFSET = 31; + + function update(bytes32 sample, int256 instLogPairPrice, uint256 currentTimestamp) internal pure returns (bytes32) { + return pack(instLogPairPrice, currentTimestamp); + } + + function timestamp(bytes32 sample) internal pure returns (uint256) { + return sample.decodeUint31(_TIMESTAMP_OFFSET); + } + + function _instLogPairPrice(bytes32 sample) private pure returns (int256) { + return sample.decodeInt22(_INST_LOG_PAIR_PRICE_OFFSET); + } + + function pack(int256 instLogPairPrice, uint256 _timestamp) internal pure returns (bytes32) { + return instLogPairPrice.encodeInt22(_INST_LOG_PAIR_PRICE_OFFSET) | _timestamp.encodeUint31(_TIMESTAMP_OFFSET); + } + + function unpack(bytes32 sample) internal pure returns (int256 logPairPrice, uint256 _timestamp) { + logPairPrice = _instLogPairPrice(sample); + _timestamp = timestamp(sample); + } +} diff --git a/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/WeightedOracleMath.sol b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/WeightedOracleMath.sol new file mode 100644 index 00000000..8ff7c43d --- /dev/null +++ b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/WeightedOracleMath.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { LogExpMath } from "@balancer-labs/v3-solidity-utils/contracts/math/LogExpMath.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +/* solhint-disable private-vars-leading-underscore */ + +library WeightedOracleMath { + using FixedPoint for uint256; + + int256 private constant _LOG_COMPRESSION_FACTOR = 1e14; + int256 private constant _HALF_LOG_COMPRESSION_FACTOR = 0.5e14; + + /** + * @dev Calculates the logarithm of the spot price of token B in token A. + * + * The return value is a 4 decimal fixed-point number: use `_fromLowResLog` to recover the original value. + */ + function _calcLogSpotPrice( + uint256 normalizedWeightA, + uint256 balanceA, + uint256 normalizedWeightB, + uint256 balanceB + ) internal pure returns (int256) { + // Max balances are 2^112 and min weights are 0.01, so the division never overflows. + + // The rounding direction is irrelevant as we're about to introduce a much larger error when converting to log + // space. We use `divUp` as it prevents the result from being zero, which would make the logarithm revert. A + // result of zero is therefore only possible with zero balances, which are prevented via other means. + uint256 spotPrice = balanceA.divUp(normalizedWeightA).divUp(balanceB.divUp(normalizedWeightB)); + return _toLowResLog(spotPrice); + } + + /** + * @dev Calculates the price of BPT in a token. `logBptTotalSupply` should be the result of calling `_toLowResLog` + * with the current BPT supply. + * + * The return value is a 4 decimal fixed-point number: use `_fromLowResLog` to recover the original value. + */ + function _calcLogBPTPrice( + uint256 normalizedWeight, + uint256 balance, + int256 logBptTotalSupply + ) internal pure returns (int256) { + // BPT price = (balance / weight) / total supply + // Since we already have ln(total supply) and want to compute ln(BPT price), we perform the computation in log + // space directly: ln(BPT price) = ln(balance / weight) - ln(total supply) + + // The rounding direction is irrelevant as we're about to introduce a much larger error when converting to log + // space. We use `divUp` as it prevents the result from being zero, which would make the logarithm revert. A + // result of zero is therefore only possible with zero balances, which are prevented via other means. + int256 logBalanceOverWeight = _toLowResLog(balance.divUp(normalizedWeight)); + + // Because we're subtracting two values in log space, this value has a larger error (+-0.0001 instead of + // +-0.00005), which results in a final larger relative error of around 0.1%. + return logBalanceOverWeight - logBptTotalSupply; + } + + /** + * @dev Returns the natural logarithm of `value`, dropping most of the decimal places to arrive at a value that, + * when passed to `_fromLowResLog`, will have a maximum relative error of ~0.05% compared to `value`. + * + * Values returned from this function should not be mixed with other fixed-point values (as they have a different + * number of digits), but can be added or subtracted. Use `_fromLowResLog` to undo this process and return to an + * 18 decimal places fixed point value. + * + * Because so much precision is lost, the logarithmic values can be stored using much fewer bits than the original + * value required. + */ + function _toLowResLog(uint256 value) internal pure returns (int256) { + int256 ln = LogExpMath.ln(int256(value)); + + // Rounding division for signed numerator + return + (ln > 0 ? ln + _HALF_LOG_COMPRESSION_FACTOR : ln - _HALF_LOG_COMPRESSION_FACTOR) / _LOG_COMPRESSION_FACTOR; + } + + /** + * @dev Restores `value` from logarithmic space. `value` is expected to be the result of a call to `_toLowResLog`, + * any other function that returns 4 decimals fixed point logarithms, or the sum of such values. + */ + function _fromLowResLog(int256 value) internal pure returns (uint256) { + return uint256(LogExpMath.exp(value * _LOG_COMPRESSION_FACTOR)); + } +} diff --git a/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/WeightedPool2TokensMiscData.sol b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/WeightedPool2TokensMiscData.sol new file mode 100644 index 00000000..bd140086 --- /dev/null +++ b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/WeightedPool2TokensMiscData.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; +pragma experimental ABIEncoderV2; + +import "../library/WordCodec.sol"; + +library WeightedPool2TokensMiscData { + using WordCodec for bytes32; + using WordCodec for uint256; + + uint256 private constant _LOG_INVARIANT_OFFSET = 0; + uint256 private constant _LOG_TOTAL_SUPPLY_OFFSET = 22; + uint256 private constant _ORACLE_SAMPLE_CREATION_TIMESTAMP_OFFSET = 44; + uint256 private constant _ORACLE_INDEX_OFFSET = 75; + uint256 private constant _ORACLE_ENABLED_OFFSET = 85; + uint256 private constant _SWAP_FEE_PERCENTAGE_OFFSET = 86; + + function logInvariant(bytes32 data) internal pure returns (int256) { + return data.decodeInt22(_LOG_INVARIANT_OFFSET); + } + + function logTotalSupply(bytes32 data) internal pure returns (int256) { + return data.decodeInt22(_LOG_TOTAL_SUPPLY_OFFSET); + } + + function oracleSampleCreationTimestamp(bytes32 data) internal pure returns (uint256) { + return data.decodeUint31(_ORACLE_SAMPLE_CREATION_TIMESTAMP_OFFSET); + } + + function oracleIndex(bytes32 data) internal pure returns (uint256) { + return data.decodeUint10(_ORACLE_INDEX_OFFSET); + } + + function oracleEnabled(bytes32 data) internal pure returns (bool) { + return data.decodeBool(_ORACLE_ENABLED_OFFSET); + } + + function swapFeePercentage(bytes32 data) internal pure returns (uint256) { + return data.decodeUint64(_SWAP_FEE_PERCENTAGE_OFFSET); + } + + function setLogInvariant(bytes32 data, int256 _logInvariant) internal pure returns (bytes32) { + return data.insertInt22(_logInvariant, _LOG_INVARIANT_OFFSET); + } + + function setLogTotalSupply(bytes32 data, int256 _logTotalSupply) internal pure returns (bytes32) { + return data.insertInt22(_logTotalSupply, _LOG_TOTAL_SUPPLY_OFFSET); + } + + function setOracleSampleCreationTimestamp(bytes32 data, uint256 _initialTimestamp) internal pure returns (bytes32) { + return data.insertUint31(_initialTimestamp, _ORACLE_SAMPLE_CREATION_TIMESTAMP_OFFSET); + } + + function setOracleIndex(bytes32 data, uint256 _oracleIndex) internal pure returns (bytes32) { + return data.insertUint10(_oracleIndex, _ORACLE_INDEX_OFFSET); + } + + function setOracleEnabled(bytes32 data, bool _oracleEnabled) internal pure returns (bytes32) { + return data.insertBoolean(_oracleEnabled, _ORACLE_ENABLED_OFFSET); + } + + function setSwapFeePercentage(bytes32 data, uint256 _swapFeePercentage) internal pure returns (bytes32) { + return data.insertUint64(_swapFeePercentage, _SWAP_FEE_PERCENTAGE_OFFSET); + } +} diff --git a/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/WordCodec.sol b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/WordCodec.sol new file mode 100644 index 00000000..ade03f1f --- /dev/null +++ b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/library/WordCodec.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +library WordCodec { + uint256 private constant _MASK_1 = 2 ** (1) - 1; + uint256 private constant _MASK_10 = 2 ** (10) - 1; + uint256 private constant _MASK_22 = 2 ** (22) - 1; + uint256 private constant _MASK_31 = 2 ** (31) - 1; + uint256 private constant _MASK_53 = 2 ** (53) - 1; + uint256 private constant _MASK_64 = 2 ** (64) - 1; + + int256 private constant _MAX_INT_22 = 2 ** (21) - 1; + int256 private constant _MAX_INT_53 = 2 ** (52) - 1; + + function insertBoolean(bytes32 word, bool value, uint256 offset) internal pure returns (bytes32) { + bytes32 clearedWord = bytes32(uint256(word) & ~(_MASK_1 << offset)); + return clearedWord | bytes32(uint256(value ? 1 : 0) << offset); + } + + function insertUint10(bytes32 word, uint256 value, uint256 offset) internal pure returns (bytes32) { + bytes32 clearedWord = bytes32(uint256(word) & ~(_MASK_10 << offset)); + return clearedWord | bytes32(value << offset); + } + + function insertUint31(bytes32 word, uint256 value, uint256 offset) internal pure returns (bytes32) { + bytes32 clearedWord = bytes32(uint256(word) & ~(_MASK_31 << offset)); + return clearedWord | bytes32(value << offset); + } + + function insertUint64(bytes32 word, uint256 value, uint256 offset) internal pure returns (bytes32) { + bytes32 clearedWord = bytes32(uint256(word) & ~(_MASK_64 << offset)); + return clearedWord | bytes32(value << offset); + } + + function insertInt22(bytes32 word, int256 value, uint256 offset) internal pure returns (bytes32) { + bytes32 clearedWord = bytes32(uint256(word) & ~(_MASK_22 << offset)); + return clearedWord | bytes32((uint256(value) & _MASK_22) << offset); + } + + function encodeUint31(uint256 value, uint256 offset) internal pure returns (bytes32) { + return bytes32(value << offset); + } + + function encodeInt22(int256 value, uint256 offset) internal pure returns (bytes32) { + return bytes32((uint256(value) & _MASK_22) << offset); + } + + function encodeInt53(int256 value, uint256 offset) internal pure returns (bytes32) { + return bytes32((uint256(value) & _MASK_53) << offset); + } + + function decodeBool(bytes32 word, uint256 offset) internal pure returns (bool) { + return (uint256(word >> offset) & _MASK_1) == 1; + } + + function decodeUint10(bytes32 word, uint256 offset) internal pure returns (uint256) { + return uint256(word >> offset) & _MASK_10; + } + + function decodeUint31(bytes32 word, uint256 offset) internal pure returns (uint256) { + return uint256(word >> offset) & _MASK_31; + } + + function decodeUint64(bytes32 word, uint256 offset) internal pure returns (uint256) { + return uint256(word >> offset) & _MASK_64; + } + + function decodeInt22(bytes32 word, uint256 offset) internal pure returns (int256) { + int256 value = int256(uint256(word >> offset) & _MASK_22); + return value > _MAX_INT_22 ? (value | int256(~_MASK_22)) : value; + } + + function decodeInt53(bytes32 word, uint256 offset) internal pure returns (int256) { + int256 value = int256(uint256(word >> offset) & _MASK_53); + return value > _MAX_INT_53 ? (value | int256(~_MASK_53)) : value; + } +} diff --git a/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/hooks/VolatilityLoyaltyHook.sol b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/hooks/VolatilityLoyaltyHook.sol new file mode 100644 index 00000000..fb8f2349 --- /dev/null +++ b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/hooks/VolatilityLoyaltyHook.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +import { IHooks } from "@balancer-labs/v3-interfaces/contracts/vault/IHooks.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { IPoolInfo } from "@balancer-labs/v3-interfaces/contracts/pool-utils/IPoolInfo.sol"; + +import { + LiquidityManagement, + AfterSwapParams, + PoolSwapParams, + SwapKind, + TokenConfig, + HookFlags, + RemoveLiquidityKind, + AddLiquidityKind +} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { IBasePoolFactory } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePoolFactory.sol"; +import { IRouterCommon } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterCommon.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { BaseHooks } from "@balancer-labs/v3-vault/contracts/BaseHooks.sol"; +import { VaultGuard } from "@balancer-labs/v3-vault/contracts/VaultGuard.sol"; + +import { IVolatilityOracle } from "../volatility-module/IVolatilityOracle.sol"; +import { ILoyaltyDiscount } from "../loyalty-module/ILoyaltyDiscount.sol"; +import { IVolatilityDiscount } from "../volatility-module/IVolatilityDiscount.sol"; + +import "forge-std/console.sol"; + +contract VolatilityLoyaltyHook is BaseHooks, VaultGuard, Ownable { + using FixedPoint for uint256; + + address private _tokenAddress; + address private _oracleAddress; + address private _loyaltyModuleAddress; + address private _volatilityModuleAddress; + address private _allowedFactory; + bool public isLoyaltyDiscountEnabled; + bool public isVolatilityFeeEnabled; + + event PriceDataUpdated(); + + error InvalidTokenAddress(); + + constructor( + IVault vault, + address tokenAddress, + address oracleAddress, + address loyaltyModuleAddress, + address volatilityModuleAddress, + address allowedFactory + ) VaultGuard(vault) Ownable(msg.sender) { + _tokenAddress = tokenAddress; + _oracleAddress = oracleAddress; + _loyaltyModuleAddress = loyaltyModuleAddress; + _volatilityModuleAddress = volatilityModuleAddress; + _allowedFactory = allowedFactory; + } + + function onRegister( + address factory, + address pool, + TokenConfig[] memory, + LiquidityManagement calldata + ) public override onlyVault returns (bool) { + return factory == _allowedFactory && IBasePoolFactory(factory).isPoolFromFactory(pool); + } + + function getHookFlags() public pure override returns (HookFlags memory hookFlags) { + hookFlags.enableHookAdjustedAmounts = false; + hookFlags.shouldCallAfterSwap = true; + hookFlags.shouldCallAfterRemoveLiquidity = true; + hookFlags.shouldCallAfterAddLiquidity = true; + hookFlags.shouldCallComputeDynamicSwapFee = true; + return hookFlags; + } + + function onAfterRemoveLiquidity( + address, + address, + RemoveLiquidityKind, + uint256, + uint256[] memory, + uint256[] memory amountsOutRaw, + uint256[] memory balancesScaled18, + bytes memory + ) public override onlyVault returns (bool, uint256[] memory) { + IVolatilityOracle volatilityOracle = IVolatilityOracle(_oracleAddress); + uint256 tokenIndex = 1; // 2 token pool, 2nd token is the token under consideration + + volatilityOracle.updateOracle(balancesScaled18[1 - tokenIndex], balancesScaled18[tokenIndex]); + + emit PriceDataUpdated(); + + return (true, amountsOutRaw); + } + + function onAfterAddLiquidity( + address, + address, + AddLiquidityKind, + uint256[] memory, + uint256[] memory amountsInRaw, + uint256, + uint256[] memory balancesScaled18, + bytes memory + ) public override onlyVault returns (bool, uint256[] memory) { + IVolatilityOracle volatilityOracle = IVolatilityOracle(_oracleAddress); + uint256 tokenIndex = 1; // 2 token pool, 2nd token is the token under consideration + + volatilityOracle.updateOracle(balancesScaled18[1 - tokenIndex], balancesScaled18[tokenIndex]); + + emit PriceDataUpdated(); + + return (true, amountsInRaw); + } + + function onAfterSwap(AfterSwapParams calldata params) public override onlyVault returns (bool, uint256) { + IVolatilityOracle volatilityOracle = IVolatilityOracle(_oracleAddress); + + if (address(params.tokenIn) == _tokenAddress) { + volatilityOracle.updateOracle(params.tokenOutBalanceScaled18, params.tokenInBalanceScaled18); + } else if (address(params.tokenOut) == _tokenAddress) { + volatilityOracle.updateOracle(params.tokenInBalanceScaled18, params.tokenOutBalanceScaled18); + } else { + revert InvalidTokenAddress(); + } + + emit PriceDataUpdated(); + + address user = IRouterCommon(params.router).getSender(); + + ILoyaltyDiscount loyaltyModule = ILoyaltyDiscount(_loyaltyModuleAddress); + + loyaltyModule.updateLoyaltyDataForUser( + user, + _tokenAddress, + params.tokenIn, + params.amountInScaled18, + params.amountOutScaled18 + ); + + return (true, params.amountCalculatedRaw); + } + + function onComputeDynamicSwapFeePercentage( + PoolSwapParams calldata params, + address, + uint256 staticSwapFeePercentage + ) public view override onlyVault returns (bool, uint256) { + address user = IRouterCommon(params.router).getSender(); + + uint256 swapFeePercentWithLoyaltyDiscount = isLoyaltyDiscountEnabled + ? getSwapFeeWithLoyaltyDiscount(user, staticSwapFeePercentage) + : staticSwapFeePercentage; + + uint256 volatilityFeePercent = isVolatilityFeeEnabled ? getVolatilityFee() : 0; + + uint256 totalSwapFeePercent = swapFeePercentWithLoyaltyDiscount + volatilityFeePercent; + + return (true, totalSwapFeePercent); + } + + function changeLoyaltyDiscountSetting() public onlyOwner { + isLoyaltyDiscountEnabled = !isLoyaltyDiscountEnabled; + } + + function changeVolatilityFeeSetting() public onlyOwner { + isVolatilityFeeEnabled = !isVolatilityFeeEnabled; + } + + function getSwapFeeWithLoyaltyDiscount( + address user, + uint256 staticSwapFeePercentage + ) internal view returns (uint256) { + ILoyaltyDiscount loyaltyModule = ILoyaltyDiscount(_loyaltyModuleAddress); + return loyaltyModule.getSwapFeeWithLoyaltyDiscount(user, staticSwapFeePercentage); + } + + function getVolatilityFee() internal view returns (uint256) { + IVolatilityDiscount volatilityModule = IVolatilityDiscount(_volatilityModuleAddress); + return volatilityModule.getVolatilityFeePercent(_oracleAddress); + } +} diff --git a/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/loyalty-module/ILoyaltyDiscount.sol b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/loyalty-module/ILoyaltyDiscount.sol new file mode 100644 index 00000000..4eda1862 --- /dev/null +++ b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/loyalty-module/ILoyaltyDiscount.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface ILoyaltyDiscount { + function getSwapFeeWithLoyaltyDiscount( + address user, + uint256 staticSwapFeePercentage + ) external view returns (uint256); + + function updateLoyaltyDataForUser( + address user, + address tokenAddress, + IERC20 tokenIn, + uint256 amountInScaled18, + uint256 amountOutScaled18 + ) external; +} diff --git a/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/loyalty-module/LoyaltyDiscount.sol b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/loyalty-module/LoyaltyDiscount.sol new file mode 100644 index 00000000..a50da301 --- /dev/null +++ b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/loyalty-module/LoyaltyDiscount.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./ILoyaltyDiscount.sol"; + +import "forge-std/console.sol"; + +contract LoyaltyDiscount is ILoyaltyDiscount { + using FixedPoint for uint256; + + uint256 private constant _LOYALTY_REFRESH_WINDOW = 30 days; + uint256 private constant _LOYALTY_FEE_CAP = 0.01e18; // 1 % + + mapping(address => LoyaltyData) public userLoyaltyData; + + struct LoyaltyData { + uint256 firstTransactionTimestamp; // first transaction in the _LOYALTY_INDEX_REFRESH_TIME window, the _LOYALTY_REFRESH_WINDOW value is same, but the start and end timestamps of the window is different for each user + uint256 cumulativeLoyalty; // change name to loyalty index, its dimension/unit it token * seconds + uint256 tokens; + uint256 lastTimestamp; // make it lastTransactionTImestamp + } + + function getSwapFeeWithLoyaltyDiscount( + address user, + uint256 staticSwapFeePercentage + ) public view returns (uint256) { + uint256 loyaltyIndex = getCurrentLoyaltyIndex(userLoyaltyData[user]); + bool isStaticFeeGreaterThanLoyaltyFeeCap = staticSwapFeePercentage > 0.01e18; + + uint256 fixedFee = staticSwapFeePercentage; + uint256 loyaltyFee = 0; + uint256 variableFee = 0; + + uint256 loyaltyDiscount = getLoyaltyDiscount(uint256(loyaltyIndex)); + + // idea is to first apply a flat fee of 1%, over and above that apply any loyalty discount but have a limit on it + // fixedFee will be minimum 1%, anything extra goes into loyalty fee unless it reaches the cap of _LOYALTY_FEE_CAP, anything above goes into variable fee + // so if staticSwapFeePercentage = 5 and _LOYALTY_FEE_CAP = 2, then fixedFee = 1%, loyaltyFee = 2% and variableFee = 3%, loyalty discount will be applied only on the 2% loyalty fee + // so maximum loyalty discount anyone can achieve is 2%, at the same time no one is paying less than 1%, maybe more (in this case, 1 + 3 = 4%) + + if (isStaticFeeGreaterThanLoyaltyFeeCap) { + fixedFee = 0.01e18; // 1% will be applied + variableFee = staticSwapFeePercentage > fixedFee + _LOYALTY_FEE_CAP + ? staticSwapFeePercentage - (fixedFee + _LOYALTY_FEE_CAP) + : 0; + + loyaltyFee = + ((staticSwapFeePercentage - (fixedFee + variableFee)) * (FixedPoint.ONE - loyaltyDiscount)) / + FixedPoint.ONE; + } + + uint256 totalFee = fixedFee + loyaltyFee + variableFee; + + return totalFee; + } + + function updateLoyaltyDataForUser( + address user, + address tokenAddress, + IERC20 tokenIn, + uint256 amountInScaled18, + uint256 amountOutScaled18 + ) public { + LoyaltyData memory loyaltyData = userLoyaltyData[user]; + + uint256 currentTimestamp = block.timestamp; + + bool isLoyaltyWindowRefreshed = (currentTimestamp - loyaltyData.firstTransactionTimestamp) >= _LOYALTY_REFRESH_WINDOW; + + // instead of old make it current + uint256 oldTimestamp = loyaltyData.lastTimestamp; + uint256 oldCumulativeLoyalty = isLoyaltyWindowRefreshed ? 0 : loyaltyData.cumulativeLoyalty; + uint256 oldTokens = isLoyaltyWindowRefreshed ? 0 : loyaltyData.tokens; + + // cumulative loyalty and tokens should always be positive + uint256 newCumulativeLoyalty = oldCumulativeLoyalty + (oldTokens * (currentTimestamp - oldTimestamp)); // remove this comment before submission, y = mx + c, y-> new cumulative loyalty, m -> tokens held, x -> time passed, c -> cumulative loyalty so far + int256 additionalTokens = (address(tokenIn) == tokenAddress) + ? -1 * int256(amountInScaled18) + : int256(amountOutScaled18); + uint256 newTimestamp = currentTimestamp; + uint256 newFirstTransactionTimestamp = isLoyaltyWindowRefreshed + ? currentTimestamp + : loyaltyData.firstTransactionTimestamp; + + uint256 newTokens = (additionalTokens + int256(oldTokens)) > 0 + ? uint256(additionalTokens + int256(oldTokens)) + : 0; // loyalty and tokens can never be zero + + userLoyaltyData[user] = LoyaltyData( + newFirstTransactionTimestamp, + newCumulativeLoyalty, + newTokens, + newTimestamp + ); + } + + // a rough estimation of tiers + // the thought behind the tiers is that how many tokens were bought at the beginning of the _LOYALTY_REFRESH_WINDOW and held till the end of the window + // so 1000 tokens are minted initially and can be bought and held for _LOYALTY_REFRESH_WINDOW days, so max loyalty index is 1000 * _LOYALTY_REFRESH_WINDOW + // so tier it in a way so that if someone has generated loyaltyIndex equivalent to buying at least 1% tokens and held it for the entire _LOYALTY_REFRESH_WINDOW duration gets full discount + function getLoyaltyDiscount(uint256 loyaltyIndex) internal pure returns (uint256) { + if (loyaltyIndex > 0 && loyaltyIndex <= 2 * _LOYALTY_REFRESH_WINDOW * FixedPoint.ONE) { + return 0.1e18; // 10% discount + } else if ( + loyaltyIndex > 2 * _LOYALTY_REFRESH_WINDOW * FixedPoint.ONE && + loyaltyIndex <= 5 * _LOYALTY_REFRESH_WINDOW * FixedPoint.ONE + ) { + return 0.3e18; // 30% discount + } else if ( + loyaltyIndex > 5 * _LOYALTY_REFRESH_WINDOW * FixedPoint.ONE && + loyaltyIndex <= 10 * _LOYALTY_REFRESH_WINDOW * FixedPoint.ONE + ) { + return 0.5e18; // 50% discount + } else if (loyaltyIndex > 10 * _LOYALTY_REFRESH_WINDOW * FixedPoint.ONE) { + return 1e18; // 100% discount -> does not mean there won't be any fee, refer to onComputeDynamicSwapFeePercentage + } else { + return 0; // Default return value if loyaltyIndex does not fit any of the above conditions + } + } + + function getCurrentLoyaltyIndex(LoyaltyData memory loyaltyData) internal view returns (uint256) { + uint256 currentTimestamp = block.timestamp; + uint256 oldFirstTransactionTimestamp = loyaltyData.firstTransactionTimestamp; + bool isLoyaltyWindowRefreshed = (currentTimestamp - oldFirstTransactionTimestamp) >= _LOYALTY_REFRESH_WINDOW; + uint256 oldTimestamp = loyaltyData.lastTimestamp; + uint256 oldCumulativeLoyalty = isLoyaltyWindowRefreshed ? 0 : loyaltyData.cumulativeLoyalty; + uint256 oldTokens = isLoyaltyWindowRefreshed ? 0 : loyaltyData.tokens; + uint256 newCumulativeLoyalty = oldCumulativeLoyalty + (oldTokens * (currentTimestamp - oldTimestamp)); + + return newCumulativeLoyalty; + } +} diff --git a/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/volatility-module/IVolatilityDiscount.sol b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/volatility-module/IVolatilityDiscount.sol new file mode 100644 index 00000000..d147f779 --- /dev/null +++ b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/volatility-module/IVolatilityDiscount.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +interface IVolatilityDiscount { + function getVolatilityFeePercent(address oracleAddress) external view returns (uint256); +} diff --git a/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/volatility-module/IVolatilityOracle.sol b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/volatility-module/IVolatilityOracle.sol new file mode 100644 index 00000000..d7353a24 --- /dev/null +++ b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/volatility-module/IVolatilityOracle.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +interface IVolatilityOracle { + function getVolatility(uint256 ago) external view returns (uint256); + + function updateOracle(uint256 balanceToken0, uint256 balanceToken1) external; +} diff --git a/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/volatility-module/VolatilityDiscount.sol b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/volatility-module/VolatilityDiscount.sol new file mode 100644 index 00000000..ab2a49ce --- /dev/null +++ b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/volatility-module/VolatilityDiscount.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import "./IVolatilityOracle.sol"; +import "./IVolatilityDiscount.sol"; + +contract VolatilityDiscount is IVolatilityDiscount { + using FixedPoint for uint256; + + uint256 private constant _VOLATILITY_WINDOW = 10 seconds; + + uint256 private constant _VOLATILITY_FEE_CAP = 0.04e18; + + function getVolatilityFeePercent(address oracleAddress) external view returns (uint256) { + IVolatilityOracle volatilityOracle = IVolatilityOracle(oracleAddress); + uint256 volatility = volatilityOracle.getVolatility(_VOLATILITY_WINDOW); + uint256 volatilityFeePercent = getVolatilityFeePercentOnCap(volatility); + uint256 volatilityFee = (_VOLATILITY_FEE_CAP * (volatilityFeePercent)) / FixedPoint.ONE; + return volatilityFee; + } + + // volatility -> percent change per second + function getVolatilityFeePercentOnCap(uint256 volatility) internal pure returns (uint256) { + if (volatility > 0 && volatility <= 0.001e18) { + // less than 0.1 %/second + return 0; // no fee + } else if (volatility > 0.001e18 && volatility <= 0.005e18) { + // less than 0.5 %/second + return 0.1e18; // 10% of max fee + } else if (volatility > 0.005e18 && volatility <= 0.015e18) { + // less than 1.5 %/second + return 0.2e18; // 20% of max fee + } else if (volatility > 0.015e18 && volatility <= 0.02e18) { + // less than 2 %/second + return 0.3e18; // 30% of max fee + } else if (volatility > 0.02e18 && volatility <= 0.05e18) { + // less than 5 %/second + return 0.5e18; // 50% of max fee + } else if (volatility > 0.05e18 && volatility <= 0.1e18) { + // less than 10 %/second + return 0.7e18; // 70% of max fee + } else if (volatility > 0.1e18 && volatility <= 0.2e18) { + // less than 20 %/second + return 0.9e18; // 90% of max fee + } else if (volatility > 0.2e18) { + // greater than 20%/second + return 1e18; // 100% of max fee + } else { + return 0; // no fee + } + } +} diff --git a/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/volatility-module/VolatilityOracle.sol b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/volatility-module/VolatilityOracle.sol new file mode 100644 index 00000000..029e6a56 --- /dev/null +++ b/packages/foundry/contracts/hooks/VolatilityLoyaltyModule/volatility-module/VolatilityOracle.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import "../balancer-v2-oracle/library/WeightedPool2TokensMiscData.sol"; +import "../balancer-v2-oracle/library/WeightedOracleMath.sol"; +import "../balancer-v2-oracle/PoolPriceOracle.sol"; +import "./IVolatilityOracle.sol"; + +contract VolatilityOracle is IVolatilityOracle, PoolPriceOracle { + using FixedPoint for uint256; + using WeightedPool2TokensMiscData for bytes32; + + bytes32 internal _miscData; + + function getVolatility(uint256 ago) external view returns (uint256 volatility) { + if (ago == 0) return 0; + + LogPairPrices[] memory logPairPrices = findAllSamples(_miscData.oracleIndex(), ago); + + if (logPairPrices.length < 2) return 0; + + uint256[] memory ratesOfChange = new uint256[](logPairPrices.length - 1); + uint256 validValues; + uint256 timeDuration; + + for (uint256 i = 1; i < logPairPrices.length; i++) { + if (logPairPrices[i - 1].timestamp == 0) continue; + + uint256 price1 = WeightedOracleMath._fromLowResLog(logPairPrices[i - 1].logPairPrice); + uint256 price2 = WeightedOracleMath._fromLowResLog(logPairPrices[i].logPairPrice); + uint256 timestamp1 = ((logPairPrices[i - 1].timestamp)); + uint256 timestamp2 = ((logPairPrices[i].timestamp)); + + uint256 priceChange = _absoluteSubtraction(price2, price1); + uint256 priceChangeFraction = priceChange.divDown(price1); + uint256 timeChange = timestamp2 - timestamp1; + + uint256 rateOfChange = priceChangeFraction.divDown(timeChange) / FixedPoint.ONE; + + ratesOfChange[i - 1] = rateOfChange; + validValues += 1; + timeDuration += (timestamp2 - timestamp1); + } + + volatility = _calculateStdDev(ratesOfChange, validValues); + volatility = volatility.mulDown(timeDuration * FixedPoint.ONE).divDown(ago * FixedPoint.ONE); + + return volatility; + } + + function updateOracle(uint256 balanceToken0, uint256 balanceToken1) public { + bytes32 miscData = _miscData; + + int256 logSpotPrice = WeightedOracleMath._calcLogSpotPrice( + FixedPoint.ONE, + balanceToken0, + FixedPoint.ONE, + balanceToken1 + ); + + int256 logBPTPrice = WeightedOracleMath._calcLogBPTPrice( + FixedPoint.ONE, + balanceToken0, + miscData.logTotalSupply() + ); + + uint256 oracleCurrentIndex = miscData.oracleIndex(); + uint256 oracleCurrentSampleInitialTimestamp = miscData.oracleSampleCreationTimestamp(); + uint256 oracleUpdatedIndex = _processPriceData( + oracleCurrentSampleInitialTimestamp, + oracleCurrentIndex, + logSpotPrice + ); + + if (oracleCurrentIndex != oracleUpdatedIndex) { + miscData = miscData.setOracleIndex(oracleUpdatedIndex); + miscData = miscData.setOracleSampleCreationTimestamp(block.timestamp); + _miscData = miscData; + } + } + + function _calculateStdDev(uint256[] memory numbers, uint256 numbersLength) internal view returns (uint256) { + if (numbersLength == 0) return 0; + if (numbers.length == 0) return 0; + + uint256 sum = 0; + for (uint256 i = numbers.length - numbersLength; i < numbers.length; i++) { + sum += numbers[i]; + } + + uint256 mean = (sum) / numbersLength; + + uint256 sumSquaredDiff = 0; + for (uint256 i = numbers.length - numbersLength; i < numbers.length; i++) { + if (numbers[i] > mean) { + uint256 diff = ((numbers[i]) - mean); + sumSquaredDiff += (diff * diff); + } else { + uint256 diff = (mean - (numbers[i])); + sumSquaredDiff += (diff * diff); + } + } + + uint256 variance = (sumSquaredDiff) / numbersLength; + + return _customSqrt(variance); + } + + function _absoluteSubtraction(uint256 a, uint256 b) internal pure returns (uint256) { + int256 result = int256(a) - int256(b); + int256 absVal = result < 0 ? -result : result; + + return uint256(absVal); + } + + function _customSqrt(uint256 x) internal pure returns (uint256) { + uint256 z = (x + 1) / 2; + uint256 y = x; + while (z < y) { + y = z; + z = (x / z + z) / 2; + } + return y; + } +} diff --git a/packages/foundry/foundry.toml b/packages/foundry/foundry.toml index d106dfe9..2ede6dbe 100644 --- a/packages/foundry/foundry.toml +++ b/packages/foundry/foundry.toml @@ -3,6 +3,7 @@ src = 'contracts' out = 'out' libs = ['node_modules', 'lib'] test = 'test' +viaIR = true ffi = true solc_version = '0.8.24' auto_detect_solc = false diff --git a/packages/foundry/script/00_DeployMockTokens.s.sol b/packages/foundry/script/00_DeployMockTokens.s.sol index 4f26e9ca..a78cf78e 100644 --- a/packages/foundry/script/00_DeployMockTokens.s.sol +++ b/packages/foundry/script/00_DeployMockTokens.s.sol @@ -20,10 +20,10 @@ contract DeployMockTokens is ScaffoldHelpers { vm.startBroadcast(deployerPrivateKey); // Used to register & initialize pool contracts - mockToken1 = address(new MockToken1("Mock Token 1", "MT1", 1000e18)); - mockToken2 = address(new MockToken2("Mock Token 2", "MT2", 1000e18)); - console.log("MockToken1 deployed at: %s", mockToken1); - console.log("MockToken2 deployed at: %s", mockToken2); + mockToken1 = address(new MockToken1("StableCoin", "Stable", 1000e18)); + mockToken2 = address(new MockToken2("NewToken", "NewToken", 1000e18)); + console.log("StableCoin deployed at: %s", mockToken1); + console.log("NewToken deployed at: %s", mockToken2); // Used for the VeBALFeeDiscountHook mockVeBAL = address(new MockVeBAL("Vote-escrow BAL", "veBAL", 1000e18)); diff --git a/packages/foundry/script/04_DeployConstantProductPoolV2.s.sol b/packages/foundry/script/04_DeployConstantProductPoolV2.s.sol new file mode 100644 index 00000000..10633274 --- /dev/null +++ b/packages/foundry/script/04_DeployConstantProductPoolV2.s.sol @@ -0,0 +1,168 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { + TokenConfig, + TokenType, + LiquidityManagement, + PoolRoleAccounts +} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; +import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; + +import { PoolHelpers, CustomPoolConfig, InitializationConfig } from "./PoolHelpers.sol"; +import { ScaffoldHelpers, console } from "./ScaffoldHelpers.sol"; +import { ConstantProductFactoryV2 } from "../contracts/factories/ConstantProductFactoryV2.sol"; +import { VolatilityLoyaltyHook } from "../contracts/hooks/VolatilityLoyaltyModule/hooks/VolatilityLoyaltyHook.sol"; +import { VolatilityOracle } from "../contracts/hooks/VolatilityLoyaltyModule/volatility-module/VolatilityOracle.sol"; +import { LoyaltyDiscount } from "../contracts/hooks/VolatilityLoyaltyModule/loyalty-module/LoyaltyDiscount.sol"; +import { + VolatilityDiscount +} from "../contracts/hooks/VolatilityLoyaltyModule/volatility-module/VolatilityDiscount.sol"; + +/** + * @title Deploy Constant Product Pool + * @notice Deploys, registers, and initializes a constant product pool that uses a Lottery Hook + */ +contract DeployConstantProductPoolV2 is PoolHelpers, ScaffoldHelpers { + function deployConstantProductPoolV2(address token1, address token2) internal { + // Set the deployment configurations + CustomPoolConfig memory poolConfig = getProductPoolConfigV2(token1, token2); + InitializationConfig memory initConfig = getProductPoolInitConfigV2(token1, token2); + + // Start creating the transactions + uint256 deployerPrivateKey = getDeployerPrivateKey(); + vm.startBroadcast(deployerPrivateKey); + + // Deploy a factory + ConstantProductFactoryV2 factory = new ConstantProductFactoryV2(vault, 365 days); //pauseWindowDuration + console.log("Constant Product Factory deployed at: %s", address(factory)); + + // Deploy a hook + address volatilityLoyaltyHook = address( + new VolatilityLoyaltyHook( + vault, + token2, + address(new VolatilityOracle()), + address(new LoyaltyDiscount()), + address(new VolatilityDiscount()), + address(factory) + ) + ); + console.log("volatilityLoyaltyHook deployed at address: %s", volatilityLoyaltyHook); + + // Deploy a pool and register it with the vault + address pool = factory.create( + poolConfig.name, + poolConfig.symbol, + poolConfig.salt, + poolConfig.tokenConfigs, + poolConfig.swapFeePercentage, + poolConfig.protocolFeeExempt, + poolConfig.roleAccounts, + volatilityLoyaltyHook, + poolConfig.liquidityManagement + ); + console.log("Constant Product Pool deployed at: %s", pool); + + // Approve the router to spend tokens for pool initialization + approveRouterWithPermit2(initConfig.tokens); + + // Seed the pool with initial liquidity using Router as entrypoint + router.initialize( + pool, + initConfig.tokens, + initConfig.exactAmountsIn, + initConfig.minBptAmountOut, + initConfig.wethIsEth, + initConfig.userData + ); + console.log("Constant Product Pool initialized successfully!"); + vm.stopBroadcast(); + } + + /** + * @dev Set all of the configurations for deploying and registering a pool here + * @notice TokenConfig encapsulates the data required for the Vault to support a token of the given type. + * For STANDARD tokens, the rate provider address must be 0, and paysYieldFees must be false. + * All WITH_RATE tokens need a rate provider, and may or may not be yield-bearing. + */ + function getProductPoolConfigV2( + address token1, + address token2 + ) internal view returns (CustomPoolConfig memory config) { + string memory name = "Constant Product Pool"; // name for the pool + string memory symbol = "CPP"; // symbol for the BPT + bytes32 salt = keccak256(abi.encode(block.number)); // salt for the pool deployment via factory + uint256 swapFeePercentage = 0.02e18; // 2% + bool protocolFeeExempt = false; + address poolHooksContract = address(0); // zero address if no hooks contract is needed + + TokenConfig[] memory tokenConfigs = new TokenConfig[](2); // An array of descriptors for the tokens the pool will manage + tokenConfigs[0] = TokenConfig({ // Make sure to have proper token order (alphanumeric) + token: IERC20(token1), + tokenType: TokenType.STANDARD, // STANDARD or WITH_RATE + rateProvider: IRateProvider(address(0)), // The rate provider for a token (see further documentation above) + paysYieldFees: false // Flag indicating whether yield fees should be charged on this token + }); + tokenConfigs[1] = TokenConfig({ // Make sure to have proper token order (alphanumeric) + token: IERC20(token2), + tokenType: TokenType.STANDARD, // STANDARD or WITH_RATE + rateProvider: IRateProvider(address(0)), // The rate provider for a token (see further documentation above) + paysYieldFees: false // Flag indicating whether yield fees should be charged on this token + }); + + PoolRoleAccounts memory roleAccounts = PoolRoleAccounts({ + pauseManager: address(0), // Account empowered to pause/unpause the pool (or 0 to delegate to governance) + swapFeeManager: address(0), // Account empowered to set static swap fees for a pool (or 0 to delegate to goverance) + poolCreator: address(0) // Account empowered to set the pool creator fee percentage + }); + LiquidityManagement memory liquidityManagement = LiquidityManagement({ + disableUnbalancedLiquidity: true, // Must be true to register pool with the Lottery Hook + enableAddLiquidityCustom: false, + enableRemoveLiquidityCustom: false, + enableDonation: false + }); + + config = CustomPoolConfig({ + name: name, + symbol: symbol, + salt: salt, + tokenConfigs: sortTokenConfig(tokenConfigs), + swapFeePercentage: swapFeePercentage, + protocolFeeExempt: protocolFeeExempt, + roleAccounts: roleAccounts, + poolHooksContract: poolHooksContract, + liquidityManagement: liquidityManagement + }); + } + + /** + * @dev Set the pool initialization configurations here + * @notice This is where the amounts of tokens to Seed the pool with initial liquidity using Router as entrypoint are set + */ + function getProductPoolInitConfigV2( + address token1, + address token2 + ) internal pure returns (InitializationConfig memory config) { + IERC20[] memory tokens = new IERC20[](2); // Array of tokens to be used in the pool + tokens[0] = IERC20(token1); + tokens[1] = IERC20(token2); + uint256[] memory exactAmountsIn = new uint256[](2); // Exact amounts of tokens to be added, sorted in token alphanumeric order + exactAmountsIn[0] = 50e18; // amount of token1 to send during pool initialization + exactAmountsIn[1] = 50e18; // amount of token2 to send during pool initialization + uint256 minBptAmountOut = 49e18; // Minimum amount of pool tokens to be received + bool wethIsEth = false; // If true, incoming ETH will be wrapped to WETH; otherwise the Vault will pull WETH tokens + bytes memory userData = bytes(""); // Additional (optional) data required for adding initial liquidity + + config = InitializationConfig({ + tokens: InputHelpers.sortTokens(tokens), + exactAmountsIn: exactAmountsIn, + minBptAmountOut: minBptAmountOut, + wethIsEth: wethIsEth, + userData: userData + }); + } +} diff --git a/packages/foundry/script/Deploy.s.sol b/packages/foundry/script/Deploy.s.sol index 9efde461..ac20ad8f 100644 --- a/packages/foundry/script/Deploy.s.sol +++ b/packages/foundry/script/Deploy.s.sol @@ -6,6 +6,7 @@ import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; import { DeployMockTokens } from "./00_DeployMockTokens.s.sol"; import { DeployConstantSumPool } from "./01_DeployConstantSumPool.s.sol"; import { DeployConstantProductPool } from "./02_DeployConstantProductPool.s.sol"; +import { DeployConstantProductPoolV2 } from "./04_DeployConstantProductPoolV2.s.sol"; import { DeployWeightedPool8020 } from "./03_DeployWeightedPool8020.s.sol"; /** @@ -18,6 +19,7 @@ contract DeployScript is DeployMockTokens, DeployConstantSumPool, DeployConstantProductPool, + DeployConstantProductPoolV2, DeployWeightedPool8020 { function run() external scaffoldExport { @@ -29,6 +31,7 @@ contract DeployScript is // Deploy, register, and initialize a constant product pool with a lottery hook deployConstantProductPool(mockToken1, mockToken2); + deployConstantProductPoolV2(mockToken1, mockToken2); // Deploy, register, and initialize a weighted pool with an exit fee hook deployWeightedPool8020(mockToken1, mockToken2); diff --git a/packages/nextjs/app/pools/_components/PoolSelector.tsx b/packages/nextjs/app/pools/_components/PoolSelector.tsx index 70fefedd..f3f0bfd7 100644 --- a/packages/nextjs/app/pools/_components/PoolSelector.tsx +++ b/packages/nextjs/app/pools/_components/PoolSelector.tsx @@ -13,11 +13,12 @@ type PoolSelectorProps = { export const PoolSelector = ({ setSelectedPoolAddress, selectedPoolAddress }: PoolSelectorProps) => { const [inputValue, setInputValue] = useState(""); - const { sumPools, productPools, weightedPools } = useFactoryHistory(); + const { sumPools, productPools, productPoolsV2, weightedPools } = useFactoryHistory(); const poolTypes = [ { label: "Constant Sum", addresses: sumPools }, { label: "Constant Product", addresses: productPools }, + { label: "Constant Product V2", addresses: productPoolsV2 }, { label: "Weighted", addresses: weightedPools }, ]; diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index 5788ab86..6693be08 100644 --- a/packages/nextjs/contracts/deployedContracts.ts +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -7,7 +7,7 @@ import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; const deployedContracts = { 31337: { MockToken1: { - address: "0xfdc90fb27105f322b384af5c3a39183047dec080", + address: "0x578330CfDA77A73bD79D3285759039045Aa7e6e6", abi: [ { type: "constructor", @@ -365,7 +365,7 @@ const deployedContracts = { }, }, MockToken2: { - address: "0xd2ed70a2ddc08f9e302b5298ef2e656959e79dd5", + address: "0x36952e04bB1604fE0d5148e9885e7e2556639f19", abi: [ { type: "constructor", @@ -723,7 +723,7 @@ const deployedContracts = { }, }, MockVeBAL: { - address: "0x8e18528aa76be18c51653a3f61fe79cea130620f", + address: "0xc561B15c374B604E5ea6Df6945EbE30AF3d9831d", abi: [ { type: "constructor", @@ -1081,7 +1081,7 @@ const deployedContracts = { }, }, ConstantSumFactory: { - address: "0x1f16730c011b43dc6a62259b30e5721265883dde", + address: "0x62847f21e852AC6E687348c85f76e0b607231726", abi: [ { type: "constructor", @@ -1465,7 +1465,7 @@ const deployedContracts = { }, }, VeBALFeeDiscountHookExample: { - address: "0x0d5e217f22a9f92f1dd2d5d5ede64ffc913a626d", + address: "0xb1b3DEbF732F7EE11aF06172235C64F591A560ff", abi: [ { type: "constructor", @@ -2175,7 +2175,7 @@ const deployedContracts = { }, }, ConstantProductFactory: { - address: "0x35b2e11b8c2b27fd74bd28da018eee10a67c95a8", + address: "0x979f7441cCB6135B774d762B5Fc3cb292B5a60D0", abi: [ { type: "constructor", @@ -2559,7 +2559,7 @@ const deployedContracts = { }, }, LotteryHookExample: { - address: "0xf92fb772445695d6d5a5a4dbbfd2d886b1500a52", + address: "0x89d9136C49ec7dfC7fC2979819ae59eD4dC5896f", abi: [ { type: "constructor", @@ -3512,8 +3512,1521 @@ const deployedContracts = { transferOwnership: "lib/openzeppelin-contracts/contracts/access/Ownable.sol", }, }, + ConstantProductFactoryV2: { + address: "0x14b2580D8c0126206A47fB6E0b9D04ca9b0a68Af", + abi: [ + { + type: "constructor", + inputs: [ + { + name: "vault", + type: "address", + internalType: "contract IVault", + }, + { + name: "pauseWindowDuration", + type: "uint32", + internalType: "uint32", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "create", + inputs: [ + { + name: "name", + type: "string", + internalType: "string", + }, + { + name: "symbol", + type: "string", + internalType: "string", + }, + { + name: "salt", + type: "bytes32", + internalType: "bytes32", + }, + { + name: "tokens", + type: "tuple[]", + internalType: "struct TokenConfig[]", + components: [ + { + name: "token", + type: "address", + internalType: "contract IERC20", + }, + { + name: "tokenType", + type: "uint8", + internalType: "enum TokenType", + }, + { + name: "rateProvider", + type: "address", + internalType: "contract IRateProvider", + }, + { + name: "paysYieldFees", + type: "bool", + internalType: "bool", + }, + ], + }, + { + name: "swapFeePercentage", + type: "uint256", + internalType: "uint256", + }, + { + name: "protocolFeeExempt", + type: "bool", + internalType: "bool", + }, + { + name: "roleAccounts", + type: "tuple", + internalType: "struct PoolRoleAccounts", + components: [ + { + name: "pauseManager", + type: "address", + internalType: "address", + }, + { + name: "swapFeeManager", + type: "address", + internalType: "address", + }, + { + name: "poolCreator", + type: "address", + internalType: "address", + }, + ], + }, + { + name: "poolHooksContract", + type: "address", + internalType: "address", + }, + { + name: "liquidityManagement", + type: "tuple", + internalType: "struct LiquidityManagement", + components: [ + { + name: "disableUnbalancedLiquidity", + type: "bool", + internalType: "bool", + }, + { + name: "enableAddLiquidityCustom", + type: "bool", + internalType: "bool", + }, + { + name: "enableRemoveLiquidityCustom", + type: "bool", + internalType: "bool", + }, + { + name: "enableDonation", + type: "bool", + internalType: "bool", + }, + ], + }, + ], + outputs: [ + { + name: "pool", + type: "address", + internalType: "address", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "disable", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "getActionId", + inputs: [ + { + name: "selector", + type: "bytes4", + internalType: "bytes4", + }, + ], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getAuthorizer", + inputs: [], + outputs: [ + { + name: "", + type: "address", + internalType: "contract IAuthorizer", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getDefaultLiquidityManagement", + inputs: [], + outputs: [ + { + name: "liquidityManagement", + type: "tuple", + internalType: "struct LiquidityManagement", + components: [ + { + name: "disableUnbalancedLiquidity", + type: "bool", + internalType: "bool", + }, + { + name: "enableAddLiquidityCustom", + type: "bool", + internalType: "bool", + }, + { + name: "enableRemoveLiquidityCustom", + type: "bool", + internalType: "bool", + }, + { + name: "enableDonation", + type: "bool", + internalType: "bool", + }, + ], + }, + ], + stateMutability: "pure", + }, + { + type: "function", + name: "getDefaultPoolHooksContract", + inputs: [], + outputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + ], + stateMutability: "pure", + }, + { + type: "function", + name: "getDeploymentAddress", + inputs: [ + { + name: "salt", + type: "bytes32", + internalType: "bytes32", + }, + ], + outputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getNewPoolPauseWindowEndTime", + inputs: [], + outputs: [ + { + name: "", + type: "uint32", + internalType: "uint32", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getOriginalPauseWindowEndTime", + inputs: [], + outputs: [ + { + name: "", + type: "uint32", + internalType: "uint32", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getPauseWindowDuration", + inputs: [], + outputs: [ + { + name: "", + type: "uint32", + internalType: "uint32", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getVault", + inputs: [], + outputs: [ + { + name: "", + type: "address", + internalType: "contract IVault", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "isDisabled", + inputs: [], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "isPoolFromFactory", + inputs: [ + { + name: "pool", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "event", + name: "FactoryDisabled", + inputs: [], + anonymous: false, + }, + { + type: "event", + name: "PoolCreated", + inputs: [ + { + name: "pool", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "error", + name: "Disabled", + inputs: [], + }, + { + type: "error", + name: "OnlyTwoTokenPoolsAllowed", + inputs: [], + }, + { + type: "error", + name: "PoolPauseWindowDurationOverflow", + inputs: [], + }, + { + type: "error", + name: "SenderNotAllowed", + inputs: [], + }, + { + type: "error", + name: "StandardPoolWithCreator", + inputs: [], + }, + ], + inheritedFunctions: { + disable: "lib/balancer-v3-monorepo/pkg/pool-utils/contracts/BasePoolFactory.sol", + getActionId: "lib/balancer-v3-monorepo/pkg/pool-utils/contracts/BasePoolFactory.sol", + getAuthorizer: "lib/balancer-v3-monorepo/pkg/pool-utils/contracts/BasePoolFactory.sol", + getDefaultLiquidityManagement: "lib/balancer-v3-monorepo/pkg/pool-utils/contracts/BasePoolFactory.sol", + getDefaultPoolHooksContract: "lib/balancer-v3-monorepo/pkg/pool-utils/contracts/BasePoolFactory.sol", + getDeploymentAddress: "lib/balancer-v3-monorepo/pkg/pool-utils/contracts/BasePoolFactory.sol", + getNewPoolPauseWindowEndTime: "lib/balancer-v3-monorepo/pkg/pool-utils/contracts/BasePoolFactory.sol", + getOriginalPauseWindowEndTime: "lib/balancer-v3-monorepo/pkg/pool-utils/contracts/BasePoolFactory.sol", + getPauseWindowDuration: "lib/balancer-v3-monorepo/pkg/pool-utils/contracts/BasePoolFactory.sol", + getVault: "lib/balancer-v3-monorepo/pkg/pool-utils/contracts/BasePoolFactory.sol", + isDisabled: "lib/balancer-v3-monorepo/pkg/pool-utils/contracts/BasePoolFactory.sol", + isPoolFromFactory: "lib/balancer-v3-monorepo/pkg/pool-utils/contracts/BasePoolFactory.sol", + }, + }, + VolatilityOracle: { + address: "0x6164eC62C3c1F8e273E59C4C7BC96d835c64CA70", + abi: [ + { + type: "function", + name: "findAllSamples", + inputs: [ + { + name: "latestIndex", + type: "uint256", + internalType: "uint256", + }, + { + name: "ago", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "", + type: "tuple[]", + internalType: "struct PoolPriceOracle.LogPairPrices[]", + components: [ + { + name: "logPairPrice", + type: "int256", + internalType: "int256", + }, + { + name: "timestamp", + type: "uint256", + internalType: "uint256", + }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getSample", + inputs: [ + { + name: "index", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "logPairPrice", + type: "int256", + internalType: "int256", + }, + { + name: "timestamp", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getVolatility", + inputs: [ + { + name: "ago", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "volatility", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "updateOracle", + inputs: [ + { + name: "balanceToken0", + type: "uint256", + internalType: "uint256", + }, + { + name: "balanceToken1", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "error", + name: "InvalidExponent", + inputs: [], + }, + { + type: "error", + name: "OutOfBounds", + inputs: [], + }, + { + type: "error", + name: "ZeroDivision", + inputs: [], + }, + ], + inheritedFunctions: { + getVolatility: "contracts/hooks/VolatilityLoyaltyModule/volatility-module/IVolatilityOracle.sol", + updateOracle: "contracts/hooks/VolatilityLoyaltyModule/volatility-module/IVolatilityOracle.sol", + findAllSamples: "contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/PoolPriceOracle.sol", + getSample: "contracts/hooks/VolatilityLoyaltyModule/balancer-v2-oracle/PoolPriceOracle.sol", + }, + }, + LoyaltyDiscount: { + address: "0x222a6E4E4796Fb50e67dF18B1cf62E62311965ad", + abi: [ + { + type: "function", + name: "getSwapFeeWithLoyaltyDiscount", + inputs: [ + { + name: "user", + type: "address", + internalType: "address", + }, + { + name: "staticSwapFeePercentage", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "updateLoyaltyDataForUser", + inputs: [ + { + name: "user", + type: "address", + internalType: "address", + }, + { + name: "tokenAddress", + type: "address", + internalType: "address", + }, + { + name: "tokenIn", + type: "address", + internalType: "contract IERC20", + }, + { + name: "amountInScaled18", + type: "uint256", + internalType: "uint256", + }, + { + name: "amountOutScaled18", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "userLoyaltyData", + inputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "firstTransactionTimestamp", + type: "uint256", + internalType: "uint256", + }, + { + name: "cumulativeLoyalty", + type: "uint256", + internalType: "uint256", + }, + { + name: "tokens", + type: "uint256", + internalType: "uint256", + }, + { + name: "lastTimestamp", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + ], + inheritedFunctions: { + getSwapFeeWithLoyaltyDiscount: "contracts/hooks/VolatilityLoyaltyModule/loyalty-module/ILoyaltyDiscount.sol", + updateLoyaltyDataForUser: "contracts/hooks/VolatilityLoyaltyModule/loyalty-module/ILoyaltyDiscount.sol", + }, + }, + VolatilityDiscount: { + address: "0x8db3E66F8b65afFa37fA156E2B4084e604D77822", + abi: [ + { + type: "function", + name: "getVolatilityFeePercent", + inputs: [ + { + name: "oracleAddress", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + ], + inheritedFunctions: { + getVolatilityFeePercent: "contracts/hooks/VolatilityLoyaltyModule/volatility-module/IVolatilityDiscount.sol", + }, + }, + VolatilityLoyaltyHook: { + address: "0x9CEDB51a11B1316a4BDB165364d3d61d8ace6636", + abi: [ + { + type: "constructor", + inputs: [ + { + name: "vault", + type: "address", + internalType: "contract IVault", + }, + { + name: "tokenAddress", + type: "address", + internalType: "address", + }, + { + name: "oracleAddress", + type: "address", + internalType: "address", + }, + { + name: "loyaltyModuleAddress", + type: "address", + internalType: "address", + }, + { + name: "volatilityModuleAddress", + type: "address", + internalType: "address", + }, + { + name: "allowedFactory", + type: "address", + internalType: "address", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "changeLoyaltyDiscountSetting", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "changeVolatilityFeeSetting", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "getHookFlags", + inputs: [], + outputs: [ + { + name: "hookFlags", + type: "tuple", + internalType: "struct HookFlags", + components: [ + { + name: "enableHookAdjustedAmounts", + type: "bool", + internalType: "bool", + }, + { + name: "shouldCallBeforeInitialize", + type: "bool", + internalType: "bool", + }, + { + name: "shouldCallAfterInitialize", + type: "bool", + internalType: "bool", + }, + { + name: "shouldCallComputeDynamicSwapFee", + type: "bool", + internalType: "bool", + }, + { + name: "shouldCallBeforeSwap", + type: "bool", + internalType: "bool", + }, + { + name: "shouldCallAfterSwap", + type: "bool", + internalType: "bool", + }, + { + name: "shouldCallBeforeAddLiquidity", + type: "bool", + internalType: "bool", + }, + { + name: "shouldCallAfterAddLiquidity", + type: "bool", + internalType: "bool", + }, + { + name: "shouldCallBeforeRemoveLiquidity", + type: "bool", + internalType: "bool", + }, + { + name: "shouldCallAfterRemoveLiquidity", + type: "bool", + internalType: "bool", + }, + ], + }, + ], + stateMutability: "pure", + }, + { + type: "function", + name: "getSwapFeeWithLoyaltyDiscount", + inputs: [ + { + name: "user", + type: "address", + internalType: "address", + }, + { + name: "staticSwapFeePercentage", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getVolatilityFee", + inputs: [], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "isLoyaltyDiscountEnabled", + inputs: [], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "isVolatilityFeeEnabled", + inputs: [], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "onAfterAddLiquidity", + inputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + { + name: "", + type: "address", + internalType: "address", + }, + { + name: "", + type: "uint8", + internalType: "enum AddLiquidityKind", + }, + { + name: "", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "amountsInRaw", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "", + type: "uint256", + internalType: "uint256", + }, + { + name: "balancesScaled18", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + { + name: "", + type: "uint256[]", + internalType: "uint256[]", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "onAfterInitialize", + inputs: [ + { + name: "", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "", + type: "uint256", + internalType: "uint256", + }, + { + name: "", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "onAfterRemoveLiquidity", + inputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + { + name: "", + type: "address", + internalType: "address", + }, + { + name: "", + type: "uint8", + internalType: "enum RemoveLiquidityKind", + }, + { + name: "", + type: "uint256", + internalType: "uint256", + }, + { + name: "", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "amountsOutRaw", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "balancesScaled18", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + { + name: "", + type: "uint256[]", + internalType: "uint256[]", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "onAfterSwap", + inputs: [ + { + name: "params", + type: "tuple", + internalType: "struct AfterSwapParams", + components: [ + { + name: "kind", + type: "uint8", + internalType: "enum SwapKind", + }, + { + name: "tokenIn", + type: "address", + internalType: "contract IERC20", + }, + { + name: "tokenOut", + type: "address", + internalType: "contract IERC20", + }, + { + name: "amountInScaled18", + type: "uint256", + internalType: "uint256", + }, + { + name: "amountOutScaled18", + type: "uint256", + internalType: "uint256", + }, + { + name: "tokenInBalanceScaled18", + type: "uint256", + internalType: "uint256", + }, + { + name: "tokenOutBalanceScaled18", + type: "uint256", + internalType: "uint256", + }, + { + name: "amountCalculatedScaled18", + type: "uint256", + internalType: "uint256", + }, + { + name: "amountCalculatedRaw", + type: "uint256", + internalType: "uint256", + }, + { + name: "router", + type: "address", + internalType: "address", + }, + { + name: "pool", + type: "address", + internalType: "address", + }, + { + name: "userData", + type: "bytes", + internalType: "bytes", + }, + ], + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "onBeforeAddLiquidity", + inputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + { + name: "", + type: "address", + internalType: "address", + }, + { + name: "", + type: "uint8", + internalType: "enum AddLiquidityKind", + }, + { + name: "", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "", + type: "uint256", + internalType: "uint256", + }, + { + name: "", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "onBeforeInitialize", + inputs: [ + { + name: "", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "onBeforeRemoveLiquidity", + inputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + { + name: "", + type: "address", + internalType: "address", + }, + { + name: "", + type: "uint8", + internalType: "enum RemoveLiquidityKind", + }, + { + name: "", + type: "uint256", + internalType: "uint256", + }, + { + name: "", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "onBeforeSwap", + inputs: [ + { + name: "", + type: "tuple", + internalType: "struct PoolSwapParams", + components: [ + { + name: "kind", + type: "uint8", + internalType: "enum SwapKind", + }, + { + name: "amountGivenScaled18", + type: "uint256", + internalType: "uint256", + }, + { + name: "balancesScaled18", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "indexIn", + type: "uint256", + internalType: "uint256", + }, + { + name: "indexOut", + type: "uint256", + internalType: "uint256", + }, + { + name: "router", + type: "address", + internalType: "address", + }, + { + name: "userData", + type: "bytes", + internalType: "bytes", + }, + ], + }, + { + name: "", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "onComputeDynamicSwapFeePercentage", + inputs: [ + { + name: "params", + type: "tuple", + internalType: "struct PoolSwapParams", + components: [ + { + name: "kind", + type: "uint8", + internalType: "enum SwapKind", + }, + { + name: "amountGivenScaled18", + type: "uint256", + internalType: "uint256", + }, + { + name: "balancesScaled18", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "indexIn", + type: "uint256", + internalType: "uint256", + }, + { + name: "indexOut", + type: "uint256", + internalType: "uint256", + }, + { + name: "router", + type: "address", + internalType: "address", + }, + { + name: "userData", + type: "bytes", + internalType: "bytes", + }, + ], + }, + { + name: "", + type: "address", + internalType: "address", + }, + { + name: "staticSwapFeePercentage", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "onRegister", + inputs: [ + { + name: "factory", + type: "address", + internalType: "address", + }, + { + name: "pool", + type: "address", + internalType: "address", + }, + { + name: "", + type: "tuple[]", + internalType: "struct TokenConfig[]", + components: [ + { + name: "token", + type: "address", + internalType: "contract IERC20", + }, + { + name: "tokenType", + type: "uint8", + internalType: "enum TokenType", + }, + { + name: "rateProvider", + type: "address", + internalType: "contract IRateProvider", + }, + { + name: "paysYieldFees", + type: "bool", + internalType: "bool", + }, + ], + }, + { + name: "", + type: "tuple", + internalType: "struct LiquidityManagement", + components: [ + { + name: "disableUnbalancedLiquidity", + type: "bool", + internalType: "bool", + }, + { + name: "enableAddLiquidityCustom", + type: "bool", + internalType: "bool", + }, + { + name: "enableRemoveLiquidityCustom", + type: "bool", + internalType: "bool", + }, + { + name: "enableDonation", + type: "bool", + internalType: "bool", + }, + ], + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "owner", + inputs: [], + outputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "renounceOwnership", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "transferOwnership", + inputs: [ + { + name: "newOwner", + type: "address", + internalType: "address", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "event", + name: "OwnershipTransferred", + inputs: [ + { + name: "previousOwner", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "newOwner", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "PriceDataUpdated", + inputs: [ + { + name: "tokenOutBalanceScaled18", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { + name: "tokenInBalanceScaled18", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { + name: "tokenPrice", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "error", + name: "OwnableInvalidOwner", + inputs: [ + { + name: "owner", + type: "address", + internalType: "address", + }, + ], + }, + { + type: "error", + name: "OwnableUnauthorizedAccount", + inputs: [ + { + name: "account", + type: "address", + internalType: "address", + }, + ], + }, + { + type: "error", + name: "SenderIsNotVault", + inputs: [ + { + name: "sender", + type: "address", + internalType: "address", + }, + ], + }, + ], + inheritedFunctions: { + getHookFlags: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", + onAfterAddLiquidity: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", + onAfterInitialize: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", + onAfterRemoveLiquidity: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", + onAfterSwap: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", + onBeforeAddLiquidity: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", + onBeforeInitialize: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", + onBeforeRemoveLiquidity: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", + onBeforeSwap: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", + onComputeDynamicSwapFeePercentage: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", + onRegister: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", + owner: "lib/openzeppelin-contracts/contracts/access/Ownable.sol", + renounceOwnership: "lib/openzeppelin-contracts/contracts/access/Ownable.sol", + transferOwnership: "lib/openzeppelin-contracts/contracts/access/Ownable.sol", + }, + }, WeightedPoolFactory: { - address: "0x6def85e32294b59ad5084a12304d1284737b4389", + address: "0x0c62E2Ed6d98D88Bdb9E960423Ba3E88fb579fFE", abi: [ { type: "constructor", @@ -3918,7 +5431,7 @@ const deployedContracts = { }, }, ExitFeeHookExample: { - address: "0x8a5450ce448a84ac5e6aea0ca03fb16d590d6227", + address: "0xb2800B0609b64F4f16ab999CCD42B3ba39BaC112", abi: [ { type: "constructor", diff --git a/packages/nextjs/hooks/balancer/useFactoryHistory.ts b/packages/nextjs/hooks/balancer/useFactoryHistory.ts index ff33c10a..ff37c402 100644 --- a/packages/nextjs/hooks/balancer/useFactoryHistory.ts +++ b/packages/nextjs/hooks/balancer/useFactoryHistory.ts @@ -8,6 +8,7 @@ const FROM_BLOCK_NUMBER = 6563900n; export const useFactoryHistory = () => { const [sumPools, setSumPools] = useState([]); const [productPools, setProductPools] = useState([]); + const [productPoolsV2, setProductPoolsV2] = useState([]); const [weightedPools, setWeightedPools] = useState([]); useScaffoldEventSubscriber({ @@ -36,6 +37,19 @@ export const useFactoryHistory = () => { }, }); + useScaffoldEventSubscriber({ + contractName: "ConstantProductFactoryV2", + eventName: "PoolCreated", + listener: logs => { + logs.forEach(log => { + const { pool } = log.args; + if (pool) { + setProductPoolsV2(pools => [...pools, pool]); + } + }); + }, + }); + useScaffoldEventSubscriber({ contractName: "WeightedPoolFactory", eventName: "PoolCreated", @@ -62,6 +76,12 @@ export const useFactoryHistory = () => { fromBlock: FROM_BLOCK_NUMBER, }); + const { data: productPoolHistoryV2, isLoading: isLoadingProductPoolHistoryV2 } = useScaffoldEventHistory({ + contractName: "ConstantProductFactoryV2", + eventName: "PoolCreated", + fromBlock: FROM_BLOCK_NUMBER, + }); + const { data: weightedPoolHistory, isLoading: isLoadingWeightedPoolHistory } = useScaffoldEventHistory({ contractName: "WeightedPoolFactory", eventName: "PoolCreated", @@ -94,6 +114,19 @@ export const useFactoryHistory = () => { }, }); + useScaffoldEventSubscriber({ + contractName: "ConstantProductFactoryV2", + eventName: "PoolCreated", + listener: logs => { + logs.forEach(log => { + const { pool } = log.args; + if (pool) { + setProductPoolsV2(pools => [...pools, pool]); + } + }); + }, + }); + useScaffoldEventSubscriber({ contractName: "WeightedPoolFactory", eventName: "PoolCreated", @@ -112,9 +145,11 @@ export const useFactoryHistory = () => { if ( !isLoadingSumPoolHistory && !isLoadingProductPoolHistory && + !isLoadingProductPoolHistoryV2 && !isLoadingWeightedPoolHistory && sumPoolHistory && productPoolHistory && + productPoolHistoryV2 && weightedPoolHistory ) { const sumPools = sumPoolHistory @@ -129,6 +164,12 @@ export const useFactoryHistory = () => { }) .filter((pool): pool is Address => typeof pool === "string"); + const productPoolsV2 = productPoolHistoryV2 + .map(({ args }) => { + if (args.pool && isAddress(args.pool)) return args.pool; + }) + .filter((pool): pool is Address => typeof pool === "string"); + const weightedPools = weightedPoolHistory .map(({ args }) => { if (args.pool && isAddress(args.pool)) return args.pool; @@ -136,6 +177,7 @@ export const useFactoryHistory = () => { .filter((pool): pool is Address => typeof pool === "string"); setProductPools(productPools); + setProductPoolsV2(productPoolsV2); setSumPools(sumPools); setWeightedPools(weightedPools); } @@ -144,12 +186,14 @@ export const useFactoryHistory = () => { [ sumPoolHistory, productPoolHistory, + productPoolHistoryV2, weightedPoolHistory, isLoadingSumPoolHistory, isLoadingProductPoolHistory, + isLoadingProductPoolHistoryV2, isLoadingWeightedPoolHistory, ], ); - return { sumPools, productPools, weightedPools }; + return { sumPools, productPools, productPoolsV2, weightedPools }; };