Before you continue, please read and agree to the Terms of Service and Optimism Community Agreement.

Before you continue, please read and agree to the Terms of Service and Optimism Community Agreement.

Alberto Cuesta Cañada

Alberto Cuesta Cañada

Alberto Cuesta Cañada

February 11, 2026

February 11, 2026

Liveness Module

Most, if not all, crypto companies at scale use multisigs to manage their operations and upgrades. Collectively these multisigs hold about a hundred billion dollars in assets, but their importance is higher than that, as those multisigs are critical to the functioning of their crypto protocols. If the multisig is lost, most likely the protocol is lost as well.

Losing keys to hackers is not the only risk. It is also possible to lose keys in a literal sense. Hardware wallets might be misplaced or destroyed in an accident. PIN codes might be forgotten or accidentally deleted from password managers. Hardware wallets can be restored from a passphrase, but storing these passphrases is a significant risk in itself, and not everyone stores them securely.

At Optimism, we have developed a simple Gnosis Safe liveness module that we believe offers good guarantees against the risk of lost multisig keys. Using this module allows multisig owners to have a more balanced security model and sleep better at night.

In this article we’ll explain how our liveness module works. It is open-source and free to use, and we hope that the industry at large will be safer because of it.

Context and Requirements

Gnosis Safe Multisigs are configured as a set of owner addresses where some threshold of addresses can sign and execute a transaction through the safe contract.

Multisig liveness means the ability for the multisig to reach consensus and execute a transaction that states "this multisig is live” within a specific time — that’s what we want to guarantee.

A multisig fails to be live if a sufficient minority of signers (total - threshold + 1) permanently prevents it from reaching consensus. This could happen, for example, by keys being accidentally lost or stolen by hostile parties.

Our first attempt at a multisig liveness solution (1, 2) didn’t prevent malicious signers from refusing to sign. They could selectively choose to sign only transactions that proved liveness, while blocking transactions that advance the multisig’s actual interests.

Our first attempt was also inconvenient for signers, as they had to regularly show they were in control or they would be assumed incapable and removed from the multisig owner set. In many multisigs, there are always a few signers that rarely act, but serve as a convenient backup for the more active signers.

Let’s see how we solved this.

The Solution

Gnosis Safes allow signers to use modules and guards to augment or restrict their capabilities.

Modules can be enabled by Safe signers to pre-approve some actions to be executed from the Safe when triggered by non-signers. The module implements some function, non-signers call it, and the Safe executes the action.

A guard can also be enabled on a Safe to execute additional actions or enforce conditions before or after the Safe executes a transaction. It’s like adding requirements for the Safe to execute a transaction.

Our original liveness guard and module removed signers if they individually failed to show liveness. In addition to that, if the number of owners in the safe fell below a minimum defined level, then all owners would be removed and sole control of the multisig would be transfered to a fallback owner.

Suitable fallback owners would be other multisigs with stronger security guarantees. They could be other multisigs in the same company, external multisigs maintained exclusively for this purpose, or multisigs handled by other organisations willing to do this service and deemed extremely trustworthy.

Our new solution uses this fallback owner as its only mechanism, and in doing so, it switches from an idea of individual liveness to that of multisig liveness, which is easier to manage.

Instead of keeping a liveness registry for signers, we implemented a simple challenge-response mechanism for the multisig itself. The multisig can be challenged to show liveness, or in other words, it can be asked “are you still alive?”.

