MultiSig Timelock

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

19Dec2025_AuditReport8_ GrantSigningRole

Author Revealed upon completion

Root + Impact

Description

  • Deployment Scripts Use Hardcoded Addresses Without Validation

  • The GrantSigningRole script contains hardcoded addresses for signers without any validation that these addresses are valid, not contracts, or not zero addresses. If any of these addresses are incorrect or compromised, the multisig could be initialized in an insecure state.

// Root cause in the codebase with @> marks to highlight the relevant section
contract GrantSigningRole is Script {
address constant SIGNER_TWO = 0x93923B42Ff4bDF533634Ea71bF626c90286D27A0;
address constant SIGNER_THREE = 0x5d4aD28bD191107E582E56E47d7407bD5F111D8b;
address constant SIGNER_FOUR = 0x86F44aA771f0ad42a037efF70C859bb1B86c188A;
address constant SIGNER_FIVE = 0x5375bB27ABEC8d0f69d035c58306936aA9991182;
function grantSigningRole(address payable multiSigTimelockContractAddress) public {
vm.startBroadcast();
// No validation of addresses before granting roles
MultiSigTimelock(multiSigTimelockContractAddress).grantSigningRole(SIGNER_TWO);
// ...
}
}

Risk

Likelihood:

  • If SIGNER_TWO through SIGNER_FIVE are not controlled by trusted parties or if any address is mistyped, the multisig could be compromised from deployment.

Impact:

  • Deployment could fail or result in an incorrectly configured multisig if addresses are invalid. Hardcoded addresses also make the script inflexible for different deployments.

Proof of Concept

Hardcoded Role Assignment

This script is a Foundry deployment or administration script used to automate the process of adding new signers to a MultiSig contract.

Fixed Identities: It defines four specific wallet addresses as constants (SIGNER_TWO through SIGNER_FIVE).

Automated Execution: The grantSigningRole function calls the MultiSig contract to officially authorize these addresses.

The Critical Risk: The comment "No validation of addresses" highlights a Trust Assumption. If these hardcoded addresses contain a typo or belong to an attacker, the script will permanently grant them control over the MultiSig. In a production environment, this could lead to a loss of funds if the addresses aren't verified against an official registry or multi-signature consensus before the script is run.

contract GrantSigningRole is Script {
address constant SIGNER_TWO = 0x93923B42Ff4bDF533634Ea71bF626c90286D27A0;
address constant SIGNER_THREE = 0x5d4aD28bD191107E582E56E47d7407bD5F111D8b;
address constant SIGNER_FOUR = 0x86F44aA771f0ad42a037efF70C859bb1B86c188A;
address constant SIGNER_FIVE = 0x5375bB27ABEC8d0f69d035c58306936aA9991182;
function grantSigningRole(address payable multiSigTimelockContractAddress) public {
vm.startBroadcast();
// No validation of addresses before granting roles
MultiSigTimelock(multiSigTimelockContractAddress).grantSigningRole(SIGNER_TWO);
// ...
}
}

Recommended Mitigation

Secure and Configurable Role Granting

This version of the script replaces hardcoded constants with a dynamic and validated approach to adding signers.

Configurability: By passing an array (address[] memory signers), you can use the same script for different environments (testnet vs. mainnet) without changing the code.

Zero-Address Guard: It ensures no one accidentally grants power to address(0), which would lock the role or cause logic errors.

EOA Enforcement: The check signers[i].code.length == 0 ensures that the addresses are Externally Owned Accounts (EOAs) and not smart contracts. This is a common security practice to prevent complex contract-based attacks or accidentally adding a non-wallet contract as a signer.

Batch Limit: It restricts the input to a maximum of 4 signers, preventing accidental mass-assignment or gas-limit issues during the broadcast.

// Add validation and make addresses configurable:
function grantSigningRole(address payable multiSigTimelockContractAddress, address[] memory signers) public {
require(signers.length <= 4, "Too many signers");
vm.startBroadcast();
for (uint i = 0; i < signers.length; i++) {
require(signers[i] != address(0), "Invalid signer address");
require(signers[i].code.length == 0, "Signer cannot be contract");
MultiSigTimelock(multiSigTimelockContractAddress).grantSigningRole(signers[i]);
}
vm.stopBroadcast();
}

Support

FAQs

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

Give us feedback!