MultiSig Timelock

First Flight #55
Beginner FriendlyWallet
100 EXP
View results
Submission Details
Severity: low
Valid

Inherited renounceOwnership() Function Permanently Locks Contract Administration

Inherited renounceOwnership() Function Permanently Locks Contract

Description

The MultiSigTimelock contract inherits from OpenZeppelin's Ownable contract, which includes a public renounceOwnership() function. If the contract owner (intentionally or accidentally) calls this function, ownership is permanently transferred to the zero address (address(0)).

  • This prevents adding or removing signers, proposing new transactions, and managing the multisig wallet entirely. The contract becomes permanently frozen with only existing signers.

@> // inherited ownable contract already has transferOwnership and renounceOwnership functions
@> // # Inherited `renounceOwnership()` Function Permanently Locks Contract Administration

Risk

Likelihood: MEDIUM-HIGH

  • Human error (wrong contract, wrong function)

  • Misunderstanding of function purpose.

  • No confirmation mechanism - Silent inheritance (not obvious in code review)

Impact:

  • CRITICAL: Complete loss of administrative control

    • Cannot add new signers (team members can't be replaced)

    • Cannot remove signers (compromised accounts can't be revoked)

    • Cannot propose new transactions (primary function broken)

Proof of Concept

  • Add the below test under MultiSigTimelockTest.t.sol and run the below test using command forge test --mt testRenounceOwnership -vvvv

function testRenounceOwnership() public {
// ARRANGE
uint256 signerCount = 5;
address[] memory signersArray = new address[](5);
// ACT
signersArray[0] = OWNER;
multiSigTimelock.grantSigningRole(SIGNER_TWO);
signersArray[1] = SIGNER_TWO;
multiSigTimelock.grantSigningRole(SIGNER_THREE);
multiSigTimelock.renounceOwnership();
signersArray[2] = SIGNER_THREE;
// check after renounce ownership, the owner is still a signer
assertTrue(multiSigTimelock.hasRole(multiSigTimelock.getSigningRole(), OWNER));
multiSigTimelock.grantSigningRole(SIGNER_FOUR);
signersArray[3] = SIGNER_FOUR;
multiSigTimelock.grantSigningRole(SIGNER_FIVE);
signersArray[4] = SIGNER_FIVE;
// ASSERT
assertEq(multiSigTimelock.getSignerCount(), signerCount);
assertEq(abi.encodePacked(multiSigTimelock.getSigners()), abi.encodePacked(signersArray));
}
// result
linux@Agniv:~/audit/2025-12-multisig-timelock$ forge test --mt testRenounceOwnership -vvvv
[⠊] Compiling...
[⠑] Compiling 1 files with Solc 0.8.30
[⠘] Solc 0.8.30 finished in 620.11ms
Compiler run successful!
Ran 1 test for test/unit/MultiSigTimelockTest.t.sol:MultiSigTimeLockTest
[FAIL: OwnableUnauthorizedAccount(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496)] testRenounceOwnership() (gas: 192283)
Traces:
[2405280] MultiSigTimeLockTest::setUp()
├─ [2364461] → new MultiSigTimelock@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ ├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: MultiSigTimeLockTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
│ ├─ emit RoleGranted(role: 0x8d12c52fe7c286acb23e8b6fad43ba0a7b1bdee59ad6818c044e478cc487c15b, account: MultiSigTimeLockTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], sender: MultiSigTimeLockTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
│ └─ ← [Return] 11123 bytes of code
└─ ← [Stop]
[192283] MultiSigTimeLockTest::testRenounceOwnership()
├─ [83181] MultiSigTimelock::grantSigningRole(signer_two: [0x884D93fF97A786C23d91993E20382Fc4C26Bc2aa])
│ ├─ emit RoleGranted(role: 0x8d12c52fe7c286acb23e8b6fad43ba0a7b1bdee59ad6818c044e478cc487c15b, account: signer_two: [0x884D93fF97A786C23d91993E20382Fc4C26Bc2aa], sender: MultiSigTimeLockTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
│ └─ ← [Stop]
├─ [74381] MultiSigTimelock::grantSigningRole(signer_three: [0x6d3780bE9713626035Ad76FfD17fCDc3FfD29428])
│ ├─ emit RoleGranted(role: 0x8d12c52fe7c286acb23e8b6fad43ba0a7b1bdee59ad6818c044e478cc487c15b, account: signer_three: [0x6d3780bE9713626035Ad76FfD17fCDc3FfD29428], sender: MultiSigTimeLockTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
│ └─ ← [Stop]
@> ├─ [5298] MultiSigTimelock::renounceOwnership()
@> │ ├─ emit OwnershipTransferred(previousOwner: MultiSigTimeLockTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], newOwner: 0x0000000000000000000000000000000000000000)
│ └─ ← [Stop]
├─ [465] MultiSigTimelock::getSigningRole() [staticcall]
│ └─ ← [Return] 0x8d12c52fe7c286acb23e8b6fad43ba0a7b1bdee59ad6818c044e478cc487c15b
├─ [3166] MultiSigTimelock::hasRole(0x8d12c52fe7c286acb23e8b6fad43ba0a7b1bdee59ad6818c044e478cc487c15b, MultiSigTimeLockTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
│ └─ ← [Return] true
├─ [0] VM::assertTrue(true) [staticcall]
│ └─ ← [Return]
├─ [4327] MultiSigTimelock::grantSigningRole(signer_four: [0xC5DD14faf50A1b629A785179e61758dCC300c01F])
│ └─ ← [Revert] OwnableUnauthorizedAccount(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496)
@> └─ ← [Revert] OwnableUnauthorizedAccount(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496)
Backtrace:
at MultiSigTimelock.grantSigningRole
at MultiSigTimeLockTest.testRenounceOwnership
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 1.28ms (99.61µs CPU time)
Ran 1 test suite in 11.13ms (1.28ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/unit/MultiSigTimelockTest.t.sol:MultiSigTimeLockTest
[FAIL: OwnableUnauthorizedAccount(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496)] testRenounceOwnership() (gas: 192283)

Analysis of Test result:
- Owner successfully renounced ownership.\

  • New owner is now 0x0000...0000 (zero address) \

  • Original owner (test contract) can no longer call onlyOwner functions \

  • Contract is permanently locked and cant assign any new signers or revoke the old signers.

Recommended Mitigation

  • Override and Disable (Recommended) the renounceOwnership()

  • Or add alternative with custom error.

- function renounceOwnership() public view override onlyOwner {
- revert("MultiSigTimelock: Cannot renounce ownership");
-}
// alternative with custom error
+ error MultiSigTimelock__OwnershipCannotBeRenounced();
+ function renounceOwnership() public view override onlyOwner {
+ revert MultiSigTimelock__OwnershipCannotBeRenounced();
+}
Updates

Lead Judging Commences

kelechikizito Lead Judge 4 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Owner calls renounceOwnership

Support

FAQs

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

Give us feedback!