/// @notice Challenges an enabled safe.
/// @param _safe The Safe address to challenge.
    function challenge(Safe _safe) external {
        // Check if the calling safe has configuration set
        _assertModuleConfigured(_safe);

        // Check that the module is still enabled on the target Safe.
        _assertModuleEnabled(_safe);

        // Check that the caller is the fallback owner
        if (msg.sender != _livenessSafeConfiguration[_safe].fallbackOwner) {
            revert LivenessModule2_UnauthorizedCaller();
        }

        // Check that no challenge already exists
        if (challengeStartTime[_safe] != 0) {
            revert LivenessModule2_ChallengeAlreadyExists();
        }

        // Set the challenge start time and emit the event
        challengeStartTime[_safe] = block.timestamp;
        emit ChallengeStarted(address(_safe), block.timestamp);
    }

    /// @notice Responds to a challenge for an enabled safe, canceling it.
    function respond() external {
        Safe callingSafe = Safe(payable(msg.sender));

        // Check if the calling safe has configuration set.
        _assertModuleConfigured(callingSafe);

        // Check that this module is enabled on the calling Safe.
        _assertModuleEnabled(callingSafe);

        // Check that a challenge exists
        uint256 startTime = challengeStartTime[callingSafe];
        if (startTime == 0) {
            revert LivenessModule2_ChallengeDoesNotExist();
        }

        // Cancel the challenge without checking if response period has expired
        // This allows the Safe to respond at any time, providing more flexibility
        _cancelChallenge(callingSafe);
}
/// @notice Challenges an enabled safe.
/// @param _safe The Safe address to challenge.
    function challenge(Safe _safe) external {
        // Check if the calling safe has configuration set
        _assertModuleConfigured(_safe);

        // Check that the module is still enabled on the target Safe.
        _assertModuleEnabled(_safe);

        // Check that the caller is the fallback owner
        if (msg.sender != _livenessSafeConfiguration[_safe].fallbackOwner) {
            revert LivenessModule2_UnauthorizedCaller();
        }

        // Check that no challenge already exists
        if (challengeStartTime[_safe] != 0) {
            revert LivenessModule2_ChallengeAlreadyExists();
        }

        // Set the challenge start time and emit the event
        challengeStartTime[_safe] = block.timestamp;
        emit ChallengeStarted(address(_safe), block.timestamp);
    }

    /// @notice Responds to a challenge for an enabled safe, canceling it.
    function respond() external {
        Safe callingSafe = Safe(payable(msg.sender));

        // Check if the calling safe has configuration set.
        _assertModuleConfigured(callingSafe);

        // Check that this module is enabled on the calling Safe.
        _assertModuleEnabled(callingSafe);

        // Check that a challenge exists
        uint256 startTime = challengeStartTime[callingSafe];
        if (startTime == 0) {
            revert LivenessModule2_ChallengeDoesNotExist();
        }

        // Cancel the challenge without checking if response period has expired
        // This allows the Safe to respond at any time, providing more flexibility
        _cancelChallenge(callingSafe);
}
/// @notice Challenges an enabled safe.
/// @param _safe The Safe address to challenge.
    function challenge(Safe _safe) external {
        // Check if the calling safe has configuration set
        _assertModuleConfigured(_safe);

        // Check that the module is still enabled on the target Safe.
        _assertModuleEnabled(_safe);

        // Check that the caller is the fallback owner
        if (msg.sender != _livenessSafeConfiguration[_safe].fallbackOwner) {
            revert LivenessModule2_UnauthorizedCaller();
        }

        // Check that no challenge already exists
        if (challengeStartTime[_safe] != 0) {
            revert LivenessModule2_ChallengeAlreadyExists();
        }

        // Set the challenge start time and emit the event
        challengeStartTime[_safe] = block.timestamp;
        emit ChallengeStarted(address(_safe), block.timestamp);
    }

    /// @notice Responds to a challenge for an enabled safe, canceling it.
    function respond() external {
        Safe callingSafe = Safe(payable(msg.sender));

        // Check if the calling safe has configuration set.
        _assertModuleConfigured(callingSafe);

        // Check that this module is enabled on the calling Safe.
        _assertModuleEnabled(callingSafe);

        // Check that a challenge exists
        uint256 startTime = challengeStartTime[callingSafe];
        if (startTime == 0) {
            revert LivenessModule2_ChallengeDoesNotExist();
        }

        // Cancel the challenge without checking if response period has expired
        // This allows the Safe to respond at any time, providing more flexibility
        _cancelChallenge(callingSafe);
}

