/
Subscriptions.sol
443 lines (389 loc) · 16.4 KB
/
Subscriptions.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
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
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/utils/math/Math.sol';
import '@openzeppelin/contracts/utils/math/SignedMath.sol';
/// @title Graph subscriptions contract.
/// @notice This contract is designed to allow users of the Graph Protocol to pay gateways for their services with limited risk of losing tokens.
/// It also allows registering authorized signers with the gateway that can create subscription tickets on behalf of the user.
/// This contract makes no assumptions about how the subscription rate is interpreted by the
/// gateway.
contract Subscriptions is Ownable {
// -- State --
/// @notice A Subscription represents a lockup of `rate` tokens per second for the half-open
/// timestamp range [start, end).
struct Subscription {
uint64 start;
uint64 end;
uint128 rate;
}
/// @notice An epoch defines the end of a span of blocks, the length of which is defined by
/// `epochSeconds`. These exist to facilitate a relatively efficient `collect` implementation
/// while allowing users to recover unlocked tokens at a block granularity.
struct Epoch {
int128 delta;
int128 extra;
}
/// @notice ERC-20 token held by this contract.
IERC20 public immutable token;
/// @notice Duration of each epoch in seconds.
uint64 public immutable epochSeconds;
/// @notice Mapping of users to their most recent subscription.
mapping(address => Subscription) public subscriptions;
/// @notice Mapping of epoch numbers to their payloads.
mapping(uint256 => Epoch) public epochs;
/// @notice Epoch cursor position.
uint256 public uncollectedEpoch;
/// @notice Epoch cursor value.
int128 public collectPerEpoch;
/// @notice Mapping of user to set of authorized signers.
mapping(address => mapping(address => bool)) public authorizedSigners;
/// @notice Mapping of user to pending subscription.
mapping(address => Subscription) public pendingSubscriptions;
// -- Events --
event Init(address token);
event Subscribe(
address indexed user,
uint256 indexed epoch,
uint64 start,
uint64 end,
uint128 rate
);
event Unsubscribe(address indexed user, uint256 indexed epoch);
event Extend(address indexed user, uint64 end);
event PendingSubscriptionCreated(
address indexed user,
uint256 indexed epoch,
uint64 start,
uint64 end,
uint128 rate
);
event AuthorizedSignerAdded(
address indexed subscriptionOwner,
address indexed authorizedSigner
);
event AuthorizedSignerRemoved(
address indexed subscriptionOwner,
address indexed authorizedSigner
);
event TokensCollected(
address indexed owner,
uint256 amount,
uint256 indexed startEpoch,
uint256 indexed endEpoch
);
// -- Functions --
/// @param _token The ERC-20 token held by this contract
/// @param _epochSeconds The Duration of each epoch in seconds.
/// @dev Contract ownership must be transfered to the gateway after deployment.
constructor(address _token, uint64 _epochSeconds) {
token = IERC20(_token);
epochSeconds = _epochSeconds;
uncollectedEpoch = block.timestamp / _epochSeconds;
emit Init(_token);
}
/// @notice Create a subscription for a user
/// This can be called by any account for a given user as long as the new subscription starts
/// after the current subscription ends. Only the owner can overwrite an active subscription.
/// @param user Owner for the new subscription.
/// @param start Start timestamp for the new subscription.
/// @param end End timestamp for the new subscription.
/// @param rate Rate for the new subscription.
function subscribe(
address user,
uint64 start,
uint64 end,
uint128 rate
) public {
require(
subscriptions[user].end <= uint64(block.timestamp) ||
user == msg.sender,
'active subscription must have ended'
);
_subscribe(user, start, end, rate);
}
/// @notice Remove the sender's subscription. Unlocked tokens will be transfered to the sender.
function unsubscribe() public {
address user = msg.sender;
require(user != address(0), 'user is null');
Subscription storage sub = subscriptions[user];
require(sub.start != 0, 'no active subscription');
uint64 _now = uint64(block.timestamp);
require(sub.end > _now, 'Subscription has expired');
uint128 tokenAmount = unlocked(sub.start, sub.end, sub.rate);
if ((sub.start <= _now) && (_now < sub.end)) {
setEpochs(sub.start, sub.end, -int128(sub.rate));
setEpochs(sub.start, _now, int128(sub.rate));
subscriptions[user].end = _now;
} else if (_now < sub.start) {
setEpochs(sub.start, sub.end, -int128(sub.rate));
delete subscriptions[user];
}
bool success = token.transfer(user, tokenAmount);
require(success, 'IERC20 token transfer failed');
uint256 epoch = currentEpoch();
emit Unsubscribe(user, epoch);
}
/// @param user Owner of the subscription that will be extended.
/// @param end New end timestamp for the user's subscription.
function extendSubscription(address user, uint64 end) public {
require(user != address(0), 'user is null');
Subscription storage sub = subscriptions[user];
require(
(sub.start <= block.timestamp) && (block.timestamp < sub.end),
'current subscription must be active'
);
require(
sub.end < end,
'end must be after that of the current subscription'
);
setEpochs(sub.start, sub.end, -int128(sub.rate));
setEpochs(sub.start, end, int128(sub.rate));
uint64 currentEnd = sub.end;
subscriptions[user].end = end;
uint128 addition = sub.rate * (end - currentEnd);
bool success = token.transferFrom(msg.sender, address(this), addition);
require(success, 'IERC20 token transfer failed');
emit Extend(user, end);
}
/// @notice Collect a subset of the locked tokens held by this contract.
function collect() public onlyOwner {
collect(0);
}
/// @notice Collect a subset of the locked tokens held by this contract.
/// @param _offset epochs before the current epoch to end collection. This should be zero unless
/// this call would otherwise be expected to run out of gas.
function collect(uint256 _offset) public onlyOwner {
address owner = owner();
uint256 startEpoch = uncollectedEpoch;
uint256 endEpoch = currentEpoch() - _offset;
int128 total = 0;
while (uncollectedEpoch < endEpoch) {
Epoch storage epoch = epochs[uncollectedEpoch];
collectPerEpoch += epoch.delta;
total += collectPerEpoch + epoch.extra;
delete epochs[uncollectedEpoch];
unchecked {
++uncollectedEpoch;
}
}
// This should never happen but we need to check due to the int > uint cast below
require(total >= 0, 'total must be non-negative');
uint256 amount = uint128(total);
bool success = token.transfer(owner, amount);
require(success, 'IERC20 token transfer failed');
emit TokensCollected(owner, amount, startEpoch, endEpoch);
}
/// @notice Creates a subscription template without requiring funds. Expected to be used with
/// `fulfil`.
/// @param user Owner for the pending subscription.
/// @param start Start timestamp for the pending subscription.
/// @param end End timestamp for the pending subscription.
/// @param rate Rate for the pending subscription.
function setPendingSubscription(
address user,
uint64 start,
uint64 end,
uint128 rate
) public {
require(
msg.sender == user,
'Can only set pending subscriptions for self'
);
pendingSubscriptions[user] = Subscription({
start: start,
end: end,
rate: rate
});
uint256 epoch = currentEpoch();
emit PendingSubscriptionCreated(user, epoch, start, end, rate);
}
/// @notice Fulfil method for the payment fulfilment service
/// @dev Second param required, but currently unused.
/// @param _to Owner of the new subscription.
/// @notice Equivalent to calling `subscribe` with the previous `setPendingSubscription`
/// arguments for the same user.
function fulfil(address _to, uint256 _amount) public {
Subscription storage pendingSub = pendingSubscriptions[_to];
require(
pendingSub.start != 0 && pendingSub.end != 0,
'No pending subscription'
);
uint256 minAmount = pendingSub.rate *
(pendingSub.end - pendingSub.start);
require(
_amount >= minAmount,
'Insufficient funds to create subscription'
);
// Create the subscription using the pending subscription details
_subscribe(_to, pendingSub.start, pendingSub.end, pendingSub.rate);
delete pendingSubscriptions[_to];
// Send any leftovers back to the user
Subscription storage sub = subscriptions[_to];
uint256 amountUsed = sub.rate * (sub.end - sub.start);
uint256 amountLeft = _amount - amountUsed;
if (amountLeft > 0) {
bool pullSuccess = token.transferFrom(
msg.sender,
address(this),
amountLeft
);
require(pullSuccess, 'IERC20 token transfer failed');
bool transferSuccess = token.transfer(_to, amountLeft);
require(transferSuccess, 'IERC20 token transfer failed');
}
}
/// @param _signer Address to be authorized to sign messages on the sender's behalf.
function addAuthorizedSigner(address _signer) public {
address user = msg.sender;
require(user != _signer, 'user is always an authorized signer');
authorizedSigners[user][_signer] = true;
emit AuthorizedSignerAdded(user, _signer);
}
/// @param _signer Address to become unauthorized to sign messages on the sender's behalf.
function removeAuthorizedSigner(address _signer) public {
address user = msg.sender;
require(user != _signer, 'user is always an authorized signer');
delete authorizedSigners[user][_signer];
emit AuthorizedSignerRemoved(user, _signer);
}
/// @param _user Subscription owner.
/// @param _signer Address authorized to sign messages on the owners behalf.
/// @return isAuthorized True if the given signer is set as an authorized signer for the given
/// user, false otherwise.
function checkAuthorizedSigner(
address _user,
address _signer
) public view returns (bool) {
if (_user == _signer) {
return true;
}
return authorizedSigners[_user][_signer];
}
/// @param _timestamp Block timestamp, in seconds.
/// @return epoch Epoch number, rouded up to the next epoch Boundary.
function timestampToEpoch(
uint256 _timestamp
) public view returns (uint256) {
return (_timestamp / epochSeconds) + 1;
}
/// @return epoch Current epoch number, rouded up to the next epoch Boundary.
function currentEpoch() public view returns (uint256) {
return timestampToEpoch(block.timestamp);
}
/// @dev Defined as `rate * max(0, min(now, end) - start)`.
/// @param _subStart Start timestamp of the active subscription.
/// @param _subEnd End timestamp of the active subscription.
/// @param _subRate Active subscription rate.
/// @return lockedTokens Amount of locked tokens for the given subscription, which are
/// collectable by the contract owner and are not recoverable by the user.
function locked(
uint64 _subStart,
uint64 _subEnd,
uint128 _subRate
) public view returns (uint128) {
uint256 len = uint256(
SignedMath.max(
0,
int256(Math.min(block.timestamp, _subEnd)) - int64(_subStart)
)
);
return _subRate * uint128(len);
}
/// @dev Defined as `rate * max(0, min(now, end) - start)`.
/// @param _user Address of the active subscription owner.
/// @return lockedTokens Amount of locked tokens for the given subscription, which are
/// collectable by the contract owner and are not recoverable by the user.
function locked(address _user) public view returns (uint128) {
Subscription storage sub = subscriptions[_user];
return locked(sub.start, sub.end, sub.rate);
}
/// @dev Defined as `rate * max(0, end - max(now, start))`.
/// @param _subStart Start timestamp of the active subscription.
/// @param _subEnd End timestamp of the active subscription.
/// @param _subRate Active subscription rate.
/// @return unlockedTokens Amount of unlocked tokens, which are recoverable by the user, and are
/// not collectable by the contract owner.
function unlocked(
uint64 _subStart,
uint64 _subEnd,
uint128 _subRate
) public view returns (uint128) {
uint256 len = uint256(
SignedMath.max(
0,
int256(int64(_subEnd)) -
int256(Math.max(block.timestamp, _subStart))
)
);
return _subRate * uint128(len);
}
/// @dev Defined as `rate * max(0, end - max(now, start))`.
/// @param _user Address of the active subscription owner.
/// @return unlockedTokens Amount of unlocked tokens, which are recoverable by the user, and are
/// not collectable by the contract owner.
function unlocked(address _user) public view returns (uint128) {
Subscription storage sub = subscriptions[_user];
return unlocked(sub.start, sub.end, sub.rate);
}
/// @notice Create a subscription for a user
/// Will always override an active subscription if it exists
/// @param user Owner for the new subscription.
/// @param start Start timestamp for the new subscription.
/// @param end End timestamp for the new subscription.
/// @param rate Rate for the new subscription.
function _subscribe(
address user,
uint64 start,
uint64 end,
uint128 rate
) private {
require(user != address(0), 'user is null');
require(user != address(this), 'invalid user');
start = uint64(Math.max(start, block.timestamp));
require(start < end, 'start must be less than end');
// Overwrite an active subscription if there is one
if (subscriptions[user].end > block.timestamp) {
unsubscribe();
}
subscriptions[user] = Subscription({
start: start,
end: end,
rate: rate
});
setEpochs(start, end, int128(rate));
uint256 subTotal = rate * (end - start);
bool success = token.transferFrom(msg.sender, address(this), subTotal);
require(success, 'IERC20 token transfer failed');
uint256 epoch = currentEpoch();
emit Subscribe(user, epoch, start, end, rate);
}
function setEpochs(uint64 start, uint64 end, int128 rate) private {
/*
Example subscription layout using
epochSeconds = 6
sub = {start: 2, end: 9, rate: 1}
blocks: |0 |1 |2 |3 |4 |5 |6 |7 |8 |9 |10|11|
^ currentBlock
^start ^end
epochs: | 1| 2|
e1^ e2^
*/
uint256 e = currentEpoch();
uint256 e1 = timestampToEpoch(start);
if (e <= e1) {
epochs[e1].delta += rate * int64(epochSeconds);
epochs[e1].extra -=
rate *
int64(start - (uint64(e1 - 1) * epochSeconds));
}
uint256 e2 = timestampToEpoch(end);
if (e <= e2) {
epochs[e2].delta -= rate * int64(epochSeconds);
epochs[e2].extra +=
rate *
int64(end - (uint64(e2 - 1) * epochSeconds));
}
}
}