MultiSig Timelock

First Flight #55
Beginner FriendlyWallet
100 EXP
Submission Details
Impact: low
Likelihood: high

Admin can remove themselves the signer role allowing up to 5 other signers

Author Revealed upon completion

revokeSigningRole() does not check if _account is the owner + the owner's signer role can be revoked allowing 5 other signers

Description

  • revokeSigningRole() should check if _account is the owner to keep the "owner + up to 4 others that possess the SIGNING_ROLE" invariant.

  • Tthe owner's signer role can be revoked allowing 5 other signers.

function revokeSigningRole(address _account) external nonReentrant onlyOwner noneZeroAddress(_account) {
@> // no checks for whether _account is the contract onner
// CHECKS
if (!s_isSigner[_account]) {
revert MultiSigTimelock__AccountIsNotASigner();
}
// Prevent revoking the first signer (would break the multisig), moreover, the first signer is the owner of the contract(wallet)
if (s_signerCount <= 1) {
revert MultiSigTimelock__CannotRevokeLastSigner();
}
// Find the index of the account in the array
uint256 indexToRemove = type(uint256).max; // Use max as "not found" indicator
for (uint256 i = 0; i < s_signerCount; i++) {
if (s_signers[i] == _account) {
indexToRemove = i;
break;
}
}
// Gas-efficient array removal: move last element to removed position
if (indexToRemove < s_signerCount - 1) {
// Move the last signer to the position of the removed signer
s_signers[indexToRemove] = s_signers[s_signerCount - 1];
}
// Clear the last position and decrement count
s_signers[s_signerCount - 1] = address(0);
s_signerCount -= 1;
s_isSigner[_account] = false;
_revokeRole(SIGNING_ROLE, _account);
}

Risk

Likelihood:

  • The owner calls revokeSigningRole() with _account = owner().

Impact:

  • If the owner no longer has the signer role, then 5 other users can be granted the signer role.

Proof of Concept

  • Lines 1 - 3: The owner and 4 other usrs are granted the signer role. This total 5 uers with the role.

  • Lines 5 - 7: The owner is revoked of their signer role.

  • Lines 9 - 11: Another user is granted the signer role

  • Lines 12 - 13: We get the signerCount again confirming that there are now 5 users with the signer role and non of them is the admin.

function testCanHaveFiveOtherSigners() public grantSigningRoles {
uint256 signerCount = multiSigTimelock.getSignerCount();
assertEq(signerCount, 5);
multiSigTimelock.revokeSigningRole(OWNER);
signerCount = multiSigTimelock.getSignerCount();
assertEq(signerCount, 4);
address SIGNER_SIX = makeAddr("signer_six");
multiSigTimelock.grantSigningRole(SIGNER_SIX);
signerCount = multiSigTimelock.getSignerCount();
assertEq(signerCount, 5);
}

Run with:

forge test -vvv --match-test testCanHaveFiveOtherSigners

Sample output:

Ran 1 test for test/unit/MultiSigTimelockTest.t.sol:MultiSigTimeLockTest
[PASS] testCanHaveFiveOtherSigners() (gas: 386846)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.88ms (1.65ms CPU time)
Ran 1 test suite in 25.34ms (8.88ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

  • In the function revokeSigningRole, add a check to test whether _account is the owner().

function revokeSigningRole(address _account) external nonReentrant onlyOwner noneZeroAddress(_account) {
// CHECKS
+
+ if (_account == owner()) {
+ revert("can't revoke owner");
+ }
+
if (!s_isSigner[_account]) {
revert MultiSigTimelock__AccountIsNotASigner();
}
// Prevent revoking the first signer (would break the multisig), moreover, the first signer is the owner of the contract(wallet)
if (s_signerCount <= 1) {
revert MultiSigTimelock__CannotRevokeLastSigner();
}
// Find the index of the account in the array
uint256 indexToRemove = type(uint256).max; // Use max as "not found" indicator
for (uint256 i = 0; i < s_signerCount; i++) {
if (s_signers[i] == _account) {
indexToRemove = i;
break;
}
}
// Gas-efficient array removal: move last element to removed position
if (indexToRemove < s_signerCount - 1) {
// Move the last signer to the position of the removed signer
s_signers[indexToRemove] = s_signers[s_signerCount - 1];
}
// Clear the last position and decrement count
s_signers[s_signerCount - 1] = address(0);
s_signerCount -= 1;
s_isSigner[_account] = false;
_revokeRole(SIGNING_ROLE, _account);
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!