The multisig can answer by executing a simple predetermined call that will cancel the challenge. Since the multisig can only execute calls if it has a quorum, this ensures that enough keys are still controlled the signers. If this call can’t be executed within a given time for lack of quorum, the fallback owner will get sole ownership and use it to restore the multisig to a live owner set.

This simple mechanism also solves the situation in which malicious owners sign only transactions that prove liveness. If there are malicious signers in the owner set, then it will be the honest owners that will refuse to sign the reply to a liveness challenge. By their refusal, the challenge reply won’t be executed and the fallback owner will get sole ownership as before.

// Get current owners
address[] memory owners = _safe.getOwners();

// Remove all owners after the first one
// Note: This loop is safe as real-world Safes have limited owners (typically <
// 10) Gas limits would only be a concern with hundreds/thousands of owners
while (owners.length > 1) {
  _safe.execTransactionFromModule({
    to : address(_safe),
    value : 0,
    operation : Enum.Operation.Call,
    data :
        abi.encodeCall(OwnerManager.removeOwner, (SENTINEL_OWNER, owners[0], 1))
  });
  owners = _safe.getOwners();
}

// Now swap the remaining single owner with the fallback owner
// Note: If the fallback owner would be the only or the last owner in the owners
// list, swapOwner would internally revert in OwnerManager, but we ignore it
// because the final owners list would still be what we want.
_safe.execTransactionFromModule({
  to : address(_safe),
  value : 0,
  operation : Enum.Operation.Call,
  data : abi.encodeCall(OwnerManager.swapOwner,
                        (SENTINEL_OWNER, owners[0],
                         _livenessSafeConfiguration[_safe].fallbackOwner))
});

// Sanity check: verify the fallback owner is now the only owner
address[] memory finalOwners = _safe.getOwners();
if (finalOwners.length != 1 ||
    finalOwners[0] != _livenessSafeConfiguration[_safe].fallbackOwner) {
  revert LivenessModule2_OwnershipTransferFailed();
}
// Get current owners
address[] memory owners = _safe.getOwners();

// Remove all owners after the first one
// Note: This loop is safe as real-world Safes have limited owners (typically <
// 10) Gas limits would only be a concern with hundreds/thousands of owners
while (owners.length > 1) {
  _safe.execTransactionFromModule({
    to : address(_safe),
    value : 0,
    operation : Enum.Operation.Call,
    data :
        abi.encodeCall(OwnerManager.removeOwner, (SENTINEL_OWNER, owners[0], 1))
  });
  owners = _safe.getOwners();
}

// Now swap the remaining single owner with the fallback owner
// Note: If the fallback owner would be the only or the last owner in the owners
// list, swapOwner would internally revert in OwnerManager, but we ignore it
// because the final owners list would still be what we want.
_safe.execTransactionFromModule({
  to : address(_safe),
  value : 0,
  operation : Enum.Operation.Call,
  data : abi.encodeCall(OwnerManager.swapOwner,
                        (SENTINEL_OWNER, owners[0],
                         _livenessSafeConfiguration[_safe].fallbackOwner))
});

// Sanity check: verify the fallback owner is now the only owner
address[] memory finalOwners = _safe.getOwners();
if (finalOwners.length != 1 ||
    finalOwners[0] != _livenessSafeConfiguration[_safe].fallbackOwner) {
  revert LivenessModule2_OwnershipTransferFailed();
}
// Get current owners
address[] memory owners = _safe.getOwners();

// Remove all owners after the first one
// Note: This loop is safe as real-world Safes have limited owners (typically <
// 10) Gas limits would only be a concern with hundreds/thousands of owners
while (owners.length > 1) {
  _safe.execTransactionFromModule({
    to : address(_safe),
    value : 0,
    operation : Enum.Operation.Call,
    data :
        abi.encodeCall(OwnerManager.removeOwner, (SENTINEL_OWNER, owners[0], 1))
  });
  owners = _safe.getOwners();
}

