From 1247e9fe2592c6665b0dba85bebc3c952bcac4d2 Mon Sep 17 00:00:00 2001 From: Zehui Zheng Date: Wed, 20 Dec 2023 00:16:43 +0800 Subject: [PATCH] feat: add quorum factory and factory tests --- .../rollups/.changeset/stupid-doors-drop.md | 5 ++ .../consensus/quorum/IQuorumFactory.sol | 45 ++++++++++ .../consensus/quorum/QuorumFactory.sol | 50 +++++++++++ .../consensus/quorum/QuorumFactory.t.sol | 89 +++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 onchain/rollups/.changeset/stupid-doors-drop.md create mode 100644 onchain/rollups/contracts/consensus/quorum/IQuorumFactory.sol create mode 100644 onchain/rollups/contracts/consensus/quorum/QuorumFactory.sol create mode 100644 onchain/rollups/test/foundry/consensus/quorum/QuorumFactory.t.sol diff --git a/onchain/rollups/.changeset/stupid-doors-drop.md b/onchain/rollups/.changeset/stupid-doors-drop.md new file mode 100644 index 00000000..fcecb40c --- /dev/null +++ b/onchain/rollups/.changeset/stupid-doors-drop.md @@ -0,0 +1,5 @@ +--- +"@cartesi/rollups": minor +--- + +Added `QuorumFactory` contract and `IQuorumFactory` interface diff --git a/onchain/rollups/contracts/consensus/quorum/IQuorumFactory.sol b/onchain/rollups/contracts/consensus/quorum/IQuorumFactory.sol new file mode 100644 index 00000000..aee21d2a --- /dev/null +++ b/onchain/rollups/contracts/consensus/quorum/IQuorumFactory.sol @@ -0,0 +1,45 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.8; + +import {Quorum} from "./Quorum.sol"; + +/// @title Quorum Factory interface +interface IQuorumFactory { + // Events + + /// @notice A new quorum was deployed. + /// @param quorum The quorum + /// @dev MUST be triggered on a successful call to `newQuorum`. + event QuorumCreated(Quorum quorum); + + // Permissionless functions + + /// @notice Deploy a new quorum. + /// @param validators the list of validators + /// @return The quorum + /// @dev On success, MUST emit a `QuorumCreated` event. + function newQuorum(address[] calldata validators) external returns (Quorum); + + /// @notice Deploy a new quorum deterministically. + /// @param validators the list of validators + /// @param salt The salt used to deterministically generate the quorum address + /// @return The quorum + /// @dev On success, MUST emit a `QuorumCreated` event. + function newQuorum( + address[] calldata validators, + bytes32 salt + ) external returns (Quorum); + + /// @notice Calculate the address of a quorum to be deployed deterministically. + /// @param validators the list of validators + /// @param salt The salt used to deterministically generate the quorum address + /// @return The deterministic quorum address + /// @dev Beware that only the `newQuorum` function with the `salt` parameter + /// is able to deterministically deploy a quorum. + function calculateQuorumAddress( + address[] calldata validators, + bytes32 salt + ) external view returns (address); +} diff --git a/onchain/rollups/contracts/consensus/quorum/QuorumFactory.sol b/onchain/rollups/contracts/consensus/quorum/QuorumFactory.sol new file mode 100644 index 00000000..332d08b1 --- /dev/null +++ b/onchain/rollups/contracts/consensus/quorum/QuorumFactory.sol @@ -0,0 +1,50 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.8; + +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; + +import {IQuorumFactory} from "./IQuorumFactory.sol"; +import {Quorum} from "./Quorum.sol"; + +/// @title Quorum Factory +/// @notice Allows anyone to reliably deploy a new `Quorum` contract. +contract QuorumFactory is IQuorumFactory { + function newQuorum( + address[] calldata validators + ) external override returns (Quorum) { + Quorum quorum = new Quorum(validators); + + emit QuorumCreated(quorum); + + return quorum; + } + + function newQuorum( + address[] calldata validators, + bytes32 salt + ) external override returns (Quorum) { + Quorum quorum = new Quorum{salt: salt}(validators); + + emit QuorumCreated(quorum); + + return quorum; + } + + function calculateQuorumAddress( + address[] calldata validators, + bytes32 salt + ) external view override returns (address) { + return + Create2.computeAddress( + salt, + keccak256( + abi.encodePacked( + type(Quorum).creationCode, + abi.encode(validators) + ) + ) + ); + } +} diff --git a/onchain/rollups/test/foundry/consensus/quorum/QuorumFactory.t.sol b/onchain/rollups/test/foundry/consensus/quorum/QuorumFactory.t.sol new file mode 100644 index 00000000..15667952 --- /dev/null +++ b/onchain/rollups/test/foundry/consensus/quorum/QuorumFactory.t.sol @@ -0,0 +1,89 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +/// @title Quorum Factory Test +pragma solidity ^0.8.8; + +import {QuorumFactory} from "contracts/consensus/quorum/QuorumFactory.sol"; +import {Quorum} from "contracts/consensus/quorum/Quorum.sol"; +import {Vm} from "forge-std/Vm.sol"; + +import {TestBase} from "../../util/TestBase.sol"; + +contract QuorumFactoryTest is TestBase { + QuorumFactory factory; + uint256 internal constant QUORUM_MAX_SIZE = 50; + + event QuorumCreated(Quorum quorum); + + function setUp() public { + factory = new QuorumFactory(); + } + + function testNewQuorum(uint256 seed) public { + uint256 numOfValidators = bound(seed, 1, QUORUM_MAX_SIZE); + address[] memory validators = generateAddresses(numOfValidators); + + vm.recordLogs(); + + Quorum quorum = factory.newQuorum(validators); + + testNewQuorumAux(validators, quorum); + } + + function testNewQuorumAux( + address[] memory validators, + Quorum quorum + ) internal { + Vm.Log[] memory entries = vm.getRecordedLogs(); + + uint256 numQuorumCreated; + for (uint256 i; i < entries.length; ++i) { + Vm.Log memory entry = entries[i]; + + if ( + entry.emitter == address(factory) && + entry.topics[0] == QuorumCreated.selector + ) { + ++numQuorumCreated; + Quorum eventQuorum = abi.decode(entry.data, (Quorum)); + assertEq(address(eventQuorum), address(quorum)); + } + } + assertEq(numQuorumCreated, 1); + + uint256 numOfValidators = validators.length; + assertEq(numOfValidators, quorum.numOfValidators()); + for (uint256 i; i < numOfValidators; ++i) { + assertEq(validators[i], quorum.validatorById(i + 1)); + } + } + + function testNewQuorumDeterministic(uint256 seed, bytes32 salt) public { + uint256 numOfValidators = bound(seed, 1, QUORUM_MAX_SIZE); + address[] memory validators = generateAddresses(numOfValidators); + + address precalculatedAddress = factory.calculateQuorumAddress( + validators, + salt + ); + + vm.recordLogs(); + + Quorum quorum = factory.newQuorum(validators, salt); + + testNewQuorumAux(validators, quorum); + + // Precalculated address must match actual address + assertEq(precalculatedAddress, address(quorum)); + + precalculatedAddress = factory.calculateQuorumAddress(validators, salt); + + // Precalculated address must STILL match actual address + assertEq(precalculatedAddress, address(quorum)); + + // Cannot deploy a quorum with the same salt twice + vm.expectRevert(); + factory.newQuorum(validators, salt); + } +}