Skip to content

Commit 3fde785

Browse files
committed
feat(contracts): unstaking period choice
1 parent 55b632b commit 3fde785

File tree

2 files changed

+79
-28
lines changed

2 files changed

+79
-28
lines changed

contracts/src/SuccinctStaking.sol

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pragma solidity ^0.8.28;
33

44
import {ProverRegistry} from "./libraries/ProverRegistry.sol";
55
import {StakedSuccinct} from "./tokens/StakedSuccinct.sol";
6-
import {ISuccinctStaking} from "./interfaces/ISuccinctStaking.sol";
6+
import {UnstakePeriod, ISuccinctStaking} from "./interfaces/ISuccinctStaking.sol";
77
import {IIntermediateSuccinct} from "./interfaces/IIntermediateSuccinct.sol";
88
import {IProver} from "./interfaces/IProver.sol";
99
import {SuccinctGovernor} from "./SuccinctGovernor.sol";
@@ -48,8 +48,8 @@ contract SuccinctStaking is
4848
/// @inheritdoc ISuccinctStaking
4949
uint256 public override maxUnstakeRequests;
5050

51-
/// @inheritdoc ISuccinctStaking
52-
uint256 public override unstakePeriod;
51+
/// @dev The old global unstake period.
52+
uint256 private deprecatedUnstakePeriod;
5353

5454
/// @inheritdoc ISuccinctStaking
5555
uint256 public override slashCancellationPeriod;
@@ -81,6 +81,9 @@ contract SuccinctStaking is
8181
/// @dev A mapping from prover to their unstaking escrow pool.
8282
mapping(address => EscrowPool) internal escrowPools;
8383

84+
/// @inheritdoc ISuccinctStaking
85+
mapping(address => UnstakePeriod) public override unstakePeriodChoice;
86+
8487
/*//////////////////////////////////////////////////////////////
8588
MODIFIER
8689
//////////////////////////////////////////////////////////////*/
@@ -110,7 +113,6 @@ contract SuccinctStaking is
110113
address _dispenser,
111114
uint256 _minStakeAmount,
112115
uint256 _maxUnstakeRequests,
113-
uint256 _unstakePeriod,
114116
uint256 _slashCancellationPeriod
115117
) external initializer {
116118
// Ensure that parameters critical for functionality are non-zero.
@@ -121,7 +123,6 @@ contract SuccinctStaking is
121123
_requireNonZeroAddress(_intermediateProve);
122124
_requireNonZeroAddress(_dispenser);
123125
_requireNonZeroNumber(_maxUnstakeRequests);
124-
_requireNonZeroNumber(_unstakePeriod);
125126
_requireNonZeroNumber(_slashCancellationPeriod);
126127

127128
// Setup the initial state.
@@ -132,7 +133,6 @@ contract SuccinctStaking is
132133
dispenser = _dispenser;
133134
minStakeAmount = _minStakeAmount;
134135
maxUnstakeRequests = _maxUnstakeRequests;
135-
unstakePeriod = _unstakePeriod;
136136
slashCancellationPeriod = _slashCancellationPeriod;
137137

138138
// Approve the $iPROVE contract to transfer $PROVE from this contract during stake().
@@ -241,7 +241,7 @@ contract SuccinctStaking is
241241
//////////////////////////////////////////////////////////////*/
242242

243243
/// @inheritdoc ISuccinctStaking
244-
function stake(address _prover, uint256 _PROVE)
244+
function stake(address _prover, uint256 _PROVE, UnstakePeriod _unstakePeriod)
245245
external
246246
override
247247
onlyForProver(_prover)
@@ -250,7 +250,7 @@ contract SuccinctStaking is
250250
// Transfer $PROVE from the staker to this contract.
251251
IERC20(prove).safeTransferFrom(msg.sender, address(this), _PROVE);
252252

253-
return _stake(msg.sender, _prover, _PROVE);
253+
return _stake(msg.sender, _prover, _PROVE, _unstakePeriod);
254254
}
255255

256256
/// @inheritdoc ISuccinctStaking
@@ -261,7 +261,8 @@ contract SuccinctStaking is
261261
uint256 _deadline,
262262
uint8 _v,
263263
bytes32 _r,
264-
bytes32 _s
264+
bytes32 _s,
265+
UnstakePeriod _unstakePeriod
265266
) external override onlyForProver(_prover) returns (uint256) {
266267
// If the $PROVE allowance is not equal to the amount being staked, permit the prover to
267268
// spend the $PROVE from the staker.
@@ -273,7 +274,7 @@ contract SuccinctStaking is
273274
// spender.
274275
IProver(_prover).transferProveToStaking(_from, _PROVE);
275276

276-
return _stake(_from, _prover, _PROVE);
277+
return _stake(_from, _prover, _PROVE, _unstakePeriod);
277278
}
278279

279280
/// @inheritdoc ISuccinctStaking
@@ -335,8 +336,11 @@ contract SuccinctStaking is
335336
// Check that this prover is not in the process of being slashed.
336337
_requireProverWithoutSlashRequests(prover);
337338

