npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

safe-counterfactual

v0.1.0

Published

Derive counterfactual Safe (Gnosis Safe) proxy addresses that commit to an exact transaction batch. Pure, dependency-light CREATE2 derivation.

Readme

safe-counterfactual

Derive counterfactual Safe proxy addresses that commit to an exact transaction batch.

Encode a batch of calls into a Safe setup() initializer, hash that initializer into the CREATE2 salt, and the resulting address becomes a cryptographic commitment to those exact transfers. You can compute and share the address before the Safe exists, fund it, and then anyone can deploy the proxy and execute the batch atomically in a single transaction.

Pure functions only — no network, no I/O, no config. viem is the single peer dependency. Full TypeScript types, ESM + CJS.

npm install safe-counterfactual viem

What is a counterfactual Safe deposit address?

A Safe proxy is deployed with CREATE2, so its address is fully determined by the factory, the singleton, the proxy creation code, and a salt — before any transaction is sent. This library builds the salt from the Safe's setup() initializer, and puts your batch inside that initializer (delegatecalled through MultiSendCallOnly). Because the batch is part of the initializer, and the initializer is hashed into the salt, the address is a one-way commitment:

  • Change any amount, recipient, or call, and the address changes completely.
  • Send funds to the address, and the only transaction that can ever deploy a contract there is the one that runs your exact batch.

This makes the address usable as a deposit address: give it out, receive funds, then deploy-and-settle. No key is generated, held, or needed.

Quickstart

import {
  buildInitializer,
  deriveAddress,
  buildDeployTx,
  assertCreationCode,
} from 'safe-counterfactual';

// Read once, at boot, from the deployed factory (see the warning below).
const proxyCreationCode = await client.readContract({
  address: proxyFactory, abi: factoryAbi, functionName: 'proxyCreationCode',
});
assertCreationCode({ expected: proxyCreationCode, onchain: proxyCreationCode });

const initializer = buildInitializer({
  owners: [ownerAddress],          // recovery path — must be non-empty, no zero address
  threshold: 1,
  multiSendCallOnly,               // read from the chain
  calls: [                         // your batch — plain { to, value, data }
    { to: token, value: 0n, data: erc20Transfer(recipient, 1_000_000n) },
  ],
});

const address = deriveAddress({
  proxyFactory, singleton, proxyCreationCode, initializer, saltNonce: 0n,
});
// → fund `address`, then send the deploy tx from anyone:
const deployTx = buildDeployTx({ proxyFactory, singleton, initializer, saltNonce: 0n });

Security model

  • The address is the authorization. There is no private key. Whoever funds the address has committed to the batch; whoever sends the deploy transaction triggers it. A different batch would be a different address.
  • Owners are the only recovery path. After deployment, overpayment and any stray tokens sent to the address can be swept only by a Safe owner. buildInitializer therefore rejects an empty owner set, the zero address, and duplicate owners — without a real owner the address is an unownable dead end.
  • This is a derivation library, not the Safe. The Safe contracts (SafeProxyFactory, the singleton, MultiSendCallOnly) are deployed and audited upstream by Safe — this package neither ships nor audits them. All this library does is compute addresses and encode calldata that the audited contracts interpret. Verify the Safe addresses you pass in against a source you trust.

⚠️ proxyCreationCode is required and load-bearing — read this

proxyCreationCode is a required, caller-supplied argument. This library deliberately bundles no creation-code constant.

You must read it at boot from the deployed factory's proxyCreationCode() view, on the chain you are deriving for:

const proxyCreationCode = await client.readContract({
  address: proxyFactory, abi: [{ name: 'proxyCreationCode', type: 'function',
    stateMutability: 'view', inputs: [], outputs: [{ type: 'bytes' }] }],
  functionName: 'proxyCreationCode',
});

