Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mid election rotation #331

Merged
merged 15 commits into from
Feb 12, 2025
76 changes: 66 additions & 10 deletions src/security-council-mgmt/SecurityCouncilManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ contract SecurityCouncilManager is
event MemberRemoved(address indexed member, Cohort indexed cohort);
event MemberReplaced(address indexed replacedMember, address indexed newMember, Cohort cohort);
event MemberRotated(address indexed replacedAddress, address indexed newAddress, Cohort cohort);
event MemberToBeRotated(address indexed replacedAddress, address indexed newAddress);
event SecurityCouncilAdded(
address indexed securityCouncil,
address indexed updateAction,
Expand Down Expand Up @@ -87,6 +88,10 @@ contract SecurityCouncilManager is
/// @dev This can be used to avoid race conditions between rotation and other actions
mapping(address => address) public rotatedTo;

/// @notice Store the address to be rotated to for new members in the future
/// @dev `rotatingTo[X] = Y` means if X is installed as a new member, Y will be installed instead
mapping(address => address) public rotatingTo;

/// @inheritdoc ISecurityCouncilManager
uint256 public minRotationPeriod;

Expand Down Expand Up @@ -205,8 +210,20 @@ contract SecurityCouncilManager is

// delete the old cohort
_cohort == Cohort.FIRST ? delete firstCohort : delete secondCohort;
address[] storage otherCohort = _cohort == Cohort.FIRST ? secondCohort : firstCohort;

for (uint256 i = 0; i < _newCohort.length; i++) {
// we have to change the array so correct _newCohort can be emitted
address rotatingAddress = rotatingTo[_newCohort[i]];
if (rotatingAddress != address(0)) {
// only replace if there is no clash
if (
!SecurityCouncilMgmtUtils.isInArray(rotatingAddress, _newCohort)
&& !SecurityCouncilMgmtUtils.isInArray(rotatingAddress, otherCohort)
) {
_newCohort[i] = rotatingAddress;
}
}
_addMemberToCohortArray(_newCohort[i], _cohort);
}

Expand Down Expand Up @@ -294,6 +311,37 @@ contract SecurityCouncilManager is
);
}

function _verifyNewAddress(address newMemberAddress, bytes calldata signature)
internal
returns (address)
{
// we enforce that a the new address is an eoa in the same way do
// in NomineeGovernor.addContender by requiring a signature
// TODO: this updateNonce is global and only updated when an update is scheduled
// permissionless `rotateForFutureMember` will not update the nonce
// permissioned `rotateMember` will allow other member to dos rotation
bytes32 digest = getRotateMemberHash(msg.sender, updateNonce);
address newAddress = ECDSAUpgradeable.recover(digest, signature);
// we safety check the new member address is the one that we expect to replace here
// this isn't strictly necessary but it guards agains the case where the wrong sig is accidentally used
if (newAddress != newMemberAddress) {
revert InvalidNewAddress(newAddress);
}
return newAddress;
}

function _checkNotRotatingSrcOrTarget(address newAddress) internal view {
address rotatingTarget = rotatingTo[newAddress];
if (rotatingTarget != address(0)) {
// if newAddress is a rotating target, it might cause a clash when new members are elected
if (rotatingTarget == newAddress) {
revert NewMemberIsRotatingTarget(newAddress);
}
// if newAddress is rotating, it likely make no sense to rotate into it now
revert NewMemberIsRotating(newAddress);
}
}

/// @inheritdoc ISecurityCouncilManager
function rotateMember(
address newMemberAddress,
Expand All @@ -305,16 +353,7 @@ contract SecurityCouncilManager is
{
revert RotationTooSoon(msg.sender, lastRotatedTimestamp + minRotationPeriod);
}

// we enforce that a the new address is an eoa in the same way do
// in NomineeGovernor.addContender by requiring a signature
bytes32 digest = getRotateMemberHash(msg.sender, updateNonce);
address newAddress = ECDSAUpgradeable.recover(digest, signature);
// we safety check the new member address is the one that we expect to replace here
// this isn't strictly necessary but it guards agains the case where the wrong sig is accidentally used
if (newAddress != newMemberAddress) {
revert InvalidNewAddress(newAddress);
}
address newAddress = _verifyNewAddress(newMemberAddress, signature);

// the cohort replacer should be the member election governor
// we don't explicitly store the member election governor in this manager
Expand Down Expand Up @@ -370,12 +409,29 @@ contract SecurityCouncilManager is
}
}

_checkNotRotatingSrcOrTarget(newAddress);

lastRotated[newAddress] = block.timestamp;
rotatedTo[msg.sender] = newAddress;
Cohort cohort = _swapMembers(msg.sender, newAddress);
emit MemberRotated({replacedAddress: msg.sender, newAddress: newAddress, cohort: cohort});
}

/// @inheritdoc ISecurityCouncilManager
function rotateForFutureMember(address newMemberAddress, bytes calldata signature) external {
// we don't have to check timestamp here, because dos is not possible
address newAddress = _verifyNewAddress(newMemberAddress, signature);

rotatingTo[msg.sender] = newAddress;

// this serves 2 purposes:
// 1. it prevents "chained" rotations
// 2. it allow one to check rotatingTo[x] to see if x can be a rotation target
_checkNotRotatingSrcOrTarget(newAddress);
rotatingTo[newAddress] = newAddress;
Copy link
Collaborator Author

@gzeoneth gzeoneth Feb 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tbh we might as well drop these check and relies on the psudo try-catch in replaceCohort entirely; say if somehow all elected member decided to set rotateForFutureMember to the same address, it simply would became a no-op. What's important is you cannot dos other user who actually want to rotate, but the signature check should suffice.

emit MemberToBeRotated({replacedAddress: msg.sender, newAddress: newAddress});
}

function _swapMembers(address _addressToRemove, address _addressToAdd)
internal
returns (Cohort)
Expand Down
12 changes: 12 additions & 0 deletions src/security-council-mgmt/interfaces/ISecurityCouncilManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,13 @@ interface ISecurityCouncilManager {
error GovernorNotReplacer();
error NewMemberIsContender(uint256 proposalId, address newMember);
error NewMemberIsNominee(uint256 proposalId, address newMember);
error NewMemberIsRotating(address newMember);
error NewMemberIsRotatingTarget(address newMember);
error InvalidNewAddress(address newAddress);

function rotatedTo(address) external view returns (address);
function rotatingTo(address) external view returns (address);

/// @notice There is a minimum period between when an address can be rotated
/// This is to ensure a single member cannot do many rotations in a row
function minRotationPeriod() external view returns (uint256);
Expand Down Expand Up @@ -127,6 +132,13 @@ interface ISecurityCouncilManager {
address memberElectionGovernor,
bytes calldata signature
) external;
/// @notice Allow rotation to another address when the sender becomes a member of the Security Council in the future through election
/// @dev Cannot rotate to a contender in an ongoing election, as this could cause a clash that would stop the election result executing
/// If this future rotation causes a clash, the rotation will not be executed and the original address will be installed
/// This rotation only applies to future replaceCohort, mainly used by the member election governor
/// @param newMemberAddress The new member address to be rotated to
/// @param signature A signature from the new member address over the 712 addMember hash
function rotateForFutureMember(address newMemberAddress, bytes calldata signature) external;
/// @notice Is the account a member of the first cohort
function firstCohortIncludes(address account) external view returns (bool);
/// @notice Is the account a member of the second cohort
Expand Down
Loading