From 6ef00dcb4be21cd646bc572bab4444ae99f281e5 Mon Sep 17 00:00:00 2001 From: ybtuti Date: Thu, 15 May 2025 10:32:32 +0300 Subject: [PATCH 01/12] Adds Uniswap V4 integration with stablecoin pool factory Introduces StablecoinPoolFactory contract for creating and managing USDC-based pools in Uniswap V4. Features include: - Optimized parameters for stablecoin trading - Support for single and batch pool creation - Combined pool creation and liquidity provision - Custom fee hook integration - Access control for pool creation Implements necessary error handling and event emission for pool creation tracking --- foundry.toml | 1 - src/uniV4Intergration/ChamaFeeHook.sol | 5 + .../StablecoinPoolFactory.sol | 148 ++++++++++++++++++ src/utils/Errors.sol | 7 + test/UniV4.t.sol | 15 ++ 5 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 src/uniV4Intergration/ChamaFeeHook.sol create mode 100644 src/uniV4Intergration/StablecoinPoolFactory.sol create mode 100644 test/UniV4.t.sol diff --git a/foundry.toml b/foundry.toml index c3fc66d..9ef5f55 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,7 +6,6 @@ libs = ["lib"] remappings = [ "@openzeppelin/contracts=lib/openzeppelin-contracts/contracts", "forge-std/=lib/forge-std/src/", - "@uniswap/v4-core/=lib/v4-core/", "forge-gas-snapshot/=lib/v4-core/lib/forge-gas-snapshot/src/", "forge-std/=lib/v4-core/lib/forge-std/src/", "permit2/=lib/v4-periphery/lib/permit2/", diff --git a/src/uniV4Intergration/ChamaFeeHook.sol b/src/uniV4Intergration/ChamaFeeHook.sol new file mode 100644 index 0000000..a66ef78 --- /dev/null +++ b/src/uniV4Intergration/ChamaFeeHook.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +contract ChamaFeeHook {} diff --git a/src/uniV4Intergration/StablecoinPoolFactory.sol b/src/uniV4Intergration/StablecoinPoolFactory.sol new file mode 100644 index 0000000..32a919d --- /dev/null +++ b/src/uniV4Intergration/StablecoinPoolFactory.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +import {IPoolInitializer_v4} from "v4-periphery/src/interfaces/IPoolInitializer_v4.sol"; +import {Currency} from "v4-core/src/types/Currency.sol"; +import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; +import {ChamaFeeHook} from "./ChamaFeeHook.sol"; +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; +import {Errors} from "../utils/Errors.sol"; +import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; +import {Actions} from "v4-periphery/src/libraries/Actions.sol"; +import {IAllowanceTransfer} from "v4-periphery/lib/permit2/src/interfaces/IAllowanceTransfer.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract StablecoinPoolFactory is AccessControl { + bytes32 public constant POOL_CREATOR_ROLE = keccak256("POOL_CREATOR_ROLE"); + + IPoolManager public immutable poolManager; + ChamaFeeHook public immutable feeHook; + address public immutable usdcToken; + IPositionManager posm; + + // Mapping to track created pools + mapping(address token => bool exists) public createdPools; + + event PoolCreated(address indexed token, uint24 fee, int24 tickSpacing); + + constructor( + IPoolManager _poolManager, + IPositionManager _positionManager, + ChamaFeeHook _feeHook, + address _usdcToken, + address _admin + ) { + poolManager = _poolManager; + feeHook = _feeHook; + usdcToken = _usdcToken; + posm = _positionManager; + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(POOL_CREATOR_ROLE, _admin); + } + + /** + * + * @notice This creates a new USDC pair with optimized parameters for stablecoins + * @param token This is the other token for the pool + * @param lpfee This is the fee for the pool expressed in pips ie 3000 = 0.30% + * @param tickSpacing is the granularity of the pool. Lower values are more precise but may be more expensive to trade on + * @param sqrtPriceX96 should be expressed as floor(sqrt(token1 / token0) * 2^96) + */ + function createStablecoinPool(address token, uint24 lpfee, int24 tickSpacing, uint160 sqrtPriceX96) + external + onlyRole(POOL_CREATOR_ROLE) + { + _createStablecoinPool(token, lpfee, tickSpacing, sqrtPriceX96); + } + + /// See createStablecoinPool for details, only change with this is that we are doing pools for multiple tokens at once + /// @param initialPrices should be expressed as floor(sqrt(token1 / token0) * 2^96) + function createMultiplePools( + address[] calldata tokens, + uint24[] calldata lpfees, + int24[] calldata tickSpacings, + uint160[] calldata initialPrices + ) external onlyRole(POOL_CREATOR_ROLE) { + if ( + tokens.length != lpfees.length && lpfees.length != tickSpacings.length + && tickSpacings.length != initialPrices.length + ) revert Errors.PoolFactory__ArrayLengthMismatch(); + + for (uint256 i = 0; i < tokens.length; i++) { + _createStablecoinPool(tokens[i], lpfees[i], tickSpacings[i], initialPrices[i]); + } + } + + function _createStablecoinPool(address token, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) internal { + if (createdPools[token]) revert Errors.PoolFactory__PoolAlreadyExists(token); + + (Currency currency0, Currency currency1) = uint160(usdcToken) < uint160(token) + ? (Currency.wrap(usdcToken), Currency.wrap(token)) + : (Currency.wrap(token), Currency.wrap(usdcToken)); + + PoolKey memory poolKey = PoolKey({ + currency0: currency0, + currency1: currency1, + fee: fee, + tickSpacing: tickSpacing, + hooks: IHooks(address(feeHook)) + }); + + poolManager.initialize(poolKey, sqrtPriceX96); + + createdPools[token] = true; + + emit PoolCreated(token, fee, tickSpacing); + } + + /// @notice startingPrice is the price of the pool at initialization expressed as floor(sqrt(token1 / token0) * 2^96) + function createPoolAndAddLiquidity( + address token, + uint24 lpfee, + int24 tickSpacing, + uint160 startingPrice, + int24 tickLower, + int24 tickUpper, + uint256 liquidity, + uint256 amount0Max, + uint256 amount1Max, + address recipient, + bytes calldata hookData, + address permit2 + ) external { + bytes[] memory params = new bytes[](2); + + (Currency currency0, Currency currency1) = uint160(usdcToken) < uint160(token) + ? (Currency.wrap(usdcToken), Currency.wrap(token)) + : (Currency.wrap(token), Currency.wrap(usdcToken)); + + PoolKey memory pool = PoolKey({ + currency0: currency0, + currency1: currency1, + fee: lpfee, + tickSpacing: tickSpacing, + hooks: IHooks(address(feeHook)) + }); + + params[0] = abi.encodeWithSelector(IPoolInitializer_v4.initializePool.selector, pool, startingPrice); + + bytes memory actions = abi.encodePacked(uint8(Actions.MINT_POSITION), uint8(Actions.SETTLE_PAIR)); + + bytes[] memory mintParams = new bytes[](2); + mintParams[0] = abi.encode(pool, tickLower, tickUpper, liquidity, amount0Max, amount1Max, recipient, hookData); + mintParams[1] = abi.encode(pool.currency0, pool.currency1); + + uint256 deadline = block.timestamp + 60; + params[1] = abi.encodeWithSelector(posm.modifyLiquidities.selector, abi.encode(actions, mintParams), deadline); + + // approve permit2 as a spender + IERC20(token).approve(address(permit2), type(uint256).max); + + // approve `PositionManager` as a spender + IAllowanceTransfer(address(permit2)).approve(token, address(posm), type(uint160).max, type(uint48).max); + + IPositionManager(posm).multicall(params); + } +} diff --git a/src/utils/Errors.sol b/src/utils/Errors.sol index a91bb36..74b3be3 100644 --- a/src/utils/Errors.sol +++ b/src/utils/Errors.sol @@ -36,4 +36,11 @@ library Errors { error Contributions__zeroAddressProvided(); error Contributions__notFactoryContract(); error Contributions__epochNotOver(); + + /*////////////////////////////////////////////////////////////// + STABLECOIN POOL FACTORY + //////////////////////////////////////////////////////////////*/ + + error PoolFactory__PoolAlreadyExists(address pool); + error PoolFactory__ArrayLengthMismatch(); } diff --git a/test/UniV4.t.sol b/test/UniV4.t.sol new file mode 100644 index 0000000..1d01103 --- /dev/null +++ b/test/UniV4.t.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {Test, console} from "forge-std/Test.sol"; + +contract UniV4Test is Test { + function testAddresses() public { + address a = makeAddr("goodLord"); + address b = makeAddr("heyThere"); + + bool result = a < b ? true : false; + + console.log("Is a less than b: ", result); + } +} From eff7932602d75095079564114c7b1047a4cbccbf Mon Sep 17 00:00:00 2001 From: ybtuti Date: Thu, 15 May 2025 11:30:59 +0300 Subject: [PATCH 02/12] Adds Uniswap dependencies and enables viaIR Adds new Uniswap protocol dependencies including: - permit2 - universal-router - v2/v3-core Updates foundry.toml with viaIR optimization and adds necessary remappings for new dependencies. Fixes casing in v4-core and v4-periphery URLs. --- .gitmodules | 16 ++++++++++++++-- foundry.toml | 7 ++++++- lib/openzeppelin-contracts | 2 +- lib/permit2 | 1 + lib/universal-router | 1 + lib/v2-core | 1 + lib/v3-core | 1 + lib/v4-core | 1 + lib/v4-periphery | 1 + 9 files changed, 27 insertions(+), 4 deletions(-) create mode 160000 lib/permit2 create mode 160000 lib/universal-router create mode 160000 lib/v2-core create mode 160000 lib/v3-core create mode 160000 lib/v4-core create mode 160000 lib/v4-periphery diff --git a/.gitmodules b/.gitmodules index da3645e..a7f0398 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,7 +6,19 @@ url = https://github.com/foundry-rs/forge-std.git [submodule "lib/v4-core"] path = lib/v4-core - url = https://github.com/Uniswap/v4-core + url = https://github.com/uniswap/v4-core [submodule "lib/v4-periphery"] path = lib/v4-periphery - url = https://github.com/Uniswap/v4-periphery + url = https://github.com/uniswap/v4-periphery +[submodule "lib/permit2"] + path = lib/permit2 + url = https://github.com/uniswap/permit2 +[submodule "lib/universal-router"] + path = lib/universal-router + url = https://github.com/Uniswap/universal-router +[submodule "lib/v3-core"] + path = lib/v3-core + url = https://github.com/uniswap/v3-core +[submodule "lib/v2-core"] + path = lib/v2-core + url = https://github.com/uniswap/v2-core diff --git a/foundry.toml b/foundry.toml index 9ef5f55..9914966 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,8 +1,9 @@ [profile.default] +viaIR = true + src = "src" out = "out" libs = ["lib"] - remappings = [ "@openzeppelin/contracts=lib/openzeppelin-contracts/contracts", "forge-std/=lib/forge-std/src/", @@ -12,5 +13,9 @@ remappings = [ "solmate/=lib/v4-core/lib/solmate/", "v4-core/=lib/v4-core/", "v4-periphery/=lib/v4-periphery/", + "@uniswap/permit2/=lib/permit2/", + "@uniswap/universal-router/=lib/universal-router/", + "@uniswap/v3-core/=lib/v3-core/", + "@uniswap/v2-core/=lib/v2-core/", ] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 9586aaf..e4f7021 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 9586aaf35241daf4b17e4858bf7c86edbb4b7247 +Subproject commit e4f70216d759d8e6a64144a9e1f7bbeed78e7079 diff --git a/lib/permit2 b/lib/permit2 new file mode 160000 index 0000000..cc56ad0 --- /dev/null +++ b/lib/permit2 @@ -0,0 +1 @@ +Subproject commit cc56ad0f3439c502c246fc5cfcc3db92bb8b7219 diff --git a/lib/universal-router b/lib/universal-router new file mode 160000 index 0000000..41183d6 --- /dev/null +++ b/lib/universal-router @@ -0,0 +1 @@ +Subproject commit 41183d6eb154f0ab0e74a0e911a5ef9ea51fc4bd diff --git a/lib/v2-core b/lib/v2-core new file mode 160000 index 0000000..4dd5906 --- /dev/null +++ b/lib/v2-core @@ -0,0 +1 @@ +Subproject commit 4dd59067c76dea4a0e8e4bfdda41877a6b16dedc diff --git a/lib/v3-core b/lib/v3-core new file mode 160000 index 0000000..e3589b1 --- /dev/null +++ b/lib/v3-core @@ -0,0 +1 @@ +Subproject commit e3589b192d0be27e100cd0daaf6c97204fdb1899 diff --git a/lib/v4-core b/lib/v4-core new file mode 160000 index 0000000..e50237c --- /dev/null +++ b/lib/v4-core @@ -0,0 +1 @@ +Subproject commit e50237c43811bd9b526eff40f26772152a42daba diff --git a/lib/v4-periphery b/lib/v4-periphery new file mode 160000 index 0000000..eeb3eff --- /dev/null +++ b/lib/v4-periphery @@ -0,0 +1 @@ +Subproject commit eeb3eff28dd5f5f17aa94180fa3610ff59b0e1c8 From 81b07f35c075dbf7604bab9f118a5e7f5fac32ef Mon Sep 17 00:00:00 2001 From: ybtuti Date: Thu, 15 May 2025 12:16:29 +0300 Subject: [PATCH 03/12] Updates universal-router submodule to latest version --- lib/universal-router | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/universal-router b/lib/universal-router index 41183d6..3663f6d 160000 --- a/lib/universal-router +++ b/lib/universal-router @@ -1 +1 @@ -Subproject commit 41183d6eb154f0ab0e74a0e911a5ef9ea51fc4bd +Subproject commit 3663f6db6e2fe121753cd2d899699c2dc75dca86 From ee7093a09825c3eb9a2e4c53cbe6d9c375be49f3 Mon Sep 17 00:00:00 2001 From: ybtuti Date: Thu, 15 May 2025 12:16:53 +0300 Subject: [PATCH 04/12] Adds ETH value handling in StablecoinPoolFactory Enhances the StablecoinPoolFactory to properly handle ETH transactions when token0 is the zero address. Implements conditional ETH value sending through the multicall function when required. --- src/uniV4Intergration/StablecoinPoolFactory.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/uniV4Intergration/StablecoinPoolFactory.sol b/src/uniV4Intergration/StablecoinPoolFactory.sol index 32a919d..e7935a5 100644 --- a/src/uniV4Intergration/StablecoinPoolFactory.sol +++ b/src/uniV4Intergration/StablecoinPoolFactory.sol @@ -144,5 +144,8 @@ contract StablecoinPoolFactory is AccessControl { IAllowanceTransfer(address(permit2)).approve(token, address(posm), type(uint160).max, type(uint48).max); IPositionManager(posm).multicall(params); + + uint256 ethTosend = currency0.isAddressZero() ? amount0Max : 0; + if (ethTosend > 0) IPositionManager(posm).multicall{value: ethTosend}(params); } } From 21b8269bdc158deab1ca3dd91c4d592c010815c6 Mon Sep 17 00:00:00 2001 From: ybtuti Date: Thu, 15 May 2025 12:18:47 +0300 Subject: [PATCH 05/12] Upgrades Uniswap V4 integration with Universal Router Replaces direct pool interaction with Universal Router integration for improved swap functionality Implements permit2 for token approvals Adds proper liquidity management through PositionManager Key changes: - Migrates from PoolSwapTest to UniversalRouter for swaps - Adds structured swap execution with proper slippage protection - Implements complete liquidity adding/removing functionality - Introduces permit2 integration for enhanced token approvals refactor: upgrade UniswapV4 integration with Universal Router Modernizes TreasuryManager's Uniswap V4 integration by: - Replacing direct pool interactions with Universal Router for safer swaps - Adding PositionManager integration for liquidity management - Implementing Permit2 for improved token approvals - Adding proper slippage protection and structured swap execution The changes enhance security and efficiency while providing more robust swap and liquidity management capabilities. --- src/uniV4Intergration/TreasuryManager.sol | 176 +++++++++++++++++----- 1 file changed, 138 insertions(+), 38 deletions(-) diff --git a/src/uniV4Intergration/TreasuryManager.sol b/src/uniV4Intergration/TreasuryManager.sol index 08f56a0..726233e 100644 --- a/src/uniV4Intergration/TreasuryManager.sol +++ b/src/uniV4Intergration/TreasuryManager.sol @@ -4,21 +4,34 @@ pragma solidity 0.8.26; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {PoolManager, IPoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + import {Errors} from "../utils/Errors.sol"; +import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; +import {Actions} from "v4-periphery/src/libraries/Actions.sol"; + +// Swap +import {UniversalRouter} from "@uniswap/universal-router/contracts/UniversalRouter.sol"; +import {Commands} from "@uniswap/universal-router/contracts/libraries/Commands.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {IV4Router, PoolKey, Currency} from "v4-periphery/src/interfaces/IV4Router.sol"; +import {Actions} from "v4-periphery/src/libraries/Actions.sol"; +import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract TreasuryManager is AccessControl { using SafeERC20 for IERC20; + using StateLibrary for IPoolManager; bytes32 public constant TREASURY_ADMIN_ROLE = keccak256("TREASURY_ADMIN_ROLE"); bytes32 public constant CHAMA_ROLE = keccak256("CHAMA_ROLE"); - PoolManager public immutable poolManager; - PoolModifyLiquidityTest public immutable modifyPositionRouter; - PoolSwapTest public immutable swapRouter; + IPositionManager posm; + + UniversalRouter public immutable router; + IPoolManager public immutable poolManager; + IPermit2 public immutable permit2; address public immutable usdcToken; @@ -37,11 +50,19 @@ contract TreasuryManager is AccessControl { event YieldDeposited(address indexed protocol, address indexed token, uint256 amount); event FeesCollected(address indexed token, uint256 amount); - constructor(address _poolManager, address _usdcToken, address _admin) { - poolManager = PoolManager(_poolManager); - modifyPositionRouter = new PoolModifyLiquidityTest(IPoolManager(_poolManager)); - swapRouter = new PoolSwapTest(IPoolManager(_poolManager)); + constructor( + address _poolManager, + address _usdcToken, + address _admin, + IPositionManager _posm, + address _router, + address _permit2 + ) { + router = UniversalRouter(payable(_router)); + poolManager = IPoolManager(_poolManager); + permit2 = IPermit2(_permit2); usdcToken = _usdcToken; + posm = _posm; _grantRole(DEFAULT_ADMIN_ROLE, _admin); _grantRole(TREASURY_ADMIN_ROLE, _admin); @@ -62,18 +83,21 @@ contract TreasuryManager is AccessControl { return token == usdcToken; } - function swapTokens(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, address recipient) - external - onlyRole(CHAMA_ROLE) - returns (uint256 amountOut) - { + function swapTokens( + PoolKey memory key, + address tokenIn, + address tokenOut, + uint128 amountIn, + uint128 minAmountOut, + address recipient + ) external onlyRole(CHAMA_ROLE) returns (uint256 amountOut) { // TODO: Implement swap logic using Uniswap v4 PoolSwapTest IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); - IERC20(tokenIn).approve(address(swapRouter), amountIn); + IERC20(tokenIn).approve(address(router), amountIn); // Execute swap through Uniswap v4 // Placeholder for actual Uniswap v4 swap logic - amountOut = executeSwap(tokenIn, tokenOut, amountIn); + amountOut = executeSwap(key, amountIn, minAmountOut); require(amountOut >= minAmountOut, "Slippage too high"); @@ -83,45 +107,121 @@ contract TreasuryManager is AccessControl { return amountOut; } - // Placeholder for swap execution (to be implemented with actual Uniswap v4 calls) - function executeSwap(address tokenIn, address tokenOut, uint256 amountIn) internal returns (uint256) { - // TODO: implement actual Uniswap v4 swap logic here - // This is just a placeholder for now - return amountIn; + function approveTokenWithPermit2(address token, uint160 amount, uint48 expiration) external { + IERC20(token).approve(address(permit2), type(uint256).max); + permit2.approve(token, address(router), amount, expiration); } - // Add liquidity to Uniswap v4 pool, Remember to add the logic to add the liquidity to actual uniswap v4 pools + function executeSwap(PoolKey memory key, uint128 amountIn, uint128 minAmountOut) + public + returns (uint256 amountOut) + { + bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP)); + bytes[] memory inputs = new bytes[](1); + + bytes memory actions = + abi.encodePacked(uint8(Actions.SWAP_EXACT_IN_SINGLE), uint8(Actions.SETTLE_ALL), uint8(Actions.TAKE_ALL)); + + bytes[] memory params = new bytes[](3); + params[0] = abi.encode( + IV4Router.ExactInputSingleParams({ + poolKey: key, + zeroForOne: true, + amountIn: amountIn, + amountOutMinimum: minAmountOut, + hookData: bytes("") + }) + ); + params[1] = abi.encode(key.currency0, amountIn); + params[2] = abi.encode(key.currency1, minAmountOut); + + inputs[0] = abi.encode(actions, params); + + // Execute the swap + uint256 deadline = block.timestamp + 20; + router.execute(commands, inputs, deadline); + + // Verify and return the output amount + amountOut = IERC20(Currency.unwrap(key.currency1)).balanceOf(address(this)); + require(amountOut >= minAmountOut, "Insufficient output amount"); + return amountOut; + } + + // Need to implement a way to repay users any excess tokens that remain in the protocol function addLiquidity( + uint256 tokenId, + uint256 liquidity, + address token0, + address token1, + uint256 amount0Max, + uint256 amount1Max, + bytes calldata hookData + ) external onlyRole(TREASURY_ADMIN_ROLE) returns (uint256 positionId) { + IERC20(token0).safeTransferFrom(msg.sender, address(this), amount0Max); + IERC20(token1).safeTransferFrom(msg.sender, address(this), amount1Max); + + IERC20(token0).approve(address(posm), amount0Max); + IERC20(token1).approve(address(posm), amount1Max); + + bytes memory actions = abi.encodePacked(uint8(Actions.INCREASE_LIQUIDITY), uint8(Actions.SETTLE_PAIR)); + + bytes[] memory params = new bytes[](2); + params[0] = abi.encode(tokenId, liquidity, amount0Max, amount1Max, hookData); + Currency currency0 = Currency.wrap(token0); + Currency currency1 = Currency.wrap(token1); + params[1] = abi.encode(currency0, currency1); + + uint256 deadline = block.timestamp + 60; + + uint256 valueToPass = currency0.isAddressZero() ? amount0Max : 0; + + posm.modifyLiquidities{value: valueToPass}(abi.encode(actions, params), deadline); + + emit LiquidityAdded(token0, token1, amount0Max, amount1Max); + return 0; + } + + function removeLiquidity( + uint256 tokenId, + uint256 liquidity, address token0, address token1, - uint256 amount0, - uint256 amount1, - int24 tickLower, - int24 tickUpper + uint256 amount0Max, + uint256 amount1Max, + bytes calldata hookData ) external onlyRole(TREASURY_ADMIN_ROLE) returns (uint256 positionId) { - IERC20(token0).safeTransferFrom(msg.sender, address(this), amount0); - IERC20(token1).safeTransferFrom(msg.sender, address(this), amount1); + IERC20(token0).safeTransferFrom(msg.sender, address(this), amount0Max); + IERC20(token1).safeTransferFrom(msg.sender, address(this), amount1Max); + + IERC20(token0).approve(address(posm), amount0Max); + IERC20(token1).approve(address(posm), amount1Max); + + bytes memory actions = abi.encodePacked(uint8(Actions.INCREASE_LIQUIDITY), uint8(Actions.SETTLE_PAIR)); + + bytes[] memory params = new bytes[](2); + params[0] = abi.encode(tokenId, liquidity, amount0Max, amount1Max, hookData); + Currency currency0 = Currency.wrap(token0); + Currency currency1 = Currency.wrap(token1); + params[1] = abi.encode(currency0, currency1); - IERC20(token0).approve(address(modifyPositionRouter), amount0); - IERC20(token1).approve(address(modifyPositionRouter), amount1); + uint256 deadline = block.timestamp + 60; - // Placeholder for actual Uniswap v4 liquidity addition - // This would call the modifyPositionRouter with the correct parameters + uint256 valueToPass = currency0.isAddressZero() ? amount0Max : 0; - emit LiquidityAdded(token0, token1, amount0, amount1); + posm.modifyLiquidities{value: valueToPass}(abi.encode(actions, params), deadline); + + emit LiquidityAdded(token0, token1, amount0Max, amount1Max); return 0; } // Deposit unused assets into yield-generating protocol + // Yet to choose the protocol to use function depositIntoYieldProtocol(address token, uint256 amount, address yieldProtocol) external onlyRole(TREASURY_ADMIN_ROLE) { IERC20(token).safeTransferFrom(msg.sender, address(this), amount); - - // Placeholder for yield protocol deposit logic - // This would integrate with lending protocols on Base - + // TODO currentYieldProtocol = yieldProtocol; emit YieldDeposited(yieldProtocol, token, amount); From 43c2a234140660fcdb6143d8d32399e25de77e6f Mon Sep 17 00:00:00 2001 From: ybtuti Date: Thu, 15 May 2025 12:27:51 +0300 Subject: [PATCH 06/12] Add matrix strategy and skip size check in CI Enhances CI workflow configuration by: - Adding matrix strategy with ubuntu-latest OS - Skips contract size check during forge build to prevent size-related failures These changes make the CI pipeline more flexible for future multi-OS testing while avoiding potential contract size limitations during development. --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1074b5f..d94d54f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,8 @@ jobs: check: strategy: fail-fast: true + matrix: + os: [ubuntu-latest] name: Foundry project runs-on: ubuntu-latest @@ -36,7 +38,7 @@ jobs: - name: Run Forge build run: | - forge build --sizes + forge build --sizes --skip-size-check id: build - name: Run Forge tests From b93bfefb1fab4616836b1e2d26f70a98a64f75de Mon Sep 17 00:00:00 2001 From: ybtuti Date: Thu, 15 May 2025 12:33:35 +0300 Subject: [PATCH 07/12] Update CI workflow for improved efficiency --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d94d54f..73a9024 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: - name: Run Forge build run: | - forge build --sizes --skip-size-check + forge build id: build - name: Run Forge tests From 3eeb310cbba9d0046c646d5d73e48c109c54e4f0 Mon Sep 17 00:00:00 2001 From: ybtuti Date: Fri, 16 May 2025 15:52:34 +0300 Subject: [PATCH 08/12] Implement ChamaYieldHook contract for liquidity management and yield protocol integration Implements yield integration hook for UniswapV4 liquidity Adds ChamaYieldHook contract to manage liquidity and yield farming: - Automatically deploys excess liquidity to yield protocols - Withdraws from yield protocols when needed for swaps - Integrates with ERC4626-compliant vaults - Updates TreasuryManager with fee collection and liquidity management Supports dynamic liquidity management while maximizing yield on idle assets --- src/uniV4Intergration/ChamaYieldHook.sol | 134 ++++++++++++++++++++++ src/uniV4Intergration/TreasuryManager.sol | 44 ++++--- 2 files changed, 159 insertions(+), 19 deletions(-) create mode 100644 src/uniV4Intergration/ChamaYieldHook.sol diff --git a/src/uniV4Intergration/ChamaYieldHook.sol b/src/uniV4Intergration/ChamaYieldHook.sol new file mode 100644 index 0000000..4af3d9e --- /dev/null +++ b/src/uniV4Intergration/ChamaYieldHook.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {Currency} from "v4-core/src/types/Currency.sol"; +import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; +import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// For now we are using morpho vaults, which are ERC4626 compliant +interface ILendingProtocol is IERC4626 {} + +import {BaseHook, Hooks, IPoolManager, SwapParams} from "v4-periphery/src/utils/BaseHook.sol"; + +contract ChamaYieldHook is BaseHook { + using SafeERC20 for IERC20; + using PoolIdLibrary for PoolKey; + // NOTE: --------------------------------------------------------- + // state variables should typically be unique to a pool + // a single hook contract should be able to service multiple pools + // --------------------------------------------------------------- + + mapping(PoolId => uint256 count) public beforeSwapCount; + mapping(PoolId => uint256 count) public afterSwapCount; + mapping(address => bool) public approvedYieldProtocols; + mapping(address => address) public tokenYieldProtocol; //current yield protocols for each token + // Minimum token balance to keep liquid (not rehypothecated) + mapping(address => uint256) public minLiquidBalance; + // Amount of tokens currently deployed in yield protocols + mapping(address => uint256) public deployed; + + event TokensWithdrawn(address indexed token, address indexed protocol, uint256 amount); + event TokensDeployed(address indexed token, address indexed protocol, uint256 amount); + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + function _beforeSwap(address, PoolKey calldata key, SwapParams calldata params, bytes calldata) + internal + returns ( + /*override*/ + bytes4, + BeforeSwapDelta, + uint24 + ) + { + // Before a swap, make sure we have enough liquidity by withdrawing from yield protocol if needed + address tokenOut = params.zeroForOne ? Currency.unwrap(key.currency1) : Currency.unwrap(key.currency0); + + // Ensure enough liquidity for tokenOut + ensureLiquidity(tokenOut, params.amountSpecified); + return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + function _afterSwap(address, PoolKey calldata key, SwapParams calldata, BalanceDelta delta, bytes calldata hookData) + internal + returns (bytes4, int128) + { + address token0 = Currency.unwrap(key.currency0); + address token1 = Currency.unwrap(key.currency1); + + // Check if we can deploy excess liquidity + deployExcessLiquidity(token0); + deployExcessLiquidity(token1); + return (BaseHook.afterSwap.selector, 0); + } + + function ensureLiquidity(address token, int256 amountNeeded) internal { + if (amountNeeded <= 0) return; + + uint256 currentBalance = IERC20(token).balanceOf(address(this)); + + // If we don't have enough liquid tokens, withdraw from lending protocol + if (currentBalance < uint256(amountNeeded) && deployed[token] > 0) { + address yieldProtocol = tokenYieldProtocol[token]; + + if (yieldProtocol != address(0)) { + uint256 amountToWithdraw = uint256(amountNeeded) - currentBalance; + if (amountToWithdraw > deployed[token]) { + amountToWithdraw = deployed[token]; + } + + // Withdraw from lending protocol + ILendingProtocol(yieldProtocol).withdraw(amountToWithdraw, token, msg.sender); + deployed[token] -= amountToWithdraw; + + emit TokensWithdrawn(token, yieldProtocol, amountToWithdraw); + } + } + } + + function deployExcessLiquidity(address token) internal { + address yieldProtocol = tokenYieldProtocol[token]; + + if (yieldProtocol != address(0)) { + uint256 currentBalance = IERC20(token).balanceOf(address(this)); + uint256 excessAmount = 0; + + if (currentBalance > minLiquidBalance[token]) { + excessAmount = currentBalance - minLiquidBalance[token]; + + if (excessAmount > 0) { + IERC20(token).safeIncreaseAllowance(yieldProtocol, excessAmount); + ILendingProtocol(yieldProtocol).deposit(excessAmount, token); + deployed[token] += excessAmount; + + emit TokensDeployed(token, yieldProtocol, excessAmount); + } + } + } + } + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } +} diff --git a/src/uniV4Intergration/TreasuryManager.sol b/src/uniV4Intergration/TreasuryManager.sol index 726233e..d740604 100644 --- a/src/uniV4Intergration/TreasuryManager.sol +++ b/src/uniV4Intergration/TreasuryManager.sol @@ -5,11 +5,8 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; - import {Errors} from "../utils/Errors.sol"; import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; -import {Actions} from "v4-periphery/src/libraries/Actions.sol"; - // Swap import {UniversalRouter} from "@uniswap/universal-router/contracts/UniversalRouter.sol"; import {Commands} from "@uniswap/universal-router/contracts/libraries/Commands.sol"; @@ -18,7 +15,6 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IV4Router, PoolKey, Currency} from "v4-periphery/src/interfaces/IV4Router.sol"; import {Actions} from "v4-periphery/src/libraries/Actions.sol"; import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract TreasuryManager is AccessControl { using SafeERC20 for IERC20; @@ -186,36 +182,46 @@ contract TreasuryManager is AccessControl { uint256 liquidity, address token0, address token1, - uint256 amount0Max, - uint256 amount1Max, + uint256 amount0Min, + uint256 amount1Min, bytes calldata hookData - ) external onlyRole(TREASURY_ADMIN_ROLE) returns (uint256 positionId) { - IERC20(token0).safeTransferFrom(msg.sender, address(this), amount0Max); - IERC20(token1).safeTransferFrom(msg.sender, address(this), amount1Max); + ) external onlyRole(TREASURY_ADMIN_ROLE) { + bytes memory actions = abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR)); - IERC20(token0).approve(address(posm), amount0Max); - IERC20(token1).approve(address(posm), amount1Max); + bytes[] memory params = new bytes[](2); + params[0] = abi.encode(tokenId, liquidity, amount0Min, amount1Min, hookData); + Currency currency0 = Currency.wrap(token0); + Currency currency1 = Currency.wrap(token1); + params[1] = abi.encode(currency0, currency1, msg.sender); - bytes memory actions = abi.encodePacked(uint8(Actions.INCREASE_LIQUIDITY), uint8(Actions.SETTLE_PAIR)); + uint256 deadline = block.timestamp + 60; + + uint256 valueToPass = currency0.isAddressZero() ? amount0Min : 0; + + posm.modifyLiquidities{value: valueToPass}(abi.encode(actions, params), deadline); + } + + /// TODO: Add a way for identifying which tokens were collected + function collectPoolFees(uint256 tokenId, bytes memory hookData, address token0, address token1) external { + bytes memory actions = abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR)); bytes[] memory params = new bytes[](2); - params[0] = abi.encode(tokenId, liquidity, amount0Max, amount1Max, hookData); + /// @dev collecting fees is achieved with liquidity=0, the second parameter + params[0] = abi.encode(tokenId, 0, 0, 0, hookData); + Currency currency0 = Currency.wrap(token0); Currency currency1 = Currency.wrap(token1); - params[1] = abi.encode(currency0, currency1); + params[1] = abi.encode(currency0, currency1, msg.sender); uint256 deadline = block.timestamp + 60; - uint256 valueToPass = currency0.isAddressZero() ? amount0Max : 0; + uint256 valueToPass = currency0.isAddressZero() ? 0 : 0; posm.modifyLiquidities{value: valueToPass}(abi.encode(actions, params), deadline); - - emit LiquidityAdded(token0, token1, amount0Max, amount1Max); - return 0; } // Deposit unused assets into yield-generating protocol - // Yet to choose the protocol to use + // For now we are going to use mopho vaults function depositIntoYieldProtocol(address token, uint256 amount, address yieldProtocol) external onlyRole(TREASURY_ADMIN_ROLE) From 9ef12e743811bd2633c2aeb8a2b54c593637ec68 Mon Sep 17 00:00:00 2001 From: ybtuti Date: Fri, 16 May 2025 16:11:18 +0300 Subject: [PATCH 09/12] Update subproject reference in v4-periphery --- lib/v4-periphery | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/v4-periphery b/lib/v4-periphery index eeb3eff..ad04c9f 160000 --- a/lib/v4-periphery +++ b/lib/v4-periphery @@ -1 +1 @@ -Subproject commit eeb3eff28dd5f5f17aa94180fa3610ff59b0e1c8 +Subproject commit ad04c9f24a170accf5ea1b2836bbafd514537ca6 From 0201640363b2b277869d372a08e0b3d5e8e3d08c Mon Sep 17 00:00:00 2001 From: ybtuti Date: Fri, 16 May 2025 16:11:28 +0300 Subject: [PATCH 10/12] Refactor ChamaYieldHook: Update import path and implement getHookPermissions function --- src/uniV4Intergration/ChamaYieldHook.sol | 48 +++++++++++++----------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/uniV4Intergration/ChamaYieldHook.sol b/src/uniV4Intergration/ChamaYieldHook.sol index 4af3d9e..b4efc16 100644 --- a/src/uniV4Intergration/ChamaYieldHook.sol +++ b/src/uniV4Intergration/ChamaYieldHook.sol @@ -14,7 +14,12 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol // For now we are using morpho vaults, which are ERC4626 compliant interface ILendingProtocol is IERC4626 {} -import {BaseHook, Hooks, IPoolManager, SwapParams} from "v4-periphery/src/utils/BaseHook.sol"; +import { + BaseHook, + Hooks, + IPoolManager, + SwapParams +} from "@uniswap/universal-router/lib/v4-periphery/src/utils/BaseHook.sol"; contract ChamaYieldHook is BaseHook { using SafeERC20 for IERC20; @@ -38,6 +43,26 @@ contract ChamaYieldHook is BaseHook { constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + /// @notice The following 2 functions should be overiden but that bringes an error figure out what the hell is the issue function _beforeSwap(address, PoolKey calldata key, SwapParams calldata params, bytes calldata) internal returns ( @@ -55,7 +80,7 @@ contract ChamaYieldHook is BaseHook { return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } - function _afterSwap(address, PoolKey calldata key, SwapParams calldata, BalanceDelta delta, bytes calldata hookData) + function _afterSwap(address, PoolKey calldata key, SwapParams calldata, BalanceDelta, bytes calldata) internal returns (bytes4, int128) { @@ -112,23 +137,4 @@ contract ChamaYieldHook is BaseHook { } } } - - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: false, - afterInitialize: false, - beforeAddLiquidity: false, - afterAddLiquidity: false, - beforeRemoveLiquidity: false, - afterRemoveLiquidity: false, - beforeSwap: true, - afterSwap: true, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); - } } From 5c5b1b48d86cfab8d0eec59a35745b9565fe4900 Mon Sep 17 00:00:00 2001 From: ybtuti Date: Fri, 16 May 2025 17:33:13 +0300 Subject: [PATCH 11/12] Update fee structure in TreasuryManager and add LPFeeLibrary import in StablecoinPoolFactory --- foundry.toml | 2 ++ .../StablecoinPoolFactory.sol | 27 +++++++--------- src/uniV4Intergration/TreasuryManager.sol | 31 ------------------- 3 files changed, 14 insertions(+), 46 deletions(-) diff --git a/foundry.toml b/foundry.toml index 9914966..fcb57b5 100644 --- a/foundry.toml +++ b/foundry.toml @@ -17,5 +17,7 @@ remappings = [ "@uniswap/universal-router/=lib/universal-router/", "@uniswap/v3-core/=lib/v3-core/", "@uniswap/v2-core/=lib/v2-core/", + + ] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/src/uniV4Intergration/StablecoinPoolFactory.sol b/src/uniV4Intergration/StablecoinPoolFactory.sol index e7935a5..f25afb7 100644 --- a/src/uniV4Intergration/StablecoinPoolFactory.sol +++ b/src/uniV4Intergration/StablecoinPoolFactory.sol @@ -13,6 +13,7 @@ import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol import {Actions} from "v4-periphery/src/libraries/Actions.sol"; import {IAllowanceTransfer} from "v4-periphery/lib/permit2/src/interfaces/IAllowanceTransfer.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {LPFeeLibrary} from "v4-core/src/libraries/LPFeeLibrary.sol"; contract StablecoinPoolFactory is AccessControl { bytes32 public constant POOL_CREATOR_ROLE = keccak256("POOL_CREATOR_ROLE"); @@ -25,7 +26,7 @@ contract StablecoinPoolFactory is AccessControl { // Mapping to track created pools mapping(address token => bool exists) public createdPools; - event PoolCreated(address indexed token, uint24 fee, int24 tickSpacing); + event PoolCreated(address indexed token, int24 tickSpacing); constructor( IPoolManager _poolManager, @@ -46,36 +47,33 @@ contract StablecoinPoolFactory is AccessControl { * * @notice This creates a new USDC pair with optimized parameters for stablecoins * @param token This is the other token for the pool - * @param lpfee This is the fee for the pool expressed in pips ie 3000 = 0.30% * @param tickSpacing is the granularity of the pool. Lower values are more precise but may be more expensive to trade on * @param sqrtPriceX96 should be expressed as floor(sqrt(token1 / token0) * 2^96) */ - function createStablecoinPool(address token, uint24 lpfee, int24 tickSpacing, uint160 sqrtPriceX96) + function createStablecoinPool(address token, int24 tickSpacing, uint160 sqrtPriceX96) external onlyRole(POOL_CREATOR_ROLE) { - _createStablecoinPool(token, lpfee, tickSpacing, sqrtPriceX96); + _createStablecoinPool(token, tickSpacing, sqrtPriceX96); } /// See createStablecoinPool for details, only change with this is that we are doing pools for multiple tokens at once /// @param initialPrices should be expressed as floor(sqrt(token1 / token0) * 2^96) function createMultiplePools( address[] calldata tokens, - uint24[] calldata lpfees, int24[] calldata tickSpacings, uint160[] calldata initialPrices ) external onlyRole(POOL_CREATOR_ROLE) { - if ( - tokens.length != lpfees.length && lpfees.length != tickSpacings.length - && tickSpacings.length != initialPrices.length - ) revert Errors.PoolFactory__ArrayLengthMismatch(); + if (tokens.length != tickSpacings.length && tickSpacings.length != initialPrices.length) { + revert Errors.PoolFactory__ArrayLengthMismatch(); + } for (uint256 i = 0; i < tokens.length; i++) { - _createStablecoinPool(tokens[i], lpfees[i], tickSpacings[i], initialPrices[i]); + _createStablecoinPool(tokens[i], tickSpacings[i], initialPrices[i]); } } - function _createStablecoinPool(address token, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) internal { + function _createStablecoinPool(address token, int24 tickSpacing, uint160 sqrtPriceX96) internal { if (createdPools[token]) revert Errors.PoolFactory__PoolAlreadyExists(token); (Currency currency0, Currency currency1) = uint160(usdcToken) < uint160(token) @@ -85,7 +83,7 @@ contract StablecoinPoolFactory is AccessControl { PoolKey memory poolKey = PoolKey({ currency0: currency0, currency1: currency1, - fee: fee, + fee: LPFeeLibrary.DYNAMIC_FEE_FLAG, tickSpacing: tickSpacing, hooks: IHooks(address(feeHook)) }); @@ -94,13 +92,12 @@ contract StablecoinPoolFactory is AccessControl { createdPools[token] = true; - emit PoolCreated(token, fee, tickSpacing); + emit PoolCreated(token, tickSpacing); } /// @notice startingPrice is the price of the pool at initialization expressed as floor(sqrt(token1 / token0) * 2^96) function createPoolAndAddLiquidity( address token, - uint24 lpfee, int24 tickSpacing, uint160 startingPrice, int24 tickLower, @@ -121,7 +118,7 @@ contract StablecoinPoolFactory is AccessControl { PoolKey memory pool = PoolKey({ currency0: currency0, currency1: currency1, - fee: lpfee, + fee: LPFeeLibrary.DYNAMIC_FEE_FLAG, tickSpacing: tickSpacing, hooks: IHooks(address(feeHook)) }); diff --git a/src/uniV4Intergration/TreasuryManager.sol b/src/uniV4Intergration/TreasuryManager.sol index d740604..1f96f26 100644 --- a/src/uniV4Intergration/TreasuryManager.sol +++ b/src/uniV4Intergration/TreasuryManager.sol @@ -33,10 +33,6 @@ contract TreasuryManager is AccessControl { mapping(address => mapping(address => uint256)) public poolIds; - // Protocol fees in basis points - uint256 public baseFee = 30; // 0.3% - uint256 public stablecoinReducedFee = 10; // 0.1% for stablecoin swaps - // Yield strategy settings address public currentYieldProtocol; bool public autoReinvestYield = true; @@ -64,21 +60,6 @@ contract TreasuryManager is AccessControl { _grantRole(TREASURY_ADMIN_ROLE, _admin); } - function getDynamicFee(address token0, address token1) public view returns (uint256) { - // If both tokens are stablecoins, apply reduced fee - if (isStablecoin(token0) && isStablecoin(token1)) { - return stablecoinReducedFee; - } - - return baseFee; - } - - function isStablecoin(address token) public view returns (bool) { - // TODO Add logic to identify stablecoins (USDC, USDT, DAI, etc.) - // For now, using a simple check for USDC - return token == usdcToken; - } - function swapTokens( PoolKey memory key, address tokenIn, @@ -233,18 +214,6 @@ contract TreasuryManager is AccessControl { emit YieldDeposited(yieldProtocol, token, amount); } - // Update fee parameters (only admin) - function updateFeeParameters(uint256 _baseFee, uint256 _stablecoinReducedFee) - external - onlyRole(TREASURY_ADMIN_ROLE) - { - require(_baseFee <= 100, "Fee too high"); // Max 1% - require(_stablecoinReducedFee <= _baseFee, "Reduced fee must be <= base fee"); - - baseFee = _baseFee; - stablecoinReducedFee = _stablecoinReducedFee; - } - // Grant CHAMA_ROLE to a Chama contract function addChamaContract(address chamaContract) external onlyRole(TREASURY_ADMIN_ROLE) { _grantRole(CHAMA_ROLE, chamaContract); From d29ffec67997afa7f9a00cf2fb50d31e410f9bfe Mon Sep 17 00:00:00 2001 From: ybtuti Date: Fri, 16 May 2025 17:33:41 +0300 Subject: [PATCH 12/12] Implements dynamic fee system with yield optimization Introduces a flexible fee structure that adapts based on token types: - Adds reduced fees for stablecoin pairs (0.1%) - Sets default base fee at 0.3% - Integrates with Uniswap v4 hooks for fee management Enhances yield optimization by: - Adding protocol approval system - Implementing automatic liquidity deployment - Managing minimum token balance requirements TODO: Add access control for fee parameter updates --- src/uniV4Intergration/ChamaFeeHook.sol | 39 ++++++++++++++++++++- src/uniV4Intergration/ChamaYieldHook.sol | 43 ++++++++++-------------- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/uniV4Intergration/ChamaFeeHook.sol b/src/uniV4Intergration/ChamaFeeHook.sol index a66ef78..526928c 100644 --- a/src/uniV4Intergration/ChamaFeeHook.sol +++ b/src/uniV4Intergration/ChamaFeeHook.sol @@ -2,4 +2,41 @@ pragma solidity 0.8.24; -contract ChamaFeeHook {} +import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; + +contract ChamaFeeHook { + uint256 public baseFee = 30; // 0.3% + uint256 public stablecoinReducedFee = 10; // 0.1% for stablecoin swaps + + mapping(PoolId => uint24) public customFees; + mapping(address => bool) public isStablecoin; + + constructor() { + // These are placeholders, for now + isStablecoin[0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48] = true; // USDC + isStablecoin[0xdAC17F958D2ee523a2206206994597C13D831ec7] = true; // USDT + isStablecoin[0x6B175474E89094C44Da98b954EedeAC495271d0F] = true; // DAI + } + + function getDynamicFee(address token0, address token1) public view returns (uint256) { + // If both tokens are stablecoins, apply reduced fee + if (isStablecoin[token0] && isStablecoin[token1]) { + return stablecoinReducedFee; + } + + return baseFee; + } + + /// TODO Add some access control on this + function updateFeeParameters(uint256 _baseFee, uint256 _stablecoinReducedFee) external { + require(_baseFee <= 100, "Fee too high"); // Max 1% + require(_stablecoinReducedFee <= _baseFee, "Reduced fee must be <= base fee"); + + baseFee = _baseFee; + stablecoinReducedFee = _stablecoinReducedFee; + } + + function setStablecoin(address token, bool status) external { + isStablecoin[token] = status; + } +} diff --git a/src/uniV4Intergration/ChamaYieldHook.sol b/src/uniV4Intergration/ChamaYieldHook.sol index b4efc16..05ee075 100644 --- a/src/uniV4Intergration/ChamaYieldHook.sol +++ b/src/uniV4Intergration/ChamaYieldHook.sol @@ -2,35 +2,26 @@ pragma solidity ^0.8.24; -import {Currency} from "v4-core/src/types/Currency.sol"; -import {PoolKey} from "v4-core/src/types/PoolKey.sol"; import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; -import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; -import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol"; +import {BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol"; import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ChamaFeeHook} from "./ChamaFeeHook.sol"; +import {IPoolManager, Currency} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; // For now we are using morpho vaults, which are ERC4626 compliant interface ILendingProtocol is IERC4626 {} import { - BaseHook, - Hooks, - IPoolManager, - SwapParams -} from "@uniswap/universal-router/lib/v4-periphery/src/utils/BaseHook.sol"; + BaseHook, Hooks, IPoolManager, SwapParams, PoolKey, BeforeSwapDelta +} from "v4-periphery/src/utils/BaseHook.sol"; -contract ChamaYieldHook is BaseHook { +contract ChamaYieldHook is ChamaFeeHook, BaseHook { using SafeERC20 for IERC20; using PoolIdLibrary for PoolKey; - // NOTE: --------------------------------------------------------- - // state variables should typically be unique to a pool - // a single hook contract should be able to service multiple pools - // --------------------------------------------------------------- - mapping(PoolId => uint256 count) public beforeSwapCount; - mapping(PoolId => uint256 count) public afterSwapCount; mapping(address => bool) public approvedYieldProtocols; mapping(address => address) public tokenYieldProtocol; //current yield protocols for each token // Minimum token balance to keep liquid (not rehypothecated) @@ -62,32 +53,34 @@ contract ChamaYieldHook is BaseHook { }); } - /// @notice The following 2 functions should be overiden but that bringes an error figure out what the hell is the issue function _beforeSwap(address, PoolKey calldata key, SwapParams calldata params, bytes calldata) internal - returns ( - /*override*/ - bytes4, - BeforeSwapDelta, - uint24 - ) + override + returns (bytes4, BeforeSwapDelta, uint24) { // Before a swap, make sure we have enough liquidity by withdrawing from yield protocol if needed address tokenOut = params.zeroForOne ? Currency.unwrap(key.currency1) : Currency.unwrap(key.currency0); // Ensure enough liquidity for tokenOut ensureLiquidity(tokenOut, params.amountSpecified); - return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + + // Here we update the fee based on whether it is a stablecoin pool + poolManager.updateDynamicLPFee( + key, uint24(getDynamicFee(Currency.unwrap(key.currency0), Currency.unwrap(key.currency1))) + ); + + return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), uint24(0)); } + /// @notice The following function should be overiden but that bringes an error figure out what the hell is the issue function _afterSwap(address, PoolKey calldata key, SwapParams calldata, BalanceDelta, bytes calldata) internal + override returns (bytes4, int128) { address token0 = Currency.unwrap(key.currency0); address token1 = Currency.unwrap(key.currency1); - // Check if we can deploy excess liquidity deployExcessLiquidity(token0); deployExcessLiquidity(token1); return (BaseHook.afterSwap.selector, 0);