forked from ethersphere/storage-incentives
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPostageStamp.sol
More file actions
495 lines (425 loc) · 20.4 KB
/
Copy pathPostageStamp.sol
File metadata and controls
495 lines (425 loc) · 20.4 KB
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
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity ^0.8.1;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "./OrderStatisticsTree/HitchensOrderStatisticsTreeLib.sol";
/**
* @title PostageStamp contract
* @author The Swarm Authors
* @dev The postage stamp contracts allows users to create and manage postage stamp batches.
* The current balance for each batch is stored ordered in descending order of normalised balance.
* Balance is normalised to be per chunk and the total spend since the contract was deployed, i.e. when a batch
* is bought, its per-chunk balance is supplemented with the current cost of storing one chunk since the beginning of time,
* as if the batch had existed since the contract's inception. During the _expiry_ process, each of these balances is
* checked against the _currentTotalOutPayment_, a similarly normalised figure that represents the current cost of
* storing one chunk since the beginning of time. A batch with a normalised balance less than _currentTotalOutPayment_
* is treated as expired.
*
* The _currentTotalOutPayment_ is calculated using _totalOutPayment_ which is updated during _setPrice_ events so
* that the applicable per-chunk prices can be charged for the relevant periods of time. This can then be multiplied
* by the amount of chunks which are allowed to be stamped by each batch to get the actual cost of storage.
*
* The amount of chunks a batch can stamp is determined by the _bucketDepth_. A batch may store a maximum of 2^depth chunks.
* The global figure for the currently allowed chunks is tracked by _validChunkCount_ and updated during batch _expiry_ events.
*/
contract PostageStamp is AccessControl, Pausable {
using HitchensOrderStatisticsTreeLib for HitchensOrderStatisticsTreeLib.Tree;
/**
* @dev Emitted when a new batch is created.
*/
event BatchCreated(
bytes32 indexed batchId,
uint256 totalAmount,
uint256 normalisedBalance,
address owner,
uint8 depth,
uint8 bucketDepth,
bool immutableFlag
);
event PotWithdrawn(address recipient, uint256 totalAmount);
/**
* @dev Emitted when an existing batch is topped up.
*/
event BatchTopUp(bytes32 indexed batchId, uint256 topupAmount, uint256 normalisedBalance);
/**
* @dev Emitted when the depth of an existing batch increases.
*/
event BatchDepthIncrease(bytes32 indexed batchId, uint8 newDepth, uint256 normalisedBalance);
/**
*@dev Emitted on every price update.
*/
event PriceUpdate(uint256 price);
struct Batch {
// Owner of this batch (0 if not valid).
address owner;
// Current depth of this batch.
uint8 depth;
//
uint8 bucketDepth;
// Whether this batch is immutable.
bool immutableFlag;
// Normalised balance per chunk.
uint256 normalisedBalance;
//
uint256 lastUpdatedBlockNumber;
}
// Role allowed to increase totalOutPayment.
bytes32 public constant PRICE_ORACLE_ROLE = keccak256("PRICE_ORACLE");
// Role allowed to pause
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
// Role allowed to withdraw the pot.
bytes32 public constant REDISTRIBUTOR_ROLE = keccak256("REDISTRIBUTOR_ROLE");
// Associate every batch id with batch data.
mapping(bytes32 => Batch) public batches;
// Store every batch id ordered by normalisedBalance.
HitchensOrderStatisticsTreeLib.Tree tree;
// Address of the ERC20 token this contract references.
address public bzzToken;
// Total out payment per chunk, at the blockheight of the last price change.
uint256 private totalOutPayment;
// Minimum allowed depth of bucket.
uint8 public minimumBucketDepth;
// Combined global chunk capacity of valid batches remaining at the blockheight expire() was last called.
uint256 public validChunkCount;
// Lottery pot at last update.
uint256 public pot;
// blocks in 24 hours ~ 24 * 60 * 60 / 5 = 17280
uint256 public minimumValidityBlocks = 17280;
// Price from the last update.
uint256 public lastPrice = 0;
// Block at which the last update occured.
uint256 public lastUpdatedBlock;
// Normalised balance at the blockheight expire() was last called.
uint256 public lastExpiryBalance;
/**
* @param _bzzToken The ERC20 token address to reference in this contract.
* @param _minimumBucketDepth The minimum bucket depth of batches that can be purchased.
*/
constructor(address _bzzToken, uint8 _minimumBucketDepth, address multisig) {
bzzToken = _bzzToken;
minimumBucketDepth = _minimumBucketDepth;
_setupRole(DEFAULT_ADMIN_ROLE, multisig);
_setupRole(PAUSER_ROLE, msg.sender);
}
/**
* @notice Create a new batch.
* @dev At least `_initialBalancePerChunk*2^depth` tokens must be approved in the ERC20 token contract.
* @param _owner Owner of the new batch.
* @param _initialBalancePerChunk Initial balance per chunk.
* @param _depth Initial depth of the new batch.
* @param _nonce A random value used in the batch id derivation to allow multiple batches per owner.
* @param _immutable Whether the batch is mutable.
*/
function createBatch(
address _owner,
uint256 _initialBalancePerChunk,
uint8 _depth,
uint8 _bucketDepth,
bytes32 _nonce,
bool _immutable
) external whenNotPaused {
require(_owner != address(0), "owner cannot be the zero address");
// bucket depth should be non-zero and smaller than the depth
require(
_bucketDepth != 0 && minimumBucketDepth <= _bucketDepth && _bucketDepth < _depth,
"invalid bucket depth"
);
// derive batchId from msg.sender to ensure another party cannot use the same batch id and frontrun us.
bytes32 batchId = keccak256(abi.encode(msg.sender, _nonce));
require(batches[batchId].owner == address(0), "batch already exists");
require(
_initialBalancePerChunk >= minimumInitialBalancePerChunk(),
"insufficient initial balance for 24h minimum validity"
);
// per chunk balance multiplied by the batch size in chunks must be transferred from the sender
uint256 totalAmount = _initialBalancePerChunk * (1 << _depth);
require(ERC20(bzzToken).transferFrom(msg.sender, address(this), totalAmount), "failed transfer");
// normalisedBalance is an absolute value per chunk, as if the batch had existed
// since the block the contract was deployed, so we must supplement this batch's
// _initialBalancePerChunk with the currentTotalOutPayment()
uint256 normalisedBalance = currentTotalOutPayment() + (_initialBalancePerChunk);
//update validChunkCount to remove currently expired batches
expireLimited(type(uint256).max);
//then add the chunks this batch will contribute
validChunkCount += 1 << _depth;
batches[batchId] = Batch({
owner: _owner,
depth: _depth,
bucketDepth: _bucketDepth,
immutableFlag: _immutable,
normalisedBalance: normalisedBalance,
lastUpdatedBlockNumber: block.number
});
require(normalisedBalance > 0, "normalisedBalance cannot be zero");
// insert into the ordered tree
tree.insert(batchId, normalisedBalance);
emit BatchCreated(batchId, totalAmount, normalisedBalance, _owner, _depth, _bucketDepth, _immutable);
}
/**
* @notice Manually create a new batch when faciliatating migration, can only be called by the Admin role.
* @dev At least `_initialBalancePerChunk*2^depth` tokens must be approved in the ERC20 token contract.
* @param _owner Owner of the new batch.
* @param _initialBalancePerChunk Initial balance per chunk of the batch.
* @param _depth Initial depth of the new batch.
* @param _batchId BatchId being copied (from previous version contract data).
* @param _immutable Whether the batch is mutable.
*/
function copyBatch(
address _owner,
uint256 _initialBalancePerChunk,
uint8 _depth,
uint8 _bucketDepth,
bytes32 _batchId,
bool _immutable
) external whenNotPaused {
require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "only administrator can use copy method");
require(_owner != address(0), "owner cannot be the zero address");
require(_bucketDepth != 0 && _bucketDepth < _depth, "invalid bucket depth");
require(batches[_batchId].owner == address(0), "batch already exists");
// per chunk balance multiplied by the batch size in chunks must be transferred from the sender
uint256 totalAmount = _initialBalancePerChunk * (1 << _depth);
require(ERC20(bzzToken).transferFrom(msg.sender, address(this), totalAmount), "failed transfer");
uint256 normalisedBalance = currentTotalOutPayment() + (_initialBalancePerChunk);
validChunkCount += 1 << _depth;
batches[_batchId] = Batch({
owner: _owner,
depth: _depth,
bucketDepth: _bucketDepth,
immutableFlag: _immutable,
normalisedBalance: normalisedBalance,
lastUpdatedBlockNumber: block.number
});
require(normalisedBalance > 0, "normalisedBalance cannot be zero");
tree.insert(_batchId, normalisedBalance);
emit BatchCreated(_batchId, totalAmount, normalisedBalance, _owner, _depth, _bucketDepth, _immutable);
}
/**
* @notice Top up an existing batch.
* @dev At least `_topupAmountPerChunk*2^depth` tokens must be approved in the ERC20 token contract.
* @param _batchId The id of an existing batch.
* @param _topupAmountPerChunk The amount of additional tokens to add per chunk.
*/
function topUp(bytes32 _batchId, uint256 _topupAmountPerChunk) external whenNotPaused {
Batch storage batch = batches[_batchId];
require(batch.owner != address(0), "batch does not exist or has expired");
require(batch.normalisedBalance > currentTotalOutPayment(), "batch already expired");
require(batch.depth > minimumBucketDepth, "batch too small to renew");
require(
remainingBalance(_batchId) + (_topupAmountPerChunk) >= minimumInitialBalancePerChunk(),
"insufficient topped up balance for 24h minimum validity"
);
// per chunk balance multiplied by the batch size in chunks must be transferred from the sender
uint256 totalAmount = _topupAmountPerChunk * (1 << batch.depth);
require(ERC20(bzzToken).transferFrom(msg.sender, address(this), totalAmount), "failed transfer");
// update by removing batch and then reinserting
tree.remove(_batchId, batch.normalisedBalance);
batch.normalisedBalance = batch.normalisedBalance + (_topupAmountPerChunk);
tree.insert(_batchId, batch.normalisedBalance);
emit BatchTopUp(_batchId, totalAmount, batch.normalisedBalance);
}
/**
* @notice Increase the depth of an existing batch.
* @dev Can only be called by the owner of the batch.
* @param _batchId the id of an existing batch.
* @param _newDepth the new (larger than the previous one) depth for this batch.
*/
function increaseDepth(bytes32 _batchId, uint8 _newDepth) external whenNotPaused {
Batch storage batch = batches[_batchId];
require(batch.owner == msg.sender, "not batch owner");
require(minimumBucketDepth < _newDepth && batch.depth < _newDepth, "depth not increasing");
require(!batch.immutableFlag, "batch is immutable");
require(batch.normalisedBalance > currentTotalOutPayment(), "batch already expired");
uint8 depthChange = _newDepth - batch.depth;
// divide by the change in batch size (2^depthChange)
uint256 newRemainingBalance = remainingBalance(_batchId) / (1 << depthChange);
require(
newRemainingBalance >= minimumInitialBalancePerChunk(),
"remaining balance after depth increase wouldn't meet 24h minimum validity"
);
// expire batches up to current block before amending validChunkCount to include
// the new chunks resultant of the depth increase
expireLimited(type(uint256).max);
validChunkCount += (1 << _newDepth) - (1 << batch.depth);
// update by removing batch and then reinserting
tree.remove(_batchId, batch.normalisedBalance);
batch.depth = _newDepth;
batch.lastUpdatedBlockNumber = block.number;
batch.normalisedBalance = currentTotalOutPayment() + (newRemainingBalance);
tree.insert(_batchId, batch.normalisedBalance);
emit BatchDepthIncrease(_batchId, _newDepth, batch.normalisedBalance);
}
/**
* @notice Return the per chunk balance not yet used up.
* @param _batchId The id of an existing batch.
*/
function remainingBalance(bytes32 _batchId) public view returns (uint256) {
Batch storage batch = batches[_batchId];
require(batch.owner != address(0), "batch does not exist or expired");
if (batch.normalisedBalance <= currentTotalOutPayment()) {
return 0;
}
return batch.normalisedBalance - currentTotalOutPayment();
}
/**
* @notice Set a new price.
* @dev Can only be called by the price oracle role.
* @param _price The new price.
*/
function setPrice(uint256 _price) external {
require(hasRole(PRICE_ORACLE_ROLE, msg.sender), "only price oracle can set the price");
// if there was a last price, add the outpayment since the last update
// using the last price to _totalOutPayment_. if there was not a lastPrice,
// the lastprice must have been zero.
if (lastPrice != 0) {
totalOutPayment = currentTotalOutPayment();
}
lastPrice = _price;
lastUpdatedBlock = block.number;
emit PriceUpdate(_price);
}
function setMinimumValidityBlocks(uint256 _value) external {
require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "only administrator can set minimum validity blocks");
minimumValidityBlocks = _value;
}
/**
* @notice Total per-chunk cost since the contract's deployment.
* @dev Returns the total normalised all-time per chunk payout.
* Only Batches with a normalised balance greater than this are valid.
*/
function currentTotalOutPayment() public view returns (uint256) {
uint256 blocks = block.number - lastUpdatedBlock;
uint256 increaseSinceLastUpdate = lastPrice * (blocks);
return totalOutPayment + (increaseSinceLastUpdate);
}
function minimumInitialBalancePerChunk() public view returns (uint256) {
return minimumValidityBlocks * lastPrice;
}
/**
* @notice Pause the contract.
* @dev Can only be called by the pauser when not paused.
* The contract can be provably stopped by renouncing the pauser role and the admin role once paused.
*/
function pause() public {
require(hasRole(PAUSER_ROLE, msg.sender), "only pauser can pause");
_pause();
}
/**
* @notice Unpause the contract.
* @dev Can only be called by the pauser role while paused.
*/
function unPause() public {
require(hasRole(PAUSER_ROLE, msg.sender), "only pauser can unpause");
_unpause();
}
/**
* @notice Return true if no batches exist
*/
function isBatchesTreeEmpty() public view returns (bool) {
return tree.count() == 0;
}
/**
* @notice Get the first batch id ordered by ascending normalised balance.
* @dev If more than one batch id, return index at 0, if no batches, revert.
*/
function firstBatchId() public view returns (bytes32) {
uint256 val = tree.first();
require(val > 0, "no batches exist");
return tree.valueKeyAtIndex(val, 0);
}
/**
* @notice Reclaims a limited number of expired batches
* @dev Can be used if reclaiming all expired batches would exceed the block gas limit, causing other
* contract method calls to fail.
* @param limit The maximum number of batches to expire.
*/
function expireLimited(uint256 limit) public {
// the lower bound of the normalised balance for which we will check if batches have expired
uint256 leb = lastExpiryBalance;
uint256 i;
for (i = 0; i < limit; i++) {
if (isBatchesTreeEmpty()) {
lastExpiryBalance = currentTotalOutPayment();
break;
}
// get the batch with the smallest normalised balance
bytes32 fbi = firstBatchId();
// if the batch with the smallest balance has not yet expired
// we have already reached the end of the batches we need
// to expire, so exit the loop
if (remainingBalance(fbi) > 0) {
// the upper bound of the normalised balance for which we will check if batches have expired
// value is updated when there are no expired batches left
lastExpiryBalance = currentTotalOutPayment();
break;
}
// otherwise, the batch with the smallest balance has expired,
// so we must remove the chunks this batch contributes to the global validChunkCount
Batch storage batch = batches[fbi];
uint256 batchSize = 1 << batch.depth;
require(validChunkCount >= batchSize, "insufficient valid chunk count");
validChunkCount -= batchSize;
// since the batch expired _during_ the period we must add
// remaining normalised payout for this batch only
pot += batchSize * (batch.normalisedBalance - leb);
tree.remove(fbi, batch.normalisedBalance);
delete batches[fbi];
}
// then, for all batches that have _not_ expired during the period
// add the total normalised payout of all batches
// multiplied by the remaining total valid chunk count
// to the pot for the period since the last expiry
require(lastExpiryBalance >= leb, "current total outpayment should never decrease");
// then, for all batches that have _not_ expired during the period
// add the total normalised payout of all batches
// multiplied by the remaining total valid chunk count
// to the pot for the period since the last expiry
pot += validChunkCount * (lastExpiryBalance - leb);
}
/**
* @notice Indicates whether expired batches exist.
*/
function expiredBatchesExist() public view returns (bool) {
if (isBatchesTreeEmpty()) {
return false;
}
return (remainingBalance(firstBatchId()) <= 0);
}
/**
* @notice The current pot.
*/
function totalPot() public returns (uint256) {
expireLimited(type(uint256).max);
uint256 balance = ERC20(bzzToken).balanceOf(address(this));
return pot < balance ? pot : balance;
}
/**
* @notice Withdraw the pot, authorised callers only.
* @param beneficiary Recieves the current total pot.
*/
function withdraw(address beneficiary) external {
require(hasRole(REDISTRIBUTOR_ROLE, msg.sender), "only redistributor can withdraw from the contract");
uint256 totalAmount = totalPot();
require(ERC20(bzzToken).transfer(beneficiary, totalAmount), "failed transfer");
emit PotWithdrawn(beneficiary, totalAmount);
pot = 0;
}
function batchOwner(bytes32 _batchId) public view returns (address) {
return batches[_batchId].owner;
}
function batchDepth(bytes32 _batchId) public view returns (uint8) {
return batches[_batchId].depth;
}
function batchBucketDepth(bytes32 _batchId) public view returns (uint8) {
return batches[_batchId].bucketDepth;
}
function batchImmutableFlag(bytes32 _batchId) public view returns (bool) {
return batches[_batchId].immutableFlag;
}
function batchNormalisedBalance(bytes32 _batchId) public view returns (uint256) {
return batches[_batchId].normalisedBalance;
}
function batchLastUpdatedBlockNumber(bytes32 _batchId) public view returns (uint256) {
return batches[_batchId].lastUpdatedBlockNumber;
}
}