// Now swap the remaining single owner with the fallback owner
// Note: If the fallback owner would be the only or the last owner in the owners
// list, swapOwner would internally revert in OwnerManager, but we ignore it
// because the final owners list would still be what we want.
_safe.execTransactionFromModule({
  to : address(_safe),
  value : 0,
  operation : Enum.Operation.Call,
  data : abi.encodeCall(OwnerManager.swapOwner,
                        (SENTINEL_OWNER, owners[0],
                         _livenessSafeConfiguration[_safe].fallbackOwner))
});

// Sanity check: verify the fallback owner is now the only owner
address[] memory finalOwners = _safe.getOwners();
if (finalOwners.length != 1 ||
    finalOwners[0] != _livenessSafeConfiguration[_safe].fallbackOwner) {
  revert LivenessModule2_OwnershipTransferFailed();
}

Using module capabilities to replace the owner set by the fallback owner

To avoid spamming attacks, only the fallback owner can issue challenges. The fallback owner is assumed to have stronger security guarantees, and be alive and not malicious. If the fallback owner would fail at keeping any of these properties, the multisig can reply to any malicious challenges and replace the malicious fallback owner by an honest one.

Finally, the reason for a multisig failing to execute transactions could be a faulty or misconfigured guard. Such a malfunctioning guard would still block the multisig even if owned by the fallback owner. For this reason, the liveness module removes any guards when replacing the owner set by the fallback owner. Removing guards involves its own risks, which can be mitigated.

Limitations

Security researchers will read this article and will point out some obvious limitations.

This mechanism only solves the scenario with malicious signers for multisigs in which the quorum is larger than half of the owner set, for example, 4/7. In multisigs where the quorum is less than half of the owner set, the malicious signers will reach quorum early on and will be able to respond to any challenges.

Hacks in which malicious parties obtain a quorum of keys from a multisig and steal all of its assets are the ones that appear in the headlines. However, there are other scenarios that can be equally catastrophic and that we prevent with sometimes heavy measures. One of these is simply too many owners irrecoverably losing their signing keys.

A second limitation is that the fallback must have stronger security guarantees, and it is impossible to keep assigning ever-stronger fallbacks in a chain of trust. The ultimate fallback cannot have a fallback with stronger security guarantees.

In practical terms, though, it would be fine for two strongly guarded multisigs with completely independent owner sets and security mechanisms to act as fallbacks for each other. These two ultimate fallbacks could be highly reputable and well-defended common goods organisations.

Conclusion

While hacks in which malicious parties obtain a quorum of keys from a multisig and steal all of its assets are the ones that appear in the headlines. There are other scenarios that can be equally catastrophic and that we prevent with sometimes heavy measures. One of these is simply too many owners irrecoverably losing their signing keys.

To allow for a better structured defense in depth of multisigs, at Optimism we came up with an extremely simple liveness module for Gnosis Safes that uses the concept of a fallback owner and a challenge and response mechanism to guarantee liveness of a multisig under workable assumptions.

We are releasing this module as a common good as part of our commitment to advance the interests of the community. Feel free to use it as-is, tailor it to your use case in a fork, or discuss it as you might find convenient. The module was audited by Spearbit.

The creation of this module was a joint effort by several people at Optimism. The vision came from Kelvin Fichter, the code was developed by John Mardlin. Josep Bové, Tom Assas, Matt Solomon and I helped in other capacities.

At Optimism, we are always looking for talented individuals who will help us on our mission to scale Ethereum and build the Superchain, pushing the edge on what is possible and finding smart solutions for complex problems. If that is something that interests you, we are hiring.

Sign up for our newsletter

By registering for our newsletter, you consent to receive updates from us. Please review our privacy policy to learn how we handle your data. You can unsubscribe at any time.

Sign up for our newsletter

By registering for our newsletter, you consent to receive updates from us. Please review our privacy policy to learn how we handle your data. You can unsubscribe at any time.

Sign up for our newsletter

By registering for our newsletter, you consent to receive updates from us. Please review our privacy policy to learn how we handle your data. You can unsubscribe at any time.