-
Notifications
You must be signed in to change notification settings - Fork 0
/
EthBalanceMonitor.sol
313 lines (287 loc) · 9.71 KB
/
EthBalanceMonitor.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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "./ConfirmedOwner.sol";
import "./interfaces/AutomationCompatibleInterface.sol";
import "./Pausable.sol";
/**
* @title The EthBalanceMonitor contract
* @notice A contract compatible with Chainlink Automation Network that monitors and funds eth addresses
*/
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
contract EthBalanceMonitor is
ConfirmedOwner,
Pausable,
AutomationCompatibleInterface
{
// observed limit of 45K + 10k buffer
uint256 private constant MIN_GAS_FOR_TRANSFER = 55_000;
event FundsAdded(uint256 amountAdded, uint256 newBalance, address sender);
event FundsWithdrawn(uint256 amountWithdrawn, address payee);
event TopUpSucceeded(address indexed recipient);
event TopUpFailed(address indexed recipient);
event KeeperRegistryAddressUpdated(address oldAddress, address newAddress);
event MinWaitPeriodUpdated(
uint256 oldMinWaitPeriod,
uint256 newMinWaitPeriod
);
error InvalidWatchList();
error OnlyKeeperRegistry();
error DuplicateAddress(address duplicate);
struct Target {
bool isActive;
uint96 minBalanceWei;
uint96 topUpAmountWei;
uint56 lastTopUpTimestamp; // enough space for 2 trillion years
}
address private s_keeperRegistryAddress;
uint256 private s_minWaitPeriodSeconds;
address[] private s_watchList;
mapping(address => Target) internal s_targets;
/**
* @param keeperRegistryAddress The address of the Chainlink Automation registry contract
* @param minWaitPeriodSeconds The minimum wait period for addresses between funding
*/
constructor(
address keeperRegistryAddress,
uint256 minWaitPeriodSeconds
) ConfirmedOwner(msg.sender) {
setKeeperRegistryAddress(keeperRegistryAddress);
setMinWaitPeriodSeconds(minWaitPeriodSeconds);
}
/**
* @notice Sets the list of addresses to watch and their funding parameters
* @param addresses the list of addresses to watch
* @param minBalancesWei the minimum balances for each address
* @param topUpAmountsWei the amount to top up each address
*/
function setWatchList(
address[] calldata addresses,
uint96[] calldata minBalancesWei,
uint96[] calldata topUpAmountsWei
) external onlyOwner {
if (
addresses.length != minBalancesWei.length ||
addresses.length != topUpAmountsWei.length
) {
revert InvalidWatchList();
}
address[] memory oldWatchList = s_watchList;
for (uint256 idx = 0; idx < oldWatchList.length; idx++) {
s_targets[oldWatchList[idx]].isActive = false;
}
for (uint256 idx = 0; idx < addresses.length; idx++) {
if (s_targets[addresses[idx]].isActive) {
revert DuplicateAddress(addresses[idx]);
}
if (addresses[idx] == address(0)) {
revert InvalidWatchList();
}
if (topUpAmountsWei[idx] == 0) {
revert InvalidWatchList();
}
s_targets[addresses[idx]] = Target({
isActive: true,
minBalanceWei: minBalancesWei[idx],
topUpAmountWei: topUpAmountsWei[idx],
lastTopUpTimestamp: 0
});
}
s_watchList = addresses;
}
/**
* @notice Gets a list of addresses that are under funded
* @return list of addresses that are underfunded
*/
function getUnderfundedAddresses() public view returns (address[] memory) {
address[] memory watchList = s_watchList;
address[] memory needsFunding = new address[](watchList.length);
uint256 count = 0;
uint256 minWaitPeriod = s_minWaitPeriodSeconds;
uint256 balance = address(this).balance;
Target memory target;
for (uint256 idx = 0; idx < watchList.length; idx++) {
target = s_targets[watchList[idx]];
if (
target.lastTopUpTimestamp + minWaitPeriod <= block.timestamp &&
balance >= target.topUpAmountWei &&
watchList[idx].balance < target.minBalanceWei
) {
needsFunding[count] = watchList[idx];
count++;
balance -= target.topUpAmountWei;
}
}
if (count != watchList.length) {
assembly {
mstore(needsFunding, count)
}
}
return needsFunding;
}
/**
* @notice Send funds to the addresses provided
* @param needsFunding the list of addresses to fund (addresses must be pre-approved)
*/
function topUp(address[] memory needsFunding) public whenNotPaused {
uint256 minWaitPeriodSeconds = s_minWaitPeriodSeconds;
Target memory target;
for (uint256 idx = 0; idx < needsFunding.length; idx++) {
target = s_targets[needsFunding[idx]];
if (
target.isActive &&
target.lastTopUpTimestamp + minWaitPeriodSeconds <=
block.timestamp &&
needsFunding[idx].balance < target.minBalanceWei
) {
bool success = payable(needsFunding[idx]).send(
target.topUpAmountWei
);
if (success) {
s_targets[needsFunding[idx]].lastTopUpTimestamp = uint56(
block.timestamp
);
emit TopUpSucceeded(needsFunding[idx]);
} else {
emit TopUpFailed(needsFunding[idx]);
}
}
if (gasleft() < MIN_GAS_FOR_TRANSFER) {
return;
}
}
}
/**
* @notice Get list of addresses that are underfunded and return payload compatible with Chainlink Automation Network
* @return upkeepNeeded signals if upkeep is needed, performData is an abi encoded list of addresses that need funds
*/
function checkUpkeep(
bytes calldata
)
external
view
override
whenNotPaused
returns (bool upkeepNeeded, bytes memory performData)
{
address[] memory needsFunding = getUnderfundedAddresses();
upkeepNeeded = needsFunding.length > 0;
performData = abi.encode(needsFunding);
return (upkeepNeeded, performData);
}
/**
* @notice Called by Chainlink Automation Node to send funds to underfunded addresses
* @param performData The abi encoded list of addresses to fund
*/
function performUpkeep(
bytes calldata performData
) external override onlyKeeperRegistry whenNotPaused {
address[] memory needsFunding = abi.decode(performData, (address[]));
topUp(needsFunding);
}
/**
* @notice Withdraws the contract balance
* @param amount The amount of eth (in wei) to withdraw
* @param payee The address to pay
*/
function withdraw(
uint256 amount,
address payable payee
) external onlyOwner {
require(payee != address(0));
emit FundsWithdrawn(amount, payee);
payee.transfer(amount);
}
/**
* @notice Receive funds
*/
receive() external payable {
emit FundsAdded(msg.value, address(this).balance, msg.sender);
}
/**
* @notice Sets the Chainlink Automation registry address
*/
function setKeeperRegistryAddress(
address keeperRegistryAddress
) public onlyOwner {
require(keeperRegistryAddress != address(0));
emit KeeperRegistryAddressUpdated(
s_keeperRegistryAddress,
keeperRegistryAddress
);
s_keeperRegistryAddress = keeperRegistryAddress;
}
/**
* @notice Sets the minimum wait period (in seconds) for addresses between funding
*/
function setMinWaitPeriodSeconds(uint256 period) public onlyOwner {
emit MinWaitPeriodUpdated(s_minWaitPeriodSeconds, period);
s_minWaitPeriodSeconds = period;
}
/**
* @notice Gets the Chainlink Automation registry address
*/
function getKeeperRegistryAddress()
external
view
returns (address keeperRegistryAddress)
{
return s_keeperRegistryAddress;
}
/**
* @notice Gets the minimum wait period
*/
function getMinWaitPeriodSeconds() external view returns (uint256) {
return s_minWaitPeriodSeconds;
}
/**
* @notice Gets the list of addresses being watched
*/
function getWatchList() external view returns (address[] memory) {
return s_watchList;
}
/**
* @notice Gets configuration information for an address on the watchlist
*/
function getAccountInfo(
address targetAddress
)
external
view
returns (
bool isActive,
uint96 minBalanceWei,
uint96 topUpAmountWei,
uint56 lastTopUpTimestamp
)
{
Target memory target = s_targets[targetAddress];
return (
target.isActive,
target.minBalanceWei,
target.topUpAmountWei,
target.lastTopUpTimestamp
);
}
/**
* @notice Pauses the contract, which prevents executing performUpkeep
*/
function pause() external onlyOwner {
_pause();
}
/**
* @notice Unpauses the contract
*/
function unpause() external onlyOwner {
_unpause();
}
modifier onlyKeeperRegistry() {
if (msg.sender != s_keeperRegistryAddress) {
revert OnlyKeeperRegistry();
}
_;
}
}