Skip to content

Commit b8af085

Browse files
unknownunknown1jaybuidl
authored andcommitted
fix: malicious arbitrable M-01
1 parent a3759ef commit b8af085

File tree

4 files changed

+116
-35
lines changed

4 files changed

+116
-35
lines changed

contracts/src/arbitration/arbitrables/ArbitrableExample.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ contract ArbitrableExample is IArbitrableV2 {
149149
}
150150

151151
/// @inheritdoc IArbitrableV2
152-
function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external override {
152+
function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external virtual override {
153153
uint256 localDisputeID = externalIDtoLocalID[_arbitratorDisputeID];
154154
DisputeStruct storage dispute = disputes[localDisputeID];
155155
if (msg.sender != address(arbitrator)) revert ArbitratorOnly();

contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -475,8 +475,8 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
475475
address payable _beneficiary,
476476
uint256 _choice
477477
) external returns (uint256 amount) {
478-
(, , , bool isRuled, ) = core.disputes(_coreDisputeID);
479-
if (!isRuled) revert DisputeNotResolved();
478+
(, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID);
479+
if (period != KlerosCore.Period.execution) revert DisputeNotResolved();
480480
if (core.paused()) revert CoreIsPaused();
481481
if (!coreDisputeIDToActive[_coreDisputeID].dispute) revert DisputeUnknownInThisDisputeKit();
482482

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {IArbitratorV2, IDisputeTemplateRegistry, IERC20, ArbitrableExample} from "../arbitration/arbitrables/ArbitrableExample.sol";
6+
7+
/// @title MaliciousArbitrableMock
8+
/// A mock contract to check intentional rule() revert.
9+
contract MaliciousArbitrableMock is ArbitrableExample {
10+
bool public doRevert;
11+
12+
function changeBehaviour(bool _doRevert) external {
13+
doRevert = _doRevert;
14+
}
15+
16+
constructor(
17+
IArbitratorV2 _arbitrator,
18+
string memory _templateData,
19+
string memory _templateDataMappings,
20+
bytes memory _arbitratorExtraData,
21+
IDisputeTemplateRegistry _templateRegistry,
22+
IERC20 _weth
23+
)
24+
ArbitrableExample(
25+
_arbitrator,
26+
_templateData,
27+
_templateDataMappings,
28+
_arbitratorExtraData,
29+
_templateRegistry,
30+
_weth
31+
)
32+
{
33+
doRevert = true;
34+
}
35+
36+
function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external override {
37+
if (doRevert) revert RuleReverted();
38+
39+
uint256 localDisputeID = externalIDtoLocalID[_arbitratorDisputeID];
40+
DisputeStruct storage dispute = disputes[localDisputeID];
41+
if (msg.sender != address(arbitrator)) revert ArbitratorOnly();
42+
if (_ruling > dispute.numberOfRulingOptions) revert RulingOutOfBounds();
43+
if (dispute.isRuled) revert DisputeAlreadyRuled();
44+
45+
dispute.isRuled = true;
46+
dispute.ruling = _ruling;
47+
48+
emit Ruling(IArbitratorV2(msg.sender), _arbitratorDisputeID, dispute.ruling);
49+
}
50+
51+
error RuleReverted();
52+
}

contracts/test/foundry/KlerosCore_Execution.t.sol

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCore.sol
99
import {IERC20} from "../../src/libraries/SafeERC20.sol";
1010
import "../../src/libraries/Constants.sol";
1111
import {console} from "forge-std/console.sol";
12+
import {MaliciousArbitrableMock} from "../../src/test/MaliciousArbitrableMock.sol";
1213

1314
/// @title KlerosCore_ExecutionTest
1415
/// @dev Tests for KlerosCore execution, rewards, and ruling finalization
@@ -655,6 +656,48 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase {
655656
assertEq(ruled, true, "Should be ruled");
656657
}
657658

659+
function test_executeRuling_arbitrableRevert() public {
660+
MaliciousArbitrableMock maliciousArbitrable = new MaliciousArbitrableMock(
661+
core,
662+
templateData,
663+
templateDataMappings,
664+
arbitratorExtraData,
665+
registry,
666+
feeToken
667+
);
668+
uint256 disputeID = 0;
669+
670+
vm.prank(staker1);
671+
core.setStake(GENERAL_COURT, 20000);
672+
vm.prank(disputer);
673+
maliciousArbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action");
674+
vm.warp(block.timestamp + minStakingTime);
675+
sortitionModule.passPhase(); // Generating
676+
vm.warp(block.timestamp + rngLookahead);
677+
sortitionModule.passPhase(); // Drawing phase
678+
679+
core.draw(disputeID, DEFAULT_NB_OF_JURORS);
680+
vm.warp(block.timestamp + timesPerPeriod[0]);
681+
core.passPeriod(disputeID); // Vote
682+
683+
uint256[] memory voteIDs = new uint256[](3);
684+
voteIDs[0] = 0;
685+
voteIDs[1] = 1;
686+
voteIDs[2] = 2;
687+
688+
vm.prank(staker1);
689+
disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ");
690+
core.passPeriod(disputeID); // Appeal
691+
692+
vm.warp(block.timestamp + timesPerPeriod[3]);
693+
core.passPeriod(disputeID); // Execution
694+
695+
vm.expectRevert(MaliciousArbitrableMock.RuleReverted.selector);
696+
core.executeRuling(disputeID); // Reverts
697+
698+
disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 2); // Should not revert even if executeRuling() reverted
699+
}
700+
658701
function test_executeRuling_appealSwitch() public {
659702
// Check that the ruling switches if only one side was funded
660703
uint256 disputeID = 0;
@@ -730,12 +773,13 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase {
730773
disputeKit.fundAppeal{value: 0.41 ether}(disputeID, 2); // Underpay a bit to not create an appeal and withdraw the funded sum fully
731774

732775
vm.warp(block.timestamp + timesPerPeriod[3]);
733-
core.passPeriod(disputeID); // Execution
734776

735777
vm.expectRevert(DisputeKitClassicBase.DisputeNotResolved.selector);
736778
disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 1);
737779

738-
core.executeRuling(disputeID);
780+
core.passPeriod(disputeID); // Execution
781+
// executeRuling() should be irrelevant for withdrawals in case malicious arbitrable reverts rule()
782+
//core.executeRuling(disputeID);
739783

740784
vm.prank(owner);
741785
core.pause();
@@ -769,22 +813,15 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase {
769813
// 4. move sortition to staking phase
770814
uint256 disputeID = 0;
771815
uint256 amountToStake = 20000;
772-
_stakePnk_createDispute_moveToDrawingPhase(disputeID, staker1, amountToStake);
816+
_stakePnk_createDispute_moveToDrawingPhase(staker1, amountToStake);
773817

774818
KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0);
775819
uint256 pnkAtStakePerJuror = round.pnkAtStakePerJuror;
776820
_stakeBalanceForJuror(staker1, type(uint256).max);
777821
_drawJurors_advancePeriodToVoting(disputeID);
778822
_vote_execute(disputeID, staker1);
779823
sortitionModule.passPhase(); // set it to staking phase
780-
_assertJurorBalance(
781-
disputeID,
782-
staker1,
783-
amountToStake,
784-
pnkAtStakePerJuror * DEFAULT_NB_OF_JURORS,
785-
amountToStake,
786-
1
787-
);
824+
_assertJurorBalance(staker1, amountToStake, pnkAtStakePerJuror * DEFAULT_NB_OF_JURORS, amountToStake, 1);
788825

789826
console.log("totalStaked before: %e", sortitionModule.totalStaked());
790827

@@ -793,14 +830,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase {
793830

794831
// post condition: inflated totalStaked
795832
console.log("totalStaked after: %e", sortitionModule.totalStaked());
796-
_assertJurorBalance(
797-
disputeID,
798-
staker1,
799-
amountToStake,
800-
pnkAtStakePerJuror * DEFAULT_NB_OF_JURORS,
801-
amountToStake,
802-
1
803-
);
833+
_assertJurorBalance(staker1, amountToStake, pnkAtStakePerJuror * DEFAULT_NB_OF_JURORS, amountToStake, 1);
804834

805835
// new juror tries to stake but totalStaked already reached type(uint256).max
806836
// it reverts with "arithmetic underflow or overflow (0x11)"
@@ -909,19 +939,18 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase {
909939
///////// Internal //////////
910940

911941
function _assertJurorBalance(
912-
uint256 disputeID,
913-
address juror,
914-
uint256 totalStakedPnk,
915-
uint256 totalLocked,
916-
uint256 stakedInCourt,
917-
uint256 nbCourts
918-
) internal {
942+
address _juror,
943+
uint256 _totalStakedPnk,
944+
uint256 _totalLocked,
945+
uint256 _stakedInCourt,
946+
uint256 _nbCourts
947+
) internal view {
919948
(uint256 totalStakedPnk, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule
920-
.getJurorBalance(juror, GENERAL_COURT);
921-
assertEq(totalStakedPnk, totalStakedPnk, "Wrong totalStakedPnk"); // jurors total staked a.k.a juror.stakedPnk
922-
assertEq(totalLocked, totalLocked, "Wrong totalLocked");
923-
assertEq(stakedInCourt, stakedInCourt, "Wrong stakedInCourt"); // juror staked in court a.k.a _stakeOf
924-
assertEq(nbCourts, nbCourts, "Wrong nbCourts");
949+
.getJurorBalance(_juror, GENERAL_COURT);
950+
assertEq(_totalStakedPnk, totalStakedPnk, "Wrong totalStakedPnk"); // jurors total staked a.k.a juror.stakedPnk
951+
assertEq(_totalLocked, totalLocked, "Wrong totalLocked");
952+
assertEq(_stakedInCourt, stakedInCourt, "Wrong stakedInCourt"); // juror staked in court a.k.a _stakeOf
953+
assertEq(_nbCourts, nbCourts, "Wrong nbCourts");
925954
}
926955

927956
function _stakeBalanceForJuror(address juror, uint256 amount) internal {
@@ -930,7 +959,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase {
930959
core.setStake(GENERAL_COURT, amount);
931960
}
932961

933-
function _stakePnk_createDispute_moveToDrawingPhase(uint256 disputeID, address juror, uint256 amount) internal {
962+
function _stakePnk_createDispute_moveToDrawingPhase(address juror, uint256 amount) internal {
934963
vm.prank(juror);
935964
core.setStake(GENERAL_COURT, amount);
936965
vm.prank(disputer);

0 commit comments

Comments
 (0)