Why this matters: the creation code is hashed into the CREATE2 preimage. A wrong value produces a correct-looking, permanently undeployable address — there is no error at any layer, because the derivation formula is still correct and only its input is wrong. Funds sent to such an address are unrecoverable. Guard it with assertCreationCode at cold start, comparing your configured value to a fresh proxyCreationCode() read, and refuse to quote addresses on any chain where it disagrees. (Non-standard factories — e.g. zkSync's different CREATE2 preimage — need their own value; never assume one default.)

The exact-amount caveat

The amounts in your batch are literals, baked into the address. Consequently:

  • Underfunding the address makes the deploy transaction revert atomically — nothing settles, and the funds remain at the address, so you can recover by topping up and redeploying.
  • Overfunding leaves the surplus at the deployed Safe, recoverable only by an owner (see the security model).

If the amount that will arrive is not known exactly in advance (for example a transfer that is netted by a fee on the way in), commit to a floor: quote the batch at amount × (1 − toleranceBps / 10_000) so a slightly-short deposit still clears the committed amount and deploys, with the small remainder swept by the owner afterwards.

The wrong-chain recovery property

By default this library uses createProxyWithNonce, whose salt does not include the chain id. The same inputs therefore derive the same address on every EVM chain.

Treat this as a recovery property, not multi-chain acceptance. The initializer commits to specific token and contract addresses, which differ per chain — a batch built for Base, if deployed on Arbitrum, would call whatever happens to live at those Base addresses on Arbitrum (usually nothing, or the wrong contract). The value of chain-agnostic addresses is that a deposit sent to the wrong chain can be rescued by deploying there and sweeping with the owner — not that the batch is safe to run anywhere.

If you want addresses that are distinct per chain, pass chainId to deriveAddress and buildDeployTx; this selects createChainSpecificProxyWithNonce, which folds the chain id into the salt. Derive and deploy with the same chainId.

API

| Function | Purpose | |---|---| | buildInitializer({ owners, threshold, multiSendCallOnly, calls, fallbackHandler? }) | Encode the Safe setup() initializer that commits to calls. Validates owners/threshold. | | deriveAddress({ proxyFactory, singleton, proxyCreationCode, initializer, saltNonce, chainId? }) | CREATE2 address of the counterfactual proxy. | | buildDeployTx({ proxyFactory, singleton, initializer, saltNonce, chainId? }) | Unsigned { to, data } deploy transaction (anyone can send it). | | assertCreationCode({ expected, onchain }) | Boot guard; throws on a proxyCreationCode mismatch. | | encodeMultiSend(calls) / encodeCall(call) | Pack a batch (or a single call) for MultiSendCallOnly. | | computeSalt(initializer, saltNonce, chainId?) | The raw CREATE2 salt. | | proxyBytecodeHash(proxyCreationCode, singleton) | The raw CREATE2 bytecodeHash. |

A Call is a plain { to, value, data }. Callers encode their own batch.

Errors are SafeCounterfactualError with a machine-readable code (isSafeCounterfactualError(e) narrows the type).

Derivation formulas

bytecodeHash = keccak256(proxyCreationCode ++ abi.encode(uint256(singleton)))
salt         = keccak256(keccak256(initializer) ++ abi.encode(uint256(saltNonce)))
address      = CREATE2(proxyFactory, salt, bytecodeHash)

The singleton (mastercopy) is a constructor argument appended to the creation code, not part of the creation code itself. With chainId, the salt gains a trailing ++ abi.encode(uint256(chainId)).

Development

This repo uses pnpm (the vitest 4 test toolchain does not resolve cleanly under npm's peer rules; consumers of the published package are unaffected — they only need dist and the viem peer).

pnpm install
pnpm build        # dual ESM + CJS into dist/, plus .d.ts and source maps
pnpm typecheck
pnpm test         # unit tests + known-good vector + fork tests
pnpm test:fork    # just the Base + Arbitrum fork tests

The fork tests read proxyCreationCode() live and assert that deriveAddress equals the address the real createProxyWithNonce would deploy to. They skip automatically when the network is unavailable. Override RPCs with BASE_RPC_URL / ARBITRUM_RPC_URL.

Publishing

pnpm build && pnpm test   # green first
npm publish               # runs prepublishOnly (clean + build); publishes dist/ + src/

The package is unscoped and public; npm publish needs no extra flags. npm pack --dry-run previews the exact tarball.

License

MIT © safe-counterfactual contributors — see LICENSE.