Skip to content

Commit 8a5fbbd

Browse files
committed
feat: do not update StakeController state if PNK transfer fails
1 parent 4c8db66 commit 8a5fbbd

File tree

3 files changed

+115
-176
lines changed

3 files changed

+115
-176
lines changed

contracts/src/arbitration/KlerosCoreXBase.sol

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,28 +1085,28 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable
10851085
if (_newStake != 0 && _newStake < courts[_courtID].minStake) {
10861086
revert StakingLessThanCourtMinStake();
10871087
}
1088-
(uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.setStake(
1088+
(uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.validateStake(
10891089
_account,
10901090
_courtID,
10911091
_newStake
10921092
);
10931093
if (stakingResult == StakingResult.Delayed) {
10941094
return true;
10951095
}
1096+
success = true;
10961097
if (pnkDeposit > 0) {
1097-
try vault.deposit(_account, pnkDeposit) {
1098-
success = true;
1099-
} catch {
1098+
try vault.deposit(_account, pnkDeposit) {} catch {
11001099
success = false;
11011100
}
11021101
}
11031102
if (pnkWithdrawal > 0) {
1104-
try vault.withdraw(_account, pnkWithdrawal) {
1105-
success = true;
1106-
} catch {
1103+
try vault.withdraw(_account, pnkWithdrawal) {} catch {
11071104
success = false;
11081105
}
11091106
}
1107+
if (success) {
1108+
stakeController.setStake(_account, _courtID, _newStake, pnkDeposit, pnkWithdrawal);
1109+
}
11101110
return success;
11111111
}
11121112

contracts/src/arbitration/StakeControllerBase.sol

Lines changed: 82 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -203,12 +203,74 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr
203203
// ************************************* //
204204

205205
/// @inheritdoc IStakeController
206-
function setStake(
206+
function validateStake(
207207
address _account,
208208
uint96 _courtID,
209209
uint256 _newStake
210-
) public override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) {
211-
return _setStake(_account, _courtID, _newStake);
210+
) external view override returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) {
211+
JurorStake storage currentJurorStake = jurorStakes[_account];
212+
uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID];
213+
214+
if (_newStake > currentStakeInCourt) {
215+
pnkDeposit = _newStake - currentStakeInCourt;
216+
} else if (_newStake < currentStakeInCourt) {
217+
pnkWithdrawal = currentStakeInCourt - _newStake;
218+
}
219+
220+
if (phase != Phase.staking) {
221+
return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed);
222+
} else {
223+
if (currentStakeInCourt == 0) {
224+
if (_newStake == 0) revert StakingZeroWhenNoStake();
225+
else if (_newStake > 0 && currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS)
226+
revert StakingInTooManyCourts();
227+
}
228+
return (pnkDeposit, pnkWithdrawal, StakingResult.Successful);
229+
}
230+
}
231+
232+
/// @inheritdoc IStakeController
233+
function setStake(
234+
address _account,
235+
uint96 _courtID,
236+
uint256 _newStake,
237+
uint256 _pnkDeposit,
238+
uint256 _pnkWithdrawal
239+
) public override onlyByCore {
240+
JurorStake storage currentJurorStake = jurorStakes[_account];
241+
uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID];
242+
243+
if (phase != Phase.staking) {
244+
revert NotInStakingPhase();
245+
}
246+
247+
// Update jurorStakes
248+
currentJurorStake.stakes[_courtID] = _newStake;
249+
if (_newStake > currentStakeInCourt) {
250+
currentJurorStake.totalStake += _pnkDeposit;
251+
} else if (_newStake < currentStakeInCourt) {
252+
currentJurorStake.totalStake -= _pnkWithdrawal;
253+
}
254+
255+
// Manage stakedCourtIDs
256+
if (currentStakeInCourt == 0 && _newStake > 0) {
257+
currentJurorStake.stakedCourtIDs.push(_courtID);
258+
} else if (currentStakeInCourt > 0 && _newStake == 0) {
259+
_removeCourt(currentJurorStake.stakedCourtIDs, _courtID);
260+
}
261+
262+
// Update sortition tree
263+
sortition.setStake(_account, _courtID, _newStake);
264+
265+
emit StakeSet(_account, _courtID, _newStake, currentJurorStake.totalStake);
266+
}
267+
268+
/// @inheritdoc IStakeController
269+
function setStakeDelayed(address _account, uint96 _courtID, uint256 _newStake) public override {
270+
DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex];
271+
delayedStake.account = _account;
272+
delayedStake.courtID = _courtID;
273+
delayedStake.stake = _newStake;
212274
}
213275

