Vulnerability Report Attackers Can Prevent `fundAppeal` By Resetting Appeal Period In Kleros External Arbitrator Integration
Hey guys, let's dive into a critical security vulnerability that could seriously disrupt the Kleros dispute resolution process. This issue, categorized as medium severity, allows a malicious actor with enough funds to effectively block the fundAppeal
function, making it impossible to challenge rulings. This article breaks down the vulnerability, how it can be exploited, and its potential impact.
Description of the Vulnerability
The core of the problem lies in the interaction between Kleros and an external arbitrator, specifically how the fundAppeal
function relies on the arbitrator's appealPeriod(...)
values. An attacker can directly call the external arbitrator’s appeal(...)
function, paying the appeal fee, which clears or resets the appeal window. Because fundAppeal
depends solely on the arbitrator’s appealPeriod(...)
values, this action causes fundAppeal
to revert with the error message: "Appeal period is over." This effectively prevents anyone from funding an appeal on the Kleros side, thus making dispute resolution through the integration impossible. This vulnerability stems from the external arbitrator's publicly accessible appeal(...)
function, which can be triggered by anyone, not just the intended parties within the Kleros dispute resolution process. Understanding the mechanics behind this issue is crucial for developers and users alike to grasp the potential risks involved.
The Technical Breakdown
To really get into the nitty-gritty, the fundAppeal
function in the Kleros integration reads the appeal period from the external arbitrator using this code snippet:
(uint256 appealPeriodStart, uint256 appealPeriodEnd) = arbitrator.appealPeriod(disputeID);
require(block.timestamp >= appealPeriodStart && block.timestamp < appealPeriodEnd, "Appeal period is over.");
The external arbitrator implementation includes a public appeal(...)
function. When called, this function sets appealPeriodStart
and appealPeriodEnd
to 0
, effectively closing the appeal window. Since any externally owned account (EOA) can call this function and pay the appeal fee, an attacker can exploit this by resetting the appeal period. Once executed, arbitrator.appealPeriod(disputeID)
returns 0,0
, causing the Kleros fundAppeal
require statement to always fail. This prevents funding via fundAppeal
, giving the attacker the power to block Kleros users from funding appeals, and thereby halting the dispute resolution process. The impact is significant, as it undermines the fundamental ability to contest rulings within the Kleros system. The simplicity of the attack, coupled with its severe consequences, makes this a critical vulnerability to address. This kind of manipulation is like pulling the rug out from under the entire appeal process, leaving legitimate users with no recourse. The code's reliance on these external values without sufficient safeguards is a key factor here. By understanding the code's flow and the external dependencies, we can better appreciate the potential for exploitation.
The Root Causes
Several underlying factors contribute to this vulnerability:
- Assumption mismatch: Kleros
fundAppeal
assumes that the arbitrator’s appeal period is solely controlled by the arbitrator owner viagiveAppealableRuling
and that the appeal window values accurately represent a live window. This assumption is flawed because theappeal
function can be triggered by anyone. - Public
appeal(...)
on arbitrator: The external arbitrator'sappeal
function is public and setsappealPeriodStart = 0
andappealPeriodEnd = 0
when called. This design allows any external caller, including an attacker, to clear the appeal window by paying the fee. - No binding of "appeal initiation" to Kleros contract: The arbitrator accepts
appeal(...)
from any caller, meaning Kleros cannot prevent third parties from directly calling the arbitrator. This lack of control is a crucial element in the vulnerability. - Final Ruling Manipulation: If a losing ruling has been paid and an attack is carried out, the ruling can be manipulated to ensure the ruling favoring the attacker remains unchanged and is finally relayed. The Round ID on Kleros will remain at ID - 0. This makes the attack particularly damaging, as it can cement an unfair outcome.
Attack Scenario
Imagine this: a dispute arises, and a ruling is made that one party wants to appeal. An attacker, with sufficient funds, can call the external arbitrator’s appeal(...)
function directly, paying the required fee. This action resets the appeal window on the arbitrator’s side. Now, when the legitimate party tries to fund the appeal via Kleros’s fundAppeal
function, the transaction fails because the appealPeriod
check fails – the window has been prematurely closed by the attacker. This attack scenario highlights the real-world impact of the vulnerability, illustrating how easily the system can be manipulated. The attacker essentially hijacks the appeal process, leaving the legitimate user without options. This is like a game of chess where an opponent can simply remove your pieces from the board whenever they want.
Proof of Concept (PoC)
To demonstrate the vulnerability, consider the following steps:
- Create a dispute ID on the Kleros contract.
- Appeal directly on the Arbitrator.
- Try appealing on RealitioForeignProxy – all attempts will fail.
This simple PoC clearly shows how the direct appeal on the arbitrator can block the intended appeal process through the RealitioForeignProxy contract. It’s a stark illustration of the proof of concept in action, proving the feasibility and severity of the threat. Seeing this play out step by step helps underscore the vulnerability's immediate and tangible impact.
Revised Code (Suggested Fix)
To mitigate this vulnerability, several measures can be taken. One approach is to modify the appeal(...)
function in the external arbitrator to restrict who can call it. It should only be callable by authorized entities, such as the Kleros contract itself, to ensure that the appeal process is initiated through the proper channels. Here is the vulnerable code snippet from the Arbitrator:
/** @dev Appeal a ruling. Note that it has to be called before the arbitrator contract calls rule.
* @param _disputeID ID of the dispute to be appealed.
* @param _extraData Can be used to give extra info on the appeal.
*/
function appeal(uint256 _disputeID, bytes memory _extraData) public payable override {
Dispute storage dispute = disputes[_disputeID];
uint256 appealFee = appealCost(_disputeID, _extraData);
require(dispute.status == DisputeStatus.Appealable, "The dispute must be appealable.");
require(
block.timestamp < dispute.appealPeriodEnd,
"The appeal must occur before the end of the appeal period."
);
require(msg.value >= appealFee, "Value is less than required appeal fee");
dispute.appealPeriodStart = 0;
dispute.appealPeriodEnd = 0;
dispute.fees += msg.value;
dispute.status = DisputeStatus.Waiting;
emit AppealDecision(_disputeID, IArbitrable(msg.sender));
}
And here is the fundAppeal
function in the Kleros integration where the vulnerability manifests:
/**
* @notice Takes up to the total amount required to fund an answer. Reimburses the rest. Creates an appeal if at least two answers are funded.
* @param _arbitrationID The ID of the arbitration, which is questionID cast into uint256.
* @param _answer One of the possible rulings the arbitrator can give that the funder considers to be the correct answer to the question.
* Note that the answer has Kleros denomination, meaning that it has '+1' offset compared to Realitio format.
* Also note that '0' answer can be funded.
* @return Whether the answer was fully funded or not.
*/
function fundAppeal(uint256 _arbitrationID, uint256 _answer) external payable override returns (bool) {
ArbitrationRequest storage arbitration = arbitrationRequests[_arbitrationID][
arbitrationIDToRequester[_arbitrationID]
];
require(arbitration.status == Status.Created, "No dispute to appeal.");
uint256 disputeID = arbitration.disputeID;
(uint256 appealPeriodStart, uint256 appealPeriodEnd) = arbitrator.appealPeriod(disputeID);
require(block.timestamp >= appealPeriodStart && block.timestamp < appealPeriodEnd, "Appeal period is over.");
uint256 multiplier;
{
uint256 winner = arbitrator.currentRuling(disputeID);
if (winner == _answer) {
multiplier = winnerMultiplier;
} else {
require(
block.timestamp - appealPeriodStart <
((appealPeriodEnd - appealPeriodStart) * (loserAppealPeriodMultiplier)) / MULTIPLIER_DIVISOR,
"Appeal period is over for loser"
);
multiplier = loserMultiplier;
}
}
uint256 lastRoundID = arbitration.rounds.length - 1;
Round storage round = arbitration.rounds[lastRoundID];
require(!round.hasPaid[_answer], "Appeal fee is already paid.");
uint256 appealCost = arbitrator.appealCost(disputeID, arbitratorExtraData);
uint256 totalCost = appealCost + ((appealCost * multiplier) / MULTIPLIER_DIVISOR);
// Take up to the amount necessary to fund the current round at the current costs.
uint256 contribution = totalCost - (round.paidFees[_answer]) > msg.value
? msg.value
: totalCost - (round.paidFees[_answer]);
emit Contribution(_arbitrationID, lastRoundID, _answer, msg.sender, contribution);
round.contributions[msg.sender][_answer] += contribution;
round.paidFees[_answer] += contribution;
if (round.paidFees[_answer] >= totalCost) {
round.feeRewards += round.paidFees[_answer]; // paid increase
round.fundedAnswers.push(_answer); // increase length of fundedAnswers array
round.hasPaid[_answer] = true; // true
emit RulingFunded(_arbitrationID, lastRoundID, _answer);
}
if (round.fundedAnswers.length > 1) {
// At least two sides are fully funded.
arbitration.rounds.push();
round.feeRewards = round.feeRewards - appealCost;
arbitrator.appeal{value: appealCost}(disputeID, arbitratorExtraData);
}
if (msg.value - contribution > 0) payable(msg.sender).safeSend(msg.value - contribution, wNative); // Sending extra value back to contributor.
return round.hasPaid[_answer];
}
By addressing this, the system can ensure the integrity of the appeal process. This revised code approach focuses on preventing unauthorized access to critical functions, bolstering the overall security of the platform. It's like adding extra locks and security cameras to protect your house from intruders. This ensures that the system behaves as intended, maintaining the fairness and reliability of dispute resolution.
Conclusion
In summary, the vulnerability stemming from the publicly accessible appeal(...)
function in the external arbitrator poses a significant threat to the Kleros dispute resolution process. By exploiting this, an attacker can prevent legitimate appeals from being funded, effectively halting the process. Addressing this issue is crucial to maintaining the integrity and reliability of the Kleros system. This conclusion underscores the importance of vigilance and proactive security measures in blockchain development. Just like any complex system, smart contracts require careful scrutiny and continuous improvement to prevent potential exploits. By understanding these vulnerabilities and implementing robust solutions, we can build more secure and trustworthy decentralized platforms.