# Post-quantum EVM multisig on a stock Safe

> In this article we show a small but useful integration: using NiceTry ERC-4337 accounts as owners of an unmodified Safe multisig. The result is a 2-of-3 Safe operated by FORS+C-authenticated accounts, without teaching the Safe anything about FORS+C.

- Author: Matteo Vicari
- Published: June 9, 2026
- Tag: Post-Quantum
- URL: https://www.riva.xyz//blog/post-quantum-evm-multisig-on-a-stock-safe

---

Rivalabs builds the infrastructure for post-quantum signatures onchain. We start with the Ephemeral Keys protocol and its reference implementation, NiceTry, and build out the primitives for post-quantum signing and digital asset custody.

In this article we show a small but useful integration: using NiceTry ERC-4337 accounts as owners of an unmodified Safe multisig. The result is a 2-of-3 Safe operated by FORS+C-authenticated accounts, without teaching the Safe anything about FORS+C.

Post-quantum wallet infrastructure on EVM chains is increasingly moving through account abstraction. The reason is simple: account abstraction lets the account decide how authorization works, without requiring the base protocol or every application contract to adopt a new signature scheme.

Safe multisigs are an interesting test case for this model. They are already widely used for custody, governance, treasury management, and operational approvals. They also have a strict signature interface built around owner addresses, thresholds, and transaction hashes.

The question is therefore:

```text
Can a post-quantum account operate a stock Safe without modifying the Safe?
```

For our infra, the answer is yes.The important point is that the Safe never verifies a post-quantum signature. FORS+C runs one layer above the Safe, inside each NiceTry account's ERC-4337 validation path. The Safe only sees ordinary owner addresses and ordinary pre-validated approvals.

## What the integration does

The integration uses NiceTry accounts as Safe owners.

Each NiceTry account authenticates a UserOperation with FORS+C, if the UserOperation is valid, the account can call the Safe. From the Safe's perspective, this is just a contract owner calling `approveHash` or `execTransaction`.

The Safe authorization path is unchanged:

```text
FORS+C signature
  -> NiceTry validateUserOp
  -> NiceTry account callData
  -> Safe approveHash / execTransaction
  -> Safe threshold check
```

This keeps the post-quantum logic in the account layer and the M-of-N logic in the Safe layer. Safe remains a stock Safe v1.4.1 deployment. NiceTry does not add a verifier to the Safe, does not modify `execTransaction`, and does not put FORS+C bytes inside the Safe signature blob.

## How Safe sees the transaction

A Safe transaction is defined by the usual tuple:

```text
(to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce)
```

The Safe hashes that tuple using its EIP-712 construction. The resulting `safeTxHash` is what owners approve. Because the Safe nonce is part of the hash, an approval is bound to one transaction at one position in the Safe's sequence.

When `execTransaction` is called, Safe runs `checkNSignatures`. For each 65-byte signature record, the `v` byte decides how the record is interpreted:

| `v` value | Safe behavior |
| --- | --- |
| `27/28`, or `>30` | ECDSA recovery with `ecrecover` |
| `0` | EIP-1271 contract signature via `isValidSignature` |
| `1` | Pre-validated approval via `msg.sender` or `approvedHashes` |

NiceTry uses `v = 1`.

There are two ways this can pass:

```text
owner == msg.sender
```

or:

```text
approvedHashes[owner][safeTxHash] != 0
```

This is the whole integration surface. The Safe counts owner approvals as usual and the only difference is how those approvals are produced.

## The layered model

