Skip to content

Commit 7d8e858

Browse files
fix(SortitionModule): delayed stake frontrun bug
1 parent 6b24daa commit 7d8e858

File tree

2 files changed

+52
-83
lines changed

2 files changed

+52
-83
lines changed

contracts/src/arbitration/SortitionModuleBase.sol

+4-4
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,14 @@ abstract contract SortitionModuleBase is ISortitionModule {
206206
DelayedStake storage delayedStake = delayedStakes[i];
207207
// Delayed stake could've been manually removed already. In this case simply move on to the next item.
208208
if (delayedStake.account != address(0)) {
209+
// Nullify the index so the delayed stake won't get deleted before its own execution.
210+
delete latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID];
209211
core.setStakeBySortitionModule(
210212
delayedStake.account,
211213
delayedStake.courtID,
212214
delayedStake.stake,
213215
delayedStake.alreadyTransferred
214216
);
215-
delete latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID];
216217
delete delayedStakes[i];
217218
}
218219
}
@@ -273,9 +274,8 @@ abstract contract SortitionModuleBase is ISortitionModule {
273274
return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); // Forbid staking 0 amount when current stake is 0 to avoid flaky behaviour.
274275
}
275276

277+
pnkWithdrawal = _deleteDelayedStake(_courtID, _account);
276278
if (phase != Phase.staking) {
277-
pnkWithdrawal = _deleteDelayedStake(_courtID, _account);
278-
279279
// Store the stake change as delayed, to be applied when the phase switches back to Staking.
280280
DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex];
281281
delayedStake.account = _account;
@@ -300,7 +300,7 @@ abstract contract SortitionModuleBase is ISortitionModule {
300300
pnkDeposit = _increaseStake(juror, _courtID, _newStake, currentStake);
301301
}
302302
} else {
303-
pnkWithdrawal = _decreaseStake(juror, _courtID, _newStake, currentStake);
303+
pnkWithdrawal += _decreaseStake(juror, _courtID, _newStake, currentStake);
304304
}
305305

306306
// Update the sortition sum tree.

contracts/test/foundry/KlerosCore.t.sol

+48-79
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,54 @@ contract KlerosCoreTest is Test {
12221222
assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2");
12231223
}
12241224

