-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathStaking.sol
213 lines (154 loc) · 8.63 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
// SPDX-License-Identifier: BUSL 1.1
pragma solidity =0.8.22;
import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import "./interfaces/IStaking.sol";
import "../interfaces/ISalt.sol";
import "./StakingRewards.sol";
import "../pools/PoolUtils.sol";
// Staking SALT provides xSALT at a 1:1 ratio.
// Unstaking xSALT to reclaim SALT has a default unstake duration of 52 weeks and a minimum duration of two weeks.
// Expedited unstaking for two weeks allows a default 20% of the SALT to be reclaimed, while unstaking for a full year allows the full 100% to be reclaimed.
contract Staking is IStaking, StakingRewards
{
event SALTStaked(address indexed user, uint256 amountStaked);
event UnstakeInitiated(address indexed user, uint256 indexed unstakeID, uint256 amountUnstaked, uint256 claimableSALT, uint256 numWeeks);
event UnstakeCancelled(address indexed user, uint256 indexed unstakeID);
event SALTRecovered(address indexed user, uint256 indexed unstakeID, uint256 saltRecovered, uint256 expeditedUnstakeFee);
event XSALTTransferredFromAirdrop(address indexed toUser, uint256 amountTransferred);
using SafeERC20 for ISalt;
// The unstakeIDs for each user - including completed and cancelled unstakes.
mapping(address => uint256[]) private _userUnstakeIDs;
// Mapping of unstake IDs to their corresponding Unstake data.
mapping(uint256=>Unstake) private _unstakesByID;
uint256 public nextUnstakeID;
constructor( IExchangeConfig _exchangeConfig, IPoolsConfig _poolsConfig, IStakingConfig _stakingConfig )
StakingRewards( _exchangeConfig, _poolsConfig, _stakingConfig )
{
}
// Stake a given amount of SALT and immediately receive the same amount of xSALT.
// Requires exchange access for the sending wallet.
function stakeSALT( uint256 amountToStake ) external nonReentrant
{
require( exchangeConfig.walletHasAccess(msg.sender), "Sender does not have exchange access" );
// Increase the user's staking share so that they will receive more future SALT rewards.
// No cooldown as it takes default 52 weeks to unstake the xSALT to receive the full amount of staked SALT back.
_increaseUserShare( msg.sender, PoolUtils.STAKED_SALT, amountToStake, false );
// Transfer the SALT from the user's wallet
salt.safeTransferFrom( msg.sender, address(this), amountToStake );
emit SALTStaked(msg.sender, amountToStake);
}
// Unstake a given amount of xSALT over a certain duration.
// Unstaking immediately reduces the user's xSALT balance even though there will be the specified delay to convert it back to SALT
// With a full unstake duration the user receives 100% of their staked amount.
// With expedited unstaking the user receives less.
function unstake( uint256 amountUnstaked, uint256 numWeeks ) external nonReentrant returns (uint256 unstakeID)
{
require( userShareForPool(msg.sender, PoolUtils.STAKED_SALT) >= amountUnstaked, "Cannot unstake more than the amount staked" );
uint256 claimableSALT = calculateUnstake( amountUnstaked, numWeeks );
uint256 completionTime = block.timestamp + numWeeks * ( 1 weeks );
unstakeID = nextUnstakeID++;
Unstake memory u = Unstake( UnstakeState.PENDING, msg.sender, amountUnstaked, claimableSALT, completionTime, unstakeID );
_unstakesByID[unstakeID] = u;
_userUnstakeIDs[msg.sender].push( unstakeID );
// Decrease the user's staking share so that they will receive less future SALT rewards
// This call will send any pending SALT rewards to msg.sender as well.
// Note: _decreaseUserShare checks to make sure that the user has the specified staking share balance.
_decreaseUserShare( msg.sender, PoolUtils.STAKED_SALT, amountUnstaked, false );
emit UnstakeInitiated(msg.sender, unstakeID, amountUnstaked, claimableSALT, numWeeks);
}
// Cancel a pending unstake.
// Caller will be able to use the xSALT again immediately
function cancelUnstake( uint256 unstakeID ) external nonReentrant
{
Unstake storage u = _unstakesByID[unstakeID];
require( u.status == UnstakeState.PENDING, "Only PENDING unstakes can be cancelled" );
require( block.timestamp < u.completionTime, "Unstakes that have already completed cannot be cancelled" );
require( msg.sender == u.wallet, "Sender is not the original staker" );
// Update the user's share of the rewards for staked SALT
_increaseUserShare( msg.sender, PoolUtils.STAKED_SALT, u.unstakedXSALT, false );
u.status = UnstakeState.CANCELLED;
emit UnstakeCancelled(msg.sender, unstakeID);
}
// Recover claimable SALT from a completed unstake
function recoverSALT( uint256 unstakeID ) external nonReentrant
{
Unstake storage u = _unstakesByID[unstakeID];
require( u.status == UnstakeState.PENDING, "Only PENDING unstakes can be claimed" );
require( block.timestamp >= u.completionTime, "Unstake has not completed yet" );
require( msg.sender == u.wallet, "Sender is not the original staker" );
u.status = UnstakeState.CLAIMED;
// See if the user unstaked early and received only a portion of their original stake.
// The portion they did not receive will be considered the expeditedUnstakeFee.
uint256 expeditedUnstakeFee = u.unstakedXSALT - u.claimableSALT;
// Burn 100% of the expeditedUnstakeFee
if ( expeditedUnstakeFee > 0 )
{
// Send the expeditedUnstakeFee to the SALT contract and burn it
salt.safeTransfer( address(salt), expeditedUnstakeFee );
salt.burnTokensInContract();
}
// Send the reclaimed SALT back to the user
salt.safeTransfer( msg.sender, u.claimableSALT );
emit SALTRecovered(msg.sender, unstakeID, u.claimableSALT, expeditedUnstakeFee);
}
// Send xSALT from the Airdrop contract to a user
function transferStakedSaltFromAirdropToUser(address wallet, uint256 amountToTransfer) external
{
require( msg.sender == address(exchangeConfig.airdrop()), "Staking.transferStakedSaltFromAirdropToUser is only callable from the Airdrop contract" );
_decreaseUserShare( msg.sender, PoolUtils.STAKED_SALT, amountToTransfer, false );
_increaseUserShare( wallet, PoolUtils.STAKED_SALT, amountToTransfer, false );
emit XSALTTransferredFromAirdrop(wallet, amountToTransfer);
}
// === VIEWS ===
function userXSalt( address wallet ) external view returns (uint256)
{
return userShareForPool(wallet, PoolUtils.STAKED_SALT);
}
// Retrieve all pending unstakes associated with a user within a specific range.
function unstakesForUser( address user, uint256 start, uint256 end ) public view returns (Unstake[] memory)
{
// Check if start and end are within the bounds of the array
require(end >= start, "Invalid range: end cannot be less than start");
uint256[] memory userUnstakes = _userUnstakeIDs[user];
require(userUnstakes.length > end, "Invalid range: end is out of bounds");
require(start < userUnstakes.length, "Invalid range: start is out of bounds");
Unstake[] memory unstakes = new Unstake[](end - start + 1);
uint256 index;
for(uint256 i = start; i <= end; i++)
unstakes[index++] = _unstakesByID[ userUnstakes[i]];
return unstakes;
}
// Retrieve all pending unstakes associated with a user.
function unstakesForUser( address user ) external view returns (Unstake[] memory)
{
// Check to see how many unstakes the user has
uint256[] memory unstakeIDs = _userUnstakeIDs[user];
if ( unstakeIDs.length == 0 )
return new Unstake[](0);
// Return them all
return unstakesForUser( user, 0, unstakeIDs.length - 1 );
}
// Returns the unstakeIDs for the user
function userUnstakeIDs( address user ) external view returns (uint256[] memory)
{
return _userUnstakeIDs[user];
}
function unstakeByID(uint256 id) external view returns (Unstake memory)
{
return _unstakesByID[id];
}
// Calculate the reclaimable amount of SALT based on the amount of unstaked xSALT and unstake duration
// By default, unstaking for two weeks allows 20% of the SALT to be reclaimed, while unstaking for a full year allows the full 100% to be reclaimed.
function calculateUnstake( uint256 unstakedXSALT, uint256 numWeeks ) public view returns (uint256)
{
uint256 minUnstakeWeeks = stakingConfig.minUnstakeWeeks();
uint256 maxUnstakeWeeks = stakingConfig.maxUnstakeWeeks();
uint256 minUnstakePercent = stakingConfig.minUnstakePercent();
require( numWeeks >= minUnstakeWeeks, "Unstaking duration too short" );
require( numWeeks <= maxUnstakeWeeks, "Unstaking duration too long" );
uint256 percentAboveMinimum = 100 - minUnstakePercent;
uint256 unstakeRange = maxUnstakeWeeks - minUnstakeWeeks;
uint256 numerator = unstakedXSALT * ( minUnstakePercent * unstakeRange + percentAboveMinimum * ( numWeeks - minUnstakeWeeks ) );
return numerator / ( 100 * unstakeRange );
}
}