Skip to content

Commit

Permalink
Update signing rules contract (#623)
Browse files Browse the repository at this point in the history
  • Loading branch information
salman01zp authored Apr 24, 2024
1 parent 483476e commit 6855ead
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 85 deletions.
85 changes: 85 additions & 0 deletions forge/src/Jobs.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;

/// @dev The Jobs contract's address.
address constant JOBS_ADDRESS = 0x0000000000000000000000000000000000000814;

/// @dev The Jobs contract's instance.
Jobs constant JOBS_CONTRACT = Jobs(JOBS_ADDRESS);

/// @author Webb Inc
/// @title Pallet Jobs Interface
/// @title The interface through which solidity contracts will interact with the jobs pallet
/// @custom:address 0x0000000000000000000000000000000000000814
interface Jobs {

/// Submit a DKG phase one job
/// @custom:selector <selector_hash>
///
/// @notice Submits a job for the first phase of the Distributed Key Generation (DKG) process.
///
/// @param expiry The expiration timestamp for the submitted job.
/// @param ttl The time-to-live for the submitted job.
/// @param participants An array of Ethereum addresses representing participants in the DKG.
/// @param threshold The minimum number of participants required for the DKG to succeed.
/// @param roleType The role type identifier.
/// @param permittedCaller The Ethereum address of the permitted caller initiating the job submission.
/// @param hdWallet A boolean indicating whether the job is for an HD wallet.
///
/// @dev This function initiates the first phase of a DKG process, allowing participants to collaborate
/// in generating cryptographic keys. The submitted job includes information such as the expiration time,
/// the list of participants, the threshold for successful completion, and the permitted caller's address.
/// It is crucial for the caller to ensure that the specified parameters align with the intended DKG process.
///
function submitDkgPhaseOneJob(
uint64 expiry,
uint64 ttl,
address[] memory participants,
uint8 threshold,
uint16 roleType,
address permittedCaller,
bool hdWallet
) external;

/// @custom:selector <selector_hash>
///
/// @notice Submits a job for the second phase of the Distributed Key Generation (DKG) process.
///
/// @param expiry The expiration timestamp for the submitted job.
/// @param ttl The time-to-live for the submitted job.
/// @param phaseOneId The identifier of the corresponding phase one DKG job.
/// @param submission The byte array containing the data submission for the DKG phase two.
/// @param derivationPath The byte array containing the derivation path for the DKG phase two.
///
/// @dev This function initiates the second phase of a Distributed Key Generation process, building upon
/// the results of a prior phase one submission. The submitted job includes an expiration time, the identifier
/// of the phase one DKG job, and the byte array representing the participant's data contribution for phase two.
/// It is important for the caller to ensure that the provided parameters align with the ongoing DKG process.
///
function submitDkgPhaseTwoJob(
uint64 expiry,
uint64 ttl,
uint64 phaseOneId,
bytes memory submission,
bytes memory derivationPath
) external;

/// @custom:selector <selector_hash>
///
/// @notice Sets a new permitted caller for a specific job type identified by the given key and job ID.
///
/// @param roleType An identifier specifying the role type to update the permitted caller for.
/// @param jobId The unique identifier of the job for which the permitted caller is being updated.
/// @param newPermittedCaller The Ethereum address of the new permitted caller.
///
/// @dev This function provides flexibility in managing permitted callers for different job types.
/// The caller can specify the job key, job ID, and the new Ethereum address that will be granted permission
/// to initiate the specified job. It is important for the caller to ensure that the provided parameters
/// align with the ongoing processes and permissions within the contract.
///
function setPermittedCaller(
uint16 roleType,
uint32 jobId,
address newPermittedCaller
) external;
}
44 changes: 25 additions & 19 deletions forge/src/SigningRules.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.13;

import "forge-std/console.sol";
import { JOBS_CONTRACT } from "./Jobs.sol";

enum ProposalStatus {Inactive, Active, Passed, Executed, Cancelled}

Expand All @@ -17,7 +18,8 @@ abstract contract SigningRules {
mapping (bytes32 => address) public admins;
mapping (bytes32 => address[]) public voters;
mapping (bytes32 => uint8) public threshold;
mapping (bytes32 => uint40) public expiry;
mapping (bytes32 => uint64) public expiry;
mapping (bytes32 => uint64) public ttl;
mapping (bytes32 => bool) public useDemocracy;
mapping (bytes32 => bool) public useValidators;

Expand Down Expand Up @@ -47,7 +49,7 @@ abstract contract SigningRules {
_;
}

function calculatePhase1ProposalId(bytes32 phase1JobId, bytes memory phase1JobDetails) public pure returns (bytes32) {
function calculatePhase1ProposalId(uint64 phase1JobId, bytes memory phase1JobDetails) public pure returns (bytes32) {
return keccak256(abi.encodePacked(phase1JobId, phase1JobDetails));
}

Expand All @@ -56,12 +58,13 @@ abstract contract SigningRules {
}

function initialize(
bytes32 phase1JobId,
uint64 phase1JobId,
bytes memory phase1JobDetails,
uint8 _threshold,
bool _useDemocracy,
address[] memory _voters,
uint40 _expiry
uint64 _expiry,
uint64 _ttl
) external {
require(_voters.length <= MAX_VOTERS, "Too many voters");
require(initialized == false, "Already initialized");
Expand All @@ -72,6 +75,7 @@ abstract contract SigningRules {
threshold[phase1ProposalId] = _threshold;
useDemocracy[phase1ProposalId] = _useDemocracy;
expiry[phase1ProposalId] = _expiry;
ttl[phase1ProposalId] = _ttl;
admins[phase1ProposalId] = msg.sender;

// If we have voters, add them to the list.
Expand All @@ -98,7 +102,7 @@ abstract contract SigningRules {
isValidForwarder[proposalId][forwarder] = valid;
}

function submitGovernanceProposal(bytes32 phase1JobId, bytes memory phase1JobDetails, bytes memory phase2JobDetails) public {
function submitGovernanceProposal(uint64 phase1JobId, bytes memory phase1JobDetails, bytes memory phase2JobDetails) public {
// Validate the governance proposal
bytes32 proposalId = keccak256(abi.encodePacked(phase1JobId, phase1JobDetails));
bytes32 phase2JobHash = keccak256(abi.encodePacked(proposalId, phase2JobDetails));
Expand All @@ -108,33 +112,32 @@ abstract contract SigningRules {
_submitToDemocracyPallet(phase1JobId, phase1JobDetails, phase2JobDetails);
}

function voteProposal(bytes32 phase1JobId, bytes memory phase1JobDetails, bytes memory phase2JobDetails) public {
function voteProposal(uint64 phase1JobId, bytes memory phase1JobDetails, bytes memory phase2JobDetails) public {
// Validate the job/details are AUP
require(_isVotableProposal(phase1JobId, phase1JobDetails, phase2JobDetails), "Proposal must be votable");
// Check that we have received enough votes for the anchor update proposal.
// Execute the proposal happens in `_voteProposal` if this vote tips the balance.
bytes32 proposalId = keccak256(abi.encodePacked(phase1JobId, phase1JobDetails));
bytes32 phase2JobHash = keccak256(abi.encodePacked(proposalId, phase2JobDetails));
_voteProposal(proposalId, phase2JobHash);
_voteProposal(phase1JobId, phase1JobDetails, phase2JobDetails );
}

/// --------------------------------------------------------------------------------------- ///
/// ------------------------------------- Internals --------------------------------------- ///
/// --------------------------------------------------------------------------------------- ///

/// @notice When called, {_msgSender()} will be marked as voting in favor of proposal.
/// @param phase1ProposalId ID of the proposal to vote on.
/// @notice Proposal must not have already been passed or executed.
/// @notice {_msgSender()} must not have already voted on proposal.
/// @notice Emits {ProposalEvent} event with status indicating the proposal status.
/// @notice Emits {ProposalVote} event.
function _voteProposal(bytes32 phase1ProposalId, bytes32 phase2JobHash) internal {
function _voteProposal(uint64 phase1JobId, bytes memory phase1JobDetails, bytes memory phase2JobDetails) internal {
bytes32 phase1ProposalId = keccak256(abi.encodePacked(phase1JobId, phase1JobDetails));
bytes32 phase2JobHash = keccak256(abi.encodePacked(phase1ProposalId, phase2JobDetails));
Proposal storage proposal = _proposals[phase2JobHash];
if (proposal._status == ProposalStatus.Passed) {
_executeProposal(phase1ProposalId, phase2JobHash);
_executeProposal(phase1ProposalId, phase1JobId, phase2JobHash, phase2JobDetails );
return;
}

address sender = _msgSender(phase1ProposalId);

require(uint(proposal._status) <= 1, "proposal already executed/cancelled");
Expand Down Expand Up @@ -169,10 +172,10 @@ abstract contract SigningRules {
emit ProposalEvent(ProposalStatus.Passed, phase1ProposalId, phase2JobHash);
}
}
_proposals[phase1ProposalId] = proposal;
_proposals[phase2JobHash] = proposal;

if (proposal._status == ProposalStatus.Passed) {
_executeProposal(phase1ProposalId, phase2JobHash);
_executeProposal(phase1ProposalId, phase1JobId, phase2JobHash, phase2JobDetails );
}
}

Expand All @@ -181,10 +184,13 @@ abstract contract SigningRules {
/// @notice Proposal must have Passed status.
/// @notice Emits {ProposalEvent} event with status {Executed}.
/// @notice Emits {FailedExecution} event with the failed reason.
function _executeProposal(bytes32 phase1ProposalId, bytes32 phase2JobHash) internal {
function _executeProposal(bytes32 phase1ProposalId, uint64 phase1JobId, bytes32 phase2JobHash, bytes memory phase2JobDetails) internal {
Proposal storage proposal = _proposals[phase2JobHash];
require(proposal._status == ProposalStatus.Passed, "Proposal must have Passed status");
proposal._status = ProposalStatus.Executed;

JOBS_CONTRACT.submitDkgPhaseTwoJob(expiry[phase1ProposalId], ttl[phase1ProposalId], phase1JobId, phase2JobDetails, bytes(""));

proposal._status = ProposalStatus.Executed;
emit ProposalEvent(ProposalStatus.Executed, phase1ProposalId, phase2JobHash);
}

Expand Down Expand Up @@ -220,9 +226,9 @@ abstract contract SigningRules {
/// -------------------------------------- Virtuals --------------------------------------- ///
/// --------------------------------------------------------------------------------------- ///

function _isVotableProposal(bytes32 phase1JobId, bytes memory phase1JobDetails, bytes memory phase2JobDetails) internal virtual returns (bool);
function _isVotableProposal(uint64 phase1JobId, bytes memory phase1JobDetails, bytes memory phase2JobDetails) internal virtual returns (bool);
function _refreshVoters(bytes32 proposalId) internal virtual;
function _submitToDemocracyPallet(bytes32 phase1JobId, bytes memory phase1JobDetails, bytes memory phase2JobDetails) internal virtual;
function _submitToDemocracyPallet(uint64 phase1JobId, bytes memory phase1JobDetails, bytes memory phase2JobDetails) internal virtual;

/// --------------------------------------------------------------------------------------- ///
/// -------------------------------------- Helpers ---------------------------------------- ///
Expand Down
50 changes: 30 additions & 20 deletions forge/test/SigningRules.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import "forge-std/console.sol";
import "../src/SigningRules.sol";
import { Proposal, ProposalStatus } from "../src/SigningRules.sol";


contract VotableSigningRules is SigningRules {
function _isVotableProposal(bytes32 phase1JobId, bytes memory phase1JobDetails, bytes memory phase2JobDetails) override pure internal returns (bool) {
require(phase1JobId != 0x0, "Phase 1 job ID must be 0x0");
function _isVotableProposal(uint64 phase1JobId, bytes memory phase1JobDetails, bytes memory phase2JobDetails) override pure internal returns (bool) {
require(phase1JobDetails.length != 0, "Job details must be non-empty");
require(phase2JobDetails.length != 0, "Job details must be non-empty");
return true;
Expand All @@ -18,7 +18,7 @@ contract VotableSigningRules is SigningRules {
// Do nothing
}

function _submitToDemocracyPallet(bytes32 phase1JobId, bytes memory phase1JobDetails, bytes memory phase2JobDetails) override internal {
function _submitToDemocracyPallet(uint64 phase1JobId, bytes memory phase1JobDetails, bytes memory phase2JobDetails) override internal {
// Do nothing
}
}
Expand All @@ -31,14 +31,15 @@ contract SigningRulesTest is Test {
}

function test_setup() public {
bytes32 phase1JobId = "1";
uint64 phase1JobId = 1;
bytes memory phase1JobDetails = "test";
uint8 threshold = 1;
bool useDemocracy = false;
address[] memory voters = new address[](0);
uint40 expiry = 1000;
uint64 expiry = 1000;
uint64 ttl = 1000;
bytes32 proposalId = rules.calculatePhase1ProposalId(phase1JobId, phase1JobDetails);
rules.initialize(phase1JobId, phase1JobDetails, threshold, useDemocracy, voters, expiry);
rules.initialize(phase1JobId, phase1JobDetails, threshold, useDemocracy, voters, expiry, ttl);
assertTrue(rules.initialized());
assertTrue(rules.threshold(proposalId) == threshold);
assertTrue(rules.useDemocracy(proposalId) == useDemocracy);
Expand All @@ -48,7 +49,7 @@ contract SigningRulesTest is Test {
}

function test_submitAndVoteOnProposal() public {
bytes32 phase1JobId = "1";
uint64 phase1JobId = 1;
bytes memory phase1JobDetails = "test";
bytes memory phase2JobDetails = "test";
uint8 threshold = 2;
Expand All @@ -57,23 +58,26 @@ contract SigningRulesTest is Test {
voters[0] = vm.addr(1);
voters[1] = vm.addr(2);
uint40 expiry = 1000;
uint64 ttl = 1000;
bytes32 phase1ProposalId = rules.calculatePhase1ProposalId(phase1JobId, phase1JobDetails);
rules.initialize(phase1JobId, phase1JobDetails, threshold, useDemocracy, voters, expiry);
bytes32 phase2JobHash = rules.calculatePhase2JobHash(phase1ProposalId, phase2JobDetails);

rules.initialize(phase1JobId, phase1JobDetails, threshold, useDemocracy, voters, expiry, ttl);
vm.prank(vm.addr(1));
rules.voteProposal(phase1JobId, phase1JobDetails, phase2JobDetails);
assertTrue(rules.getProposalState(phase1ProposalId) == ProposalStatus.Active);
assertTrue(rules.getProposalState(phase2JobHash) == ProposalStatus.Active);

vm.expectRevert("relayer already voted");
vm.prank(vm.addr(1));
rules.voteProposal(phase1JobId, phase1JobDetails, phase2JobDetails);

vm.prank(vm.addr(2));
rules.voteProposal(phase1JobId, phase1JobDetails, phase2JobDetails);
assertTrue(rules.getProposalState(phase1ProposalId) == ProposalStatus.Passed);
assertTrue(rules.getProposalState(phase2JobHash) == ProposalStatus.Executed);
}

function test_submitAndVote255Participants() public {
bytes32 phase1JobId = "1";
uint64 phase1JobId = 1;
bytes memory phase1JobDetails = "test";
bytes memory phase2JobDetails = "test";
uint8 threshold = 255;
Expand All @@ -83,21 +87,23 @@ contract SigningRulesTest is Test {
voters[i] = vm.addr(i + 1);
}
uint40 expiry = 1000;
uint64 ttl = 1000;
bytes32 phase1ProposalId = rules.calculatePhase1ProposalId(phase1JobId, phase1JobDetails);
rules.initialize(phase1JobId, phase1JobDetails, threshold, useDemocracy, voters, expiry);
bytes32 phase2JobHash = rules.calculatePhase2JobHash(phase1ProposalId, phase2JobDetails);
rules.initialize(phase1JobId, phase1JobDetails, threshold, useDemocracy, voters, expiry, ttl);
for (uint8 i = 0; i < 255; i++) {
vm.prank(vm.addr(i + 1));
rules.voteProposal(phase1JobId, phase1JobDetails, phase2JobDetails);

if (i < 254) {
assertTrue(rules.getProposalState(phase1ProposalId) == ProposalStatus.Active);
assertTrue(rules.getProposalState(phase2JobHash) == ProposalStatus.Active);
}
}
assertTrue(rules.getProposalState(phase1ProposalId) == ProposalStatus.Passed);
assertTrue(rules.getProposalState(phase2JobHash) == ProposalStatus.Executed);
}

function test_submitVoteAndExpireProposal() public {
bytes32 phase1JobId = "1";
uint64 phase1JobId = 1;
bytes memory phase1JobDetails = "test";
bytes memory phase2JobDetails = "test";
uint8 threshold = 2;
Expand All @@ -106,33 +112,37 @@ contract SigningRulesTest is Test {
voters[0] = vm.addr(1);
voters[1] = vm.addr(2);
uint40 expiry = 10;
uint64 ttl = 10;
uint nowBlockNumber = block.number;
bytes32 phase1ProposalId = rules.calculatePhase1ProposalId(phase1JobId, phase1JobDetails);
rules.initialize(phase1JobId, phase1JobDetails, threshold, useDemocracy, voters, expiry);
bytes32 phase2JobHash = rules.calculatePhase2JobHash(phase1ProposalId, phase2JobDetails);

rules.initialize(phase1JobId, phase1JobDetails, threshold, useDemocracy, voters, expiry, ttl);
vm.prank(vm.addr(1));
rules.voteProposal(phase1JobId, phase1JobDetails, phase2JobDetails);
assertTrue(rules.getProposalState(phase1ProposalId) == ProposalStatus.Active);
assertTrue(rules.getProposalState(phase2JobHash) == ProposalStatus.Active);

vm.roll(nowBlockNumber + expiry + 1);
vm.prank(vm.addr(2));
rules.voteProposal(phase1JobId, phase1JobDetails, phase2JobDetails);
assertTrue(rules.getProposalState(phase1ProposalId) == ProposalStatus.Cancelled);
assertTrue(rules.getProposalState(phase2JobHash) == ProposalStatus.Cancelled);
vm.expectRevert("proposal already executed/cancelled");
vm.prank(vm.addr(2));
rules.voteProposal(phase1JobId, phase1JobDetails, phase2JobDetails);
}

function test_adminFunctions() public {
bytes32 phase1JobId = "1";
uint64 phase1JobId = 1;
bytes memory phase1JobDetails = "test";
uint8 threshold = 2;
bool useDemocracy = false;
address[] memory voters = new address[](2);
voters[0] = vm.addr(1);
voters[1] = vm.addr(2);
uint40 expiry = 1000;
uint64 ttl = 1000;
bytes32 phase1ProposalId = rules.calculatePhase1ProposalId(phase1JobId, phase1JobDetails);
rules.initialize(phase1JobId, phase1JobDetails, threshold, useDemocracy, voters, expiry);
rules.initialize(phase1JobId, phase1JobDetails, threshold, useDemocracy, voters, expiry, ttl);

rules.adminSetForwarder(phase1ProposalId, vm.addr(100), true);
assertTrue(rules.isValidForwarder(phase1ProposalId, vm.addr(100)));
Expand Down
Loading

0 comments on commit 6855ead

Please sign in to comment.