-
Notifications
You must be signed in to change notification settings - Fork 10
/
Staking.sol
311 lines (262 loc) · 11.6 KB
/
Staking.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
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
interface IPriceOracle {
function currentPrice() external view returns (uint32);
}
/**
* @title Staking contract for the Swarm storage incentives
* @author The Swarm Authors
* @dev Allows users to stake tokens in order to be eligible for the Redistribution Schelling co-ordination game.
* Stakes are frozen or slashed by the Redistribution contract in response to violations of the
* protocol.
*/
contract StakeRegistry is AccessControl, Pausable {
// ----------------------------- State variables ------------------------------
struct Stake {
// Overlay of the node that is being staked
bytes32 overlay;
// Stake balance expressed through price oracle
uint256 committedStake;
// Stake balance expressed in BZZ
uint256 potentialStake;
// Block height the stake was updated, also used as flag to check if the stake is set
uint256 lastUpdatedBlockNumber;
}
// Associate every stake id with node address data.
mapping(address => Stake) public stakes;
// Role allowed to freeze and slash entries
bytes32 public constant REDISTRIBUTOR_ROLE = keccak256("REDISTRIBUTOR_ROLE");
// Swarm network ID
uint64 NetworkId;
// The miniumum stake allowed to be staked using the Staking contract.
uint64 private constant MIN_STAKE = 100000000000000000;
// Address of the staked ERC20 token
address public immutable bzzToken;
// The address of the linked PriceOracle contract.
IPriceOracle public OracleContract;
// ----------------------------- Events ------------------------------
/**
* @dev Emitted when a stake is created or updated by `owner` of the `overlay` by `committedStake`, and `potentialStake` during `lastUpdatedBlock`.
*/
event StakeUpdated(
address indexed owner,
uint256 committedStake,
uint256 potentialStake,
bytes32 overlay,
uint256 lastUpdatedBlock
);
/**
* @dev Emitted when a stake for address `slashed` is slashed by `amount`.
*/
event StakeSlashed(address slashed, bytes32 overlay, uint256 amount);
/**
* @dev Emitted when a stake for address `frozen` is frozen for `time` blocks.
*/
event StakeFrozen(address frozen, bytes32 overlay, uint256 time);
/**
* @dev Emitted when a address changes overlay it uses
*/
event OverlayChanged(address owner, bytes32 overlay);
/**
* @dev Emitted when a stake for address is withdrawn
*/
event StakeWithdrawn(address node, uint256 amount);
// ----------------------------- Errors ------------------------------
error TransferFailed(); // Used when token transfers fail
error Frozen(); // Used when an action cannot proceed because the overlay is frozen
error Unauthorized(); // Used where only the owner can perform the action
error OnlyRedistributor(); // Used when only the redistributor role is allowed
error OnlyPauser(); // Used when only the pauser role is allowed
error BelowMinimumStake(); // Node participating in game has stake below minimum treshold
// ----------------------------- CONSTRUCTOR ------------------------------
/**
* @param _bzzToken Address of the staked ERC20 token
* @param _NetworkId Swarm network ID
*/
constructor(address _bzzToken, uint64 _NetworkId, address _oracleContract) {
NetworkId = _NetworkId;
bzzToken = _bzzToken;
OracleContract = IPriceOracle(_oracleContract);
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
////////////////////////////////////////
// STATE SETTING //
////////////////////////////////////////
/**
* @notice Create a new stake or update an existing one, change overlay of node
* @dev At least `_initialBalancePerChunk*2^depth` number of tokens need to be preapproved for this contract.
* @param _setNonce Nonce that was used for overlay calculation.
* @param _addAmount Deposited amount of ERC20 tokens, equals to added Potential stake value
*/
function manageStake(bytes32 _setNonce, uint256 _addAmount) external whenNotPaused {
bytes32 _previousOverlay = stakes[msg.sender].overlay;
uint256 _stakingSet = stakes[msg.sender].lastUpdatedBlockNumber;
bytes32 _newOverlay = keccak256(abi.encodePacked(msg.sender, reverse(NetworkId), _setNonce));
uint256 _addCommittedStake = _addAmount / OracleContract.currentPrice(); // losing some decimals from start 10n16 becomes 99999999999984000
// First time adding stake, check the minimum is added
if (_addAmount < MIN_STAKE && _stakingSet == 0) {
revert BelowMinimumStake();
}
if (_stakingSet != 0 && !addressNotFrozen(msg.sender)) revert Frozen();
uint256 updatedCommittedStake = stakes[msg.sender].committedStake + _addCommittedStake;
uint256 updatedPotentialStake = stakes[msg.sender].potentialStake + _addAmount;
stakes[msg.sender] = Stake({
overlay: _newOverlay,
committedStake: updatedCommittedStake,
potentialStake: updatedPotentialStake,
lastUpdatedBlockNumber: block.number
});
// Transfer tokens and emit event that stake has been updated
if (_addAmount > 0) {
if (!ERC20(bzzToken).transferFrom(msg.sender, address(this), _addAmount)) revert TransferFailed();
emit StakeUpdated(msg.sender, updatedCommittedStake, updatedPotentialStake, _newOverlay, block.number);
}
// Emit overlay change event
if (_previousOverlay != _newOverlay) {
emit OverlayChanged(msg.sender, _newOverlay);
}
}
/**
* @dev Withdraw node stake surplus
*/
function withdrawFromStake() external {
uint256 _potentialStake = stakes[msg.sender].potentialStake;
uint256 _surplusStake = _potentialStake -
calculateEffectiveStake(stakes[msg.sender].committedStake, _potentialStake);
if (_surplusStake > 0) {
stakes[msg.sender].potentialStake -= _surplusStake;
if (!ERC20(bzzToken).transfer(msg.sender, _surplusStake)) revert TransferFailed();
emit StakeWithdrawn(msg.sender, _surplusStake);
}
}
/**
* @dev Migrate stake only when the staking contract is paused,
* can only be called by the owner of the stake
*/
function migrateStake() external whenPaused {
// We take out all the stake so user can migrate stake to other contract
if (lastUpdatedBlockNumberOfAddress(msg.sender) != 0) {
if (!ERC20(bzzToken).transfer(msg.sender, stakes[msg.sender].potentialStake)) revert TransferFailed();
delete stakes[msg.sender];
}
}
/**
* @dev Freeze an existing stake, can only be called by the redistributor
* @param _owner the addres selected
* @param _time penalty length in blocknumbers
*/
function freezeDeposit(address _owner, uint256 _time) external {
if (!hasRole(REDISTRIBUTOR_ROLE, msg.sender)) revert OnlyRedistributor();
if (stakes[_owner].lastUpdatedBlockNumber != 0) {
stakes[_owner].lastUpdatedBlockNumber = block.number + _time;
emit StakeFrozen(_owner, stakes[_owner].overlay, _time);
}
}
/**
* @dev Slash an existing stake, can only be called by the `redistributor`
* @param _owner the _owner adress selected
* @param _amount the amount to be slashed
*/
function slashDeposit(address _owner, uint256 _amount) external {
if (!hasRole(REDISTRIBUTOR_ROLE, msg.sender)) revert OnlyRedistributor();
if (stakes[_owner].lastUpdatedBlockNumber != 0) {
if (stakes[_owner].potentialStake > _amount) {
stakes[_owner].potentialStake -= _amount;
stakes[_owner].lastUpdatedBlockNumber = block.number;
} else {
delete stakes[_owner];
}
}
emit StakeSlashed(_owner, stakes[_owner].overlay, _amount);
}
function changeNetworkId(uint64 _NetworkId) external {
if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert Unauthorized();
NetworkId = _NetworkId;
}
/**
* @dev Pause the contract. The contract is provably stopped by renouncing
the pauser role and the admin role after pausing, can only be called by the `PAUSER`
*/
function pause() public {
if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert OnlyPauser();
_pause();
}
/**
* @dev Unpause the contract, can only be called by the pauser when paused
*/
function unPause() public {
if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert OnlyPauser();
_unpause();
}
////////////////////////////////////////
// STATE READING //
////////////////////////////////////////
/**
* @dev Checks to see if `address` is frozen.
* @param _owner owner of staked address
*
* Returns a boolean value indicating whether the operation succeeded.
*/
function addressNotFrozen(address _owner) internal view returns (bool) {
return stakes[_owner].lastUpdatedBlockNumber < block.number;
}
/**
* @dev Returns the current `effectiveStake` of `address`. previously usable stake
* @param _owner _owner of node
*/
function nodeEffectiveStake(address _owner) public view returns (uint256) {
return
addressNotFrozen(_owner)
? calculateEffectiveStake(stakes[_owner].committedStake, stakes[_owner].potentialStake)
: 0;
}
/**
* @dev Check the amount that is possible to withdraw as surplus
*/
function withdrawableStake() public view returns (uint256) {
uint256 _potentialStake = stakes[msg.sender].potentialStake;
return _potentialStake - calculateEffectiveStake(stakes[msg.sender].committedStake, _potentialStake);
}
/**
* @dev Returns the `lastUpdatedBlockNumber` of `address`.
*/
function lastUpdatedBlockNumberOfAddress(address _owner) public view returns (uint256) {
return stakes[_owner].lastUpdatedBlockNumber;
}
/**
* @dev Returns the currently used overlay of the address.
* @param _owner address of node
*/
function overlayOfAddress(address _owner) public view returns (bytes32) {
return stakes[_owner].overlay;
}
function calculateEffectiveStake(
uint256 committedStake,
uint256 potentialStakeBalance
) internal view returns (uint256) {
// Calculate the product of committedStake and unitPrice to get price in BZZ
uint256 committedStakeBzz = committedStake * OracleContract.currentPrice();
// Return the minimum value between committedStakeBzz and potentialStakeBalance
if (committedStakeBzz < potentialStakeBalance) {
return committedStakeBzz;
} else {
return potentialStakeBalance;
}
}
/**
* @dev Please both Endians 🥚.
* @param input Eth address used for overlay calculation.
*/
function reverse(uint64 input) internal pure returns (uint64 v) {
v = input;
// swap bytes
v = ((v & 0xFF00FF00FF00FF00) >> 8) | ((v & 0x00FF00FF00FF00FF) << 8);
// swap 2-byte long pairs
v = ((v & 0xFFFF0000FFFF0000) >> 16) | ((v & 0x0000FFFF0000FFFF) << 16);
// swap 4-byte long pairs
v = (v >> 32) | (v << 32);
}
}