-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathAutoRange.sol
315 lines (275 loc) · 13.2 KB
/
AutoRange.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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import "../automators/Automator.sol";
/// @title AutoRange
/// @notice Allows operator of AutoRange contract (Revert controlled bot) to change range for configured positions
/// Positions need to be approved (setApprovalForAll) for the contract and configured with configToken method
/// When executed a new position is created and automatically configured the same way as the original position
/// When position is inside Vault - transform is called
contract AutoRange is Automator {
event RangeChanged(uint256 indexed oldTokenId, uint256 indexed newTokenId);
event PositionConfigured(
uint256 indexed tokenId,
int32 lowerTickLimit,
int32 upperTickLimit,
int32 lowerTickDelta,
int32 upperTickDelta,
uint64 token0SlippageX64,
uint64 token1SlippageX64,
bool onlyFees,
uint64 maxRewardX64
);
constructor(
INonfungiblePositionManager _npm,
address _operator,
address _withdrawer,
uint32 _TWAPSeconds,
uint16 _maxTWAPTickDifference,
address _zeroxRouter,
address _universalRouter
) Automator(_npm, _operator, _withdrawer, _TWAPSeconds, _maxTWAPTickDifference, _zeroxRouter, _universalRouter) {}
// defines when and how a position can be changed by operator
// when a position is adjusted config for the position is cleared and copied to the newly created position
struct PositionConfig {
// needs more than int24 because it can be [-type(uint24).max,type(uint24).max]
int32 lowerTickLimit; // if negative also in-range positions may be adjusted / if 0 out of range positions may be adjusted
int32 upperTickLimit; // if negative also in-range positions may be adjusted / if 0 out of range positions may be adjusted
int32 lowerTickDelta; // this amount is added to current tick (floored to tickspacing) to define lowerTick of new position
int32 upperTickDelta; // this amount is added to current tick (floored to tickspacing) to define upperTick of new position
uint64 token0SlippageX64; // max price difference from current pool price for swap / Q64 for token0
uint64 token1SlippageX64; // max price difference from current pool price for swap / Q64 for token1
bool onlyFees; // if only fees maybe used for protocol reward
uint64 maxRewardX64; // max allowed reward percentage of fees or full position
}
// configured tokens
mapping(uint256 => PositionConfig) public positionConfigs;
/// @notice params for execute()
struct ExecuteParams {
uint256 tokenId;
bool swap0To1;
uint256 amountIn; // if this is set to 0 no swap happens
bytes swapData;
uint128 liquidity; // liquidity the calculations are based on
uint256 amountRemoveMin0; // min amount to be removed from liquidity
uint256 amountRemoveMin1; // min amount to be removed from liquidity
uint256 deadline; // for uniswap operations - operator promises fair value
uint64 rewardX64; // which reward will be used for protocol, can be max configured amount (considering onlyFees)
}
struct ExecuteState {
address owner;
address realOwner;
IUniswapV3Pool pool;
address token0;
address token1;
uint24 fee;
int24 tickLower;
int24 tickUpper;
int24 currentTick;
uint256 amount0;
uint256 amount1;
uint256 feeAmount0;
uint256 feeAmount1;
uint256 maxAddAmount0;
uint256 maxAddAmount1;
uint256 amountAdded0;
uint256 amountAdded1;
uint128 liquidity;
uint256 protocolReward0;
uint256 protocolReward1;
uint256 amountOutMin;
uint256 amountInDelta;
uint256 amountOutDelta;
uint256 newTokenId;
}
/**
* @notice Adjust token (which is in a Vault) - via transform method
* Can only be called from configured operator account - vault must be configured as well
* Swap needs to be done with max price difference from current pool price - otherwise reverts
*/
function executeWithVault(ExecuteParams calldata params, address vault) external {
if (!operators[msg.sender] || !vaults[vault]) {
revert Unauthorized();
}
IVault(vault).transform(
params.tokenId, address(this), abi.encodeWithSelector(AutoRange.execute.selector, params)
);
}
/**
* @notice Adjust token directly (must be in correct state)
* Can only be called only from configured operator account, or vault via transform
* Swap needs to be done with max price difference from current pool price - otherwise reverts
*/
function execute(ExecuteParams calldata params) external {
if (!operators[msg.sender] && !vaults[msg.sender]) {
revert Unauthorized();
}
ExecuteState memory state;
PositionConfig memory config = positionConfigs[params.tokenId];
if (config.lowerTickDelta == config.upperTickDelta) {
revert NotConfigured();
}
if (
config.onlyFees && params.rewardX64 > config.maxRewardX64
|| !config.onlyFees && params.rewardX64 > config.maxRewardX64
) {
revert ExceedsMaxReward();
}
// get position info
(,, state.token0, state.token1, state.fee, state.tickLower, state.tickUpper, state.liquidity,,,,) =
nonfungiblePositionManager.positions(params.tokenId);
if (state.liquidity != params.liquidity) {
revert LiquidityChanged();
}
(state.amount0, state.amount1, state.feeAmount0, state.feeAmount1) = _decreaseFullLiquidityAndCollect(
params.tokenId, state.liquidity, params.amountRemoveMin0, params.amountRemoveMin1, params.deadline
);
// if only fees reward is removed before adding
if (config.onlyFees) {
state.protocolReward0 = state.feeAmount0 * params.rewardX64 / Q64;
state.protocolReward1 = state.feeAmount1 * params.rewardX64 / Q64;
state.amount0 -= state.protocolReward0;
state.amount1 -= state.protocolReward1;
}
if (params.swap0To1 && params.amountIn > state.amount0 || !params.swap0To1 && params.amountIn > state.amount1) {
revert SwapAmountTooLarge();
}
// get pool info
state.pool = _getPool(state.token0, state.token1, state.fee);
// check oracle for swap
(state.amountOutMin, state.currentTick,,) = _validateSwap(
params.swap0To1,
params.amountIn,
state.pool,
TWAPSeconds,
maxTWAPTickDifference,
params.swap0To1 ? config.token0SlippageX64 : config.token1SlippageX64
);
if (
state.currentTick < state.tickLower - config.lowerTickLimit
|| state.currentTick >= state.tickUpper + config.upperTickLimit
) {
int24 tickSpacing = _getTickSpacing(state.fee);
int24 baseTick = state.currentTick - (((state.currentTick % tickSpacing) + tickSpacing) % tickSpacing);
// check if new range same as old range
if (
baseTick + config.lowerTickDelta == state.tickLower
&& baseTick + config.upperTickDelta == state.tickUpper
) {
revert SameRange();
}
(state.amountInDelta, state.amountOutDelta) = _routerSwap(
Swapper.RouterSwapParams(
params.swap0To1 ? IERC20(state.token0) : IERC20(state.token1),
params.swap0To1 ? IERC20(state.token1) : IERC20(state.token0),
params.amountIn,
state.amountOutMin,
params.swapData
)
);
state.amount0 = params.swap0To1 ? state.amount0 - state.amountInDelta : state.amount0 + state.amountOutDelta;
state.amount1 = params.swap0To1 ? state.amount1 + state.amountOutDelta : state.amount1 - state.amountInDelta;
// max amount to add - removing max potential fees (if config.onlyFees - the have been removed already)
state.maxAddAmount0 = config.onlyFees ? state.amount0 : state.amount0 * Q64 / (params.rewardX64 + Q64);
state.maxAddAmount1 = config.onlyFees ? state.amount1 : state.amount1 * Q64 / (params.rewardX64 + Q64);
INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams(
address(state.token0),
address(state.token1),
state.fee,
SafeCast.toInt24(baseTick + config.lowerTickDelta), // reverts if out of valid range
SafeCast.toInt24(baseTick + config.upperTickDelta), // reverts if out of valid range
state.maxAddAmount0,
state.maxAddAmount1,
0,
0,
address(this), // is sent to real recipient aftwards
params.deadline
);
// approve npm
SafeERC20.safeApprove(IERC20(state.token0), address(nonfungiblePositionManager), state.maxAddAmount0);
SafeERC20.safeApprove(IERC20(state.token1), address(nonfungiblePositionManager), state.maxAddAmount1);
// mint is done to address(this) first - its not a safemint
(state.newTokenId,, state.amountAdded0, state.amountAdded1) = nonfungiblePositionManager.mint(mintParams);
// remove remaining approval
SafeERC20.safeApprove(IERC20(state.token0), address(nonfungiblePositionManager), 0);
SafeERC20.safeApprove(IERC20(state.token1), address(nonfungiblePositionManager), 0);
state.owner = nonfungiblePositionManager.ownerOf(params.tokenId);
// get the real owner - if owner is vault - for sending leftover tokens
state.realOwner = state.owner;
if (vaults[state.owner]) {
state.realOwner = IVault(state.owner).ownerOf(params.tokenId);
}
// send the new nft to the owner / vault
nonfungiblePositionManager.safeTransferFrom(address(this), state.owner, state.newTokenId);
// protocol reward is calculated based on added amount (to incentivize optimal swap done by operator)
if (!config.onlyFees) {
state.protocolReward0 = state.amountAdded0 * params.rewardX64 / Q64;
state.protocolReward1 = state.amountAdded1 * params.rewardX64 / Q64;
state.amount0 -= state.protocolReward0;
state.amount1 -= state.protocolReward1;
}
// send leftover to real owner
if (state.amount0 - state.amountAdded0 > 0) {
_transferToken(state.realOwner, IERC20(state.token0), state.amount0 - state.amountAdded0, true);
}
if (state.amount1 - state.amountAdded1 > 0) {
_transferToken(state.realOwner, IERC20(state.token1), state.amount1 - state.amountAdded1, true);
}
// copy token config for new token
positionConfigs[state.newTokenId] = config;
emit PositionConfigured(
state.newTokenId,
config.lowerTickLimit,
config.upperTickLimit,
config.lowerTickDelta,
config.upperTickDelta,
config.token0SlippageX64,
config.token1SlippageX64,
config.onlyFees,
config.maxRewardX64
);
// delete config for old position
delete positionConfigs[params.tokenId];
emit PositionConfigured(params.tokenId, 0, 0, 0, 0, 0, 0, false, 0);
emit RangeChanged(params.tokenId, state.newTokenId);
} else {
revert NotReady();
}
}
// function to configure a token to be used with this runner
// it needs to have approvals set for this contract beforehand
function configToken(uint256 tokenId, address vault, PositionConfig calldata config) external {
_validateOwner(tokenId, vault);
// lower tick must be always below or equal to upper tick - if they are equal - range adjustment is deactivated
if (config.lowerTickDelta > config.upperTickDelta) {
revert InvalidConfig();
}
positionConfigs[tokenId] = config;
emit PositionConfigured(
tokenId,
config.lowerTickLimit,
config.upperTickLimit,
config.lowerTickDelta,
config.upperTickDelta,
config.token0SlippageX64,
config.token1SlippageX64,
config.onlyFees,
config.maxRewardX64
);
}
// get tick spacing for fee tier (cached when possible)
function _getTickSpacing(uint24 fee) internal view returns (int24) {
if (fee == 10000) {
return 200;
} else if (fee == 3000) {
return 60;
} else if (fee == 500) {
return 10;
} else {
int24 spacing = IUniswapV3Factory(factory).feeAmountTickSpacing(fee);
if (spacing <= 0) {
revert NotSupportedFeeTier();
}
return spacing;
}
}
}