generated from crispymangoes/forge-template
-
Notifications
You must be signed in to change notification settings - Fork 8
/
TradeManager.sol
289 lines (255 loc) · 11.4 KB
/
TradeManager.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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import { ERC20 } from "@solmate/tokens/ERC20.sol";
import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol";
import { AutomationCompatibleInterface } from "@chainlink/contracts/src/v0.8/interfaces/AutomationCompatibleInterface.sol";
import { Owned } from "@solmate/auth/Owned.sol";
import { LinkTokenInterface } from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
import { IKeeperRegistrar, RegistrationParams } from "src/interfaces/chainlink/IKeeperRegistrar.sol";
import { LimitOrderRegistry } from "src/LimitOrderRegistry.sol";
import { UniswapV3Pool } from "src/interfaces/uniswapV3/UniswapV3Pool.sol";
/**
* @title Trade Manager
* @notice Automates claiming limit orders for the LimitOrderRegistry.
* @author crispymangoes
* @dev Future improvements.
* - could add logic into the LOR that checks if the caller is a users TradeManager, and if so that allows the caller to
* create/edit orders on behalf of the user.
* - add some bool that dictates where assets go, like on claim should assets be returned here, or to the owner
* - Could allow users to funds their upkeep through this contract, which would interact with pegswap if needed.
*/
contract TradeManager is Initializable, AutomationCompatibleInterface, Owned {
using SafeTransferLib for ERC20;
using EnumerableSet for EnumerableSet.UintSet;
/*//////////////////////////////////////////////////////////////
STRUCTS
//////////////////////////////////////////////////////////////*/
/**
* @notice Stores information used to claim orders in `performUpkeep`.
* @param batchId The order batch id to claim
* @param fee The Native fee required to claim the order
*/
struct ClaimInfo {
uint128 batchId;
uint128 fee;
}
/*//////////////////////////////////////////////////////////////
GLOBAL STATE
//////////////////////////////////////////////////////////////*/
/**
* @notice Set of batch IDs that the owner currently has orders in.
*/
EnumerableSet.UintSet private ownerOrders;
/**
* @notice The limit order registry contract this trade manager interacts with.
*/
LimitOrderRegistry public limitOrderRegistry;
/**
* @notice The gas limit used when the Trade Managers upkeep is created.
*/
uint32 public constant UPKEEP_GAS_LIMIT = 500_000;
/**
* @notice The max amount of claims that can happen in a single upkeep.
* @dev When changing this value also change line 260, abi.decode.
*/
uint256 public constant MAX_CLAIMS = 10;
/**
* @notice Allows owner to specify whether they want claimed tokens to be left
* in the TradeManager, or sent to their address.
* -true send tokens to their address
* -false leave tokens in the trade manager
*/
bool public claimToOwner;
/**
* @notice Function signature used to create V1 Upkeep versions.
*/
string private constant FUNC_SIGNATURE = "register(string,bytes,address,uint32,address,bytes,uint96,uint8,address)";
/**
* @notice Function selector used to create V1 Upkeep versions.
*/
bytes4 private constant FUNC_SELECTOR = bytes4(keccak256(bytes(FUNC_SIGNATURE)));
constructor() Owned(address(0)) {}
/*//////////////////////////////////////////////////////////////
INITIALIZE LOGIC
//////////////////////////////////////////////////////////////*/
/**
* @notice Initialize function to setup this contract.
* @param user The owner of this contract
* @param _limitOrderRegistry The limit order registry this contract interacts with
* @param LINK The Chainlink token needed to create an upkeep
* @param registrar The Chainlink Automation Registrar contract
* @param initialUpkeepFunds Amount of link to fund the upkeep with
*/
function initialize(
address user,
LimitOrderRegistry _limitOrderRegistry,
LinkTokenInterface LINK,
IKeeperRegistrar registrar,
uint256 initialUpkeepFunds
) external initializer {
owner = user;
limitOrderRegistry = _limitOrderRegistry;
// Create a new upkeep.
if (initialUpkeepFunds > 0) {
// Owner wants to automatically create an upkeep for new pool.
ERC20(address(LINK)).safeTransferFrom(msg.sender, address(this), initialUpkeepFunds);
if (bytes(registrar.typeAndVersion())[16] == bytes("1")[0]) {
// Use V1 Upkeep Registration.
bytes memory data = abi.encodeWithSelector(
FUNC_SELECTOR,
"Trade Manager",
abi.encode(0),
address(this),
UPKEEP_GAS_LIMIT,
user,
abi.encode(0),
uint96(initialUpkeepFunds),
77,
address(this)
);
LINK.transferAndCall(address(registrar), initialUpkeepFunds, data);
} else {
// Use V2 Upkeep Registration.
ERC20(address(LINK)).safeApprove(address(registrar), initialUpkeepFunds);
RegistrationParams memory params = RegistrationParams({
name: "Trade Manager",
encryptedEmail: abi.encode(0),
upkeepContract: address(this),
gasLimit: UPKEEP_GAS_LIMIT,
adminAddress: user,
checkData: abi.encode(0),
offchainConfig: abi.encode(0),
amount: uint96(initialUpkeepFunds)
});
registrar.registerUpkeep(params);
}
}
}
/*//////////////////////////////////////////////////////////////
OWNER LOGIC
//////////////////////////////////////////////////////////////*/
/**
* @notice Allows owner to adjust `claimToOwner`.
*/
function setClaimToOwner(bool state) external onlyOwner {
claimToOwner = state;
}
/**
* @notice See `LimitOrderRegistry.sol:newOrder`.
*/
function newOrder(
UniswapV3Pool pool,
ERC20 assetIn,
int24 targetTick,
uint128 amount,
bool direction,
uint256 startingNode,
uint256 deadline
) external onlyOwner {
uint256 managerBalance = assetIn.balanceOf(address(this));
// If manager lacks funds, transfer delta into manager.
if (managerBalance < amount) assetIn.safeTransferFrom(msg.sender, address(this), amount - managerBalance);
// grant approval to the limit order registry
assetIn.safeApprove(address(limitOrderRegistry), amount);
// create the order itself, fund moving in here if needed.
uint128 batchId = limitOrderRegistry.newOrder(pool, targetTick, amount, direction, startingNode, deadline);
// add the batch id to the owners orders so that they can claim later
ownerOrders.add(batchId);
}
/**
* @notice See `LimitOrderRegistry.sol:cancelOrder`.
*/
function cancelOrder(UniswapV3Pool pool, int24 targetTick, bool direction, uint256 deadline) external onlyOwner {
(uint128 amount0, uint128 amount1, uint128 batchId) = limitOrderRegistry.cancelOrder(
pool,
targetTick,
direction,
deadline
);
if (amount0 > 0) ERC20(pool.token0()).safeTransfer(owner, amount0);
if (amount1 > 0) ERC20(pool.token1()).safeTransfer(owner, amount1);
ownerOrders.remove(batchId);
}
/**
* @notice See `LimitOrderRegistry.sol:claimOrder`.
*/
function claimOrder(uint128 batchId) external onlyOwner {
uint256 value = limitOrderRegistry.getFeePerUser(batchId);
limitOrderRegistry.claimOrder{ value: value }(batchId, address(this));
ownerOrders.remove(batchId);
}
/**
@notice Allows owner to withdraw Native asset from this contract.
*/
function withdrawNative(uint256 amount) external onlyOwner {
payable(owner).transfer(amount);
}
/**
* @notice Allows owner to withdraw any ERC20 from this contract.
*/
function withdrawERC20(ERC20 token, uint256 amount) external onlyOwner {
token.safeTransfer(owner, amount);
}
receive() external payable {}
/*//////////////////////////////////////////////////////////////
CHAINLINK AUTOMATION LOGIC
//////////////////////////////////////////////////////////////*/
/**
* @notice Iterates through `ownerOrders` and stops early if total fee is greater than this contract native balance, or if max claims is met.
*/
function checkUpkeep(bytes calldata) external view returns (bool upkeepNeeded, bytes memory performData) {
uint256 nativeBalance = address(this).balance;
// Iterate through owner orders, and build a claim array.
uint256 count = ownerOrders.length();
ClaimInfo[MAX_CLAIMS] memory claimInfo;
uint256 claimCount;
for (uint256 i; i < count; ++i) {
uint128 batchId = uint128(ownerOrders.at(i));
// Current order is not fulfilled.
if (!limitOrderRegistry.isOrderReadyForClaim(batchId)) continue;
uint128 fee = limitOrderRegistry.getFeePerUser(batchId);
// Break if manager does not have enough native to pay for claim.
if (fee > nativeBalance) break;
// Subtract fee from balance.
nativeBalance -= fee;
claimInfo[claimCount].batchId = batchId;
claimInfo[claimCount].fee = fee;
claimCount++;
// Break if max claims is reached.
if (claimCount == MAX_CLAIMS) break;
}
if (claimCount > 0) {
upkeepNeeded = true;
performData = abi.encode(claimInfo);
}
// else nothing to do.
}
/**
* @notice Accepts array of ClaimInfo.
* @dev Passing in incorrect fee values will at worst cost the caller excess gas.
* If fee is too large, excess is returned, or LimitOrderRegistry reverts when it tries to transfer Wrapped Native.
* If fee is too small LimitOrderRegistry reverts when it tries to transfer Wrapped Native.
*/
function performUpkeep(bytes calldata performData) external {
// Accept claim array and claim all orders
ClaimInfo[MAX_CLAIMS] memory claimInfo = abi.decode(performData, (ClaimInfo[10]));
for (uint256 i; i < MAX_CLAIMS; ++i) {
if (limitOrderRegistry.isOrderReadyForClaim(claimInfo[i].batchId)) {
(ERC20 asset, uint256 assets) = limitOrderRegistry.claimOrder{ value: claimInfo[i].fee }(
claimInfo[i].batchId,
address(this)
);
ownerOrders.remove(claimInfo[i].batchId);
if (claimToOwner) asset.safeTransfer(owner, assets);
}
}
}
// @notice get batch ids for the owner of the manager
// @return an array of ids
function getOwnerBatchIds() external view returns (uint256[] memory ids) {
ids = new uint256[](ownerOrders.length());
for (uint256 i; i < ids.length; ++i) ids[i] = ownerOrders.at(i);
}
}