contracts/src/arbitration/KlerosGovernor.sol
// SPDX-License-Identifier: MIT
/// @custom:authors: [@unknownunknown1]
/// @custom:reviewers: []
/// @custom:auditors: []
/// @custom:deployments: []
pragma solidity 0.8.24;
import {IArbitrableV2, IArbitratorV2} from "./interfaces/IArbitrableV2.sol";
import "./interfaces/IDisputeTemplateRegistry.sol";
import "../libraries/CappedMath.sol";
/// @title KlerosGovernor for V2. Note that appeal functionality and evidence submission will be handled by the court.
contract KlerosGovernor is IArbitrableV2 {
using CappedMath for uint256;
// ************************************* //
// * Enums / Structs * //
// ************************************* //
enum Status {
NoDispute,
DisputeCreated,
Resolved
}
struct Session {
uint256 ruling; // The ruling that was given in this session, if any.
uint256 disputeID; // ID given to the dispute of the session, if any.
uint256[] submittedLists; // Tracks all lists that were submitted in a session in the form submittedLists[submissionID].
uint256 sumDeposit; // Sum of all submission deposits in a session (minus arbitration fees). This is used to calculate the reward.
Status status; // Status of a session.
mapping(bytes32 listHash => bool) alreadySubmitted; // Indicates whether or not the transaction list was already submitted in order to catch duplicates in the form alreadySubmitted[listHash].
uint256 durationOffset; // Time in seconds that prolongs the submission period after the first submission, to give other submitters time to react.
}
struct Transaction {
address target; // The address to call.
uint256 value; // Value paid by governor contract that will be used as msg.value in the execution.
bytes data; // Calldata of the transaction.
bool executed; // Whether the transaction was already executed or not.
}
struct Submission {
address payable submitter; // The one who submits the list.
uint256 deposit; // Value of the deposit paid upon submission of the list.
Transaction[] txs; // Transactions stored in the list in the form txs[_transactionIndex].
bytes32 listHash; // A hash chain of all transactions stored in the list. This is used as a unique identifier within a session.
uint256 submissionTime; // The time when the list was submitted.
bool approved; // Whether the list was approved for execution or not.
uint256 approvalTime; // The time when the list was approved.
}
IArbitratorV2 public arbitrator; // Arbitrator contract.
bytes public arbitratorExtraData; // Extra data for arbitrator.
IDisputeTemplateRegistry public templateRegistry; // The dispute template registry.
uint256 public templateId; // The current dispute template identifier.
uint256 public submissionBaseDeposit; // The base deposit in wei that needs to be paid in order to submit the list.
uint256 public submissionTimeout; // Time in seconds allowed for submitting the lists. Once it's passed the contract enters the approval period.
uint256 public executionTimeout; // Time in seconds allowed for the execution of approved lists.
uint256 public withdrawTimeout; // Time in seconds allowed to withdraw a submitted list.
uint256 public lastApprovalTime; // The time of the last approval of a transaction list.
uint256 public reservedETH; // Sum of contract's submission deposits. These funds are not to be used in the execution of transactions.
Submission[] public submissions; // Stores all created transaction lists. submissions[_listID].
Session[] public sessions; // Stores all submitting sessions. sessions[_session].
// ************************************* //
// * Function Modifiers * //
// ************************************* //
modifier duringSubmissionPeriod() {
uint256 offset = sessions[sessions.length - 1].durationOffset;
require(block.timestamp - lastApprovalTime <= submissionTimeout.addCap(offset), "Submission time has ended.");
_;
}
modifier duringApprovalPeriod() {
uint256 offset = sessions[sessions.length - 1].durationOffset;
require(
block.timestamp - lastApprovalTime > submissionTimeout.addCap(offset),
"Approval time not started yet."
);
_;
}
modifier onlyByGovernor() {
require(address(this) == msg.sender, "Only the governor allowed.");
_;
}
// ************************************* //
// * Events * //
// ************************************* //
/// @dev Emitted when a new list is submitted.
/// @param _listID The index of the transaction list in the array of lists.
/// @param _submitter The address that submitted the list.
/// @param _session The number of the current session.
/// @param _description The string in CSV format that contains labels of list's transactions.
/// Note that the submitter may give bad descriptions of correct actions, but this is to be seen as UI enhancement, not a critical feature and that would play against him in case of dispute.
event ListSubmitted(
uint256 indexed _listID,
address indexed _submitter,
uint256 indexed _session,
string _description
);
// ************************************* //
// * Constructor * //
// ************************************* //
/// @dev Constructor.
/// @param _arbitrator The arbitrator of the contract.
/// @param _arbitratorExtraData Extra data for the arbitrator.
/// @param _templateData The dispute template data.
/// @param _templateDataMappings The dispute template data mappings.
/// @param _submissionBaseDeposit The base deposit required for submission.
/// @param _submissionTimeout Time in seconds allocated for submitting transaction list.
/// @param _executionTimeout Time in seconds after approval that allows to execute transactions of the approved list.
/// @param _withdrawTimeout Time in seconds after submission that allows to withdraw submitted list.
constructor(
IArbitratorV2 _arbitrator,
bytes memory _arbitratorExtraData,
string memory _templateData,
string memory _templateDataMappings,
uint256 _submissionBaseDeposit,
uint256 _submissionTimeout,
uint256 _executionTimeout,
uint256 _withdrawTimeout
) {
arbitrator = _arbitrator;
arbitratorExtraData = _arbitratorExtraData;
lastApprovalTime = block.timestamp;
submissionBaseDeposit = _submissionBaseDeposit;
submissionTimeout = _submissionTimeout;
executionTimeout = _executionTimeout;
withdrawTimeout = _withdrawTimeout;
sessions.push();
templateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings);
}
// ************************************* //
// * Governance * //
// ************************************* //
/// @dev Changes the value of the base deposit required for submitting a list.
/// @param _submissionBaseDeposit The new value of the base deposit, in wei.
function changeSubmissionDeposit(uint256 _submissionBaseDeposit) external onlyByGovernor {
submissionBaseDeposit = _submissionBaseDeposit;
}
/// @dev Changes the time allocated for submission. Note that it can't be changed during approval period because there can be an active dispute in the old arbitrator contract
/// and prolonging submission timeout might switch it back to submission period.
/// @param _submissionTimeout The new duration of the submission period, in seconds.
function changeSubmissionTimeout(uint256 _submissionTimeout) external onlyByGovernor duringSubmissionPeriod {
submissionTimeout = _submissionTimeout;
}
/// @dev Changes the time allocated for list's execution.
/// @param _executionTimeout The new duration of the execution timeout, in seconds.
function changeExecutionTimeout(uint256 _executionTimeout) external onlyByGovernor {
executionTimeout = _executionTimeout;
}
/// @dev Changes list withdrawal timeout. Note that withdrawals are only possible in the first half of the submission period.
/// @param _withdrawTimeout The new duration of withdraw period, in seconds.
function changeWithdrawTimeout(uint256 _withdrawTimeout) external onlyByGovernor {
withdrawTimeout = _withdrawTimeout;
}
/// @dev Changes the arbitrator of the contract. Note that it can't be changed during approval period because there can be an active dispute in the old arbitrator contract.
/// @param _arbitrator The new trusted arbitrator.
/// @param _arbitratorExtraData The extra data used by the new arbitrator.
function changeArbitrator(
IArbitratorV2 _arbitrator,
bytes memory _arbitratorExtraData
) external onlyByGovernor duringSubmissionPeriod {
arbitrator = _arbitrator;
arbitratorExtraData = _arbitratorExtraData;
}
/// @dev Update the dispute template data.
/// @param _templateData The new dispute template data.
/// @param _templateDataMappings The new dispute template data mappings.
function changeDisputeTemplate(
string memory _templateData,
string memory _templateDataMappings
) external onlyByGovernor {
templateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings);
}
// ************************************* //
// * State Modifiers * //
// ************************************* //
/// @dev Creates transaction list based on input parameters and submits it for potential approval and execution.
/// Transactions must be ordered by their hash.
/// @param _target List of addresses to call.
/// @param _value List of values required for respective addresses.
/// @param _data Concatenated calldata of all transactions of this list.
/// @param _dataSize List of lengths in bytes required to split calldata for its respective targets.
/// @param _description String in CSV format that describes list's transactions.
function submitList(
address[] memory _target,
uint256[] memory _value,
bytes memory _data,
uint256[] memory _dataSize,
string memory _description
) external payable duringSubmissionPeriod {
require(_target.length == _value.length, "Wrong input: target and value");
require(_target.length == _dataSize.length, "Wrong input: target and datasize");
Session storage session = sessions[sessions.length - 1];
Submission storage submission = submissions.push();
submission.submitter = payable(msg.sender);
// Do the assignment first to avoid creating a new variable and bypass a 'stack too deep' error.
submission.deposit = submissionBaseDeposit + arbitrator.arbitrationCost(arbitratorExtraData);
require(msg.value >= submission.deposit, "Not enough ETH to cover deposit");
// Using an array to get around the stack limit.
// 0 - List hash.
// 1 - Previous transaction hash.
// 2 - Current transaction hash.
bytes32[3] memory hashes;
uint256 readingPosition;
for (uint256 i = 0; i < _target.length; i++) {
bytes memory readData = new bytes(_dataSize[i]);
Transaction storage transaction = submission.txs.push();
transaction.target = _target[i];
transaction.value = _value[i];
for (uint256 j = 0; j < _dataSize[i]; j++) {
readData[j] = _data[readingPosition + j];
}
transaction.data = readData;
readingPosition += _dataSize[i];
hashes[2] = keccak256(abi.encodePacked(transaction.target, transaction.value, transaction.data));
require(uint256(hashes[2]) >= uint256(hashes[1]), "Incorrect tx order");
hashes[0] = keccak256(abi.encodePacked(hashes[2], hashes[0]));
hashes[1] = hashes[2];
}
require(!session.alreadySubmitted[hashes[0]], "List already submitted");
session.alreadySubmitted[hashes[0]] = true;
submission.listHash = hashes[0];
submission.submissionTime = block.timestamp;
session.sumDeposit += submission.deposit;
session.submittedLists.push(submissions.length - 1);
if (session.submittedLists.length == 1) session.durationOffset = block.timestamp.subCap(lastApprovalTime);
emit ListSubmitted(submissions.length - 1, msg.sender, sessions.length - 1, _description);
uint256 remainder = msg.value - submission.deposit;
if (remainder > 0) payable(msg.sender).send(remainder);
reservedETH += submission.deposit;
}
/// @dev Withdraws submitted transaction list. Reimburses submission deposit.
/// Withdrawal is only possible during the first half of the submission period and during withdrawTimeout after the submission is made.
/// @param _submissionID Submission's index in the array of submitted lists of the current sesssion.
/// @param _listHash Hash of a withdrawing list.
function withdrawTransactionList(uint256 _submissionID, bytes32 _listHash) external {
Session storage session = sessions[sessions.length - 1];
Submission storage submission = submissions[session.submittedLists[_submissionID]];
require(block.timestamp - lastApprovalTime <= submissionTimeout / 2, "Should be in first half");
// This require statement is an extra check to prevent _submissionID linking to the wrong list because of index swap during withdrawal.
require(submission.listHash == _listHash, "Wrong list hash");
require(submission.submitter == msg.sender, "Only submitter can withdraw");
require(block.timestamp - submission.submissionTime <= withdrawTimeout, "Withdrawing time has passed.");
session.submittedLists[_submissionID] = session.submittedLists[session.submittedLists.length - 1];
session.alreadySubmitted[_listHash] = false;
session.submittedLists.pop();
session.sumDeposit = session.sumDeposit.subCap(submission.deposit);
payable(msg.sender).transfer(submission.deposit);
reservedETH = reservedETH.subCap(submission.deposit);
}
/// @dev Approves a transaction list or creates a dispute if more than one list was submitted.
/// If nothing was submitted changes session.
function executeSubmissions() external duringApprovalPeriod {
Session storage session = sessions[sessions.length - 1];
require(session.status == Status.NoDispute, "Already disputed");
if (session.submittedLists.length == 0) {
lastApprovalTime = block.timestamp;
session.status = Status.Resolved;
sessions.push();
} else if (session.submittedLists.length == 1) {
Submission storage submission = submissions[session.submittedLists[0]];
submission.approved = true;
submission.approvalTime = block.timestamp;
uint256 sumDeposit = session.sumDeposit;
session.sumDeposit = 0;
submission.submitter.send(sumDeposit);
lastApprovalTime = block.timestamp;
session.status = Status.Resolved;
sessions.push();
reservedETH = reservedETH.subCap(sumDeposit);
} else {
session.status = Status.DisputeCreated;
uint256 arbitrationCost = arbitrator.arbitrationCost(arbitratorExtraData);
session.disputeID = arbitrator.createDispute{value: arbitrationCost}(
session.submittedLists.length,
arbitratorExtraData
);
session.sumDeposit = session.sumDeposit.subCap(arbitrationCost);
reservedETH = reservedETH.subCap(arbitrationCost);
emit DisputeRequest(arbitrator, session.disputeID, sessions.length - 1, templateId, "");
}
}
/// @dev Gives a ruling for a dispute. Must be called by the arbitrator.
/// @param _disputeID ID of the dispute in the Arbitrator contract.
/// @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Refuse to arbitrate".
/// Note If the final ruling is "0" nothing is approved and deposits will stay locked in the contract.
function rule(uint256 _disputeID, uint256 _ruling) external override {
Session storage session = sessions[sessions.length - 1];
require(msg.sender == address(arbitrator), "Only arbitrator allowed");
require(session.status == Status.DisputeCreated, "Wrong status");
require(_ruling <= session.submittedLists.length, "Ruling is out of bounds.");
if (_ruling != 0) {
Submission storage submission = submissions[session.submittedLists[_ruling - 1]];
submission.approved = true;
submission.approvalTime = block.timestamp;
submission.submitter.send(session.sumDeposit);
}
// If the ruling is "0" the reserved funds of this session become expendable.
reservedETH = reservedETH.subCap(session.sumDeposit);
session.sumDeposit = 0;
lastApprovalTime = block.timestamp;
session.status = Status.Resolved;
session.ruling = _ruling;
sessions.push();
emit Ruling(IArbitratorV2(msg.sender), _disputeID, _ruling);
}
/// @dev Executes selected transactions of the list.
/// @param _listID The index of the transaction list in the array of lists.
/// @param _cursor Index of the transaction from which to start executing.
/// @param _count Number of transactions to execute. Executes until the end if set to "0" or number higher than number of transactions in the list.
function executeTransactionList(uint256 _listID, uint256 _cursor, uint256 _count) external {
Submission storage submission = submissions[_listID];
require(submission.approved, "Should be approved");
require(block.timestamp - submission.approvalTime <= executionTimeout, "Time to execute has passed");
for (uint256 i = _cursor; i < submission.txs.length && (_count == 0 || i < _cursor + _count); i++) {
Transaction storage transaction = submission.txs[i];
uint256 expendableFunds = getExpendableFunds();
if (!transaction.executed && transaction.value <= expendableFunds) {
(bool callResult, ) = transaction.target.call{value: transaction.value}(transaction.data);
// An extra check to prevent re-entrancy through target call.
if (callResult == true) {
require(!transaction.executed, "Already executed");
transaction.executed = true;
}
}
}
}
/// @dev Receive function to receive funds for the execution of transactions.
receive() external payable {}
/// @dev Gets the sum of contract funds that are used for the execution of transactions.
/// @return Contract balance without reserved ETH.
function getExpendableFunds() public view returns (uint256) {
return address(this).balance.subCap(reservedETH);
}
/// @dev Gets the info of the specific transaction in the specific list.
/// @param _listID The index of the transaction list in the array of lists.
/// @param _transactionIndex The index of the transaction.
/// @return target The target of the transaction.
/// @return value The value of the transaction.
/// @return data The data of the transaction.
/// @return executed Whether the transaction was executed or not.
function getTransactionInfo(
uint256 _listID,
uint256 _transactionIndex
) external view returns (address target, uint256 value, bytes memory data, bool executed) {
Submission storage submission = submissions[_listID];
Transaction storage transaction = submission.txs[_transactionIndex];
return (transaction.target, transaction.value, transaction.data, transaction.executed);
}
/// @dev Gets the array of submitted lists in the session.
/// Note that this function is O(n), where n is the number of submissions in the session. This could exceed the gas limit, therefore this function should only be used for interface display and not by other contracts.
/// @param _session The ID of the session.
/// @return submittedLists Indexes of lists that were submitted during the session.
function getSubmittedLists(uint256 _session) external view returns (uint256[] memory submittedLists) {
Session storage session = sessions[_session];
submittedLists = session.submittedLists;
}
/// @dev Gets the number of transactions in the list.
/// @param _listID The index of the transaction list in the array of lists.
/// @return txCount The number of transactions in the list.
function getNumberOfTransactions(uint256 _listID) external view returns (uint256 txCount) {
Submission storage submission = submissions[_listID];
return submission.txs.length;
}
/// @dev Gets the number of lists created in contract's lifetime.
/// @return The number of created lists.
function getNumberOfCreatedLists() external view returns (uint256) {
return submissions.length;
}
/// @dev Gets the number of the ongoing session.
/// @return The number of the ongoing session.
function getCurrentSessionNumber() external view returns (uint256) {
return sessions.length - 1;
}
}