-
Notifications
You must be signed in to change notification settings - Fork 1
/
KeeperKeeper.vy
410 lines (344 loc) · 12.5 KB
/
KeeperKeeper.vy
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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# @version 0.3.7
"""
@title KeeperKeeper
@author gosuto.eth
@license GPL-3.0-only
@notice Register Chainlink upkeeps for a swarm of smart contracts and
automatically maintain the $LINK balance for each individual member
@dev WIP: contract can deploy and register itself
"""
# external interfaces
interface IWETH9:
# def balanceOf(guy: address) -> uint256: view
# def deposit(): payable
def withdraw(wad: uint256): nonpayable
interface IERC677:
# https://github.com/ethereum/EIPs/issues/677
def allowance(_owner: address, _spender: address) -> uint256: view
def approve(_spender: address, _value: uint256) -> bool: nonpayable
def balanceOf(_owner: address) -> uint256: view
def transferAndCall(
_to: address, _value: uint256, _data: Bytes[388]
) -> bool: nonpayable
interface IAutomationRegistry:
# @chainlink/v0.8/interfaces/AutomationRegistryInterface1_3.sol
def getState() -> (State, Config, address[1]): view
interface IKeeperRegistrar:
# @chainlink/v0.8/KeeperRegistrar.sol
def register(
name: String[64],
encryptedEmail: bytes32,
upkeepContract: address,
gasLimit: uint32,
adminAddress: address,
checkData: bytes32,
amount: uint96,
source: uint8,
sender: address
) -> uint256: nonpayable
def getRegistrationConfig() -> (
AutoApproveType, uint32, uint32, address, uint256
): view
interface IEACAggregatorProxy:
# @chainlink/v0.6/EACAggregatorProxy.sol
def latestRoundData() -> (uint80, int256, uint256, uint256, uint80): view
interface IUniswapV2Router02:
# @uniswap-v2-periphery/UniswapV2Router02.sol
def swapETHForExactTokens(
amountOut: uint256,
path: DynArray[address, 2],
to: address,
deadline: uint256
) -> DynArray[uint256, 2]: payable
def swapExactTokensForETH(
amountIn: uint256,
amountOutMin: uint256,
path: DynArray[address, 2],
to: address,
deadline: uint256
) -> DynArray[uint256, 2]: view
interface ISwapRouter:
# @uniswap-v3-periphery/SwapRouter.sol
def exactInputSingle(params: ExactInputSingleParams) -> uint256: payable
def exactOutputSingle(params: ExactOutputSingleParams) -> uint256: payable
def refundETH(): nonpayable
# structs
struct Config: # IAutomationRegistry
paymentPremiumPPB: uint32
flatFeeMicroLink: uint32 # min 0.000001 LINK, max 4294 LINK
blockCountPerTurn: uint24
checkGasLimit: uint32
stalenessSeconds: uint24
gasCeilingMultiplier: uint16
minUpkeepSpend: uint96
maxPerformGas: uint32
fallbackGasPrice: uint256
fallbackLinkPrice: uint256
transcoder: address
registrar: address
struct State: # IAutomationRegistry
nonce: uint32
ownerLinkBalance: uint96
expectedLinkBalance: uint256
numUpkeeps: uint256
struct ExactInputSingleParams: # ISwapRouter
tokenIn: address
tokenOut: address
fee: uint24
recipient: address
deadline: uint256
amountIn: uint256
amountOutMinimum: uint256
sqrtPriceLimitX96: uint160
struct ExactOutputSingleParams: # ISwapRouter
tokenIn: address
tokenOut: address
fee: uint24
recipient: address
deadline: uint256
amountOut: uint256
amountInMaximum: uint256
sqrtPriceLimitX96: uint160
# enums
enum AutoApproveType: # IKeeperRegistrar
DISABLED
ENABLED_SENDER_ALLOWLIST
ENABLED_ALL
# events
event LinkSwappedIn:
amount: uint256
event LinkSwappedOut:
amount: uint256
# constants
MAX_BPS: constant(uint256) = 1_000_000_000
MAX_SWARM_SIZE: constant(uint8) = 16
SELF_UPKEEP_GAS: constant(int256) = 1_000_000 # TODO: replace, still an estimation
LINK: constant(address) = 0x514910771AF9Ca656af840dff83E8264EcF986CA
WETH: constant(address) = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
CL_REGISTRY: constant(address) = 0x02777053d6764996e594c3E88AF1D58D5363a2e6
CL_REGISTRAR: constant(address) = 0xDb8e8e2ccb5C033938736aa89Fe4fa1eDfD15a1d
UNIV3_ROUTER: constant(address) = 0xE592427A0AEce92De3Edee1F18E0157C05861564
FASTGAS_ORACLE: constant(address) = 0x169E633A2D1E6c10dD91238Ba11c4A708dfEF37C
LINKETH_ORACLE: constant(address) = 0xDC530D9457755926550b59e8ECcdaE7624181557
# storage vars
owner: public(address)
swarm: public(uint256[MAX_SWARM_SIZE])
premium: uint32
gas_factor: uint16 # TODO: needs to be implemented or removed
max_gas: uint32
min_link: uint256
@external
def __init__(_owner: address):
"""
@notice Populate storage with relevant config variables from the registrar
"""
self.owner = _owner
self._refresh_registrar_config()
@payable
@external
def initialise():
"""
@notice Register self as the first member of the swarm so that our
performUpkeep is automatically called by a Chainlink keeper
"""
# can only be called if swarm is not initialised yet
assert self.swarm[0] == 0 # dev: swarm already initialised
# add upkeep entry in chainlink's registry and get id back
upkeep_id: uint256 = self._register_member(self, "KeeperKeeper", SELF_UPKEEP_GAS)
# add new upkeep id to the swarm
self.swarm[0] = upkeep_id
@internal
def _refresh_registry_config_and_get_nonce() -> uint32:
"""
@notice Retrieve current state of the registry and save its relevant
variables to storage
@return The registry's current nonce
"""
state: State = empty(State)
config: Config = empty(Config)
_a: address[1] = empty(address[1])
state, config, _a = IAutomationRegistry(CL_REGISTRY).getState()
self.premium = config.paymentPremiumPPB
self.gas_factor = config.gasCeilingMultiplier
self.max_gas = config.maxPerformGas
return state.nonce
@internal
def _refresh_registrar_config():
"""
@notice Get minimum amount of $LINK registrar requires in order to
register an upkeep. Save value to storage
@dev See variable minLINKJuels in @chainlink/v0.8/KeeperRegistrar.sol
"""
self.min_link = IKeeperRegistrar(
0xDb8e8e2ccb5C033938736aa89Fe4fa1eDfD15a1d
).getRegistrationConfig()[4]
@payable
@external
def register_member(member: address, name: String[64], gas_limit: int256) -> uint256:
"""
@notice Allow owner to register a new member to the swarm
@param member Address of the member to register in the swarm
@param name Name of the upkeep
@param gas_limit Max gas that the member's performUpkeep will need
"""
assert msg.sender == self.owner # dev: can only be called by owner
return self._register_member(member, name, gas_limit)
@internal
def _register_member(member: address, name: String[64], gas_limit: int256) -> uint256:
"""
@notice Register an upkeep on the automation registrar and predict its id
@dev https://docs.chain.link/chainlink-automation/register-upkeep/
@param member Address of the member to register in the swarm
@param name Name of the upkeep
@param gas_limit Max gas that the member's performUpkeep will need
@return The id of the newly registered upkeep
"""
# calc amount of $link needed for initial funding of the upkeep
link_threshold: uint256 = self._link_threshold(gas_limit)
# buy more $link if there is not enough to fund the upkeep
if not self._enough_link(gas_limit):
self._swap_link_in(link_threshold)
# get old nonce from registry to compare against new nonce later
# get the registry's config and save to storage
old_nonce: uint32 = self._refresh_registry_config_and_get_nonce()
# confirm gas costs for member's performUpkeep are not too high
assert gas_limit <= convert(self.max_gas, int256) # dev: gas_limit too high
# build registration payload and send to registrar via erc677
payload: Bytes[388] = _abi_encode(
name,
empty(bytes32),
self,
convert(gas_limit, uint32),
self,
empty(bytes32),
link_threshold,
empty(uint256),
self,
method_id=0x3659d666
)
IERC677(LINK).transferAndCall(CL_REGISTRAR, link_threshold, payload)
# get new nonce from registry
state: State = IAutomationRegistry(CL_REGISTRY).getState()[0]
new_nonce: uint32 = state.nonce
assert new_nonce == old_nonce + 1 # dev: upkeep was not successfully registered!
# predict upkeep id
upkeep_hash: bytes32 = keccak256(
concat(
blockhash(block.number - 1),
convert(CL_REGISTRY, bytes32),
convert(old_nonce, bytes32)
)
)
upkeep_id: uint256 = convert(upkeep_hash, uint256)
return upkeep_id
@view
@internal
def _enough_link(gas_per_upkeep: int256) -> bool:
""""
@notice Check if KeeperKeeper's $LINK's balance is high enough to register
an upkeep with given gas value
@param gas_per_upkeep Amount of gas a single call to performUpkeep costs
@return True if there is enough balance, False if not
"""
if IERC677(LINK).balanceOf(self) >= self._link_threshold(gas_per_upkeep):
return True
return False
@view
@internal
def _link_threshold(gas_per_upkeep: int256, n: int256 = 10) -> uint256:
"""
@notice Minimal $LINK balance needed for a single swarm member to perform
at least n upkeeps
@dev https://docs.chain.link/chainlink-automation/automation-economics/
@param gas_per_upkeep Amount of gas a single call to performUpkeep costs
@param n Amount of times the performUpkeep should be able to be called
before needing a topup
@return Amount of $LINK in wei needed for n upkeeps in wei
"""
max_bps: int256 = convert(MAX_BPS, int256)
premium: int256 = convert(self.premium, int256)
gas_price: int256 = IEACAggregatorProxy(FASTGAS_ORACLE).latestRoundData()[1]
link_rate: int256 = IEACAggregatorProxy(LINKETH_ORACLE).latestRoundData()[1]
ether_per_upkeep: int256 = gas_per_upkeep * gas_price
incl_premium: int256 = ether_per_upkeep * (max_bps + premium) / max_bps
incl_overhead: int256 = incl_premium + (80_000 * gas_price)
link_threshold_in_wei: int256 = incl_overhead * 10 ** 18 / link_rate
# assure upkeep can be performed n times
link_threshold_in_wei *= n
# either way make sure we use at least min_link as enforced by registrar
return max(convert(link_threshold_in_wei, uint256), self.min_link)
@payable
@external
def swap_link_in(mantissa: uint256):
"""
@notice Allow any external actor to provide ether for buying $LINK
@param mantissa Amount of $LINK tokens to buy
"""
self._swap_link_in(mantissa)
@payable
@internal
def _swap_link_in(mantissa: uint256):
"""
@notice Swap ether for a specific amount of $LINK
@param mantissa Amount of $LINK tokens required
"""
ISwapRouter(UNIV3_ROUTER).exactOutputSingle(
ExactOutputSingleParams({
tokenIn: WETH,
tokenOut: LINK,
fee: convert(3000, uint24),
recipient: self,
deadline: block.timestamp + 180 * 60,
amountOut: mantissa,
amountInMaximum: msg.value,
sqrtPriceLimitX96: empty(uint160)
}),
value=msg.value
)
ISwapRouter(UNIV3_ROUTER).refundETH()
@external
def swap_link_out(mantissa: uint256):
"""
@notice Allow owner to sell specific amount of $LINK being held
@param mantissa Amount of $LINK tokens to sell
"""
assert msg.sender == self.owner # dev: can only be called by owner
self._swap_link_out(mantissa)
@internal
def _swap_link_out(mantissa: uint256):
"""
@notice Swap a specific amount of $LINK for ether
@param mantissa Amount of $LINK tokens to sell
"""
amount_out_min: uint256 = 0 # TODO
IERC677(LINK).approve(UNIV3_ROUTER, mantissa)
result: uint256 = ISwapRouter(UNIV3_ROUTER).exactInputSingle(
ExactInputSingleParams({
tokenIn: LINK,
tokenOut: WETH,
fee: convert(3000, uint24),
recipient: self,
deadline: block.timestamp + 180 * 60,
amountIn: mantissa,
amountOutMinimum: amount_out_min,
sqrtPriceLimitX96: empty(uint160)
})
)
IWETH9(WETH).withdraw(result)
@view
@external
def checkUpkeep() -> bool:
"""
@notice Loop over every member in the swarm and make sure their upkeeper's
$LINK balance is sufficient
"""
return False
@payable
@external
def __default__():
"""
@notice Assure we are able to receive ether, for example sent back by the
Uniswap router
@dev https://vyper.readthedocs.io/en/stable/control-structures.html#the-default-function
"""
pass