Normal behavior: When the owner revokes a signer, the contract should locate the signer in s_signers[0..s_signerCount-1] and remove exactly that entry using swap-and-pop, keeping s_signers, s_signerCount, s_isSigner, and AccessControl membership consistent.
Issue: revokeSigningRole assumes that any account with s_isSigner[_account] == true must also exist in the s_signers array, but it never validates that the search found the index. If the signer is not found, indexToRemove stays as type(uint256).max, and the function silently clears the last signer slot and decrements s_signerCount, while revoking the role for _account. This desynchronizes signer enumeration and role membership, potentially bricking signer administration.
Likelihood:
The function initializes indexToRemove as a sentinel value. It then searches for the signer in the array. However, if the signer is not found, no check is performed before continuing execution. The function then blindly executes swap-and-pop logic using the invalid index. Because indexToRemove == type(uint256).max, the condition is false and the function removes the last signer in the array, not the intended one.
Impact:
Removes/clears the wrong signer entry (the last active slot), corrupting the enumerated signer set and s_signerCount.
Paste the following test into MultiSigTimelock.t.sol.
This PoC forces a realistic invariant break (custom signer mapping says “signer”, but signer array does not include them) by writing to storage, then demonstrates that revokeSigningRole silently removes the wrong signer.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.