diff --git a/.husky/pre-commit b/.husky/pre-commit index d827c7000..dd535a8e0 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,6 +2,4 @@ . "$(dirname "$0")/_/husky.sh" yarn lint-staged \ - && yarn depcheck \ - && yarn changelog \ - && git add CHANGELOG.md + && yarn depcheck diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 007f79389..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,30 +0,0 @@ -## 0.1.0 (2021-12-17) - -- refactor: add arbitrator data index getter ([47a1623](https://github.com/kleros/kleros-v2/commit/47a1623)) -- refactor: add evidence contracts ([09a34f3](https://github.com/kleros/kleros-v2/commit/09a34f3)) -- refactor: add interfaces + capped math ([e25b21f](https://github.com/kleros/kleros-v2/commit/e25b21f)) -- refactor: add simple evidence home contract ([1b82f62](https://github.com/kleros/kleros-v2/commit/1b82f62)) -- refactor: fix contract name ([6eb744a](https://github.com/kleros/kleros-v2/commit/6eb744a)) -- refactor: remove foreign evidence interface ([ff8c50c](https://github.com/kleros/kleros-v2/commit/ff8c50c)) -- refactor(sdk): rename ([3241d10](https://github.com/kleros/kleros-v2/commit/3241d10)) -- test: add evidence contract tests ([590d800](https://github.com/kleros/kleros-v2/commit/590d800)) -- test: added a test for IncrementalNG ([65a996b](https://github.com/kleros/kleros-v2/commit/65a996b)) -- test(EvidenceModule): add test file ([9f00f98](https://github.com/kleros/kleros-v2/commit/9f00f98)) -- chore: added GitHub code scanning ([4a70475](https://github.com/kleros/kleros-v2/commit/4a70475)) -- chore: added the hardhat config for layer 2 networks, added hardhat-deploy and mocha ([a12ea0e](https://github.com/kleros/kleros-v2/commit/a12ea0e)) -- chore: gitignore typechain ([b50f777](https://github.com/kleros/kleros-v2/commit/b50f777)) -- chore(typechain): clean generated files ([775ddd0](https://github.com/kleros/kleros-v2/commit/775ddd0)) -- fix: according to evidence standard + comments ([5c95828](https://github.com/kleros/kleros-v2/commit/5c95828)) -- fix: unused code ([26b5dc3](https://github.com/kleros/kleros-v2/commit/26b5dc3)) -- fix(Arbitrator): memory to calldata ([4770b1f](https://github.com/kleros/kleros-v2/commit/4770b1f)) -- fix(EvidenceModule): typos + castings + imports ([789c022](https://github.com/kleros/kleros-v2/commit/789c022)) -- fix(IArbitrator): appeals removed from the standard ([02c20ce](https://github.com/kleros/kleros-v2/commit/02c20ce)) -- fix(IArbitrator): change name to arbitration cost ([0ba4f29](https://github.com/kleros/kleros-v2/commit/0ba4f29)) -- fix(IArbitrator): interface simplification ([e81fb8b](https://github.com/kleros/kleros-v2/commit/e81fb8b)) -- fix(IArbitrator): replaced appealCost with fundingStatus ([f189dd9](https://github.com/kleros/kleros-v2/commit/f189dd9)) -- feat: modern toolchain setup and simple RNG smart contracts ([17f6a76](https://github.com/kleros/kleros-v2/commit/17f6a76)) -- feat(Arbitration): standard update ([ed930de](https://github.com/kleros/kleros-v2/commit/ed930de)) -- docs: initial commit ([23356e7](https://github.com/kleros/kleros-v2/commit/23356e7)) -- docs: license file added ([cb62d2c](https://github.com/kleros/kleros-v2/commit/cb62d2c)) -- docs: readme and spdx headers ([8a5b397](https://github.com/kleros/kleros-v2/commit/8a5b397)) -- docs: updated ([5b9a8f1](https://github.com/kleros/kleros-v2/commit/5b9a8f1)) diff --git a/contracts/src/kleros-v1/IKlerosLiquid.sol b/contracts/src/kleros-v1/IKlerosLiquid.sol new file mode 100644 index 000000000..f55f67c8c --- /dev/null +++ b/contracts/src/kleros-v1/IKlerosLiquid.sol @@ -0,0 +1,89 @@ +pragma solidity ^0.8; + +import "../arbitration/IArbitrator.sol"; + +interface IKlerosLiquid is IArbitrator { + enum Period { + evidence, // Evidence can be submitted. This is also when drawing has to take place. + commit, // Jurors commit a hashed vote. This is skipped for courts without hidden votes. + vote, // Jurors reveal/cast their vote depending on whether the court has hidden votes or not. + appeal, // The dispute can be appealed. + execution // Tokens are redistributed and the ruling is executed. + } + + enum Phase { + staking, // Stake sum trees can be updated. Pass after `minStakingTime` passes and there is at least one dispute without jurors. + generating, // Waiting for a random number. Pass as soon as it is ready. + drawing // Jurors can be drawn. Pass after all disputes have jurors or `maxDrawingTime` passes. + } + + struct Dispute { + // Note that appeal `0` is equivalent to the first round of the dispute. + uint96 subcourtID; // The ID of the subcourt the dispute is in. + address arbitrated; // The arbitrated arbitrable contract. + // The number of choices jurors have when voting. This does not include choice `0` which is reserved for "refuse to arbitrate"/"no ruling". + uint256 numberOfChoices; + Period period; // The current period of the dispute. + uint256 lastPeriodChange; // The last time the period was changed. + uint256 drawsInRound; // A counter of draws made in the current round. + uint256 commitsInRound; // A counter of commits made in the current round. + bool ruled; // True if the ruling has been executed, false otherwise. + } + + struct Juror { + uint256 stakedTokens; // The juror's total amount of tokens staked in subcourts. + uint256 lockedTokens; // The juror's total amount of tokens locked in disputes. + } + + function phase() external view returns (Phase); + + function lockInsolventTransfers() external view returns (bool); + + function minStakingTime() external view returns (uint256); + + function pinakion() external view returns (address); + + function disputes(uint256 _index) external view returns (Dispute memory); + + function jurors(address _account) external view returns (Juror memory); + + function changeSubcourtTimesPerPeriod(uint96 _subcourtID, uint256[4] calldata _timesPerPeriod) external; + + function executeGovernorProposal( + address _destination, + uint256 _amount, + bytes calldata _data + ) external; + + // Getters + function getVote( + uint256 _disputeID, + uint256 _appeal, + uint256 _voteID + ) + external + view + returns ( + address account, + bytes32 commit, + uint256 choice, + bool voted + ); + + function getDispute(uint256 _disputeID) + external + view + returns ( + uint256[] memory votesLengths, + uint256[] memory tokensAtStakePerJuror, + uint256[] memory totalFeesForJurors, + uint256[] memory votesInEachRound, + uint256[] memory repartitionsInEachRound, + uint256[] memory penaltiesInEachRound + ); + + function getSubcourt(uint96 _subcourtID) + external + view + returns (uint256[] memory children, uint256[4] memory timesPerPeriod); +} diff --git a/contracts/src/kleros-v1/ITokenController.sol b/contracts/src/kleros-v1/ITokenController.sol new file mode 100644 index 000000000..cddb9287c --- /dev/null +++ b/contracts/src/kleros-v1/ITokenController.sol @@ -0,0 +1,33 @@ +pragma solidity ^0.8; + +/// @dev The token controller contract must implement these functions. See https://github.com/Giveth/minime/blob/master/contracts/TokenController.sol +interface ITokenController { + /// @notice Called when `_owner` sends ether to the MiniMe Token contract + /// @param _owner The address that sent the ether to create tokens + /// @return True if the ether is accepted, false if it throws + function proxyPayment(address _owner) external payable returns (bool); + + /// @notice Notifies the controller about a token transfer allowing the + /// controller to react if desired + /// @param _from The origin of the transfer + /// @param _to The destination of the transfer + /// @param _amount The amount of the transfer + /// @return False if the controller does not authorize the transfer + function onTransfer( + address _from, + address _to, + uint256 _amount + ) external returns (bool); + + /// @notice Notifies the controller about an approval allowing the + /// controller to react if desired + /// @param _owner The address that calls `approve()` + /// @param _spender The spender in the `approve()` call + /// @param _amount The amount in the `approve()` call + /// @return False if the controller does not authorize the approval + function onApprove( + address _owner, + address _spender, + uint256 _amount + ) external returns (bool); +} diff --git a/contracts/src/kleros-v1/KlerosV1Governor.sol b/contracts/src/kleros-v1/KlerosV1Governor.sol new file mode 100644 index 000000000..484b1971f --- /dev/null +++ b/contracts/src/kleros-v1/KlerosV1Governor.sol @@ -0,0 +1,196 @@ +pragma solidity ^0.8; + +import "./IKlerosLiquid.sol"; +import "./ITokenController.sol"; +import "../arbitration/IArbitrable.sol"; +import "../arbitration/IArbitrator.sol"; + +/** + * @title ERC20 interface + */ +interface IPinakion { + function balanceOf(address who) external view returns (uint256); +} + +contract KlerosV1Governor is IArbitrable, ITokenController { + struct DisputeData { + uint256 klerosLiquidDisputeID; + bool ruled; + } + + IArbitrator public immutable foreignGateway; + IKlerosLiquid public immutable klerosLiquid; + address public governor; + + mapping(uint256 => uint256) public klerosLiquidDisputeIDtoGatewayDisputeID; + mapping(uint256 => DisputeData) public disputes; // disputes[gatewayDisputeID] + mapping(address => uint256) public frozenTokens; // frozenTokens[account] locked token which shouldn't have been blocked. + mapping(uint256 => mapping(uint256 => bool)) public isDisputeNotified; // isDisputeNotified[disputeID][roundID] used to track the notification of frozen tokens. + + modifier onlyByGovernor() { + require(governor == msg.sender); + _; + } + + /** @dev Constructor. Before this contract is made the new governor of KlerosLiquid, the evidence period of all subcourts has to be set to uint(-1). + * @param _klerosLiquid The trusted arbitrator to resolve potential disputes. + * @param _governor The trusted governor of the contract. + * @param _foreignGateway The trusted gateway that acts as an arbitrator, relaying disputes to v2. + */ + constructor( + IKlerosLiquid _klerosLiquid, + address _governor, + IArbitrator _foreignGateway + ) { + klerosLiquid = _klerosLiquid; + governor = _governor; + foreignGateway = _foreignGateway; + } + + /** @dev Lets the governor call anything on behalf of the contract. + * @param _destination The destination of the call. + * @param _amount The value sent with the call. + * @param _data The data sent with the call. + */ + function executeGovernorProposal( + address _destination, + uint256 _amount, + bytes calldata _data + ) external onlyByGovernor { + (bool success, ) = _destination.call{value: _amount}(_data); // solium-disable-line security/no-call-value + require(success, "Call execution failed."); + } + + /** @dev Changes the `governor` storage variable. + * @param _governor The new value for the `governor` storage variable. + */ + function changeGovernor(address _governor) external onlyByGovernor { + governor = _governor; + } + + /** @dev Relays disputes from KlerosLiquid to Kleros v2. Only disputes in the evidence period of the initial round can be realyed. + * @param _disputeID The ID of the dispute as defined in KlerosLiquid. + */ + function relayDispute(uint256 _disputeID) external { + require(klerosLiquidDisputeIDtoGatewayDisputeID[_disputeID] == 0, "Dispute already relayed"); + IKlerosLiquid.Dispute memory KlerosLiquidDispute = klerosLiquid.disputes(_disputeID); + (uint256[] memory votesLengths, , uint256[] memory totalFeesForJurors, , , ) = klerosLiquid.getDispute( + _disputeID + ); + + require(KlerosLiquidDispute.period == IKlerosLiquid.Period.evidence, "Invalid dispute period."); + require(votesLengths.length == 1, "Cannot relay appeals."); + + klerosLiquid.executeGovernorProposal(address(this), totalFeesForJurors[0], ""); + + uint256 minJurors = votesLengths[0]; + bytes memory extraData = abi.encode(KlerosLiquidDispute.subcourtID, minJurors); + uint256 arbitrationCost = foreignGateway.arbitrationCost(extraData); + require(totalFeesForJurors[0] >= arbitrationCost, "Fees not high enough."); // If this doesn't hold at some point, it could be a big issue. + uint256 gatewayDisputeID = foreignGateway.createDispute{value: arbitrationCost}( + KlerosLiquidDispute.numberOfChoices, + extraData + ); + klerosLiquidDisputeIDtoGatewayDisputeID[_disputeID] = gatewayDisputeID; + require(gatewayDisputeID != 0, "ID must be greater than 0."); + + DisputeData storage dispute = disputes[gatewayDisputeID]; + dispute.klerosLiquidDisputeID = _disputeID; + } + + /** @dev Give a ruling for a dispute. Can only be called by the arbitrator. TRUSTED. + * Triggers rule() from KlerosLiquid to the arbitrable contract which created the dispute. + * @param _disputeID ID of the dispute in the arbitrator contract. + * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Refused to arbitrate". + */ + function rule(uint256 _disputeID, uint256 _ruling) public { + require(msg.sender == address(foreignGateway), "Not the arbitrator."); + DisputeData storage dispute = disputes[_disputeID]; + require(dispute.klerosLiquidDisputeID != 0, "Dispute does not exist."); + require(!dispute.ruled, "Dispute already ruled."); + + dispute.ruled = true; + + emit Ruling(foreignGateway, _disputeID, _ruling); + + IKlerosLiquid.Dispute memory klerosLiquidDispute = klerosLiquid.disputes(dispute.klerosLiquidDisputeID); + + bytes4 functionSelector = IArbitrable.rule.selector; + bytes memory data = abi.encodeWithSelector(functionSelector, dispute.klerosLiquidDisputeID, _ruling); + klerosLiquid.executeGovernorProposal(klerosLiquidDispute.arbitrated, 0, data); + } + + /** @dev Registers jurors' tokens which where locked due to relaying a given dispute. These tokens don't count as locked. + * @param _disputeID The ID of the dispute as defined in KlerosLiquid. + */ + function notifyFrozenTokens(uint256 _disputeID) external { + require(klerosLiquidDisputeIDtoGatewayDisputeID[_disputeID] != 0, "Dispute not relayed."); + (uint256[] memory votesLengths, uint256[] memory tokensAtStakePerJuror, , , , ) = klerosLiquid.getDispute( + _disputeID + ); + + uint256 minStakingTime = klerosLiquid.minStakingTime(); + IKlerosLiquid.Phase phase = klerosLiquid.phase(); + bool isDrawingForbidden = phase == IKlerosLiquid.Phase.staking && minStakingTime == type(uint256).max; + + for (uint256 round = 0; round < votesLengths.length; round++) { + if (isDisputeNotified[_disputeID][round]) continue; + + for (uint256 voteID = 0; voteID < votesLengths[round]; voteID++) { + (address account, , , ) = klerosLiquid.getVote(_disputeID, round, voteID); + require(account != address(0x0) || isDrawingForbidden, "Juror not drawn yet."); + if (account != address(0x0)) frozenTokens[account] += tokensAtStakePerJuror[round]; + } + isDisputeNotified[_disputeID][round] = true; + } + } + + /** @dev Called when `_owner` sends ether to the MiniMe Token contract. + * @param _owner The address that sent the ether to create tokens. + * @return allowed Whether the operation should be allowed or not. + */ + function proxyPayment(address _owner) external payable returns (bool allowed) { + allowed = false; + } + + /** @dev Notifies the controller about a token transfer allowing the controller to react if desired. + * @param _from The origin of the transfer. + * @param _to The destination of the transfer. + * @param _amount The amount of the transfer. + * @return allowed Whether the operation should be allowed or not. + */ + function onTransfer( + address _from, + address _to, + uint256 _amount + ) external returns (bool allowed) { + if (klerosLiquid.lockInsolventTransfers()) { + // Never block penalties or rewards. + IPinakion pinakion = IPinakion(klerosLiquid.pinakion()); + uint256 newBalance = pinakion.balanceOf(_from) - _amount; // Overflow already checked in the Minime token contract. + + IKlerosLiquid.Juror memory juror = klerosLiquid.jurors(_from); + + // frozenTokens <= lockedTokens always. + if (newBalance < juror.stakedTokens || newBalance < juror.lockedTokens - frozenTokens[_from]) return false; + } + allowed = true; + } + + /** @dev Notifies the controller about an approval allowing the controller to react if desired. + * @param _owner The address that calls `approve()`. + * @param _spender The spender in the `approve()` call. + * @param _amount The amount in the `approve()` call. + * @return allowed Whether the operation should be allowed or not. + */ + function onApprove( + address _owner, + address _spender, + uint256 _amount + ) external returns (bool allowed) { + allowed = true; + } + + /// @dev This contract should be able to receive arbitration fees from KlerosLiquid. + receive() external payable {} +}