diff --git a/onchain/rollups/contracts/consensus/quorum/Quorum.sol b/onchain/rollups/contracts/consensus/quorum/Quorum.sol new file mode 100644 index 000000000..409fa2ca6 --- /dev/null +++ b/onchain/rollups/contracts/consensus/quorum/Quorum.sol @@ -0,0 +1,133 @@ +// Copyright Cartesi Pte. Ltd. + +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +pragma solidity ^0.8.8; + +import {AccessControlEnumerable} from "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {AbstractConsensus} from "../AbstractConsensus.sol"; +import {IConsensus} from "../IConsensus.sol"; +import {IHistory} from "../../history/IHistory.sol"; + +/// @title Quorum consensus +/// @notice A consensus model controlled by a small set of addresses, the validators. +/// @dev This contract inherits from `AbstractConsensus` and OpenZeppelin's `AccessControlEnumerable` contract. +/// For more information on `AccessControlEnumerable`, please consult OpenZeppelin's official documentation. +contract Quorum is AbstractConsensus, AccessControlEnumerable { + using EnumerableSet for EnumerableSet.AddressSet; + + /// @notice The validator role. + /// @dev Only validators can submit claims. + bytes32 public constant VALIDATOR_ROLE = keccak256("VALIDATOR_ROLE"); + + /// @notice The history contract. + /// @dev See the `getHistory` function. + IHistory internal immutable history; + + /// @notice For each claim, the set of validators that agree + /// that it should be submitted to the history contract. + mapping(bytes => EnumerableSet.AddressSet) internal yeas; + + /// @notice An ERC-20 token transfer failed (`transfer` returned `false`). + error ERC20TokenTransferFailed(); + + /// @notice Construct a Quorum consensus + /// @param _validators the list of validators + constructor(IHistory _history, address[] memory _validators) { + // Iterate through the array of validators, + // and grant to each the validator role. + for (uint256 i; i < _validators.length; ++i) { + grantRole(VALIDATOR_ROLE, _validators[i]); + } + + // Set history. + history = _history; + } + + /// @notice Get the history contract. + /// @return The history contract + function getHistory() external view returns (IHistory) { + return history; + } + + /// @notice Get a claim from the current history. + /// The encoding of `_proofContext` might vary depending on the + /// implementation of the current history contract. + /// @inheritdoc IConsensus + function getClaim( + address _dapp, + bytes calldata _proofContext + ) external view override returns (bytes32, uint256, uint256) { + return history.getClaim(_dapp, _proofContext); + } + + /// @notice Submits a claim for voting. + /// If this is the claim that reaches the majority, then + /// the claim is submitted to the history contract. + /// The encoding of `_claimData` might vary depending on the + /// implementation of the current history contract. + /// @param _claimData Data for submitting a claim + /// @dev Can only be called by a validator, + /// and the `Quorum` contract must have ownership over + /// its current history contract. + function submitClaim( + bytes calldata _claimData + ) external onlyRole(VALIDATOR_ROLE) { + // Get the set of validators in favour of the claim + EnumerableSet.AddressSet storage claimYeas = yeas[_claimData]; + + // Add the message sender to such set. + claimYeas.add(msg.sender); + + // Get number of validators in favour of the claim. + uint256 numOfVotesInFavour = claimYeas.length(); + + // Get the number of validators in the quorum. + uint256 quorumSize = getRoleMemberCount(VALIDATOR_ROLE); + + // If this claim already has half of the quorum's approval, + // then we can submit it to the history contract. + if (numOfVotesInFavour > quorumSize / 2) { + history.submitClaim(_claimData); + } + } + + /// @notice Equally share all tokens from some ERC-20 contract + /// amongst all validators in the quorum. + /// @param _token The token contract + function shareERC20Tokens(IERC20 _token) external { + // Get the total amount of ERC-20 tokens held by the quorum. + uint256 balance = _token.balanceOf(address(this)); + + // Get the number of validators in the quorum. + uint256 quorumSize = getRoleMemberCount(VALIDATOR_ROLE); + + // Calculate the share of ERC-20 tokens for each validator. + uint256 tokensPerValidator = balance / quorumSize; + + // Iterate through the validator set. + for (uint256 i; i < quorumSize; ++i) { + // Get the i-th validator. + address validator = getRoleMember(VALIDATOR_ROLE, i); + + // Transfer the share of ERC-20 tokens to the i-th validator. + bool success = _token.transfer(validator, tokensPerValidator); + + // If the transfer fails, revert. + if (!success) { + revert ERC20TokenTransferFailed(); + } + } + } +}