339+
// Get the staker's unstake period choice.
340+
UnstakePeriod unstakePeriod = unstakePeriodChoice[_staker];
341+
338342
// Process the available unstake claims.
339-
PROVE += _finishUnstake(_staker, prover, claims);
343+
PROVE += _finishUnstake(_staker, prover, claims, unstakePeriod);
340344

341345
// Reset the slash factor if all $iPROVE has been removed.
342346
EscrowPool storage pool = escrowPools[prover];
@@ -345,11 +349,15 @@ contract SuccinctStaking is
345349
}
346350

347351
// If the staker has no remaining balance and no pending unstakes, remove the staker's
348-
// delegate. This allows them to choose a different prover if they stake again.
352+
// delegate and reset their unstake period choice. This allows them to choose a different
353+
// prover if they stake again.
349354
if (balanceOf(_staker) == 0 && claims.length == 0) {
350355
// Remove the staker's prover delegation.
351356
stakerToProver[_staker] = address(0);
352357

358+
// Remove the staker's unstake period choice.
359+
unstakePeriodChoice[_staker] = UnstakePeriod.UNSPECIFIED;
360+
353361
emit ProverUnbound(_staker, prover);
354362
}
355363
}
@@ -526,7 +534,7 @@ contract SuccinctStaking is
526534

527535
/// @dev Deposit a staker's $PROVE to mint $iPROVE, then deposit $iPROVE to mint $PROVER-N, and
528536
/// then directly mint $stPROVE to the staker, which acts as the receipt token for staking.
529-
function _stake(address _staker, address _prover, uint256 _PROVE)
537+
function _stake(address _staker, address _prover, uint256 _PROVE, UnstakePeriod _unstakePeriod)
530538
internal
531539
stakingOperation
532540
returns (uint256 stPROVE)
@@ -543,6 +551,15 @@ contract SuccinctStaking is
543551
// Check that this prover is not in the process of being slashed.
544552
_requireProverWithoutSlashRequests(_prover);
545553

