From 64e3c57e7d5f9788d264f9e1b602451f25ab00b4 Mon Sep 17 00:00:00 2001 From: nobita851 Date: Sun, 1 Dec 2024 03:26:46 +0530 Subject: [PATCH] Tick Oracle Contract --- contracts/src/IHooks.sol | 156 ++++++++++++++++++++ contracts/src/IPoolManager.sol | 55 +++++++ contracts/src/ITickOracleServiceManager.sol | 32 ++++ contracts/src/TickOracleServiceManager.sol | 110 ++++++++++++++ 4 files changed, 353 insertions(+) create mode 100644 contracts/src/IHooks.sol create mode 100644 contracts/src/IPoolManager.sol create mode 100644 contracts/src/ITickOracleServiceManager.sol create mode 100644 contracts/src/TickOracleServiceManager.sol diff --git a/contracts/src/IHooks.sol b/contracts/src/IHooks.sol new file mode 100644 index 0000000..463f1d6 --- /dev/null +++ b/contracts/src/IHooks.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {PoolKey, IPoolManager} from "./IPoolManager.sol"; + +type Currency is address; +type BalanceDelta is int256; +type BeforeSwapDelta is int256; + +/// @notice V4 decides whether to invoke specific hooks by inspecting the least significant bits +/// of the address that the hooks contract is deployed to. +/// For example, a hooks contract deployed to address: 0x0000000000000000000000000000000000002400 +/// has the lowest bits '10 0100 0000 0000' which would cause the 'before initialize' and 'after add liquidity' hooks to be used. +/// See the Hooks library for the full spec. +/// @dev Should only be callable by the v4 PoolManager. +interface IHooks { + /// @notice The hook called before the state of a pool is initialized + /// @param sender The initial msg.sender for the initialize call + /// @param key The key for the pool being initialized + /// @param sqrtPriceX96 The sqrt(price) of the pool as a Q64.96 + /// @return bytes4 The function selector for the hook + function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96) external returns (bytes4); + + /// @notice The hook called after the state of a pool is initialized + /// @param sender The initial msg.sender for the initialize call + /// @param key The key for the pool being initialized + /// @param sqrtPriceX96 The sqrt(price) of the pool as a Q64.96 + /// @param tick The current tick after the state of a pool is initialized + /// @return bytes4 The function selector for the hook + function afterInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96, int24 tick) + external + returns (bytes4); + + /// @notice The hook called before liquidity is added + /// @param sender The initial msg.sender for the add liquidity call + /// @param key The key for the pool + /// @param params The parameters for adding liquidity + /// @param hookData Arbitrary data handed into the PoolManager by the liquidity provider to be passed on to the hook + /// @return bytes4 The function selector for the hook + function beforeAddLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + bytes calldata hookData + ) external returns (bytes4); + + /// @notice The hook called after liquidity is added + /// @param sender The initial msg.sender for the add liquidity call + /// @param key The key for the pool + /// @param params The parameters for adding liquidity + /// @param delta The caller's balance delta after adding liquidity; the sum of principal delta, fees accrued, and hook delta + /// @param feesAccrued The fees accrued since the last time fees were collected from this position + /// @param hookData Arbitrary data handed into the PoolManager by the liquidity provider to be passed on to the hook + /// @return bytes4 The function selector for the hook + /// @return BalanceDelta The hook's delta in token0 and token1. Positive: the hook is owed/took currency, negative: the hook owes/sent currency + function afterAddLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + BalanceDelta delta, + BalanceDelta feesAccrued, + bytes calldata hookData + ) external returns (bytes4, BalanceDelta); + + /// @notice The hook called before liquidity is removed + /// @param sender The initial msg.sender for the remove liquidity call + /// @param key The key for the pool + /// @param params The parameters for removing liquidity + /// @param hookData Arbitrary data handed into the PoolManager by the liquidity provider to be be passed on to the hook + /// @return bytes4 The function selector for the hook + function beforeRemoveLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + bytes calldata hookData + ) external returns (bytes4); + + /// @notice The hook called after liquidity is removed + /// @param sender The initial msg.sender for the remove liquidity call + /// @param key The key for the pool + /// @param params The parameters for removing liquidity + /// @param delta The caller's balance delta after adding liquidity; the sum of principal delta, fees accrued, and hook delta + /// @param feesAccrued The fees accrued since the last time fees were collected from this position + /// @param hookData Arbitrary data handed into the PoolManager by the liquidity provider to be be passed on to the hook + /// @return bytes4 The function selector for the hook + /// @return BalanceDelta The hook's delta in token0 and token1. Positive: the hook is owed/took currency, negative: the hook owes/sent currency + function afterRemoveLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + BalanceDelta delta, + BalanceDelta feesAccrued, + bytes calldata hookData + ) external returns (bytes4, BalanceDelta); + + /// @notice The hook called before a swap + /// @param sender The initial msg.sender for the swap call + /// @param key The key for the pool + /// @param params The parameters for the swap + /// @param hookData Arbitrary data handed into the PoolManager by the swapper to be be passed on to the hook + /// @return bytes4 The function selector for the hook + /// @return BeforeSwapDelta The hook's delta in specified and unspecified currencies. Positive: the hook is owed/took currency, negative: the hook owes/sent currency + /// @return uint24 Optionally override the lp fee, only used if three conditions are met: 1. the Pool has a dynamic fee, 2. the value's 2nd highest bit is set (23rd bit, 0x400000), and 3. the value is less than or equal to the maximum fee (1 million) + function beforeSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + bytes calldata hookData + ) external returns (bytes4, BeforeSwapDelta, uint24); + + /// @notice The hook called after a swap + /// @param sender The initial msg.sender for the swap call + /// @param key The key for the pool + /// @param params The parameters for the swap + /// @param delta The amount owed to the caller (positive) or owed to the pool (negative) + /// @param hookData Arbitrary data handed into the PoolManager by the swapper to be be passed on to the hook + /// @return bytes4 The function selector for the hook + /// @return int128 The hook's delta in unspecified currency. Positive: the hook is owed/took currency, negative: the hook owes/sent currency + function afterSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + BalanceDelta delta, + bytes calldata hookData + ) external returns (bytes4, int128); + + /// @notice The hook called before donate + /// @param sender The initial msg.sender for the donate call + /// @param key The key for the pool + /// @param amount0 The amount of token0 being donated + /// @param amount1 The amount of token1 being donated + /// @param hookData Arbitrary data handed into the PoolManager by the donor to be be passed on to the hook + /// @return bytes4 The function selector for the hook + function beforeDonate( + address sender, + PoolKey calldata key, + uint256 amount0, + uint256 amount1, + bytes calldata hookData + ) external returns (bytes4); + + /// @notice The hook called after donate + /// @param sender The initial msg.sender for the donate call + /// @param key The key for the pool + /// @param amount0 The amount of token0 being donated + /// @param amount1 The amount of token1 being donated + /// @param hookData Arbitrary data handed into the PoolManager by the donor to be be passed on to the hook + /// @return bytes4 The function selector for the hook + function afterDonate( + address sender, + PoolKey calldata key, + uint256 amount0, + uint256 amount1, + bytes calldata hookData + ) external returns (bytes4); +} diff --git a/contracts/src/IPoolManager.sol b/contracts/src/IPoolManager.sol new file mode 100644 index 0000000..222218e --- /dev/null +++ b/contracts/src/IPoolManager.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IHooks} from "./IHooks.sol"; + +type Currency is address; +type BalanceDelta is int256; + +/// @notice Returns the key for identifying a pool +struct PoolKey { + /// @notice The lower currency of the pool, sorted numerically + Currency currency0; + /// @notice The higher currency of the pool, sorted numerically + Currency currency1; + /// @notice The pool LP fee, capped at 1_000_000. If the highest bit is 1, the pool has a dynamic fee and must be exactly equal to 0x800000 + uint24 fee; + /// @notice Ticks that involve positions must be a multiple of tick spacing + int24 tickSpacing; + /// @notice The hooks of the pool + IHooks hooks; +} + +interface IPoolManager { + + struct SwapParams { + /// Whether to swap token0 for token1 or vice versa + bool zeroForOne; + /// The desired input amount if negative (exactIn), or the desired output amount if positive (exactOut) + int256 amountSpecified; + /// The sqrt price at which, if reached, the swap will stop executing + uint160 sqrtPriceLimitX96; + } + + /// @notice Swap against the given pool + /// @param key The pool to swap in + /// @param params The parameters for swapping + /// @param hookData The data to pass through to the swap hooks + /// @return swapDelta The balance delta of the address swapping + /// @dev Swapping on low liquidity pools may cause unexpected swap amounts when liquidity available is less than amountSpecified. + /// Additionally note that if interacting with hooks that have the BEFORE_SWAP_RETURNS_DELTA_FLAG or AFTER_SWAP_RETURNS_DELTA_FLAG + /// the hook may alter the swap input/output. Integrators should perform checks on the returned swapDelta. + function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData) + external + returns (BalanceDelta swapDelta); + + struct ModifyLiquidityParams { + // the lower and upper tick of the position + int24 tickLower; + int24 tickUpper; + // how to modify the liquidity + int256 liquidityDelta; + // a value to set if you want unique liquidity positions at the same range + bytes32 salt; + } +} \ No newline at end of file diff --git a/contracts/src/ITickOracleServiceManager.sol b/contracts/src/ITickOracleServiceManager.sol new file mode 100644 index 0000000..48be26a --- /dev/null +++ b/contracts/src/ITickOracleServiceManager.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.9; + +import {PoolKey, IPoolManager} from "./IPoolManager.sol"; + +interface ITickOracleServiceManager { + event NewTaskCreated(uint32 indexed taskIndex); + + event TaskResponded(uint32 indexed taskIndex, address operator); + + function latestTaskNum() external view returns (uint32); + + function allTaskBlocks( + uint32 taskIndex + ) external view returns (uint32 blockNumber); + + function allTaskResponses( + address operator, + uint32 taskIndex + ) external view returns (bytes memory); + + function createNewTask( + ) external; + + function respondToTask( + PoolKey memory key, + IPoolManager.SwapParams calldata swapParams, + bytes calldata hookData, + uint32 referenceTaskIndex, + bytes calldata signature + ) external; +} diff --git a/contracts/src/TickOracleServiceManager.sol b/contracts/src/TickOracleServiceManager.sol new file mode 100644 index 0000000..b45e792 --- /dev/null +++ b/contracts/src/TickOracleServiceManager.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.9; + +import {ECDSAServiceManagerBase} from + "@eigenlayer-middleware/src/unaudited/ECDSAServiceManagerBase.sol"; +import {ECDSAStakeRegistry} from "@eigenlayer-middleware/src/unaudited/ECDSAStakeRegistry.sol"; +import {IServiceManager} from "@eigenlayer-middleware/src/interfaces/IServiceManager.sol"; +import {ECDSAUpgradeable} from + "@openzeppelin-upgrades/contracts/utils/cryptography/ECDSAUpgradeable.sol"; +import {IERC1271Upgradeable} from "@openzeppelin-upgrades/contracts/interfaces/IERC1271Upgradeable.sol"; +import {ITickOracleServiceManager} from "./ITickOracleServiceManager.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {PoolKey, IPoolManager} from "./IPoolManager.sol"; + +/** + * @title Primary entrypoint for procuring services from TickOracle. + * @author Eigen Labs, Inc. + */ +contract TickOracleServiceManager is ECDSAServiceManagerBase, ITickOracleServiceManager { + using ECDSAUpgradeable for bytes32; + + IPoolManager public poolManager; + + uint32 public latestTaskNum; + + // mapping of task indices to all tasks hashes + // when a task is created, task hash is stored here, + // and responses need to pass the actual task, + // which is hashed onchain and checked against this mapping + mapping(uint32 taskNumber => uint32 blockNumber) public allTaskBlocks; + + // mapping of task indices to hash of abi.encode(taskResponse, taskResponseMetadata) + mapping(address => mapping(uint32 => bytes)) public allTaskResponses; + + modifier onlyOperator() { + require( + ECDSAStakeRegistry(stakeRegistry).operatorRegistered(msg.sender), + "Operator must be the caller" + ); + _; + } + + constructor( + address _avsDirectory, + address _stakeRegistry, + address _rewardsCoordinator, + address _delegationManager, + address _poolManager + ) + ECDSAServiceManagerBase( + _avsDirectory, + _stakeRegistry, + _rewardsCoordinator, + _delegationManager + ) + { + poolManager = IPoolManager(_poolManager); + } + + /* FUNCTIONS */ + // NOTE: this function creates new task, assigns it a taskId + function createNewTask( + ) public { + // NOTE: creates new task only if the previous task was created 5 blocks ago + if (allTaskBlocks[latestTaskNum] + 5 < block.number) { + return; + } + latestTaskNum = latestTaskNum + 1; + emit NewTaskCreated(latestTaskNum); + } + + function respondToTask( + PoolKey memory key, + IPoolManager.SwapParams calldata swapParams, + bytes calldata hookData, + uint32 referenceTaskIndex, + bytes memory signature + ) external { + // check that the task is valid, hasn't been responsed yet, and is being responded in time + require( + referenceTaskIndex == latestTaskNum, + "Task too old to respond to" + ); + require( + allTaskResponses[msg.sender][referenceTaskIndex].length == 0, + "Operator has already responded to the task" + ); + + // The message that was signed + bytes32 messageHash = keccak256(abi.encode(key, swapParams, hookData)); + bytes32 ethSignedMessageHash = messageHash.toEthSignedMessageHash(); + bytes4 magicValue = IERC1271Upgradeable.isValidSignature.selector; + if (!(magicValue == ECDSAStakeRegistry(stakeRegistry).isValidSignature(ethSignedMessageHash,signature))){ + revert(); + } + + poolManager.swap(key, swapParams, hookData); + + // updating the storage with task responses + allTaskResponses[msg.sender][referenceTaskIndex] = signature; + + // emitting event + emit TaskResponded(referenceTaskIndex, msg.sender); + + createNewTask(); + } +}