diff --git a/README.md b/README.md index 1397b2da..019a0b93 100644 --- a/README.md +++ b/README.md @@ -1,301 +1,28 @@ -# πŸ—οΈŽ Scaffold Balancer v3 +# Insurance Premium Hook -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. +Insurance is crucial for new or high-risk protocols in DeFi to provide coverage in the event of failures or other risks. However, for the insurance to be effective, a premium must be paid to the insurer. This hook collects a fee when liquidity is added or removed proportionally, and uses it to pay the insurance premium. -[![intro-to-scaffold-balancer](https://github.com/user-attachments/assets/f862091d-2fe9-4b4b-8d70-cb2fdc667384)](https://www.youtube.com/watch?v=m6q5M34ZdXw) +![diagram](./diagram.png) -### πŸ” Development Life Cycle +Currently, the hook does this: +1. Takes a fee when the addition/removal of liquidity is proportionalΒΉ +2. Uses the fee to add liquidity back and receives a BPT +3. Deposits the BPT into an [atomica pool](https://www.atomica.org/), since an atomica pool is basically an ERC4626, the user will receive the shares of the atomica pool, essentially wrappedBPTs +4. The hook will also work as an outflow processor for the atomica pool, it will determine permissionlessly if a user can be granted an outflow request based on when he deposited, this is decided based on when the premium was deposited, if the premium was deposited more than 10 days ago, then the request is approved, otherwise it is rejectedΒ². -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/) +ΒΉ It needs to be proportional because of how the hooks work now, as of now, from the current documentation there seem to be no way of receiving directly the bpts as fee, and using an unproportional amount of tokens would require the hook to receive the fee as bpts -### πŸͺ§ Table Of Contents +Β² For example this is done to insure that no predatorial funds can come in and claim, an insurance refund on a stablecoin after it has already depegged. -- [πŸ§‘β€πŸ’» 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) +# Roadmap -## πŸ§‘β€πŸ’» Environment Setup +![roadmap](./roadmap.png) -### 1. Requirements πŸ“œ +For now this is more of a proof of concept and can be improved upon greatly. +The improvements are: +Phase 1: (we are currently in this phase) Present the idea on the hackaton and see if it has traction. +Phase 2: Improve the calculation of the fee from a static fee decided by an owner to a dynamic fee that is calculated based on how much insurance is needed for the pool +Phase 3: Improve the permissionaless side of the hook and make it act as an outflow processor also for the insurer side (right now is being an outflow processor only for the user) +Phase 4: Being able to receive directly BPTs (so even liquidity that is added or removed not proportionally) and being able to insure also only 1 of the tokens of the pool -- [Node (>= v18.17)](https://nodejs.org/en/download/) -- Yarn ([v1](https://classic.yarnpkg.com/en/docs/install/) or [v2+](https://yarnpkg.com/getting-started/install)) -- [Git](https://git-scm.com/downloads) -- [Foundry](https://book.getfoundry.sh/getting-started/installation) (>= v0.2.0) - -### 2. Quickstart πŸƒ - -1. Ensure you have the latest version of foundry installed - -``` -foundryup -``` - -2. Clone this repo & install dependencies - -```bash -git clone https://github.com/balancer/scaffold-balancer-v3.git -cd scaffold-balancer-v3 -yarn install -``` - -3. Set a `SEPOLIA_RPC_URL` in the `packages/foundry/.env` file - -``` -SEPOLIA_RPC_URL=... -``` - -4. Start a local anvil fork of the Sepolia testnet - -```bash -yarn fork -``` - -5. Deploy the mock tokens, pool factories, pool hooks, and custom pools contracts - > By default, the anvil account #0 will be the deployer and recieve the mock tokens and BPT from pool initialization - -```bash -yarn deploy -``` - -6. Start the nextjs frontend - -```bash -yarn start -``` - -7. Explore the frontend - -- Navigate to http://localhost:3000 to see the home page -- Visit the [Pools Page](http://localhost:3000/pools) to search by address or select using the pool buttons -- Vist the [Debug Page](http://localhost:3000/debug) to see the mock tokens, factory, and hooks contracts - -8. Run the Foundry tests - -``` -yarn test -``` - -### 3. Scaffold ETH 2 Tips πŸ—οΈ - -SE-2 offers a variety of configuration options for connecting an account, choosing networks, and deploying contracts - -
πŸ”₯ Burner Wallet - -If you do not have an active wallet extension connected to your web browser, then scaffold eth will automatically connect to a "burner wallet" that is randomly generated on the frontend and saved to the browser's local storage. When using the burner wallet, transactions will be instantly signed, which is convenient for quick iterative development. - -To force the use of burner wallet, disable your browsers wallet extensions and refresh the page. Note that the burner wallet comes with 0 ETH to pay for gas so you will need to click the faucet button in top right corner. Also the mock tokens for the pool are minted to your deployer account set in `.env` so you will want to navigate to the "Debug Contracts" page to mint your burner wallet some mock tokens to use with the pool. - -![Burner Wallet](https://github.com/Dev-Rel-as-a-Service/scaffold-balancer-v3/assets/73561520/0a1f3456-f22a-46b5-9e05-0ef5cd17cce7) - -![Debug Tab Mint](https://github.com/Dev-Rel-as-a-Service/scaffold-balancer-v3/assets/73561520/fbb53772-8f6d-454d-a153-0e7a2925ef9f) - -
- -
πŸ‘› Browser Extension Wallet - -- To use your preferred browser extension wallet, ensure that the account you are using matches the PK you previously provided in the `foundry/.env` file -- You may need to add a local development network with rpc url `http://127.0.0.1:8545/` and chain id `31337`. Also, you may need to reset the nonce data for your wallet exension if it gets out of sync. - -
- -
πŸ› Debug Contracts Page - -The [Debug Contracts Page](http://localhost:3000/debug) can be useful for viewing and interacting with all of the externally avaiable read and write functions of a contract. The page will automatically hot reload with contracts that are deployed via the `01_DeployConstantSumFactory.s.sol` script. We use this handy setup to mint `mockERC20` tokens to any connected wallet - -
- -
🌐 Changing The Frontend Network Connection - -- The network the frontend points at is set via `targetNetworks` in the `scaffold.config.ts` file using `chains` from viem. -- By default, the frontend runs on a local node at `http://127.0.0.1:8545` - -```typescript -const scaffoldConfig = { - targetNetworks: [chains.foundry], -``` - -
- -
🍴 Changing The Forked Network - -- By default, the `yarn fork` command points at sepolia, but any of the network aliases from the `[rpc_endpoints]` of `foundry.toml` can be used to modify the `"fork"` alias in the `packages/foundry/package.json` file - -```json - "fork": "anvil --fork-url ${0:-sepolia} --chain-id 31337 --config-out localhost.json", -``` - -- To point the frontend at a different forked network, change the `targetFork` in `scaffold.config.ts` - -```typescript -const scaffoldConfig = { - // The networks the frontend can connect to - targetNetworks: [chains.foundry], - - // If using chains.foundry as your targetNetwork, you must specify a network to fork - targetFork: chains.sepolia, -``` - -
- -## πŸ‘©β€πŸ« Learn Core Concepts - -- [Contract Architecture](https://docs-v3.balancer.fi/concepts/core-concepts/architecture.html) -- [Balancer Pool Tokens](https://docs-v3.balancer.fi/concepts/core-concepts/balancer-pool-tokens.html) -- [Balancer Pool Types](https://docs-v3.balancer.fi/concepts/explore-available-balancer-pools/) -- [Building Custom AMMs](https://docs-v3.balancer.fi/build-a-custom-amm/) -- [Exploring Hooks and Custom Routers](https://pitchandrolls.com/2024/08/30/unlocking-the-power-of-balancer-v3-exploring-hooks-and-custom-routers/) -- [Hook Development Tips](https://medium.com/@johngrant/unlocking-the-power-of-balancer-v3-hook-development-made-simple-831391a68296) - -![v3-components](https://github.com/user-attachments/assets/ccda9323-790f-4276-b092-c867fd80bf9e) - -## πŸ•΅οΈ Explore the Examples - -Each of the following examples have turn key deploy scripts that can be found in the [foundry/script/](https://github.com/balancer/scaffold-balancer-v3/tree/main/packages/foundry/script) directory - -### 1. Constant Sum Pool with Dynamic Swap Fee Hook - -The swap fee percentage is altered by the hook contract before the pool calculates the amount for the swap - -![dynamic-fee-hook](https://github.com/user-attachments/assets/5ba69ea3-6894-4eeb-befa-ed87cfeb6b13) - -### 2. Constant Product Pool with Lottery Hook - -An after swap hook makes a request to an oracle contract for a random number - -![after-swap-hook](https://github.com/user-attachments/assets/594ce1ac-2edc-4d16-9631-14feb2d085f8) - -### 3. Weighted Pool with Exit Fee Hook - -An after remove liquidity hook adjusts the amounts before the vault transfers tokens to the user - -![after-remove-liquidity-hook](https://github.com/user-attachments/assets/2e8f4a5c-f168-4021-b316-28a79472c8d1) - -## 🌊 Create a Custom Pool - -[![custom-amm-video](https://github.com/user-attachments/assets/e6069a51-f1b5-4f98-a2a9-3a2098696f96)](https://www.youtube.com/watch?v=kXynS3jAu0M) - -### 1. Review the Docs πŸ“– - -- [Create a custom AMM with a novel invariant](https://docs-v3.balancer.fi/build-a-custom-amm/build-an-amm/create-custom-amm-with-novel-invariant.html) - -### 2. Recall the Key Requirements πŸ”‘ - -- Must inherit from `IBasePool` and `BalancerPoolToken` -- Must implement `onSwap`, `computeInvariant`, and `computeBalance` -- Must implement `getMaximumSwapFeePercentage` and `getMinimumSwapFeePercentage` - -### 3. Write a Custom Pool Contract πŸ“ - -- To get started, edit the`ConstantSumPool.sol` contract directly or make a copy - -## 🏭 Create a Pool Factory - -After designing a pool contract, the next step is to prepare a factory contract because Balancer's off-chain infrastructure uses the factory address as a means to identify the type of pool, which is important for integration into the UI, SDK, and external aggregators - -### 1. Review the Docs πŸ“– - -- [Deploy a Custom AMM Using a Factory](https://docs-v3.balancer.fi/build-a-custom-amm/build-an-amm/deploy-custom-amm-using-factory.html) - -### 2. Recall the Key Requirements πŸ”‘ - -- A pool factory contract must inherit from [BasePoolFactory](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/vault/contracts/factories/BasePoolFactory.sol) -- Use the internal `_create` function to deploy a new pool -- Use the internal `_registerPoolWithVault` fuction to register a pool immediately after creation - -### 3. Write a Factory Contract πŸ“ - -- To get started, edit the`ConstantSumFactory.sol` contract directly or make a copy - -## πŸͺ Create a Pool Hook - -[![hook-video](https://github.com/user-attachments/assets/96e12c29-53c2-4a52-9437-e477f6d992d1)](https://www.youtube.com/watch?v=kaz6duliRPA) - -### 1. Review the Docs πŸ“– - -- [Extend an Existing Pool Type Using Hooks](https://docs-v3.balancer.fi/build-a-custom-amm/build-an-amm/extend-existing-pool-type-using-hooks.html) - -### 2. Recall the Key Requirements πŸ”‘ - -- A hooks contract must inherit from [BasePoolHooks.sol](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/vault/contracts/BaseHooks.sol) -- A hooks contract should also inherit from [VaultGuard.sol](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/vault/contracts/VaultGuard.sol) -- Must implement `onRegister` to determine if a pool is allowed to use the hook contract -- Must implement `getHookFlags` to define which hooks are supported -- The `onlyVault` modifier should be applied to all hooks functions (i.e. `onRegister`, `onBeforeSwap`, `onAfterSwap` ect.) - -### 3. Write a Hook Contract πŸ“ - -- To get started, edit the `VeBALFeeDiscountHook.sol` contract directly or make a copy - -## 🚒 Deploy the Contracts - -The deploy scripts are located in the [foundry/script/](https://github.com/balancer/scaffold-balancer-v3/tree/main/packages/foundry/script) directory. To better understand the lifecycle of deploying a pool that uses a hooks contract, see the diagram below - -![pool-deploy-scripts](https://github.com/user-attachments/assets/bb906080-8f42-46c0-af90-ba01ba1754fc) - -### 1. Modifying the Deploy Scripts πŸ› οΈ - -For all the scaffold integrations to work properly, each deploy script must be imported into `Deploy.s.sol` and inherited by the `DeployScript` contract in `Deploy.s.sol` - -### 2. Broadcast the Transactions πŸ“‘ - -#### Deploy to local fork - -1. Run the following command - -```bash -yarn deploy -``` - -#### Deploy to a live network - -1. Add a `DEPLOYER_PRIVATE_KEY` to the `packages/foundry/.env` file - -``` -DEPLOYER_PRIVATE_KEY=0x... -SEPOLIA_RPC_URL=... -``` - -> The `DEPLOYER_PRIVATE_KEY` must start with `0x` and must hold enough Sepolia ETH to deploy the contracts. This account will receive the BPT from pool initialization - -2. Run the following command - -``` -yarn deploy --network sepolia -``` - -## πŸ§ͺ Test the Contracts - -The [balancer-v3-monorepo](https://github.com/balancer/balancer-v3-monorepo) provides testing utility contracts like [BasePoolTest](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/vault/test/foundry/utils/BasePoolTest.sol) and [BaseVaultTest](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/vault/test/foundry/utils/BaseVaultTest.sol). Therefore, the best way to begin writing tests for custom factories, pools, and hooks contracts is to leverage the examples established by the source code. - -### 1. Testing Factories πŸ‘¨β€πŸ”¬ - -The `ConstantSumFactoryTest` roughly mirrors the [WeightedPool8020FactoryTest -](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/pool-weighted/test/foundry/WeightedPool8020Factory.t.sol) - -``` -yarn test --match-contract ConstantSumFactoryTest -``` - -### 2. Testing Pools 🏊 - -The `ConstantSumPoolTest` roughly mirrors the [WeightedPoolTest](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/pool-weighted/test/foundry/WeightedPool.t.sol) - -``` -yarn test --match-contract ConstantSumPoolTest -``` - -### 3. Testing Hooks 🎣 - -The `VeBALFeeDiscountHookExampleTest` mirrors the [VeBALFeeDiscountHookExampleTest](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/pool-hooks/test/foundry/VeBALFeeDiscountHookExample.t.sol) - -``` -yarn test --match-contract VeBALFeeDiscountHookExampleTest -``` +Pool Lifecycle Implementation Point - Pool Lifecycle Implementation Point, onAfterAddLiquidity \ No newline at end of file diff --git a/diagram.png b/diagram.png new file mode 100644 index 00000000..3331d7c7 Binary files /dev/null and b/diagram.png differ diff --git a/packages/foundry/contracts/hooks/InsurancePremiumHook.sol b/packages/foundry/contracts/hooks/InsurancePremiumHook.sol new file mode 100644 index 00000000..fa543a1c --- /dev/null +++ b/packages/foundry/contracts/hooks/InsurancePremiumHook.sol @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IRouterCommon } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterCommon.sol"; +import { IHooks } from "@balancer-labs/v3-interfaces/contracts/vault/IHooks.sol"; +import { IBasePoolFactory } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePoolFactory.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { + AddLiquidityKind, + LiquidityManagement, + RemoveLiquidityKind, + AfterSwapParams, + SwapKind, + TokenConfig, + HookFlags, + AddLiquidityParams +} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { VaultGuard } from "@balancer-labs/v3-vault/contracts/VaultGuard.sol"; +import { BaseHooks } from "@balancer-labs/v3-vault/contracts/BaseHooks.sol"; +import { IOutflowProcessor } from "./interfaces/IOutflowProcessor.sol"; +import { IPoolImpl } from "./interfaces/IPoolImpl.sol"; + +/** + * @notice A hook that takes a fee on add/remove liquidity. + * @dev This hook extracts fees on all operations liquidity related (add and remove liquidity), adds the fee back as liquidity, then sends + * the bpt to an atomica pool and the shares of the atomica pool are sent to the user. + * + * Since the Vault always takes fees on the calculated amounts, and only supports taking fees in tokens, this hook + * must be restricted to pools that require proportional liquidity operations. For example, the calculated amount + * for EXACT_OUT withdrawals would be in BPT, and charging fees on BPT is unsupported. + * + * Since the fee must be taken *after* the `amountOut` is calculated - and the actual `amountOut` returned to the Vault + * must be modified in order to charge the fee - `enableHookAdjustedAmounts` must also be set to true in the + * pool configuration. Otherwise, the Vault would ignore the adjusted values, and not recognize the fee. + */ +contract InsurancePremiumHook is BaseHooks, VaultGuard, Ownable { + using FixedPoint for uint256; + using SafeERC20 for IERC20; + // Only pools from a specific factory are able to register and use this hook. + address private immutable _allowedFactory; + // Only trusted routers are allowed to call this hook, because the hook relies on the `getSender` implementation + // implementation to work properly. + address private immutable _trustedRouter; + + //here a struct could be used to enclose all of this variables + + mapping(address => mapping(uint256 => uint256)) public bptPaid; + mapping(address => mapping(uint256 => uint256)) public timestamp; + mapping(address => uint256) public depositID; + + /* + * The outflow processor is an atomica contract that allows withdrawals, this hook is the only authorized approver for withdrawals + */ + IOutflowProcessor _wrappedOutflow; + IPoolImpl _wrappedPool; + + // Percentages are represented as 18-decimal FP numbers, which have a maximum value of FixedPoint.ONE (100%), + // so 60 bits are sufficient. + uint64 public addLiquidityHookFeePercentage; + uint64 public removeLiquidityHookFeePercentage; + + /** + * @notice A new `InsurancePremiumHook` contract has been registered successfully for a given factory and pool. + * @dev If the registration fails the call will revert, so there will be no event. + * @param hooksContract This contract + * @param pool The pool on which the hook was registered + */ + event InsurancePremiumHookRegistered(address indexed hooksContract, address indexed pool); + + /** + * @notice The hooks contract has charged a fee. + * @param btpPaid btp contributed to the premium + * @param depositID nonce of the deposit + * @param sender original sender of the transaction + */ + event HookFeeCharged(uint256 btpPaid, uint256 depositID, address sender); + + /** + * @notice The add liquidity hook fee percentage has been changed. + * @dev Note that the initial fee will be zero, and no event is emitted on deployment. + * @param hooksContract The hooks contract charging the fee + * @param hookFeePercentage The new hook swap fee percentage + */ + event HookAddLiquidityFeePercentageChanged(address indexed hooksContract, uint256 hookFeePercentage); + + /** + * @notice The remove liquidity hook fee percentage has been changed. + * @dev Note that the initial fee will be zero, and no event is emitted on deployment. + * @param hooksContract The hooks contract charging the fee + * @param hookFeePercentage The new hook swap fee percentage + */ + event HookRemoveLiquidityFeePercentageChanged(address indexed hooksContract, uint256 hookFeePercentage); + + /** + * @notice The hooks contract owner has withdrawn tokens. + * @param depositID the id of the deposit + * @param outflowRequestID_ the ID of the outflow request + * @param approved a flag indicating if the request has been denied or approved + * @param amount the amount requested + * @param timestamp at which the request has taken place + */ + event ApproveOutflowInsurance( + uint256 depositID, + uint256 outflowRequestID_, + bool approved, + uint256 amount, + uint256 timestamp); + + constructor( + IVault vault, + address allowedFactory, + address trustedRouter, + address wrappedOutflow, + address wrappedPool + ) VaultGuard(vault) Ownable(msg.sender) { + require( + allowedFactory != address(0x0) && + trustedRouter != address(0x0) && + wrappedOutflow != address(0x0) && + wrappedPool != address(0x0), + "Addresses cannot be 0"); + _allowedFactory = allowedFactory; + _trustedRouter = trustedRouter; + _wrappedOutflow = IOutflowProcessor(wrappedOutflow); + _wrappedPool = IPoolImpl(wrappedPool); + } + + /// @inheritdoc IHooks + function onRegister( + address factory, + address pool, + TokenConfig[] memory, + LiquidityManagement calldata + ) public override onlyVault returns (bool) { + // This hook implements a restrictive approach, where we check if the factory is an allowed factory and if + // the pool was created by the allowed factory. + + emit InsurancePremiumHookRegistered(address(this), pool); + + return factory == _allowedFactory && IBasePoolFactory(factory).isPoolFromFactory(pool); + } + + /// @inheritdoc IHooks + function getHookFlags() public pure override returns (HookFlags memory) { + HookFlags memory hookFlags; + // `enableHookAdjustedAmounts` must be true for all contracts that modify the `amountCalculated` + // in after hooks. Otherwise, the Vault will ignore any "hookAdjusted" amounts, and the transaction + // might not settle. (It should be false if the after hooks do something else.) + hookFlags.enableHookAdjustedAmounts = true; + hookFlags.shouldCallAfterAddLiquidity = true; + hookFlags.shouldCallAfterRemoveLiquidity = true; + return hookFlags; + } + + /// @inheritdoc IHooks + function onAfterAddLiquidity( + address router, + address pool, + AddLiquidityKind kind, + uint256[] memory, + uint256[] memory amountsInRaw, + uint256, + uint256[] memory, + bytes memory + ) public override onlyVault returns (bool success, uint256[] memory hookAdjustedAmountsInRaw) { + + if (router != _trustedRouter) { + return (false, amountsInRaw); + } + + if (kind != AddLiquidityKind.PROPORTIONAL) { + // Returning false will make the transaction revert, so the second argument does not matter. + return (false, amountsInRaw); + } + + address sender = IRouterCommon(router).getSender(); + hookAdjustedAmountsInRaw = amountsInRaw; + + if (addLiquidityHookFeePercentage > 0) { + IERC20[] memory tokens = _vault.getPoolTokens(pool); + uint256[] memory accruedFees = new uint256[](tokens.length); + // Charge fees proportional to amounts in of each token. + for (uint256 i = 0; i < amountsInRaw.length; i++) { + uint256 hookFee = amountsInRaw[i].mulDown(addLiquidityHookFeePercentage); + accruedFees[i] = hookFee; + hookAdjustedAmountsInRaw[i] += hookFee; + _vault.sendTo(tokens[i], address(this), hookFee); + } + + // Sends the hook fee to the hook and registers the debt in the Vault. + (,uint256 tokensOut,) = _vault.addLiquidity( + AddLiquidityParams({ + pool: pool, + to: address(this), + maxAmountsIn: accruedFees, + minBptAmountOut: 0, + kind: AddLiquidityKind.PROPORTIONAL, + userData: bytes("") + }) + ); + + uint256 shares = _wrappedPool.deposit(tokensOut, sender, 0); + bptPaid[sender][depositID[sender]] = shares; + timestamp[sender][depositID[sender]] = block.timestamp; + + emit HookFeeCharged(shares, depositID[sender], sender); + depositID[sender] += 1; + } + return (true, hookAdjustedAmountsInRaw); + } + + /// @inheritdoc IHooks + function onAfterRemoveLiquidity( + address router, + address pool, + RemoveLiquidityKind kind, + uint256 , + uint256[] memory, + uint256[] memory amountsOutRaw, + uint256[] memory, + bytes memory + ) public override onlyVault returns (bool, uint256[] memory hookAdjustedAmountsOutRaw) { + if (router != _trustedRouter) { + return (false, amountsOutRaw); + } + + if (kind != RemoveLiquidityKind.PROPORTIONAL) { + // Returning false will make the transaction revert, so the second argument does not matter. + return (false, amountsOutRaw); + } + + address sender = IRouterCommon(router).getSender(); + hookAdjustedAmountsOutRaw = amountsOutRaw; + + if (removeLiquidityHookFeePercentage > 0) { + IERC20[] memory tokens = _vault.getPoolTokens(pool); + uint256[] memory accruedFees = new uint256[](tokens.length); + // Charge fees proportional to amounts in of each token. + for (uint256 i = 0; i < amountsOutRaw.length; i++) { + uint256 hookFee = amountsOutRaw[i].mulDown(addLiquidityHookFeePercentage); + accruedFees[i] = hookFee; + hookAdjustedAmountsOutRaw[i] -= hookFee; + _vault.sendTo(tokens[i], address(this), hookFee); + } + + // Sends the hook fee to the hook and registers the debt in the Vault. + (,uint256 tokensOut,) = _vault.addLiquidity( + AddLiquidityParams({ + pool: pool, + to: address(this), + maxAmountsIn: accruedFees, + minBptAmountOut: 0, + kind: AddLiquidityKind.PROPORTIONAL, + userData: bytes("") + }) + ); + + uint256 shares = _wrappedPool.deposit(tokensOut, sender, 0); + bptPaid[sender][depositID[sender]] = shares; + timestamp[sender][depositID[sender]] = block.timestamp; + + emit HookFeeCharged(shares, depositID[sender], sender); + depositID[sender] += 1; + + } + return (true, hookAdjustedAmountsOutRaw); + } + + /** + * @notice acts as the outflowApprover for the atomica pool + * @dev this function is used to permissionlessly check if the user's outflow request can be approved + */ + function approveOutflowInsurance(uint256 depositID_, uint256 outflowRequestID_) external returns(bool approved){ + uint256 requestedAmount = _wrappedOutflow.outflowRequest(outflowRequestID_); + + if( + requestedAmount == bptPaid[msg.sender][depositID_] && + timestamp[msg.sender][depositID_] + 864000 >= block.timestamp + ){ + _wrappedOutflow.approveOutflowRequest(outflowRequestID_); + emit ApproveOutflowInsurance(depositID_, outflowRequestID_, true, requestedAmount, block.timestamp); + return true; + } + + _wrappedOutflow.declineOutflowRequest(outflowRequestID_); + emit ApproveOutflowInsurance(depositID_, outflowRequestID_, false, requestedAmount, block.timestamp ); + return false; + } + + // Permissioned functions + + /** + * @notice Sets the hook add liquidity fee percentage, charged on every add liquidity operation. + * @dev This function must be permissioned. + */ + function setAddLiquidityHookFeePercentage(uint64 hookFeePercentage) external onlyOwner { + addLiquidityHookFeePercentage = hookFeePercentage; + + emit HookAddLiquidityFeePercentageChanged(address(this), hookFeePercentage); + } + + /** + * @notice Sets the hook remove liquidity fee percentage, charged on every remove liquidity operation. + * @dev This function must be permissioned. + */ + function setRemoveLiquidityHookFeePercentage(uint64 hookFeePercentage) external onlyOwner { + removeLiquidityHookFeePercentage = hookFeePercentage; + + emit HookRemoveLiquidityFeePercentageChanged(address(this), hookFeePercentage); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/interfaces/IERC4626.sol b/packages/foundry/contracts/hooks/interfaces/IERC4626.sol new file mode 100644 index 00000000..3800f038 --- /dev/null +++ b/packages/foundry/contracts/hooks/interfaces/IERC4626.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +/// @title ERC4626 interface +/// See: https://eips.ethereum.org/EIPS/eip-462 +interface IERC4626 { + /*//////////////////////////////////////////////////////// + Events + ////////////////////////////////////////////////////////*/ + + /// @notice `sender` has exchanged `assets` for `shares`, + /// and transferred those `shares` to `receiver`. + event Deposit(address indexed sender, address indexed receiver, uint256 assets, uint256 shares); + + /// @notice `sender` has exchanged `shares` for `assets`, + /// and transferred those `assets` to `receiver`. + event Withdraw( + address indexed sender, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); + + /*//////////////////////////////////////////////////////// + Vault properties + ////////////////////////////////////////////////////////*/ + + /// @notice The address of the underlying ERC20 token used for + /// the Vault for accounting, depositing, and withdrawing. + function asset() external view returns (address); + + /// @notice Total amount of the underlying asset that + /// is "managed" by Vault. + function totalAssets() external view returns (uint256); + + /*//////////////////////////////////////////////////////// + Deposit/Withdrawal Logic + ////////////////////////////////////////////////////////*/ + + /// @notice Mints `shares` Vault shares to `receiver` by + /// depositing exactly `assets` of underlying tokens. + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /// @notice Mints exactly `shares` Vault shares to `receiver` + /// by depositing `assets` of underlying tokens. + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /// @notice Redeems `shares` from `owner` and sends `assets` + /// of underlying tokens to `receiver`. + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); + + /// @notice Redeems `shares` from `owner` and sends `assets` + /// of underlying tokens to `receiver`. + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); + + /*//////////////////////////////////////////////////////// + Vault Accounting Logic + ////////////////////////////////////////////////////////*/ + + /// @notice The amount of shares that the vault would + /// exchange for the amount of assets provided, in an + /// ideal scenario where all the conditions are met. + function convertToShares(uint256 assets) external view returns (uint256 shares); + + /// @notice The amount of assets that the vault would + /// exchange for the amount of shares provided, in an + /// ideal scenario where all the conditions are met. + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + /// @notice Total number of underlying assets that can + /// be deposited by `owner` into the Vault, where `owner` + /// corresponds to the input parameter `receiver` of a + /// `deposit` call. + function maxDeposit(address owner) external view returns (uint256 maxAssets); + + /// @notice Allows an on-chain or off-chain user to simulate + /// the effects of their deposit at the current block, given + /// current on-chain conditions. + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + /// @notice Total number of underlying shares that can be minted + /// for `owner`, where `owner` corresponds to the input + /// parameter `receiver` of a `mint` call. + function maxMint(address owner) external view returns (uint256 maxShares); + + /// @notice Allows an on-chain or off-chain user to simulate + /// the effects of their mint at the current block, given + /// current on-chain conditions. + function previewMint(uint256 shares) external view returns (uint256 assets); + + /// @notice Total number of underlying assets that can be + /// withdrawn from the Vault by `owner`, where `owner` + /// corresponds to the input parameter of a `withdraw` call. + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + /// @notice Allows an on-chain or off-chain user to simulate + /// the effects of their withdrawal at the current block, + /// given current on-chain conditions. + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + /// @notice Total number of underlying shares that can be + /// redeemed from the Vault by `owner`, where `owner` corresponds + /// to the input parameter of a `redeem` call. + function maxRedeem(address owner) external view returns (uint256 maxShares); + + /// @notice Allows an on-chain or off-chain user to simulate + /// the effects of their redeemption at the current block, + /// given current on-chain conditions. + function previewRedeem(uint256 shares) external view returns (uint256 assets); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/interfaces/IOutflowProcessor.sol b/packages/foundry/contracts/hooks/interfaces/IOutflowProcessor.sol new file mode 100644 index 00000000..c23049aa --- /dev/null +++ b/packages/foundry/contracts/hooks/interfaces/IOutflowProcessor.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +interface IOutflowProcessor { + error ErrLocked(); + error ErrInvalidAddress(); + error ErrZeroAmount(); + error ErrOnlyOutflowRequester(); + error ErrOnlyOutflowApprover(); + error ErrInvalidSourceID(); + error ErrInconsistency(); + error ErrInvalidOutflowRequestID(); + error ErrInvalidOutflowRequestStatus(); + error ErrMinResolutionPeriod(); + error ErrMerkleTreeRootHashNotProvided(); + + event LogOutflowRequester(address outflowRequester); + event LogOutflowApprover(address outflowApprover); + event LogOutflowRequest(uint256 outflowRequestID); + event LogDistributor(address distributor); + event LogOutflowRequestApproved(uint256 outflowRequestID); + event LogOutflowRequestDeclined(uint256 outflowRequestID); + event LogPaidOut( + uint256 outflowRequestID, + address pool, + address asset, + uint256 requestedAmount, + uint256 releasedAmount, + address recipient + ); + + enum OutflowRequestStatus { + REQUESTED, + DECLINED, + PROCESSED + } + + struct OutflowRequest { + address protocol; + uint256 sourceID; + uint256 entityID; + address recipient; + bool merkleTreeDistribution; + address baseAsset; + address[] outflowPools; + uint256[] outflowPoolAssetAmounts; + uint256 requestedBaseAssetAmount; + uint256 createdAt; + OutflowRequestStatus status; + bytes32 merkleTreeDistributionRootHash; + string data; + } + + struct CreateOutflowRequestParams { + address protocol; + uint256 sourceID; + uint256 entityID; + address recipient; + address baseAsset; + uint256 baseAssetAmount; + address[] outflowPools; + uint256[] outflowPoolAssetAmounts; + bytes32 merkleTreeDistributionRootHash; + string data; + } + + function createOutflowRequest( + CreateOutflowRequestParams calldata params + ) external returns (uint256 outflowRequestID); + + function approveOutflowRequest(uint256 outflowRequestID) external; + + function declineOutflowRequest(uint256 outflowRequestID) external; + + function outflowRequest(uint256 outflowRequestID) view external returns(uint requestedAmount); + + function factory() external view returns (address); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/interfaces/IPoolImpl.sol b/packages/foundry/contracts/hooks/interfaces/IPoolImpl.sol new file mode 100644 index 00000000..368f16fb --- /dev/null +++ b/packages/foundry/contracts/hooks/interfaces/IPoolImpl.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "./IERC4626.sol"; + +interface IPoolImpl is IERC20, IERC4626 { + error ErrZeroValue(); + error ErrMaxDepositExceeded(); + error ErrMaxMintExceeded(); + error ErrRequiredWithdrawRequest(); + error ErrWithdrawDelayed(uint256 timestamp); + error ErrRedeemDelayed(uint256 timestamp); + error ErrTooBigAmount(uint256 allowedAmount); + error ErrWithdrawRequestExpired(); + error ErrAmountExceedsAvailableAssets(); + error ErrOnlyAssetToken(address assetToken); + error ErrInsufficientAllowance(address recipient, uint256 currentAllowance, uint256 value); + error ErrInsufficientBalance(address sender, uint256 currentBalance, uint256 value); + error ErrMaxInflowDropTokens(uint256); + error ErrDepositSlippageProtection(uint256 shares, uint256 minShares); + error ErrMintSlippageProtection(uint256 assets, uint256 maxAssets); + error ErrWithdrawSlippageProtection(uint256 shares, uint256 maxShares); + error ErrRedeemSlippageProtection(uint256 assets, uint256 maxAssets_); + + event LogOutflow(address token, uint256 amount, address recipient); + event LogInflowDrop( + uint256 sourceId, + address sender, + address token, + uint256 bucketId, + uint256 amount, + uint256 fee, + bool airdrop + ); + event LogInflowDropClaimed(address erc20, address account, address recipient, uint256 amount); + + function updateAccount(address account) external; + + function accumulatedInflowDrop(address account, address erc20) external view returns (uint256); + + function pullOutflow(address erc20, uint256 amount, address recipient) external returns (uint256 transferredAmount); + + function pushAirdropAsInflowDrop(uint256 sourceId, address erc20) external; + + function pushInflowDrop(uint256 sourceId, address erc20, uint256 amount) external; + + function claimAllInflowDrops(address account, address recipient) external; + + function claimInflowDrop(address erc20, address account, address recipient) external; + + function createWithdrawRequest(uint256 assets) external returns (uint256); + + function createRedeemRequest(uint256 shares) external returns (uint256); + + function inflowDropTokenSupported(address erc20) external view returns (bool); + + function deposit(uint256 assets, address receiver, uint256 minShares) external returns (uint256); + + function mint(uint256 shares, address receiver, uint256 maxAssets) external returns (uint256); + + function withdraw( + uint256 assets, + address receiver, + address owner, + uint256 maxShares + ) external returns (uint256); + + function redeem(uint256 shares, address receiver, address owner, uint256 minAssets) external returns (uint256); + + function depositWithErc2612Permit( + uint256 assets, + address receiver, + uint256 minShares, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 shares); + + function mintWithErc2612Permit( + uint256 shares, + address receiver, + uint256 maxAssets, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 assets); +} \ No newline at end of file diff --git a/packages/foundry/test/InsurancePremiumHook.t.sol b/packages/foundry/test/InsurancePremiumHook.t.sol new file mode 100644 index 00000000..911863aa --- /dev/null +++ b/packages/foundry/test/InsurancePremiumHook.t.sol @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IHooks } from "@balancer-labs/v3-interfaces/contracts/vault/IHooks.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { + AddLiquidityKind, + LiquidityManagement, + PoolRoleAccounts, + RemoveLiquidityKind, + AfterSwapParams, + SwapKind +} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; +import { ArrayHelpers } from "@balancer-labs/v3-solidity-utils/contracts/test/ArrayHelpers.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { BasePoolMath } from "@balancer-labs/v3-vault/contracts/BasePoolMath.sol"; + +import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol"; +import { BalancerPoolToken } from "@balancer-labs/v3-vault/contracts/BalancerPoolToken.sol"; +import { PoolMock } from "@balancer-labs/v3-vault/contracts/test/PoolMock.sol"; + +import { InsurancePremiumHook } from "../contracts/InsurancePremiumHook.sol"; + +contract InsurancePremiumHookTest is BaseVaultTest { + using CastingHelpers for address[]; + using FixedPoint for uint256; + using ArrayHelpers for *; + + uint256 internal daiIdx; + uint256 internal usdcIdx; + + function setUp() public virtual override { + BaseVaultTest.setUp(); + + (daiIdx, usdcIdx) = getSortedIndexes(address(dai), address(usdc)); + } + + // Sets the hook of the pool and stores the address in the variable poolHooksContract + function createHook() internal override returns (address) { + // lp will be the owner of the hook. Only LP is able to set hook fee percentages. + vm.prank(lp); + InsurancePremiumHook hook = new InsurancePremiumHook(IVault(address(vault))); + return address(hook); + } + + // Overrides pool creation to set liquidityManagement (disables unbalanced liquidity) + function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + PoolMock newPool = deployPoolMock(IVault(address(vault)), "ERC20 Pool", "ERC20POOL"); + vm.label(address(newPool), label); + + PoolRoleAccounts memory roleAccounts; + roleAccounts.poolCreator = lp; + + LiquidityManagement memory liquidityManagement; + liquidityManagement.disableUnbalancedLiquidity = true; + + vm.expectEmit(); + emit InsurancePremiumHook.InsurancePremiumHookRegistered(poolHooksContract, address(newPool)); + + factoryMock.registerPool( + address(newPool), + vault.buildTokenConfig(tokens.asIERC20()), + roleAccounts, + poolHooksContract, + liquidityManagement + ); + + return address(newPool); + } + + function testHookFeeAddLiquidityExactIn__Fuzz(uint256 expectedBptOut, uint64 hookFeePercentage) public { + // Add fee between 0 and 100% + hookFeePercentage = uint64(bound(hookFeePercentage, 0, FixedPoint.ONE)); + + vm.expectEmit(); + emit InsurancePremiumHook.HookAddLiquidityFeePercentageChanged(poolHooksContract, hookFeePercentage); + + vm.prank(lp); + InsurancePremiumHook(poolHooksContract).setAddLiquidityHookFeePercentage(hookFeePercentage); + + // Since operation is not settled in advance, max expected bpt out can't generate a hook fee higher than + // pool liquidity, or else the hook won't be able to charge fees + expectedBptOut = bound( + expectedBptOut, + POOL_MINIMUM_TOTAL_SUPPLY * PRODUCTION_MIN_TRADE_AMOUNT, + hookFeePercentage == 0 ? MAX_UINT256 : poolInitAmount.divDown(hookFeePercentage) + ); + + // Make sure bob has enough to pay for the transaction + if (expectedBptOut > dai.balanceOf(bob)) { + expectedBptOut = dai.balanceOf(bob); + } + + uint256[] memory actualAmountsIn = BasePoolMath.computeProportionalAmountsIn( + [poolInitAmount, poolInitAmount].toMemoryArray(), + BalancerPoolToken(pool).totalSupply(), + expectedBptOut + ); + uint256 actualAmountIn = actualAmountsIn[daiIdx]; // Proportional, so doesn't matter which token + uint256 hookFee = actualAmountIn.mulDown(hookFeePercentage); + + uint256[] memory expectedBalances = [poolInitAmount + actualAmountIn, poolInitAmount + actualAmountIn] + .toMemoryArray(); + + BaseVaultTest.Balances memory balancesBefore = getBalances(bob); + + vm.prank(bob); + vm.expectCall( + address(poolHooksContract), + abi.encodeCall( + IHooks.onAfterAddLiquidity, + ( + address(router), + pool, + AddLiquidityKind.PROPORTIONAL, + actualAmountsIn, + actualAmountsIn, + expectedBptOut, + expectedBalances, + bytes("") + ) + ) + ); + + if (hookFee > 0) { + vm.expectEmit(); + emit InsurancePremiumHook.HookFeeCharged(poolHooksContract, IERC20(dai), hookFee); + + vm.expectEmit(); + emit InsurancePremiumHook.HookFeeCharged(poolHooksContract, IERC20(usdc), hookFee); + } + + uint256[] memory maxAmountsIn = [actualAmountIn + hookFee, actualAmountIn + hookFee].toMemoryArray(); + router.addLiquidityProportional(pool, maxAmountsIn, expectedBptOut, false, bytes("")); + + _checkAddLiquidityHookTestResults(balancesBefore, actualAmountsIn, expectedBptOut, hookFee); + _checkWithdrawals(hookFee, hookFee); + } + + function testHookFeeRemoveLiquidityExactIn__Fuzz(uint256 expectedBptIn, uint64 hookFeePercentage) public { + // Add liquidity so bob has BPT to remove liquidity + vm.prank(bob); + router.addLiquidityProportional( + pool, + [poolInitAmount, poolInitAmount].toMemoryArray(), + 2 * poolInitAmount, + false, + bytes("") + ); + + // Add fee between 0 and 100% + hookFeePercentage = uint64(bound(hookFeePercentage, 0, FixedPoint.ONE)); + + vm.expectEmit(); + emit InsurancePremiumHook.HookRemoveLiquidityFeePercentageChanged(poolHooksContract, hookFeePercentage); + + vm.prank(lp); + InsurancePremiumHook(poolHooksContract).setRemoveLiquidityHookFeePercentage(hookFeePercentage); + + // Make sure bob has enough to pay for the transaction + expectedBptIn = bound( + expectedBptIn, + POOL_MINIMUM_TOTAL_SUPPLY * PRODUCTION_MIN_TRADE_AMOUNT, + BalancerPoolToken(pool).balanceOf(bob) + ); + + // Since bob added poolInitAmount in each token of the pool, the pool balances are doubled + uint256[] memory actualAmountsOut = BasePoolMath.computeProportionalAmountsOut( + [2 * poolInitAmount, 2 * poolInitAmount].toMemoryArray(), + BalancerPoolToken(pool).totalSupply(), + expectedBptIn + ); + uint256 actualAmountOut = actualAmountsOut[usdcIdx]; + uint256 hookFee = actualAmountOut.mulDown(hookFeePercentage); + + uint256[] memory expectedBalances = [2 * poolInitAmount - actualAmountOut, 2 * poolInitAmount - actualAmountOut] + .toMemoryArray(); + + BaseVaultTest.Balances memory balancesBefore = getBalances(bob); + + vm.prank(bob); + vm.expectCall( + address(poolHooksContract), + abi.encodeCall( + IHooks.onAfterRemoveLiquidity, + ( + address(router), + pool, + RemoveLiquidityKind.PROPORTIONAL, + expectedBptIn, + actualAmountsOut, + actualAmountsOut, + expectedBalances, + bytes("") + ) + ) + ); + + if (hookFee > 0) { + vm.expectEmit(); + emit InsurancePremiumHook.HookFeeCharged(poolHooksContract, IERC20(dai), hookFee); + + vm.expectEmit(); + emit InsurancePremiumHook.HookFeeCharged(poolHooksContract, IERC20(usdc), hookFee); + } + + uint256[] memory minAmountsOut = [actualAmountOut - hookFee, actualAmountOut - hookFee].toMemoryArray(); + router.removeLiquidityProportional(pool, expectedBptIn, minAmountsOut, false, bytes("")); + + _checkRemoveLiquidityHookTestResults(balancesBefore, actualAmountsOut, expectedBptIn, hookFee); + _checkWithdrawals(hookFee, hookFee); + } + + function _checkAddLiquidityHookTestResults( + BaseVaultTest.Balances memory balancesBefore, + uint256[] memory actualAmountsIn, + uint256 expectedBptOut, + uint256 expectedHookFee + ) private view { + BaseVaultTest.Balances memory balancesAfter = getBalances(bob); + + assertEq(balancesAfter.userBpt - balancesBefore.userBpt, expectedBptOut, "Bob BPT balance is wrong"); + + assertEq( + balancesAfter.poolTokens[daiIdx] - balancesBefore.poolTokens[daiIdx], + actualAmountsIn[daiIdx], + "Pool DAI balance is wrong" + ); + assertEq( + balancesAfter.poolTokens[usdcIdx] - balancesBefore.poolTokens[usdcIdx], + actualAmountsIn[usdcIdx], + "Pool USDC balance is wrong" + ); + assertEq(balancesAfter.poolSupply - balancesBefore.poolSupply, expectedBptOut, "Pool Supply is wrong"); + + assertEq( + balancesAfter.vaultTokens[daiIdx] - balancesBefore.vaultTokens[daiIdx], + actualAmountsIn[daiIdx], + "Vault DAI balance is wrong" + ); + assertEq( + balancesAfter.vaultTokens[usdcIdx] - balancesBefore.vaultTokens[usdcIdx], + actualAmountsIn[usdcIdx], + "Vault USDC balance is wrong" + ); + + assertEq( + balancesBefore.userTokens[daiIdx] - balancesAfter.userTokens[daiIdx], + actualAmountsIn[daiIdx] + expectedHookFee, + "Bob DAI balance is wrong" + ); + assertEq( + balancesBefore.userTokens[usdcIdx] - balancesAfter.userTokens[usdcIdx], + actualAmountsIn[usdcIdx] + expectedHookFee, + "Bob USDC balance is wrong" + ); + + assertEq( + balancesAfter.hookTokens[daiIdx] - balancesBefore.hookTokens[daiIdx], + expectedHookFee, + "Hook DAI balance is wrong" + ); + assertEq( + balancesAfter.hookTokens[usdcIdx] - balancesBefore.hookTokens[usdcIdx], + expectedHookFee, + "Hook USDC balance is wrong" + ); + } + + function _checkRemoveLiquidityHookTestResults( + BaseVaultTest.Balances memory balancesBefore, + uint256[] memory actualAmountsOut, + uint256 expectedBptIn, + uint256 expectedHookFee + ) private view { + BaseVaultTest.Balances memory balancesAfter = getBalances(bob); + + assertEq(balancesBefore.userBpt - balancesAfter.userBpt, expectedBptIn, "Bob BPT balance is wrong"); + + assertEq( + balancesBefore.poolTokens[daiIdx] - balancesAfter.poolTokens[daiIdx], + actualAmountsOut[daiIdx], + "Pool DAI balance is wrong" + ); + assertEq( + balancesBefore.poolTokens[usdcIdx] - balancesAfter.poolTokens[usdcIdx], + actualAmountsOut[usdcIdx], + "Pool USDC balance is wrong" + ); + assertEq(balancesBefore.poolSupply - balancesAfter.poolSupply, expectedBptIn, "Pool Supply is wrong"); + + assertEq( + balancesBefore.vaultTokens[daiIdx] - balancesAfter.vaultTokens[daiIdx], + actualAmountsOut[daiIdx], + "Vault DAI balance is wrong" + ); + assertEq( + balancesBefore.vaultTokens[usdcIdx] - balancesAfter.vaultTokens[usdcIdx], + actualAmountsOut[usdcIdx], + "Vault USDC balance is wrong" + ); + + assertEq( + balancesAfter.userTokens[daiIdx] - balancesBefore.userTokens[daiIdx], + actualAmountsOut[daiIdx] - expectedHookFee, + "Bob DAI balance is wrong" + ); + assertEq( + balancesAfter.userTokens[usdcIdx] - balancesBefore.userTokens[usdcIdx], + actualAmountsOut[usdcIdx] - expectedHookFee, + "Bob USDC balance is wrong" + ); + + assertEq( + balancesAfter.hookTokens[daiIdx] - balancesBefore.hookTokens[daiIdx], + expectedHookFee, + "Hook DAI balance is wrong" + ); + assertEq( + balancesAfter.hookTokens[usdcIdx] - balancesBefore.hookTokens[usdcIdx], + expectedHookFee, + "Hook USDC balance is wrong" + ); + } +} \ No newline at end of file diff --git a/roadmap.png b/roadmap.png new file mode 100644 index 00000000..c9e9b0c3 Binary files /dev/null and b/roadmap.png differ