554+
// If the staker has not set a unstake period choice, set it to their choice. If they have
555+
// already set a choice, ensure it matches their choice.
556+
UnstakePeriod existingUnstakePeriod = unstakePeriodChoice[_staker];
557+
if (existingUnstakePeriod == UnstakePeriod.UNSPECIFIED) {
558+
unstakePeriodChoice[_staker] = _unstakePeriod;
559+
} else if (existingUnstakePeriod != _unstakePeriod) {
560+
revert AlreadySetUnstakePeriod(existingUnstakePeriod);
561+
}
562+
546563
// Ensure the staker is not already staked with a different prover.
547564
address existingProver = stakerToProver[_staker];
548565
if (existingProver != address(0) && existingProver != _prover) {
@@ -553,7 +570,7 @@ contract SuccinctStaking is
553570
if (existingProver == address(0)) {
554571
stakerToProver[_staker] = _prover;
555572

556-
emit ProverBound(_staker, _prover);
573+
emit ProverBound(_staker, _prover, _unstakePeriod);
557574
}
558575

559576
// Deposit $PROVE to mint $iPROVE, sending it to this contract.
@@ -620,13 +637,15 @@ contract SuccinctStaking is
620637

621638
/// @dev Iterate over the unstake claims, processing each one that has passed the unstake
622639
/// period.
623-
function _finishUnstake(address _staker, address _prover, UnstakeClaim[] storage _claims)
624-
internal
625-
returns (uint256 PROVE)
626-
{
640+
function _finishUnstake(
641+
address _staker,
642+
address _prover,
643+
UnstakeClaim[] storage _claims,
644+
UnstakePeriod _unstakePeriod
645+
) internal returns (uint256 PROVE) {
627646
uint256 i = 0;
628647
while (i < _claims.length) {
629-
if (block.timestamp >= _claims[i].timestamp + unstakePeriod) {
648+
if (block.timestamp >= _claims[i].timestamp + _unstakePeriodToSeconds(_unstakePeriod)) {
630649
// Store claim before modifying the array.
631650
UnstakeClaim memory claim = _claims[i];
632651

@@ -678,6 +697,20 @@ contract SuccinctStaking is
678697
if (slashClaimCount[_prover] > 0) revert ProverHasSlashRequest();
679698
}
680699

700+
/// @dev Converts a UnstakePeriod enum to a number of seconds. Defaults to 14 days if the
701+
/// unstake period is unspecified, which is relevant for backward compatibility.
702+
function _unstakePeriodToSeconds(UnstakePeriod _unstakePeriod)
703+
internal
704+
pure
705+
returns (uint256)
706+
{
707+
if (_unstakePeriod == UnstakePeriod.TWENTY_FOUR_MONTHS) return 730 days;
708+
if (_unstakePeriod == UnstakePeriod.TWELVE_MONTHS) return 365 days;
709+
if (_unstakePeriod == UnstakePeriod.SIX_MONTHS) return 183 days;
710+
if (_unstakePeriod == UnstakePeriod.FOURTEEN_DAYS) return 14 days;
711+
return 14 days;
712+
}
713+
681714
/// @dev Authorizes an ERC1967 proxy upgrade to a new implementation contract.
682715
function _authorizeUpgrade(address _newImplementation) internal override onlyOwner {}
683716
}

contracts/src/interfaces/ISuccinctStaking.sol

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@ pragma solidity ^0.8.28;
33

44
import {IProverRegistry} from "./IProverRegistry.sol";
55

6+
/// @dev Represents the period that a staker must wait before finishing unstaking from their
7+
/// prover.
8+
enum UnstakePeriod {
9+
UNSPECIFIED,
10+
FOURTEEN_DAYS,
11+
SIX_MONTHS,
12+
TWELVE_MONTHS,
13+
TWENTY_FOUR_MONTHS
14+
}
15+
616
interface ISuccinctStaking is IProverRegistry {
717
/// @dev Represents a claim for unstaking.
818
/// @param iPROVEEscrow The escrowed amount of $iPROVE at request time.
@@ -38,7 +48,7 @@ interface ISuccinctStaking is IProverRegistry {
3848

3949
/// @dev Emitted when a staker first stakes to their delegated prover. This indicates that any
4050
/// additional stake from the staker can only be added to this prover, unless unbound.
41-
event ProverBound(address indexed staker, address indexed prover);
51+
event ProverBound(address indexed staker, address indexed prover, UnstakePeriod unstakePeriod);
4252

4353
/// @dev Emitted when a staker fully unstakes from their delegated prover. This indicates that
4454
/// the staker can now stake to a different prover.
@@ -105,6 +115,10 @@ interface ISuccinctStaking is IProverRegistry {
105115
/// @dev Thrown if the staker tries to deposit while already staked with a different prover.
106116
error AlreadyStakedWithDifferentProver(address existingProver);
107117

118+
/// @dev Thrown if the staker tries to stake with a different unstake period choice than they
119+
/// have already set.
120+
error AlreadySetUnstakePeriod(UnstakePeriod existingUnstakePeriod);
121+
108122
/// @dev Thrown if staking or unstaking while the prover has one or more pending slash requests.
109123
error ProverHasSlashRequest();
110124

@@ -138,12 +152,6 @@ interface ISuccinctStaking is IProverRegistry {
138152
/// Immutable after deployment.
139153
function maxUnstakeRequests() external view returns (uint256);
140154

141-
/// @notice The minimum delay (in seconds) for an unstake request be finished.
142-
/// @dev Ensures that a staker cannot frontrun an upcoming prover slash by unstaking early.
143-
/// Should be greater than the longest length that a VApp `step` can be delayed by.
144-
/// Immutable after deployment.
145-
function unstakePeriod() external view returns (uint256);
146-
147155
/// @notice The minimum delay (in seconds), plus governance parameters, for a slash request
148156
/// to be cancelled.
149157
/// @dev Cancelling a slash requires `slashCancellationPeriod` + `votingDelay` +
@@ -217,14 +225,22 @@ interface ISuccinctStaking is IProverRegistry {
217225
/// @return The maximum amount of $PROVE available to dispense.
218226
function maxDispense() external view returns (uint256);
219227

228+
/// @notice The unstake period choice for a staker.
229+
/// @param staker The address of the staker.
230+
/// @return The unstake period choice.
231+
function unstakePeriodChoice(address staker) external view returns (UnstakePeriod);
232+
220233
/// @notice Stake $PROVE to a prover. Must have approved $PROVE with this contract as the
221234
/// spender. You may only stake to one prover at a time.
222235
/// @dev Deposits $PROVE into the iPROVE vault to mint $iPROVE, then deposits $iPROVE into the
223236
/// chosen prover to mint $PROVER-N/$stPROVE.
224237
/// @param prover The address of the prover to delegate $iPROVE to.
225238
/// @param PROVE The amount of $PROVE to deposit.
239+
/// @param unstakePeriod The period that the staker must wait before finishing unstaking.
226240
/// @return The amount of $stPROVE received.
227-
function stake(address prover, uint256 PROVE) external returns (uint256);
241+
function stake(address prover, uint256 PROVE, UnstakePeriod unstakePeriod)
242+
external
243+
returns (uint256);
228244

229245
/// @notice Stake $PROVE to a prover. You may only stake to one prover at a time.
230246
/// @dev Deposits $PROVE to mint $iPROVE, then deposits $iPROVE into the chosen
@@ -239,6 +255,7 @@ interface ISuccinctStaking is IProverRegistry {
239255
/// @param v The v component of the permit signature.
240256
/// @param r The r component of the permit signature.
241257
/// @param s The s component of the permit signature.
258+
/// @param unstakePeriod The period that the staker must wait before finishing unstaking.
242259
/// @return The amount of $stPROVE the staker received.
243260
function permitAndStake(
244261
address prover,
@@ -247,7 +264,8 @@ interface ISuccinctStaking is IProverRegistry {
247264
uint256 deadline,
248265
uint8 v,
249266
bytes32 r,
250-
bytes32 s
267+
bytes32 s,
268+
UnstakePeriod unstakePeriod
251269
) external returns (uint256);
252270

253271
/// @notice Creates a request to unstake $stPROVE from the prover for the specified amount. Only

0 commit comments

Comments
 (0)