![Layered call flow: a FORS+C-signed UserOp is validated at the EntryPoint, the NiceTry account then runs its callData, and the stock Safe checks v=1 approvals against its threshold.](https://hackmd.io/_uploads/S1LuROBZGx.jpg)

_FORS+C is verified at the account layer. The Safe only checks `v=1` owner approvals against its threshold._

The model has three layers:

| Layer | Component | Role |
| --- | --- | --- |
| 1 | EntryPoint / NiceTry validation | Verifies FORS+C and rotates the key |
| 2 | NiceTry account execution | Calls the Safe |
| 3 | Safe | Counts owner approvals and enforces M-of-N |

The Safe remains therefore only a threshold counter over approvals​.

The owner account must not expose a second route that can call the Safe without passing the FORS+C check. If an account has an unrestricted execution function, an unsafe upgrade path, or a module that bypasses validation, the post-quantum argument fails.

The owner set also matters: no threshold-satisfying subset can be entirely quantum-breakable. In a hypothetical 2-of-3 over `{Alice (PQ), Bob (PQ), Charlie (classical)}`, one classical owner is not enough to break custody, because every valid pair still needs Alice or Bob.

## The Sepolia deployment

The demo uses:

| Role | Address |
| --- | --- |
| Alice | `0xE572f04357958B334AbC2BF2C9E31492eF95FE14` |
| Bob | `0x58a6de8bFb517d3ABebacA88cFC6081A9255Dfbf` |
| Charlie | `0xf7FeE1BfBb5b22eC67362c0d26D7Aed99A309992` |
| Safe | `0x0C5222D605B167a192E1bbe2119cEEbb02DE9977` |
| EntryPoint v0.7.0 | `0x0000000071727De22E5E9d8BAf0edAc6f37da032` |

FORS+C is a few-time signature scheme.Reusing a key degrades security, so the NiceTry account consumes a key and rotates to the next one during validation. The `OwnerRotated` event marks that rotation.

The lifecycle has three transactions.

### 1. Enroll

[tx `0x7c688d7c...f5c10`](https://sepolia.etherscan.io/tx/0x7c688d7ccd36bd9347e8107a01615bde81ce6c4843c25a0a24a25d17547f5c10) uses `UserOperationEvent.actualGasUsed = 469,897`.

Alice's UserOperation deploys the Safe through the canonical `SafeProxyFactory`, with owners `{Alice, Bob, Charlie}` and threshold 2. The deployment uses the standard `SafeToL2Setup` migration. During validation, Alice rotates:

```text
0x79dFA6... -> 0x1731861D...
```

The Safe sees three owner addresses. It does not receive any FORS+C signature material.

### 2. Vote

[tx `0xaf5297fe...0c1e8d`](https://sepolia.etherscan.io/tx/0xaf5297fe022a06d7f691ead104575714b280c22ef37d59f3c88ae36ad80c1e8d) uses `UserOperationEvent.actualGasUsed = 201,437`.

Alice calls:

```text
Safe.approveHash(0x5024ae...)
```

The Safe records:

```text
approvedHashes[Alice][0x5024ae...] = 1
```

and emits `ApproveHash`. Alice rotates again:

```text
0x1731861D... -> 0x920A6960...
```

This is the first approval. It is an onchain commitment to a Safe transaction hash, not an inline signature.

### 3. Execute

[tx `0x6350196b...95c494`](https://sepolia.etherscan.io/tx/0x6350196bba66153d9d1a26a9e85bdceae1db409718466b034ef71cc11795c494) uses `UserOperationEvent.actualGasUsed = 227,837`.

Bob calls:

```text
Safe.execTransaction(to=Charlie, value=0.05 ETH, ...)
```

The Safe signature blob contains two `v=1` records:

```text
r = Bob (0x58a6...), s = 0, v = 1
r = Alice (0xE572...), s = 0, v = 1
```

Bob passes because Bob is the executor, so `owner == msg.sender`. Alice passes because Alice approved the same hash in the previous transaction. Together they meet the threshold of 2.

The Safe sends 0.05 ETH to Charlie and emits:

```text
ExecutionSuccess(0x5024ae...)
```

Bob rotates:

```text
0x4f5017... -> 0x244A1a...
```

No ECDSA recovery is used for either owner and no FORS+C bytes are included in the Safe calldata.

## The hash link

The vote and execution are linked by the Safe transaction hash:

```text
0x5024aebff8b8417da15ad3f369680c85fac0d0eae68d616902019479882809ea
```

This hash is the value Alice approved in tx 2 and the value the Safe executed in tx 3.

It reproduces from the Safe v1.4.1 EIP-712 construction:

```text
domainSeparator = keccak256(abi.encode(
    keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"),
    11155111,
    Safe
))

structHash = keccak256(abi.encode(
    SAFE_TX_TYPEHASH,
    Charlie,
    0.05e18,
    keccak256(0x),
    0,
    0,
    0,
    0,
    0x0,
    0x0,
    0
))

safeTxHash = keccak256(0x19 || 0x01 || domainSeparator || structHash)
           = 0x5024aebff8b8417da15ad3f369680c85fac0d0eae68d616902019479882809ea
```

This is the same coordination model as an ordinary Safe multisig: the owners agree on the exact Safe transaction tuple and approve its hash. Only the approval medium changes, while the Safe transaction model stays the same and sees no difference.

## Conclusions

NiceTry does not turn Safe into a post-quantum multisig, it keeps Safe exactly where Safe is strong: owner sets, thresholds, transaction hashes, and execution.

The post-quantum part lives in the owners: each NiceTry account authenticates a UserOperation with FORS+C, rotates its key, and then creates or uses a normal Safe `v=1` approval. The Safe counts those approvals as it always has, without questioning their source's authentication method.

The resulting model is:

```text
post-quantum account authorization
  -> ordinary Safe approval
  -> ordinary Safe threshold execution
```

This design composes post-quantum accounts with existing Safe custody, without modifying the Safe or asking it to understand post-quantum cryptography.

The same mechanism also gives existing Safe deployments a path to post-quantum custody, swapping owners for NiceTry accounts one at a time so a live Safe migrates without redeployment or threshold changes.