214276
/// @inheritdoc IStakeController
@@ -235,14 +297,24 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr
235297

236298
/// @inheritdoc IStakeController
237299
function setJurorInactive(address _account) external override onlyByCore returns (uint256 pnkToWithdraw) {
238-
uint96[] storage courtIDsForJuror = jurorStakes[_account].stakedCourtIDs;
239-
while (courtIDsForJuror.length > 0) {
240-
uint96 courtID = courtIDsForJuror[0];
241-
_setStake(_account, courtID, 0);
300+
JurorStake storage currentJurorStake = jurorStakes[_account];
301+
uint96[] storage stakedCourtIDs = currentJurorStake.stakedCourtIDs;
302+
while (stakedCourtIDs.length > 0) {
303+
uint96 courtID = stakedCourtIDs[0];
304+
uint256 currentStakeInCourt = currentJurorStake.stakes[courtID];
305+
if (phase == Phase.staking) {
306+
setStake(_account, courtID, 0, 0, currentStakeInCourt);
307+
} else {
308+
setStakeDelayed(_account, courtID, 0);
309+
}
310+
}
311+
if (phase == Phase.staking) {
312+
pnkToWithdraw = vault.getAvailableBalance(_account);
313+
emit JurorSetInactive(_account, false);
314+
} else {
315+
pnkToWithdraw = 0;
316+
emit JurorSetInactive(_account, true);
242317
}
243-
jurorStakes[_account].totalStake = 0;
244-
pnkToWithdraw = vault.getAvailableBalance(_account);
245-
emit JurorSetInactive(_account);
246318
}
247319

248320
// ************************************* //
@@ -324,64 +396,6 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr
324396
// * Internal * //
325397
// ************************************* //
326398

327-
/// @dev Internal implementation of setStake with phase-aware delayed stake logic
328-
/// @param _account The account to set the stake for.
329-
/// @param _courtID The ID of the court to set the stake for.
330-
/// @param _newStake The new stake.
331-
/// @return pnkDeposit The amount of PNK to deposit.
332-
/// @return pnkWithdrawal The amount of PNK to withdraw.
333-
/// @return stakingResult The result of the staking operation.
334-
function _setStake(
335-
address _account,
336-
uint96 _courtID,
337-
uint256 _newStake
338-
) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) {
339-
JurorStake storage currentJurorStake = jurorStakes[_account];
340-
uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID];
341-
342-
if (phase == Phase.staking) {
343-
if (currentStakeInCourt == 0) {
344-
if (_newStake == 0) revert StakingZeroWhenNoStake();
345-
else if (_newStake > 0 && currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS)
346-
revert StakingInTooManyCourts();
347-
}
348-
349-
currentJurorStake.stakes[_courtID] = _newStake;
350-
351-
if (_newStake > currentStakeInCourt) {
352-
pnkDeposit = _newStake - currentStakeInCourt;
353-
currentJurorStake.totalStake += pnkDeposit;
354-
} else if (_newStake < currentStakeInCourt) {
355-
pnkWithdrawal = currentStakeInCourt - _newStake;
356-
currentJurorStake.totalStake -= pnkWithdrawal;
357-
}
358-
359-
// Manage stakedCourtIDs
360-
if (currentStakeInCourt == 0 && _newStake > 0) {
361-
currentJurorStake.stakedCourtIDs.push(_courtID);
362-
} else if (currentStakeInCourt > 0 && _newStake == 0) {
363-
_removeCourt(currentJurorStake.stakedCourtIDs, _courtID);
364-
}
365-
366-
sortition.setStake(_account, _courtID, _newStake);
367-
368-
emit StakeSet(_account, _courtID, _newStake, currentJurorStake.totalStake);
369-
return (pnkDeposit, pnkWithdrawal, StakingResult.Successful);
370-
}
371-
372-
if (_newStake > currentStakeInCourt) {
373-
pnkDeposit = _newStake - currentStakeInCourt;
374-
} else if (_newStake < currentStakeInCourt) {
375-
pnkWithdrawal = currentStakeInCourt - _newStake;
376-
}
377-
378-
DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex];
379-
delayedStake.account = _account;
380-
delayedStake.courtID = _courtID;
381-
delayedStake.stake = _newStake;
382-
return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed);
383-
}
384-
385399
/// @dev Removes a court from a juror's list of staked courts.
386400
/// @param _stakedCourts Storage pointer to the juror's array of staked court IDs.
387401
/// @param _courtID The ID of the court to remove.
@@ -410,101 +424,6 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr
410424
}
411425
}
412426

