MultiSig Timelock

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

`getTransaction()` returns an all-zero struct for non-existent tx IDs

Author Revealed upon completion

Description:
getTransaction(uint256) reads from s_transactions[transactionId] without validating transactionId < s_transactionCount. For non-existent transactions, Solidity returns default values (zero address, 0 value, executed=false).

Impact:
Off-chain indexers / UIs can misinterpret non-existent transaction IDs as valid “empty” transactions.

Proof of Concept:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {MultiSigTimelock} from "src/MultiSigTimelock.sol";
contract Receiver {
receive() external payable {}
}
contract MultiSigTimelock_PoC is Test {
MultiSigTimelock internal ms;
address internal OWNER = address(this);
address internal SIGNER_TWO = makeAddr("signer_two");
address internal SIGNER_THREE = makeAddr("signer_three");
address internal SIGNER_FOUR = makeAddr("signer_four");
address internal SIGNER_FIVE = makeAddr("signer_five");
address internal NEW_SIGNER = makeAddr("newSigner");
address internal ATTACKER = makeAddr("attacker");
function setUp() public {
ms = new MultiSigTimelock();
ms.grantSigningRole(SIGNER_TWO);
ms.grantSigningRole(SIGNER_THREE);
ms.grantSigningRole(SIGNER_FOUR);
ms.grantSigningRole(SIGNER_FIVE);
vm.deal(address(ms), 200 ether);
}
function test_getTransaction_returnsZeroStruct_forNonExistentTxId() public {
// Create 1 real tx so that "1" is definitely non-existent (avoids ambiguity when count==0).
Receiver r = new Receiver();
uint256 id0 = ms.proposeTransaction(address(r), 1 ether, "");
assertEq(id0, 0);
assertEq(ms.getTransactionCount(), 1);
// Query a tx id that doesn't exist
MultiSigTimelock.Transaction memory missing = ms.getTransaction(1);
// Demonstrate the issue: all fields are default / zeroed (no revert)
assertEq(missing.to, address(0));
assertEq(missing.value, 0);
assertEq(missing.data.length, 0);
assertEq(missing.confirmations, 0);
assertEq(missing.proposedAt, 0);
assertEq(missing.executed, false);
}
}

Mitigation:
Add a transactionExists(transactionId) check (or return a (bool exists, Transaction memory txn) tuple).

Support

FAQs

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

Give us feedback!