diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1074b5f..73a9024 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 id: build - name: Run Forge tests 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 c3fc66d..fcb57b5 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,17 +1,23 @@ [profile.default] +viaIR = true + src = "src" out = "out" 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/", "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..3663f6d --- /dev/null +++ b/lib/universal-router @@ -0,0 +1 @@ +Subproject commit 3663f6db6e2fe121753cd2d899699c2dc75dca86 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..ad04c9f --- /dev/null +++ b/lib/v4-periphery @@ -0,0 +1 @@ +Subproject commit ad04c9f24a170accf5ea1b2836bbafd514537ca6 diff --git a/src/uniV4Intergration/ChamaFeeHook.sol b/src/uniV4Intergration/ChamaFeeHook.sol new file mode 100644 index 0000000..526928c --- /dev/null +++ b/src/uniV4Intergration/ChamaFeeHook.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +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 new file mode 100644 index 0000000..05ee075 --- /dev/null +++ b/src/uniV4Intergration/ChamaYieldHook.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.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, PoolKey, BeforeSwapDelta +} from "v4-periphery/src/utils/BaseHook.sol"; + +contract ChamaYieldHook is ChamaFeeHook, BaseHook { + using SafeERC20 for IERC20; + using PoolIdLibrary for PoolKey; + + 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 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 + }); + } + + function _beforeSwap(address, PoolKey calldata key, SwapParams calldata params, bytes calldata) + internal + 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); + + // 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); + + 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); + } + } + } + } +} diff --git a/src/uniV4Intergration/StablecoinPoolFactory.sol b/src/uniV4Intergration/StablecoinPoolFactory.sol new file mode 100644 index 0000000..f25afb7 --- /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"; +import {LPFeeLibrary} from "v4-core/src/libraries/LPFeeLibrary.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, 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 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, int24 tickSpacing, uint160 sqrtPriceX96) + external + onlyRole(POOL_CREATOR_ROLE) + { + _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, + int24[] calldata tickSpacings, + uint160[] calldata initialPrices + ) external onlyRole(POOL_CREATOR_ROLE) { + if (tokens.length != tickSpacings.length && tickSpacings.length != initialPrices.length) { + revert Errors.PoolFactory__ArrayLengthMismatch(); + } + + for (uint256 i = 0; i < tokens.length; i++) { + _createStablecoinPool(tokens[i], tickSpacings[i], initialPrices[i]); + } + } + + 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) + ? (Currency.wrap(usdcToken), Currency.wrap(token)) + : (Currency.wrap(token), Currency.wrap(usdcToken)); + + PoolKey memory poolKey = PoolKey({ + currency0: currency0, + currency1: currency1, + fee: LPFeeLibrary.DYNAMIC_FEE_FLAG, + tickSpacing: tickSpacing, + hooks: IHooks(address(feeHook)) + }); + + poolManager.initialize(poolKey, sqrtPriceX96); + + createdPools[token] = true; + + 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, + 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: LPFeeLibrary.DYNAMIC_FEE_FLAG, + 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); + + uint256 ethTosend = currency0.isAddressZero() ? amount0Max : 0; + if (ethTosend > 0) IPositionManager(posm).multicall{value: ethTosend}(params); + } +} diff --git a/src/uniV4Intergration/TreasuryManager.sol b/src/uniV4Intergration/TreasuryManager.sol index 08f56a0..1f96f26 100644 --- a/src/uniV4Intergration/TreasuryManager.sol +++ b/src/uniV4Intergration/TreasuryManager.sol @@ -4,30 +4,35 @@ 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"; +// 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"; 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; 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; @@ -37,43 +42,39 @@ 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); } - 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(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,60 +84,134 @@ 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 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); - IERC20(token0).approve(address(modifyPositionRouter), amount0); - IERC20(token1).approve(address(modifyPositionRouter), amount1); + bytes memory actions = abi.encodePacked(uint8(Actions.INCREASE_LIQUIDITY), uint8(Actions.SETTLE_PAIR)); - // Placeholder for actual Uniswap v4 liquidity addition - // This would call the modifyPositionRouter with the correct parameters + 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); - emit LiquidityAdded(token0, token1, amount0, amount1); + 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; } - // Deposit unused assets into yield-generating protocol - function depositIntoYieldProtocol(address token, uint256 amount, address yieldProtocol) - external - onlyRole(TREASURY_ADMIN_ROLE) - { - IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + function removeLiquidity( + uint256 tokenId, + uint256 liquidity, + address token0, + address token1, + uint256 amount0Min, + uint256 amount1Min, + bytes calldata hookData + ) external onlyRole(TREASURY_ADMIN_ROLE) { + bytes memory actions = abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR)); - // Placeholder for yield protocol deposit logic - // This would integrate with lending protocols on Base + 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); - currentYieldProtocol = yieldProtocol; + uint256 deadline = block.timestamp + 60; - emit YieldDeposited(yieldProtocol, token, amount); + uint256 valueToPass = currency0.isAddressZero() ? amount0Min : 0; + + posm.modifyLiquidities{value: valueToPass}(abi.encode(actions, params), deadline); } - // Update fee parameters (only admin) - function updateFeeParameters(uint256 _baseFee, uint256 _stablecoinReducedFee) + /// 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); + /// @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, msg.sender); + + uint256 deadline = block.timestamp + 60; + + uint256 valueToPass = currency0.isAddressZero() ? 0 : 0; + + posm.modifyLiquidities{value: valueToPass}(abi.encode(actions, params), deadline); + } + + // Deposit unused assets into yield-generating protocol + // For now we are going to use mopho vaults + function depositIntoYieldProtocol(address token, uint256 amount, address yieldProtocol) external onlyRole(TREASURY_ADMIN_ROLE) { - require(_baseFee <= 100, "Fee too high"); // Max 1% - require(_stablecoinReducedFee <= _baseFee, "Reduced fee must be <= base fee"); + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + // TODO + currentYieldProtocol = yieldProtocol; - baseFee = _baseFee; - stablecoinReducedFee = _stablecoinReducedFee; + emit YieldDeposited(yieldProtocol, token, amount); } // Grant CHAMA_ROLE to a Chama contract 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); + } +}