413-
// ************************************* //
414-
// * Migration Utilities * //
415-
// ************************************* //
416-
417-
/// @dev Import existing stakes from old sortition module for migration
418-
/// @param _accounts Array of juror accounts
419-
/// @param _courtIDs Array of court IDs
420-
/// @param _stakes Array of stake amounts
421-
function importExistingStakes(
422-
address[] calldata _accounts,
423-
uint96[] calldata _courtIDs,
424-
uint256[] calldata _stakes
425-
) external onlyByGovernor {
426-
if (_accounts.length != _courtIDs.length || _accounts.length != _stakes.length) {
427-
revert InvalidMigrationData();
428-
}
429-
430-
uint256 totalImportedSuccess = 0;
431-
for (uint256 i = 0; i < _accounts.length; i++) {
432-
if (_stakes[i] > 0) {
433-
address account = _accounts[i];
434-
uint96 courtID = _courtIDs[i];
435-
uint256 stakeToImport = _stakes[i];
436-
437-
// Ensure no prior stake exists for this specific account/courtID combination in this contract's state for a clean import.
438-
// This check assumes importExistingStakes is for a fresh population or controlled append.
439-
// If overwriting/updating was intended, this check might differ.
440-
if (jurorStakes[account].stakes[courtID] > 0) {
441-
// Skip or revert, depending on desired import semantics. For now, skip and log.
442-
// emit ImportSkippedDuplicate(account, courtID, stakeToImport);
443-
continue;
444-
}
445-
446-
// _setStake will update local juror stake mappings (jurorStakes) AND call sortition.setStake.
447-
// The pnkDeposit/pnkWithdrawal are calculated but not used by this import function.
448-
(, , StakingResult stakingResult) = _setStake(account, courtID, stakeToImport);
449-
450-
if (stakingResult == StakingResult.Successful) {
451-
totalImportedSuccess++;
452-
emit StakeImported(account, courtID, stakeToImport);
453-
} else {
454-
// Log or handle failed import for a specific entry
455-
// emit StakeImportFailed(account, courtID, stakeToImport, stakingResult);
456-
}
457-
}
458-
}
459-
460-
emit MigrationCompleted(_accounts.length, totalImportedSuccess);
461-
}
462-
463-
/// @dev Import delayed stakes from old system for migration
464-
/// @param _delayedStakes Array of delayed stake data
465-
function importDelayedStakes(DelayedStake[] calldata _delayedStakes) external onlyByGovernor {
466-
for (uint256 i = 0; i < _delayedStakes.length; i++) {
467-
DelayedStake memory delayedStake = _delayedStakes[i];
468-
if (delayedStake.account != address(0)) {
469-
delayedStakeWriteIndex++;
470-
delayedStakes[delayedStakeWriteIndex] = delayedStake;
471-
472-
emit DelayedStakeImported(
473-
delayedStake.account,
474-
delayedStake.courtID,
475-
delayedStake.stake,
476-
delayedStakeWriteIndex
477-
);
478-
}
479-
}
480-
}
481-
482-
/// @dev Migrate phase state from old sortition module
483-
/// @param _phase The phase to set
484-
/// @param _lastPhaseChange The last phase change timestamp
485-
/// @param _disputesWithoutJurors Number of disputes without jurors
486-
function migratePhaseState(
487-
Phase _phase,
488-
uint256 _lastPhaseChange,
489-
uint256 _disputesWithoutJurors
490-
) external onlyByGovernor {
491-
phase = _phase;
492-
lastPhaseChange = _lastPhaseChange;
493-
disputesWithoutJurors = _disputesWithoutJurors;
494-
495-
emit PhaseStateMigrated(_phase, _lastPhaseChange, _disputesWithoutJurors);
496-
}
497-
498-
/// @dev Emergency coordination reset for critical issues
499-
function emergencyCoordinationReset() external onlyByGovernor {
500-
phase = Phase.staking;
501-
lastPhaseChange = block.timestamp;
502-
disputesWithoutJurors = 0;
503-
randomNumber = 0;
504-
505-
emit EmergencyReset(block.timestamp);
506-
}
507-
508427
// ************************************* //
509428
// * Errors * //
510429
// ************************************* //

