-
Notifications
You must be signed in to change notification settings - Fork 505
/
TruncGeoOracle.sol
167 lines (140 loc) · 6.78 KB
/
TruncGeoOracle.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.8.19;
import {IPoolManager} from "@uniswap/core-next/contracts/interfaces/IPoolManager.sol";
import {PoolId} from "@uniswap/core-next/contracts/libraries/PoolId.sol";
import {Hooks} from "@uniswap/core-next/contracts/libraries/Hooks.sol";
import {TickMath} from "@uniswap/core-next/contracts/libraries/TickMath.sol";
import {TruncatedOracle} from "../libraries/TruncatedOracle.sol";
import {BaseHook} from "../BaseHook.sol";
/// @notice A hook for a pool that allows a Uniswap pool to act as an oracle. Pools that use this hook must have full range
/// tick spacing and liquidity is always permanently locked in these pools. This is the suggested configuration
/// for protocols that wish to use a V3 style geomean oracle.
contract TruncGeoOracle is BaseHook {
using TruncatedOracle for TruncatedOracle.Observation[65535];
using PoolId for IPoolManager.PoolKey;
/// @notice Oracle pools do not have fees because they exist to serve as an oracle for a pair of tokens
error OnlyOneOraclePoolAllowed();
/// @notice Oracle positions must be full range
error OraclePositionsMustBeFullRange();
/// @notice Oracle pools must have liquidity locked so that they cannot become more susceptible to price manipulation
error OraclePoolMustLockLiquidity();
/// @member index The index of the last written observation for the pool
/// @member cardinality The cardinality of the observations array for the pool
/// @member cardinalityNext The cardinality target of the observations array for the pool, which will replace cardinality when enough observations are written
struct ObservationState {
uint16 index;
uint16 cardinality;
uint16 cardinalityNext;
}
/// @notice The list of observations for a given pool ID
mapping(bytes32 => TruncatedOracle.Observation[65535]) public observations;
/// @notice The current observation array state for the given pool ID
mapping(bytes32 => ObservationState) public states;
/// @notice Returns the observation for the given pool key and observation index
function getObservation(IPoolManager.PoolKey calldata key, uint256 index)
external
view
returns (TruncatedOracle.Observation memory observation)
{
observation = observations[keccak256(abi.encode(key))][index];
}
/// @notice Returns the state for the given pool key
function getState(IPoolManager.PoolKey calldata key) external view returns (ObservationState memory state) {
state = states[keccak256(abi.encode(key))];
}
/// @dev For mocking
function _blockTimestamp() internal view virtual returns (uint32) {
return uint32(block.timestamp);
}
constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
function getHooksCalls() public pure override returns (Hooks.Calls memory) {
return Hooks.Calls({
beforeInitialize: true,
afterInitialize: true,
beforeModifyPosition: true,
afterModifyPosition: false,
beforeSwap: true,
afterSwap: false,
beforeDonate: false,
afterDonate: false
});
}
function beforeInitialize(address, IPoolManager.PoolKey calldata key, uint160)
external
view
override
poolManagerOnly
returns (bytes4)
{
// This is to limit the fragmentation of pools using this oracle hook. In other words,
// there may only be one pool per pair of tokens that use this hook. The tick spacing is set to the maximum
// because we only allow max range liquidity in this pool.
if (key.fee != 0 || key.tickSpacing != poolManager.MAX_TICK_SPACING()) revert OnlyOneOraclePoolAllowed();
return TruncGeoOracle.beforeInitialize.selector;
}
function afterInitialize(address, IPoolManager.PoolKey calldata key, uint160, int24 tick)
external
override
poolManagerOnly
returns (bytes4)
{
bytes32 id = key.toId();
(states[id].cardinality, states[id].cardinalityNext) = observations[id].initialize(_blockTimestamp(), tick);
return TruncGeoOracle.afterInitialize.selector;
}
/// @dev Called before any action that potentially modifies pool price or liquidity, such as swap or modify position
function _updatePool(IPoolManager.PoolKey calldata key) private {
bytes32 id = key.toId();
(, int24 tick,) = poolManager.getSlot0(id);
uint128 liquidity = poolManager.getLiquidity(id);
(states[id].index, states[id].cardinality) = observations[id].write(
states[id].index, _blockTimestamp(), tick, liquidity, states[id].cardinality, states[id].cardinalityNext
);
}
function beforeModifyPosition(
address,
IPoolManager.PoolKey calldata key,
IPoolManager.ModifyPositionParams calldata params
) external override poolManagerOnly returns (bytes4) {
if (params.liquidityDelta < 0) revert OraclePoolMustLockLiquidity();
int24 maxTickSpacing = poolManager.MAX_TICK_SPACING();
if (
params.tickLower != TickMath.minUsableTick(maxTickSpacing)
|| params.tickUpper != TickMath.maxUsableTick(maxTickSpacing)
) revert OraclePositionsMustBeFullRange();
_updatePool(key);
return TruncGeoOracle.beforeModifyPosition.selector;
}
function beforeSwap(address, IPoolManager.PoolKey calldata key, IPoolManager.SwapParams calldata)
external
override
poolManagerOnly
returns (bytes4)
{
_updatePool(key);
return TruncGeoOracle.beforeSwap.selector;
}
/// @notice Observe the given pool for the timestamps
function observe(IPoolManager.PoolKey calldata key, uint32[] calldata secondsAgos)
external
view
returns (int48[] memory tickCumulatives, uint144[] memory secondsPerLiquidityCumulativeX128s)
{
bytes32 id = key.toId();
ObservationState memory state = states[id];
(, int24 tick,) = poolManager.getSlot0(id);
uint128 liquidity = poolManager.getLiquidity(id);
return observations[id].observe(_blockTimestamp(), secondsAgos, tick, state.index, liquidity, state.cardinality);
}
/// @notice Increase the cardinality target for the given pool
function increaseCardinalityNext(IPoolManager.PoolKey calldata key, uint16 cardinalityNext)
external
returns (uint16 cardinalityNextOld, uint16 cardinalityNextNew)
{
bytes32 id = keccak256(abi.encode(key));
ObservationState storage state = states[id];
cardinalityNextOld = state.cardinalityNext;
cardinalityNextNew = observations[id].grow(cardinalityNextOld, cardinalityNext);
state.cardinalityNext = cardinalityNextNew;
}
}