From 9ec48de205bc60b6fc0ab0841eb1e2c9574a09c1 Mon Sep 17 00:00:00 2001 From: dov Date: Sun, 15 Sep 2024 14:40:58 -0400 Subject: [PATCH] added quoter and testing passed, additional functions make on twamm and interfaces to make work --- src/TWAMM.sol | 259 +++++++++++++++++++++++ src/interfaces/ITWAMM.sol | 15 ++ src/libraries/TWAMM/OrderPool.sol | 12 ++ src/quoter/TWAMMQuoter.sol | 122 +++++------ test/TWAMMQuoter.t.sol | 329 +++++++++++++++++------------- 5 files changed, 529 insertions(+), 208 deletions(-) diff --git a/src/TWAMM.sol b/src/TWAMM.sol index 009c19f..1bb76f7 100644 --- a/src/TWAMM.sol +++ b/src/TWAMM.sol @@ -22,6 +22,9 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {SqrtPriceMath} from "@uniswap/v4-core/src/libraries/SqrtPriceMath.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + contract TWAMM is BaseHook, ITWAMM { using TransferHelper for IERC20Minimal; @@ -69,6 +72,8 @@ contract TWAMM is BaseHook, ITWAMM { expirationInterval = _expirationInterval; } + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { return Hooks.Permissions({ beforeInitialize: true, @@ -688,4 +693,258 @@ contract TWAMM is BaseHook, ITWAMM { emit TransferOrderOwnership(poolId, orderKey.owner, newOwner, orderKey.expiration, orderKey.zeroForOne); } + + function quoteSwap( + PoolKey calldata key, + int256 amountSpecified, + bool zeroForOne, + uint160 sqrtPriceLimitX96 +) external view returns (int256 amount0Delta, int256 amount1Delta, uint160 sqrtPriceX96After) { + State storage twamm = twammStates[key.toId()]; + + // Simulate executing TWAMM orders up to the current block + uint160 simulatedSqrtPriceX96 = _simulateExecuteTWAMMOrders(twamm, key); + + // If there were pending TWAMM orders that would affect the price, use the simulated price + if (simulatedSqrtPriceX96 != 0) { + (amount0Delta, amount1Delta, sqrtPriceX96After) = _simulateSwap( + twamm, + key, + zeroForOne, + amountSpecified, + sqrtPriceLimitX96, + simulatedSqrtPriceX96 + ); + } else { + // If no pending TWAMM orders, use the current pool price + (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(key.toId()); + (amount0Delta, amount1Delta, sqrtPriceX96After) = _simulateSwap( + twamm, + key, + zeroForOne, + amountSpecified, + sqrtPriceLimitX96, + sqrtPriceX96 + ); + } } + +function _simulateExecuteTWAMMOrders(State storage twamm, PoolKey memory key) + private + view + returns (uint160 newSqrtPriceX96) +{ + if (!_hasOutstandingOrders(twamm)) { + return 0; + } + + (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(key.toId()); + uint128 liquidity = poolManager.getLiquidity(key.toId()); + + PoolParamsOnExecute memory pool = PoolParamsOnExecute({ + sqrtPriceX96: sqrtPriceX96, + liquidity: liquidity + }); + + // Simulate TWAMM order execution + (, newSqrtPriceX96) = _simulateTWAMMOrders( + twamm, + key, + pool + ); + + return newSqrtPriceX96; +} + +function _simulateTWAMMOrders( + State storage self, + PoolKey memory key, + PoolParamsOnExecute memory pool +) private view returns (bool zeroForOne, uint160 newSqrtPriceX96) { + if (!_hasOutstandingOrders(self)) { + return (false, pool.sqrtPriceX96); + } + + uint256 prevTimestamp = self.lastVirtualOrderTimestamp; + uint256 currentTimestamp = block.timestamp; + + // Create snapshots of the order pools + OrderPool.Snapshot memory orderPool0For1 = OrderPool.Snapshot({ + sellRateCurrent: self.orderPool0For1.sellRateCurrent, + earningsFactorCurrent: self.orderPool0For1.earningsFactorCurrent + }); + + OrderPool.Snapshot memory orderPool1For0 = OrderPool.Snapshot({ + sellRateCurrent: self.orderPool1For0.sellRateCurrent, + earningsFactorCurrent: self.orderPool1For0.earningsFactorCurrent + }); + + PoolParamsOnExecute memory simulatedPool = pool; + + // Simulate order execution over time + uint256 timeElapsed = currentTimestamp - prevTimestamp; + uint256 secondsElapsedX96 = timeElapsed * FixedPoint96.Q96; + + // Simulate the TWAMM order execution logic + TwammMath.ExecutionUpdateParams memory executionParams = TwammMath.ExecutionUpdateParams( + secondsElapsedX96, + simulatedPool.sqrtPriceX96, + simulatedPool.liquidity, + orderPool0For1.sellRateCurrent, + orderPool1For0.sellRateCurrent + ); + + uint160 finalSqrtPriceX96 = TwammMath.getNewSqrtPriceX96(executionParams); + + // Update earnings factors (in memory) + (uint256 earningsFactorPool0, uint256 earningsFactorPool1) = + TwammMath.calculateEarningsUpdates(executionParams, finalSqrtPriceX96); + + // Use simulation functions + OrderPool.simulateAdvanceToCurrentTime(orderPool0For1, earningsFactorPool0); + OrderPool.simulateAdvanceToCurrentTime(orderPool1For0, earningsFactorPool1); + + // Update pool parameters + simulatedPool.sqrtPriceX96 = finalSqrtPriceX96; + + zeroForOne = pool.sqrtPriceX96 > simulatedPool.sqrtPriceX96; + newSqrtPriceX96 = simulatedPool.sqrtPriceX96; +} + +function _simulateSwap( + State storage self, + PoolKey memory key, + bool zeroForOne, + int256 amountSpecified, + uint160 sqrtPriceLimitX96, + uint160 currentSqrtPriceX96 +) private view returns (int256 amount0Delta, int256 amount1Delta, uint160 sqrtPriceX96After) { + uint128 liquidity = poolManager.getLiquidity(key.toId()); + + int256 remainingAmountSpecified = amountSpecified; + uint160 sqrtPriceX96 = currentSqrtPriceX96; + + uint24 fee = key.fee; + + while (remainingAmountSpecified > 0 && sqrtPriceX96 != sqrtPriceLimitX96) { + uint160 nextSqrtPriceX96; + uint256 amountIn; + uint256 amountOut; + uint256 feeAmount; + + uint256 amountSpecifiedUint = uint256(remainingAmountSpecified); + + if (zeroForOne) { + // Reuse variables to reduce stack usage + { + uint160 sqrtPriceNext = SqrtPriceMath.getNextSqrtPriceFromInput( + sqrtPriceX96, + liquidity, + amountSpecifiedUint, + true + ); + + amountIn = SqrtPriceMath.getAmount0Delta( + sqrtPriceX96, + sqrtPriceNext, + liquidity, + true + ); + + amountOut = SqrtPriceMath.getAmount1Delta( + sqrtPriceX96, + sqrtPriceNext, + liquidity, + false + ); + + nextSqrtPriceX96 = sqrtPriceNext; + } + } else { + // Reuse variables to reduce stack usage + { + uint160 sqrtPriceNext = SqrtPriceMath.getNextSqrtPriceFromInput( + sqrtPriceX96, + liquidity, + amountSpecifiedUint, + false + ); + + amountIn = SqrtPriceMath.getAmount1Delta( + sqrtPriceX96, + sqrtPriceNext, + liquidity, + true + ); + + amountOut = SqrtPriceMath.getAmount0Delta( + sqrtPriceX96, + sqrtPriceNext, + liquidity, + false + ); + + nextSqrtPriceX96 = sqrtPriceNext; + } + } + + // Include fee + feeAmount = (amountIn * fee) / 1_000_000; // Assuming fee is in hundredths of a basis point + + amountIn += feeAmount; + + if (amountIn > amountSpecifiedUint) { + amountIn = amountSpecifiedUint; + + // Recalculate nextSqrtPriceX96 based on adjusted amountIn + if (zeroForOne) { + nextSqrtPriceX96 = SqrtPriceMath.getNextSqrtPriceFromInput( + sqrtPriceX96, + liquidity, + amountIn - feeAmount, // Subtract fee to get the net amountIn + true + ); + amountOut = SqrtPriceMath.getAmount1Delta( + sqrtPriceX96, + nextSqrtPriceX96, + liquidity, + false + ); + } else { + nextSqrtPriceX96 = SqrtPriceMath.getNextSqrtPriceFromInput( + sqrtPriceX96, + liquidity, + amountIn - feeAmount, + false + ); + amountOut = SqrtPriceMath.getAmount0Delta( + sqrtPriceX96, + nextSqrtPriceX96, + liquidity, + false + ); + } + remainingAmountSpecified = 0; + } else { + remainingAmountSpecified -= int256(amountIn); + } + + sqrtPriceX96 = nextSqrtPriceX96; + + if (zeroForOne) { + amount0Delta -= int256(amountIn); + amount1Delta += int256(amountOut); + } else { + amount1Delta -= int256(amountIn); + amount0Delta += int256(amountOut); + } + + if (sqrtPriceX96 == sqrtPriceLimitX96) { + break; + } + } + + sqrtPriceX96After = sqrtPriceX96; +} + +} \ No newline at end of file diff --git a/src/interfaces/ITWAMM.sol b/src/interfaces/ITWAMM.sol index 3b932d3..9f4c232 100644 --- a/src/interfaces/ITWAMM.sol +++ b/src/interfaces/ITWAMM.sol @@ -133,4 +133,19 @@ interface ITWAMM { function executeTWAMMOrders(PoolKey memory key) external; function tokensOwed(Currency token, address owner) external returns (uint256); + + /// @notice Simulates a swap without executing it, used for quoting + /// @param key The PoolKey for which to identify the amm pool + /// @param amountSpecified The amount of tokens to swap + /// @param zeroForOne The direction of the swap (true for 0 -> 1, false for 1 -> 0) + /// @param sqrtPriceLimitX96 The price limit of the swap + /// @return amount0Delta The delta of token0 + /// @return amount1Delta The delta of token1 + /// @return sqrtPriceX96After The sqrt price after the swap + function quoteSwap( + PoolKey calldata key, + int256 amountSpecified, + bool zeroForOne, + uint160 sqrtPriceLimitX96 + ) external view returns (int256 amount0Delta, int256 amount1Delta, uint160 sqrtPriceX96After); } diff --git a/src/libraries/TWAMM/OrderPool.sol b/src/libraries/TWAMM/OrderPool.sol index 4822dbf..6d2c508 100644 --- a/src/libraries/TWAMM/OrderPool.sol +++ b/src/libraries/TWAMM/OrderPool.sol @@ -16,6 +16,12 @@ library OrderPool { mapping(uint256 => uint256) earningsFactorAtInterval; } + // Define the Snapshot struct without mappings + struct Snapshot { + uint256 sellRateCurrent; + uint256 earningsFactorCurrent; + } + // Performs all updates on an OrderPool that must happen when hitting an expiration interval with expiring orders function advanceToInterval(State storage self, uint256 expiration, uint256 earningsFactor) internal { unchecked { @@ -31,4 +37,10 @@ library OrderPool { self.earningsFactorCurrent += earningsFactor; } } + + // Simulation function for advancing to current time + function simulateAdvanceToCurrentTime(Snapshot memory self, uint256 earningsFactor) internal pure { + // Simulate the logic without modifying storage + self.earningsFactorCurrent = earningsFactor; + } } diff --git a/src/quoter/TWAMMQuoter.sol b/src/quoter/TWAMMQuoter.sol index 1b35519..d8dcefd 100644 --- a/src/quoter/TWAMMQuoter.sol +++ b/src/quoter/TWAMMQuoter.sol @@ -1,83 +1,83 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; +pragma solidity ^0.8.23; -import {IQuoter} from "v4-periphery/src/interfaces/IQuoter.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; import {TWAMMGovernance} from "../governance/TWAMMGovernance.sol"; -import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; -import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import {ITWAMM} from "../interfaces/ITWAMM.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; contract TWAMMQuoter { - IQuoter public quoter; - IPoolManager public poolManager; - TWAMMGovernance public governanceContract; + IPoolManager public immutable poolManager; + TWAMMGovernance public immutable governanceContract; + ITWAMM public immutable twamm; - PoolKey public poolKey; + // PoolKey components + Currency public immutable currency0; + Currency public immutable currency1; + uint24 public immutable fee; + int24 public immutable tickSpacing; + IHooks public immutable hooks; uint160 constant MIN_SQRT_RATIO = 4295128739; uint160 constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; - event QuotedSwap(uint256 indexed proposalId, int128[] deltaAmounts, uint160 sqrtPriceX96After); + event QuotedSwap(uint256 indexed proposalId, int256 amount0Delta, int256 amount1Delta, uint160 sqrtPriceX96After); - constructor(address _poolManager, address _governanceContract, address _quoter, PoolKey memory _poolKey) { + constructor( + address _poolManager, + address _governanceContract, + address _twamm, + PoolKey memory _poolKey + ) { poolManager = IPoolManager(_poolManager); governanceContract = TWAMMGovernance(_governanceContract); - quoter = IQuoter(_quoter); - poolKey = _poolKey; - } - - function quoteProposalExactInput(uint256 proposalId) - public - returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After) - { - TWAMMGovernance.Proposal memory proposal = governanceContract.getProposal(proposalId); - - uint160 MAX_SLIPPAGE = proposal.zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1; + twamm = ITWAMM(_twamm); - (deltaAmounts, sqrtPriceX96After,) = quoter.quoteExactInputSingle( - IQuoter.QuoteExactSingleParams({ - poolKey: poolKey, - zeroForOne: proposal.zeroForOne, - exactAmount: uint128(proposal.amount), - sqrtPriceLimitX96: MAX_SLIPPAGE, - hookData: "" - }) - ); - - emit QuotedSwap(proposalId, deltaAmounts, sqrtPriceX96After); + // Store PoolKey components + currency0 = _poolKey.currency0; + currency1 = _poolKey.currency1; + fee = _poolKey.fee; + tickSpacing = _poolKey.tickSpacing; + hooks = _poolKey.hooks; } - function quoteProposalExactOutput(uint256 proposalId) - public - returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After) - { - TWAMMGovernance.Proposal memory proposal = governanceContract.getProposal(proposalId); - - uint160 MAX_SLIPPAGE = proposal.zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1; - - (deltaAmounts, sqrtPriceX96After,) = quoter.quoteExactOutputSingle( - IQuoter.QuoteExactSingleParams({ - poolKey: poolKey, - zeroForOne: proposal.zeroForOne, - exactAmount: uint128(proposal.amount), - sqrtPriceLimitX96: MAX_SLIPPAGE, - hookData: "" - }) - ); - - emit QuotedSwap(proposalId, deltaAmounts, sqrtPriceX96After); + function getPoolKey() public view returns (PoolKey memory) { + return PoolKey({ + currency0: currency0, + currency1: currency1, + fee: fee, + tickSpacing: tickSpacing, + hooks: hooks + }); } - function getQuoteForProposal(uint256 proposalId, bool exactInput) - external - returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After) - { - if (exactInput) { - return quoteProposalExactInput(proposalId); - } else { - return quoteProposalExactOutput(proposalId); - } - } + function quoteProposal(uint256 proposalId) +public +view +returns (int256 amount0Delta, int256 amount1Delta, uint160 sqrtPriceX96After) +{ + TWAMMGovernance.Proposal memory proposal = governanceContract.getProposal(proposalId); + require(proposal.startTime != 0, "Proposal does not exist"); + uint160 sqrtPriceLimitX96 = proposal.zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1; + PoolKey memory key = getPoolKey(); + // Call the quoteSwap function from the TWAMM contract + return twamm.quoteSwap( + key, + int256(proposal.amount), + proposal.zeroForOne, + sqrtPriceLimitX96 + ); + // Remove the emit statement +} + +function getQuoteForProposal(uint256 proposalId) +external +view +returns (int256 amount0Delta, int256 amount1Delta, uint160 sqrtPriceX96After) +{ + return quoteProposal(proposalId); } +} \ No newline at end of file diff --git a/test/TWAMMQuoter.t.sol b/test/TWAMMQuoter.t.sol index b462d3d..2b39a5d 100644 --- a/test/TWAMMQuoter.t.sol +++ b/test/TWAMMQuoter.t.sol @@ -1,147 +1,182 @@ -// // SPDX-License-Identifier: UNLICENSED -// pragma solidity ^0.8.19; - -// import "forge-std/Test.sol"; -// import "../src/TWAMM.sol"; -// import "../src/mocks/MockDaoToken.sol"; -// import "../src/governance/WrappedGovernanceToken.sol"; -// import {TWAMMGovernance} from "../src/governance/TWAMMGovernance.sol"; -// import {TWAMMQuoter} from "../src/quoter/TWAMMQuoter.sol"; -// import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -// import {IQuoter} from "v4-periphery/src/interfaces/IQuoter.sol"; -// import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -// import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; -// import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -// import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; - -// contract TWAMMQuoterTest is Test { -// IPoolManager poolManager; -// IQuoter quoter; -// TWAMMGovernance governance; -// WrappedGovernanceToken governanceToken; -// MockDAOToken public daoToken; -// TWAMMQuoter twammQuoter; -// TWAMM flags = -// TWAMM(address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG))); - -// PoolKey poolKey; -// uint256 initialProposalAmount = 1e18; -// bool zeroForOne = true; - -// function setUp() public { -// // Deploy mock contracts -// poolManager = IPoolManager(address(new MockPoolManager())); -// quoter = IQuoter(address(new MockQuoter())); - -// // Create pool key -// poolKey = PoolKey({ -// currency0: Currency.wrap(address(new MockERC20("Test Token 0", "T0", 18))), -// currency1: Currency.wrap(address(new MockERC20("Test Token 1", "T1", 18))), -// fee: 3000, -// tickSpacing: 60, -// hooks: flags -// }); - -// // Deploy governance contract -// governance = new TWAMMGovernance( -// manager, -// expirationInterval, -// daoToken, -// currency0, -// currency1, -// fee, -// tickSpacing, -// twamm // Add this missing argument -// ); - -// // Deploy TWAMMQuoter -// twammQuoter = new TWAMMQuoter(address(poolManager), address(governance), address(quoter), poolKey); -// } - -// function testCreateGovernanceProposal() public { - -// governanceToken.mint(address(this), 200 * 10**18); // mint to meet the proposal threshold - -// governance.createProposal(initialProposalAmount, 7 days, zeroForOne); - -// // Check proposal created -// TWAMMGovernance.Proposal memory proposal = governance.getProposal(0); -// assertEq(proposal.amount, initialProposalAmount, "Proposal not created correctly"); -// } - -// function testQuoteProposalExactInput() public { -// // Create a proposal -// createSampleProposal(); - -// // Get quote for the created proposal -// (int128[] memory deltaAmounts, uint160 sqrtPriceX96After) = twammQuoter.getQuoteForProposal(0, true); - -// // Log results for visual verification -// emit log_int(int256(deltaAmounts[1])); // Expected output amount -// emit log_uint(sqrtPriceX96After); // Expected price after swap - -// // Check if event emitted correctly -// vm.expectEmit(true, true, true, true); -// emit QuotedSwap(0, deltaAmounts, sqrtPriceX96After); -// twammQuoter.getQuoteForProposal(0, true); -// } - -// function testQuoteProposalExactOutput() public { -// // Create a proposal -// createSampleProposal(); - -// // Get quote for exact output of the created proposal -// (int128[] memory deltaAmounts, uint160 sqrtPriceX96After) = twammQuoter.getQuoteForProposal(0, false); - -// // Log results for visual verification -// emit log_int(int256(deltaAmounts[0])); // Expected input amount -// emit log_uint(sqrtPriceX96After); // Expected price after swap - -// // Check if event emitted correctly -// vm.expectEmit(true, true, true, true); -// emit QuotedSwap(0, deltaAmounts, sqrtPriceX96After); -// twammQuoter.getQuoteForProposal(0, false); -// } - -// function createSampleProposal() internal { -// governanceToken.mint(address(this), 200 * 10**18); // Mint governance tokens for proposal creation - -// governance.createProposal(initialProposalAmount, 7 days, zeroForOne); -// } - -// event QuotedSwap(uint256 indexed proposalId, int128[] deltaAmounts, uint160 sqrtPriceX96After); -// } - -// // Mock contracts -// contract MockPoolManager { -// function initialize(PoolKey memory, uint160, bytes memory) external {} -// } - -// contract MockQuoter { -// function quoteExactInputSingle(IQuoter.QuoteExactSingleParams memory) -// external -// pure -// returns (int128[] memory, uint160, uint32) -// { -// int128[] memory deltaAmounts = new int128[](2); -// deltaAmounts[0] = -1e18; -// deltaAmounts[1] = 9e17; -// return (deltaAmounts, 1 << 96, 1); -// } - -// function quoteExactOutputSingle(IQuoter.QuoteExactSingleParams memory) -// external -// pure -// returns (int128[] memory, uint160, uint32) -// { -// int128[] memory deltaAmounts = new int128[](2); -// deltaAmounts[0] = -11e17; -// deltaAmounts[1] = 1e18; -// return (deltaAmounts, 1 << 96, 1); -// } -// } - -// contract MockERC20 { -// constructor(string memory, string memory, uint8) {} -// function mint(address, uint256) public {} -// } +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "../src/quoter/TWAMMQuoter.sol"; +import "../src/TWAMM.sol"; +import "../src/governance/TWAMMGovernance.sol"; +import "../src/mocks/MockERC20.sol"; +import "../src/implementation/TWAMMImplementation.sol"; +import "@uniswap/v4-core/src/libraries/Hooks.sol"; +import "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; + +contract TWAMMQuoterTest is Test, Deployers { + using PoolIdLibrary for PoolKey; + + TWAMMQuoter public quoter; + TWAMMGovernance public governance; + TWAMMImplementation public twammImpl; + address public twamm; + PoolKey public poolKey; + PoolId public poolId; + MockERC20 public token0; + MockERC20 public token1; + MockERC20 public daoToken; + + address public alice = address(0x1); + address public bob = address(0x2); + + function setUp() public { + // Initialize the manager and routers + deployFreshManagerAndRouters(); + + // Deploy currencies + (currency0, currency1) = deployMintAndApprove2Currencies(); + token0 = MockERC20(Currency.unwrap(currency0)); + token1 = MockERC20(Currency.unwrap(currency1)); + + // Set up the TWAMM hook + twamm = address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG)); + + // Deploy the TWAMMImplementation contract + twammImpl = new TWAMMImplementation(manager, 10000, TWAMM(twamm)); + + // Etch the code of the implementation into the twamm address + (, bytes32[] memory writes) = vm.accesses(address(twammImpl)); + vm.etch(twamm, address(twammImpl).code); + + // Copy storage values + unchecked { + for (uint256 i = 0; i < writes.length; i++) { + bytes32 slot = writes[i]; + vm.store(twamm, slot, vm.load(address(twammImpl), slot)); + } + } + + // Initialize the pool with the TWAMM hook + (poolKey, poolId) = initPool(currency0, currency1, IHooks(twamm), 3000, 60, 1 << 96, bytes("")); + + // Add liquidity to the pool + token0.mint(address(this), 100 ether); + token1.mint(address(this), 100 ether); + token0.approve(address(modifyLiquidityRouter), 100 ether); + token1.approve(address(modifyLiquidityRouter), 100 ether); + modifyLiquidityRouter.modifyLiquidity(poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether, 0), bytes("")); + + // Deploy DAO token + daoToken = new MockERC20("DAO Token", "DAO", 18); + + // Deploy the governance contract + governance = new TWAMMGovernance(manager, 10000, IERC20(address(daoToken)), currency0, currency1, 3000, 60, twamm); + + // Deploy the quoter contract + quoter = new TWAMMQuoter(address(manager), address(governance), twamm, poolKey); + + // Distribute tokens to test addresses + daoToken.mint(alice, 200000e18); + daoToken.mint(bob, 200000e18); + + // Approve governance contract to spend tokens + vm.prank(alice); + daoToken.approve(address(governance), type(uint256).max); + vm.prank(bob); + daoToken.approve(address(governance), type(uint256).max); + } + + function testQuoterSetup() public { + assertEq(address(quoter.poolManager()), address(manager), "Pool manager address mismatch"); + assertEq(address(quoter.governanceContract()), address(governance), "Governance contract address mismatch"); + assertEq(address(quoter.twamm()), twamm, "TWAMM address mismatch"); + assertEq(Currency.unwrap(quoter.currency0()), Currency.unwrap(poolKey.currency0), "Currency0 mismatch"); + assertEq(Currency.unwrap(quoter.currency1()), Currency.unwrap(poolKey.currency1), "Currency1 mismatch"); + assertEq(quoter.fee(), poolKey.fee, "Fee mismatch"); + assertEq(quoter.tickSpacing(), poolKey.tickSpacing, "Tick spacing mismatch"); + assertEq(address(quoter.hooks()), address(poolKey.hooks), "Hooks address mismatch"); + } + + function testQuoteProposal() public { + // Create a proposal + uint256 proposalAmount = 1 ether; + uint256 proposalDuration = TWAMM(twamm).expirationInterval(); + vm.prank(alice); + governance.createProposal(proposalAmount, proposalDuration, true, "Test proposal"); + uint256 proposalId = governance.proposalCount() - 1; + + // Get quote for the proposal + (int256 amount0Delta, int256 amount1Delta, uint160 sqrtPriceX96After) = quoter.getQuoteForProposal(proposalId); + + // Log the values for debugging + console.log("amount0Delta:", amount0Delta); + console.log("amount1Delta:", amount1Delta); + console.log("sqrtPriceX96After:", sqrtPriceX96After); + + // Check that the quote is not zero and within reasonable bounds + assertTrue(amount0Delta != 0 || amount1Delta != 0, "Quote should not be zero"); + assertTrue(sqrtPriceX96After != 0, "SqrtPriceX96After should not be zero"); + assertTrue(amount0Delta > -1e27 && amount0Delta < 1e27, "amount0Delta out of reasonable bounds"); + assertTrue(amount1Delta > -1e27 && amount1Delta < 1e27, "amount1Delta out of reasonable bounds"); +} + + function testQuoteProposalWithNoLiquidity() public { + // Remove all liquidity from the pool + modifyLiquidityRouter.modifyLiquidity(poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, -10 ether, 0), bytes("")); + + // Create a proposal + uint256 proposalAmount = 1 ether; + uint256 proposalDuration = TWAMM(twamm).expirationInterval(); + vm.prank(alice); + governance.createProposal(proposalAmount, proposalDuration, true, "Test proposal"); + uint256 proposalId = governance.proposalCount() - 1; + + // Attempt to get quote for the proposal + vm.expectRevert(); // Expect revert due to lack of liquidity + quoter.getQuoteForProposal(proposalId); + } + + function testQuoteMultipleProposals() public { + // Create multiple proposals + uint256 proposalAmount = 1 ether; + uint256 proposalDuration = TWAMM(twamm).expirationInterval(); + vm.startPrank(alice); + governance.createProposal(proposalAmount, proposalDuration, true, "Proposal 1"); + governance.createProposal(proposalAmount, proposalDuration, false, "Proposal 2"); + vm.stopPrank(); + + uint256 proposalId1 = governance.proposalCount() - 2; + uint256 proposalId2 = governance.proposalCount() - 1; + + // Get quotes for both proposals + (int256 amount0Delta1, int256 amount1Delta1, uint160 sqrtPriceX96After1) = quoter.getQuoteForProposal(proposalId1); + (int256 amount0Delta2, int256 amount1Delta2, uint160 sqrtPriceX96After2) = quoter.getQuoteForProposal(proposalId2); + + // Log the values for debugging + console.log("Proposal 1 - amount0Delta:", amount0Delta1); + console.log("Proposal 1 - amount1Delta:", amount1Delta1); + console.log("Proposal 1 - sqrtPriceX96After:", sqrtPriceX96After1); + console.log("Proposal 2 - amount0Delta:", amount0Delta2); + console.log("Proposal 2 - amount1Delta:", amount1Delta2); + console.log("Proposal 2 - sqrtPriceX96After:", sqrtPriceX96After2); + + // Check that the quotes are different and within reasonable bounds + assertTrue(amount0Delta1 != amount0Delta2 || amount1Delta1 != amount1Delta2, "Quotes should be different"); + assertTrue(sqrtPriceX96After1 != sqrtPriceX96After2, "SqrtPriceX96After should be different"); + assertTrue(amount0Delta1 > -1e27 && amount0Delta1 < 1e27, "amount0Delta1 out of reasonable bounds"); + assertTrue(amount1Delta1 > -1e27 && amount1Delta1 < 1e27, "amount1Delta1 out of reasonable bounds"); + assertTrue(amount0Delta2 > -1e27 && amount0Delta2 < 1e27, "amount0Delta2 out of reasonable bounds"); + assertTrue(amount1Delta2 > -1e27 && amount1Delta2 < 1e27, "amount1Delta2 out of reasonable bounds"); + } + + function testQuoteNonExistentProposal() public { + uint256 nonExistentProposalId = 9999; + + vm.expectRevert(); // Expect revert when quoting a non-existent proposal + quoter.getQuoteForProposal(nonExistentProposalId); + } +} \ No newline at end of file