contracts/src/arbitration/interfaces/IStakeController.sol

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface IStakeController {
2727
event JurorPenaltyExecuted(address indexed _account, uint256 _penalty, uint256 _actualPenalty);
2828
event StakeLocked(address indexed _account, uint256 _amount);
2929
event StakeUnlocked(address indexed _account, uint256 _amount);
30-
event JurorSetInactive(address indexed _account);
30+
event JurorSetInactive(address indexed _account, bool _delayed);
3131

3232
/// @notice Emitted when a juror's stake is set in a court
3333
/// @param _account The address of the juror
@@ -66,18 +66,38 @@ interface IStakeController {
6666
// * Stake Management * //
6767
// ************************************* //
6868

69-
/// @notice Set stake for a juror with vault coordination
69+
/// @notice Validate a stake change for a juror
7070
/// @param _account The juror's account
7171
/// @param _courtID The ID of the court
7272
/// @param _newStake The new stake amount
73-
/// @return pnkDeposit The amount of PNK to deposit
74-
/// @return pnkWithdrawal The amount of PNK to withdraw
73+
/// @return pnkDeposit The amount of PNK validated for deposit
74+
/// @return pnkWithdrawal The amount of PNK validated for withdrawal
7575
/// @return stakingResult The result of the staking operation
76-
function setStake(
76+
function validateStake(
7777
address _account,
7878
uint96 _courtID,
7979
uint256 _newStake
80-
) external returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult);
80+
) external view returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult);
81+
82+
/// @notice Set stake for a juror with vault coordination
83+
/// @param _account The juror's account
84+
/// @param _courtID The ID of the court
85+
/// @param _newStake The new stake amount
86+
/// @param _pnkDeposit The amount of PNK validated for deposit
87+
/// @param _pnkWithdrawal The amount of PNK validated for withdrawal
88+
function setStake(
89+
address _account,
90+
uint96 _courtID,
91+
uint256 _newStake,
92+
uint256 _pnkDeposit,
93+
uint256 _pnkWithdrawal
94+
) external;
95+
96+
/// @notice Set a delayed stake change for a juror to be executed in the next staking phase
97+
/// @param _account The juror's account
98+
/// @param _courtID The ID of the court
99+
/// @param _newStake The new stake amount
100+
function setStakeDelayed(address _account, uint96 _courtID, uint256 _newStake) external;
81101

82102
/// @notice Lock stake for dispute participation
83103
/// @param _account The account to lock stake for

0 commit comments

Comments
 (0)