1225+
function test_deleteDelayedStake() public {
1226+
// Check that the delayed stake gets deleted without execution if the juror changed his stake in staking phase before its execution.
1227+
vm.prank(staker1);
1228+
core.setStake(GENERAL_COURT, 1000);
1229+
1230+
vm.prank(disputer);
1231+
arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action");
1232+
vm.warp(block.timestamp + minStakingTime);
1233+
sortitionModule.passPhase(); // Generating
1234+
vm.roll(block.number + rngLookahead + 1);
1235+
sortitionModule.passPhase(); // Drawing phase
1236+
1237+
vm.prank(staker1);
1238+
core.setStake(GENERAL_COURT, 1500); // Create delayed stake
1239+
1240+
(uint256 totalStaked, , uint256 stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT);
1241+
assertEq(totalStaked, 1500, "Wrong amount total staked");
1242+
assertEq(stakedInCourt, 1000, "Wrong amount staked in court");
1243+
assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of the staker1");
1244+
assertEq(pinakion.balanceOf(address(core)), 1500, "Wrong token balance of the core");
1245+
1246+
(address account, uint96 courtID, uint256 stake, bool alreadyTransferred) = sortitionModule.delayedStakes(1);
1247+
assertEq(account, staker1, "Wrong account");
1248+
assertEq(courtID, GENERAL_COURT, "Wrong court id");
1249+
assertEq(stake, 1500, "Wrong amount for delayed stake");
1250+
assertEq(alreadyTransferred, true, "Should be true");
1251+
1252+
vm.warp(block.timestamp + maxDrawingTime);
1253+
sortitionModule.passPhase(); // Staking phase
1254+
1255+
vm.prank(staker1);
1256+
core.setStake(GENERAL_COURT, 1700); // Set stake 2nd time, this time in staking phase to see that the delayed stake will be nullified.
1257+
1258+
(totalStaked, , stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT);
1259+
assertEq(totalStaked, 1700, "Wrong amount total staked");
1260+
assertEq(stakedInCourt, 1700, "Wrong amount staked in court");
1261+
assertEq(pinakion.balanceOf(staker1), 999999999999998300, "Wrong token balance of the staker1");
1262+
assertEq(pinakion.balanceOf(address(core)), 1700, "Wrong token balance of the core");
1263+
1264+
sortitionModule.executeDelayedStakes(1);
1265+
(account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(1);
1266+
// Check that delayed stake is deleted
1267+
assertEq(account, address(0), "Wrong staker account after delayed stake deletion");
1268+
assertEq(courtID, 0, "Court id should be nullified");
1269+
assertEq(stake, 0, "No amount to stake");
1270+
assertEq(alreadyTransferred, false, "Should be false");
1271+
}
1272+
12251273
function test_setStakeBySortitionModule() public {
12261274
// Note that functionality of this function was checked during delayed stakes execution
12271275
vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector);
@@ -2654,83 +2702,4 @@ contract KlerosCoreTest is Test {
26542702
assertEq(crowdfunder2.balance, 10 ether, "Wrong balance of the crowdfunder2");
26552703
assertEq(address(disputeKit).balance, 0, "Wrong balance of the DK");
26562704
}
2657-
2658-
// ***************************** //
2659-
// * Bug * //
2660-
// ***************************** //
2661-
2662-
function test_delayedStakesBug() public {
2663-
// Executes the bug of delayed stake frontrun.
2664-
vm.prank(staker1);
2665-
core.setStake(GENERAL_COURT, 1000);
2666-
2667-
vm.prank(disputer);
2668-
arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action");
2669-
vm.warp(block.timestamp + minStakingTime);
2670-
sortitionModule.passPhase(); // Generating
2671-
vm.roll(block.number + rngLookahead + 1);
2672-
sortitionModule.passPhase(); // Drawing phase
2673-
2674-
vm.prank(staker1);
2675-
core.setStake(GENERAL_COURT, 1500); // Create delayed stake
2676-
2677-
(uint256 totalStaked, , uint256 stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT);
2678-
assertEq(totalStaked, 1500, "Wrong amount total staked");
2679-
assertEq(stakedInCourt, 1000, "Wrong amount staked in court");
2680-
assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of the staker1");
2681-
assertEq(pinakion.balanceOf(address(core)), 1500, "Wrong token balance of the core");
2682-
2683-
(address account, uint96 courtID, uint256 stake, bool alreadyTransferred) = sortitionModule.delayedStakes(1);
2684-
assertEq(account, staker1, "Wrong account");
2685-
assertEq(courtID, GENERAL_COURT, "Wrong court id");
2686-
assertEq(stake, 1500, "Wrong amount for delayed stake");
2687-
assertEq(alreadyTransferred, true, "Should be true");
2688-
2689-
vm.warp(block.timestamp + maxDrawingTime);
2690-
sortitionModule.passPhase(); // Staking phase
2691-
2692-
vm.prank(staker1);
2693-
core.setStake(GENERAL_COURT, 1700); // Update your stake before delayed stake execution and see them clash
2694-
2695-
// Discrepancy happens because the token transfer performed in Drawing phase is not negated in Staking phase
2696-
(totalStaked, , stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT);
2697-
assertEq(totalStaked, 2200, "Wrong amount total staked");
2698-
assertEq(stakedInCourt, 1700, "Wrong amount staked in court");
2699-
assertEq(pinakion.balanceOf(staker1), 999999999999997800, "Wrong token balance of the staker1");
2700-
assertEq(pinakion.balanceOf(address(core)), 2200, "Wrong token balance of the core");
2701-
2702-
sortitionModule.executeDelayedStakes(1);
2703-
(account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(1);
2704-
// Check that delayed stake is executed
2705-
assertEq(account, address(0), "Wrong staker account after delayed stake deletion");
2706-
assertEq(courtID, 0, "Court id should be nullified");
2707-
assertEq(stake, 0, "No amount to stake");
2708-
assertEq(alreadyTransferred, false, "Should be false");
2709-
2710-
// Check balance discrepancy after all the actions
2711-
(totalStaked, , stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT);
2712-
assertEq(totalStaked, 2000, "Wrong amount total staked");
2713-
assertEq(stakedInCourt, 1500, "Wrong amount staked in court");
2714-
assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of the staker1");
2715-
assertEq(pinakion.balanceOf(address(core)), 2000, "Wrong token balance of the core");
2716-
2717-
// Withdraw full stake and see tokens get stuck
2718-
vm.prank(staker1);
2719-
core.setStake(GENERAL_COURT, 0);
2720-
2721-
(totalStaked, , stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT);
2722-
assertEq(totalStaked, 500, "Wrong amount total staked");
2723-
assertEq(stakedInCourt, 0, "Wrong amount staked in court");
2724-
assertEq(pinakion.balanceOf(staker1), 999999999999999500, "Wrong token balance of the staker1");
2725-
assertEq(pinakion.balanceOf(address(core)), 500, "Wrong token balance of the core");
2726-
2727-
// Stake tokens back to see the offset. At this point it doesn't seem possible to get them back
2728-
vm.prank(staker1);
2729-
core.setStake(GENERAL_COURT, 1500);
2730-
(totalStaked, , stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT);
2731-
assertEq(totalStaked, 2000, "Wrong amount total staked");
2732-
assertEq(stakedInCourt, 1500, "Wrong amount staked in court");
2733-
assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of the staker1");
2734-
assertEq(pinakion.balanceOf(address(core)), 2000, "Wrong token balance of the core");
2735-
}
27362705
}

0 commit comments

Comments
 (0)