forked from uniswapfoundation/v4-template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
BackGeoOracle.sol
269 lines (227 loc) · 10.8 KB
/
BackGeoOracle.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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {BaseHook} from "v4-periphery/src/base/hooks/BaseHook.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {SafeCast} from "v4-core/src/libraries/SafeCast.sol";
import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {BalanceDelta, BalanceDeltaLibrary} from "v4-core/src/types/BalanceDelta.sol";
import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {Oracle} from "../libraries/Oracle.sol";
contract BackGeoOracle is BaseHook {
using Oracle for Oracle.Observation[65535];
using PoolIdLibrary for PoolKey;
using CurrencyLibrary for Currency;
using SafeCast for int256;
using StateLibrary for IPoolManager;
/// @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 Only exactInput swap types are allowed
error NotExactIn();
/// @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(PoolId => Oracle.Observation[65535]) public observations;
/// @notice The current observation array state for the given pool ID
mapping(PoolId => ObservationState) public states;
constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: true,
afterInitialize: true,
beforeAddLiquidity: true,
afterAddLiquidity: false,
beforeRemoveLiquidity: true,
afterRemoveLiquidity: false,
beforeSwap: true,
afterSwap: true,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false,
afterSwapReturnDelta: true,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
function beforeInitialize(address, PoolKey calldata key, uint160)
external
view
override
onlyPoolManager
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.
require(key.fee == 0 && key.tickSpacing == TickMath.MAX_TICK_SPACING, OnlyOneOraclePoolAllowed());
return BackGeoOracle.beforeInitialize.selector;
}
function afterInitialize(address, PoolKey calldata key, uint160, int24 tick)
external
override
onlyPoolManager
returns (bytes4)
{
PoolId id = key.toId();
(states[id].cardinality, states[id].cardinalityNext) = observations[id].initialize(_blockTimestamp(), tick);
return BackGeoOracle.afterInitialize.selector;
}
function beforeAddLiquidity(
address,
PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata params,
bytes calldata
) external override onlyPoolManager returns (bytes4) {
int24 maxTickSpacing = TickMath.MAX_TICK_SPACING;
require(
params.tickLower == TickMath.minUsableTick(maxTickSpacing)
&& params.tickUpper == TickMath.maxUsableTick(maxTickSpacing),
OraclePositionsMustBeFullRange()
);
_updatePool(key);
return BackGeoOracle.beforeAddLiquidity.selector;
}
function beforeRemoveLiquidity(
address,
PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata,
bytes calldata
) external override onlyPoolManager returns (bytes4) {
_updatePool(key);
return BackGeoOracle.beforeRemoveLiquidity.selector;
}
function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata)
external
override
onlyPoolManager
returns (bytes4, BeforeSwapDelta, uint24)
{
// only exactIn swaps are supported
require(params.amountSpecified < 0, NotExactIn());
_updatePool(key);
return (BackGeoOracle.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}
function afterSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
BalanceDelta,
bytes calldata
) external override onlyPoolManager returns (bytes4, int128) {
// the unspecified currency is always the one user is buying, so we charge a fee to settle the backrun
(BalanceDelta hookDelta, bool isBackrun) = _backrun(sender, key, params);
if (isBackrun) {
bool _isCurrency0Specified = (params.amountSpecified < 0 == params.zeroForOne);
// in backrun we invert zeroForOne and sign of amountSpecified, currency specified is same
(Currency currencySpecified, int128 backAmountSpecified, int128 backAmountUnspecified) = (
_isCurrency0Specified
? (key.currency0, hookDelta.amount0(), hookDelta.amount1())
: (key.currency1, hookDelta.amount1(), hookDelta.amount0())
);
// the following condition must always be true since we only support ExactInput swaps
assert(backAmountUnspecified < 0 && backAmountSpecified > 0);
// return to the user backrun amount in the specified currency
poolManager.mint(sender, CurrencyLibrary.toId(currencySpecified), uint256(int256(backAmountSpecified)));
return (BackGeoOracle.afterSwap.selector, -backAmountUnspecified);
}
return (BackGeoOracle.afterSwap.selector, BalanceDelta.unwrap(hookDelta).toInt128());
}
/// @notice Increase the cardinality target for the given pool
function increaseCardinalityNext(PoolKey calldata key, uint16 cardinalityNext)
external
returns (uint16 cardinalityNextOld, uint16 cardinalityNextNew)
{
PoolId id = PoolId.wrap(keccak256(abi.encode(key)));
ObservationState storage state = states[id];
cardinalityNextOld = state.cardinalityNext;
cardinalityNextNew = observations[id].grow(cardinalityNextOld, cardinalityNext);
state.cardinalityNext = cardinalityNextNew;
}
function _backrun(address, PoolKey calldata key, IPoolManager.SwapParams memory params)
private
returns (BalanceDelta hookDelta, bool shouldBackrun)
{
PoolId poolId = key.toId();
(, int24 tick,,) = poolManager.getSlot0(poolId);
// we only have 1 stored observation
Oracle.Observation memory last = observations[PoolId.wrap(keccak256(abi.encode(key)))][0];
int24 tickDelta = tick - last.prevTick;
// we are only interested in the absolute tick delta
if (tickDelta < 0) {
tickDelta = -tickDelta;
}
// we apply a 1 bps tolerance for rounding errors that prevent full amount backrun
if (tickDelta <= Oracle.MIN_ABS_TICK_MOVE) {
// early escape to save gas for normal transactions
} else if (tickDelta < Oracle.LIMIT_ABS_TICK_MOVE) {
shouldBackrun = true;
int128 numerator = int128(tickDelta) * 9999;
int128 denominator = int128(Oracle.LIMIT_ABS_TICK_MOVE) * 10000;
params.amountSpecified = params.amountSpecified * numerator / denominator;
} else {
// Full backrun
shouldBackrun = true;
params.amountSpecified = params.amountSpecified * 9999 / 10000;
}
// early escape if no backrun is necessary, to save gas on small impact swaps
if (shouldBackrun) {
params.zeroForOne = !params.zeroForOne;
params.amountSpecified = -params.amountSpecified;
params.sqrtPriceLimitX96 = params.zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1;
// transfer deltas to user
hookDelta = poolManager.swap(key, params, "");
} else {
hookDelta = BalanceDeltaLibrary.ZERO_DELTA;
}
}
/// @dev Called before any action that potentially modifies pool price or liquidity, such as swap or modify position
function _updatePool(PoolKey calldata key) private {
PoolId 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
);
}
/// @notice Returns the observation for the given pool key and observation index
function getObservation(PoolKey calldata key, uint256 index)
external
view
returns (Oracle.Observation memory observation)
{
observation = observations[PoolId.wrap(keccak256(abi.encode(key)))][index];
}
/// @notice Returns the state for the given pool key
function getState(PoolKey calldata key) external view returns (ObservationState memory state) {
state = states[PoolId.wrap(keccak256(abi.encode(key)))];
}
/// @notice Observe the given pool for the timestamps
/// @dev Method to be used to extract TWAP.
function observe(PoolKey calldata key, uint32[] calldata secondsAgos)
external
view
returns (int48[] memory tickCumulatives, uint144[] memory secondsPerLiquidityCumulativeX128s)
{
PoolId 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);
}
/// @dev For mocking
function _blockTimestamp() internal view virtual returns (uint32) {
return uint32(block.timestamp);
}
}