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

mymultisig-contract

v0.5.1

Published

Multi-Signatures Solidity Smart Contract for mymultisig.app

Readme

💰 MyMultiSig.app Smart Contract (Beta) 🚀

license

A minimalistic Solidity smart contract designed for secure and streamlined transactions, MyMultiSig.app simplifies the multisig process for an easy and convenient experience 💻. The contract is integrated with the mymultisig.app web app for an enhanced user experience 📱.

🔥 This smart contract is a multi-signature wallet, which means that a certain number of owners need to sign off on a transaction before it's executed. 💰

💻 The code is written in Solidity and uses three external libraries, ReentrancyGuard, EIP712 and IERC1271, for added security and interop. 🔒

✨ EIP-1271 support: the wallet exposes the standard isValidSignature(bytes32,bytes) entry point so it can act as a signer for other Safe / multisig instances, SIWE verifiers, NFT marketplaces, etc. Contract owners ("nested wallets") can also vote on the wallet's transactions via their own EIP-1271 entry — the wallet's signature validation is agnostic to whether each vote is an EOA ECDSA signature or a contract-owned EIP-1271 blob.

📈 The contract keeps track of various important details, like the name of the contract, the transaction nonce, the number of owners, and who the owners are. 📝

🎉 The contract also has events for adding and removing owners, changing the threshold, executing and failing transactions, and reaching the end of its life (when the nonce hits a certain limit). 💬

💬 When you're ready to make a transaction, you can call the execTransaction function and pass along the destination address, the amount of Ether to transfer, any data you want to include, the gas limit, and the signatures from the necessary owners. 💸

💻 The signatures are verified using the _validateSignature function, and the transaction is executed using the call opcode in assembly. 💻

🎉 If the signatures are valid and there's enough gas, the transaction is a success and a TransactionExecuted event is emitted. If not, a TransactionFailed event is emitted. 💬

🔒 Advantages of Using MyMultiSig.app

  1. Security: 🔒 A multisig contract requires multiple signatures before a transaction can be executed, making it more secure compared to a single signature transaction.

  2. Decentralization: 💪 The multisig contract can be managed by multiple parties, promoting decentralization and reducing the risk of a single point of control.

  3. Flexibility: 💡 The contract can be customized to fit the specific requirements of different organizations, including the number of signatures required and the threshold.

  4. Transparency: 🔍 All transactions executed by the multisig contract are recorded on the Ethereum blockchain, providing a transparent and auditable trail of all transactions.

Note: This smart contract is currently in beta, use at your own risk.

🔌 EIP-1271

The wallet implements the EIP-1271 standard:

function isValidSignature(bytes32 hash, bytes calldata signature)
    external view returns (bytes4 magicValue);

signature is an ABI-encoded (address owner, bytes sig)[] of owner votes. For each entry the wallet either ecrecovers a 65-byte ECDSA signature (EOA owner) or staticcalls isValidSignature(hash, sig) on the owner with a 200k gas stipend (contract owner). If the count of valid votes reaches threshold, returns the magic value 0x1626ba7e; otherwise 0xffffffff.

Off-chain, building the signature blob is one line:

import { ethers } from 'ethers'
const blob = ethers.utils.defaultAbiCoder.encode(
  ['tuple(address owner, bytes sig)[]'],
  [[{ owner: owner1, sig: sig1 }, { owner: owner2, sig: sig2 }]],
)

The same encoding is used by execTransaction and isValidSignature(address,...,bytes). The pre-0.2.0 flat 65-byte chunk format is no longer supported — this is a breaking change documented in the v0.2.0 release notes.

⏰ v0.3.0 — deadlines, revokes, atomic batches

v0.3.0 hardens the wallet against three treasury pain points:

  1. validUntil deadline. Every EIP-712 transaction hash now binds a uint256 validUntil field. Signers set a Unix timestamp past which the signature is invalid; execTransaction reverts with SignatureExpired() if a stale payload shows up. validUntil == 0 means "no expiry" — legacy wallets keep working as long as they pass 0. Breaking: every v0.2.0 off-chain payload is invalidated; update your signer to include validUntil in the typed-data Transaction struct and pass it through to execTransaction. The new 6-arg base-wallet overload execTransaction(to, value, data, gas, validUntil, signatures) and the 7-arg Extended overload execTransaction(to, value, data, gas, nonce, validUntil, signatures) are the supported entry points.

  2. revokeApproval(bytes32 hash). An owner can now withdraw their own on-chain approval without burning the whole nonce. Self-only — no admin override — reverts with NotApproved() if you try to revoke something you never approved. Emits RevokeApproval(address indexed owner, bytes32 indexed hash).

  3. multiRequestStrict(address[], uint256[], bytes[], uint256[]). New atomic-batch entry point: reverts the whole transaction on first failure (no partial side effects, no MultiRequestExecuted event). Use it when the second call depends on the first (e.g. approve-then-swap). Failure bubbles as BatchCallFailed(uint256 index, bytes reason). The original multiRequest continues to be best-effort — every call runs, partial failures are surfaced via the existing successes[] / returnData[] arrays.

As of v0.5.0 every wallet class — MyMultiSig, MyMultiSigExtended, MyMultiSigFactorable, and the MyMultiSigFactory proxy — returns the same canonical '0.5.0' from version(). The EIP-712 domain separator is fixed at deploy time and is now shared across the wallet family; only the typehash differs (base 6-field vs extended 7-field with operation). Helpers in test/shared/signatures.ts, test/shared/functions.ts, and the Foundry equivalents have been updated to thread validUntil / operation through the right typehash; see the test suite for usage patterns.

🛡️ v0.4.0 — Timelock, Guard, Allowances, Modules

MyMultiSigExtended v0.4.0 adds four optional features, all disabled by default so previously-deployed wallets behave unchanged until the new setters are called. Every wallet class returns '0.5.0' from version() (as of v0.5.0).

1. ⏰ Timelock on sensitive calls

Schedule/ready pattern. A call is "sensitive" when it targets address(this) at a registered sensitive selector, or when its value meets the configured wei threshold. The constructor pre-registers the wallet's admin selectors (incl. enableModule/disableModule) so the timelock applies to every privileged action by default.

wallet.execTransaction(address(wallet), 0,
    abi.encodeWithSignature("setTimelockDelay(uint256)", 1 days), gas, sigs);
// Direct addOwner(...) now reverts SensitiveCallRequiresDelay. Schedule instead:
wallet.scheduleTransaction(target, 0, addOwnerCalldata, gas, nonce, validUntil, sigs);
// After timelockDelay seconds:
wallet.executeScheduled(target, 0, addOwnerCalldata, gas, nonce, validUntil, sigs);

Gotchas: schedule is keyed by txHash, so any payload mutation produces a new id. Replays are blocked by a type(uint256).max sentinel. validUntil bounds the whole schedule window (re-checked at execute). executeScheduled deliberately does NOT bump _txnNonce (the (nonce, owner) anti-replay slots consumed at schedule time already block any further tx at this nonce without fresh sigs).

2. 🛡️ Transaction guard + built-in allowlist

Pluggable ITransactionGuard (pre-call checkTransaction + silent post-call checkAfterExecution). Reverts are wrapped into GuardReverted(guard, reason). A built-in target allowlist is also available for off-chain / no-guard use.

wallet.execTransaction(address(wallet), 0,
    abi.encodeWithSignature("setGuard(address)", guardAddr), gas, sigs);
wallet.execTransaction(address(wallet), 0,
    abi.encodeWithSignature("setAllowedTarget(address,bool)", safeTarget, true), gas, sigs);

Gotchas: the allowlist is OFF until the first setAllowedTarget(...) flips it on. Guard + allowlist apply inside multiRequest* (per inner call) and execTransactionFromModule. checkAfterExecution failures are NEVER reverts.

3. 💸 Per-owner daily spending allowance

Single-signer entry point: execTransactionWithSpendingAllowance(to, value, data, gas, validUntil, signatures). Requires a single 65-byte ECDSA sig that recovers to msg.sender, who must be a current owner with a non-zero daily cap.

wallet.execTransaction(address(wallet), 0,
    abi.encodeWithSignature("setDailySpendingLimit(address,uint256)", owner01, 5 ether), gas, sigs);
bytes memory sig = sign(owner01, wallet, recipient, 1 ether, "0x", gas, nonce, 0);
wallet.execTransactionWithSpendingAllowance(recipient, 1 ether, "0x", gas, 0, sig);

Gotchas: 24h relative window per owner (not UTC midnight). Commit-on-success: failed inner calls don't burn the cap. The allowance path does NOT bump _txnNonce (it's a UX shortcut).

4. 🧩 Modules / plugins

Linked-list enabled module registry (Safe ModuleManager pattern). Modules bypass the signature threshold — they're operational plugins (recovery, streaming, automation). Use MyMultiSigFactory.createMyMultiSigAdvanced for distinct factory bookkeeping.

wallet.execTransaction(address(wallet), 0,
    abi.encodeWithSignature("enableModule(address)", moduleAddr), gas, sigs);
module.execCall(target, value, data);     // CALL  (op=0)
module.execDelegateCall(calldata);       // DELEGATECALL (op=1, to == wallet only)

Gotchas: disableModule(prev, module) follows Safe's strict pattern — when the module is the head, prev MUST be address(0); otherwise _modulesNext[prev] == module must hold. Module-driven calls do NOT bump _txnNonce. Guard + allowlist still apply; timelock does NOT.

Factory + introspection

MyMultiSigFactory.createMyMultiSigAdvanced(...) produces the v0.4.0 wallet through a new MyMultiSigAdvancedDeployer (a tiny wrapper that defers to MyMultiSigExtendedDeployer; the v0.4.0 wallet bytecode is currently identical to v0.3.0 Extended, and the distinction lives in factory bookkeeping until a future Advanced-only release).

advancedFeaturesEnabled() returns a bitmask (1=timelock, 2=guard, 4=allowlist, 8=allowance, 16=module) for UI/explorer use.

Factory bookkeeping (v0.4.0)

The factory now tracks per-type counts and exposes an address-keyed type lookup:

uint256 simpleCount();      // base MyMultiSig wallets
uint256 extendedCount();     // MyMultiSigExtended (incl. Advanced)
uint256 advancedCount();     // wallets created via the Advanced deployer
uint256 creationTypeCount(MyMultiSigFactorableModels.CreationType);
MyMultiSigFactorableModels.CreationType creationTypeOf(address wallet);
bool isExtended(address wallet);    // true for Extended + Advanced

MyMultiSigAdvancedTests() is the new exported test group in test/shared/tests.ts (entry files test/MyMultiSigAdvanced.test.ts and test/MyMultiSigAdvancedFromFactory.test.ts); Foundry mirrors under contracts/test/shared/tests.t.sol. Helpers (setTimelockDelay, enableModule, …) live in test/shared/functions.ts.

Cleanups bundled with v0.4.0

  • MyMultiSig.verifyNonce(uint256) removed (zero references).
  • MyMultiSig._changeThreshold now emits ThresholdChanged(uint256) (event declared at line 52 but never previously emitted).

🚀 v0.5.0 — DELEGATECALL op + ERC-4337 rolled into MyMultiSigExtended

⚠️ v0.5.0 ships the new wallet features as additions to the existing MyMultiSigExtended class — there is no separate wallet contract beyond MyMultiSigExtended. All wallets deployed through the factory after this release are MyMultiSigExtended instances that now also expose the operation byte and the IAccount surface.

MyMultiSigExtended v0.5.0 adds three forward-looking capabilities on top of the existing v0.4.0 features (timelock, guard, allowance, modules):

1. 🛡️ operation byte on the owner-signed execTransaction

A new uint8 operation parameter is bound into the EIP-712 typed-data payload. Three new execTransaction overloads:

execTransaction(to, value, data, gas, operation, signatures) -> success
execTransaction(to, value, data, gas, validUntil, operation, signatures) -> success
execTransaction(to, value, data, gas, txnNonce, validUntil, operation, signatures) -> success

operation == 0 (default) → standard CALL. operation == 1 → DELEGATECALL into to, gated to to == address(this) (mirrors the same gating already used by MyMultiSigExtended.execTransactionFromModule). Anything else reverts with the new custom error InvalidOperation(uint8 op).

⚠️ Breaking: the pre-existing 5/6/7-arg execTransaction overloads on MyMultiSigExtended are disabled — they now revert with RequiresOperationByte(). All frontends must switch to the new overloads that include operation. The base MyMultiSig wallet is untouched and continues to expose the old 5/6-arg signatures against the v0.4.0 typehash.

⚠️ EIP-712 footer changed. The new typehash binds operation:

Transaction(address to, uint256 value, bytes data, uint256 gas, uint96 nonce, uint256 validUntil, uint8 operation)

Existing v0.4.0 MyMultiSigExtended signatures do not validate on v0.5.0 wallets because the typehash includes operation. Migrate your off-chain signer to bind operation (default 0) into the typed data before deploying.

2. ⚡ ERC-4337 v0.7 account abstraction

MyMultiSigExtended implements IAccount against an immutable IEntryPoint v0.7 address passed at construction. The canonical EntryPoint v0.7 address (same on every EVM chain) is 0x0000000071727De22E5E9d8BDe0dFeC0CEB6a7d7.

Constructor signature: new MyMultiSigExtended(name, owners, threshold, isOnlyOwnerRequest, entryPoint). Bundler flow:

  1. Bundler calls EntryPoint.handleOps([userOp], beneficiary).
  2. EntryPoint calls the wallet's validateUserOp(PackedUserOperation, bytes32 userOpHash, uint256 missingAccountFunds). Returns 0 on success / 1 (SIG_VALIDATION_FAILED) on failure. Requires msg.sender == ENTRY_POINT, userOp.sender == address(this), operation == 0, userOp.nonce == _txnNonce, and threshold reached via the same signature-validation path used by execTransaction.
  3. EntryPoint funds execution and calls executeUserOp(PackedUserOperation), which reconstructs the inner call from userOp.callData = abi.encode(address to, uint256 value, bytes data, uint256 gas, uint256 validUntil, uint8 operation) and runs it through the gated _execExtended path.

Paymaster support drops out naturally: a paymaster can pre-fund via EntryPoint.depositTo(address(this)); bundlers assemble UserOps with paymasterAndData != 0 and the wallet's validateUserOp is permissive on value-funded flows.

3. 🌐 CREATE2 deterministic wallets

The factory exposes a deterministic (CREATE2) creation path next to each classic one. Wallets deploy via salted creation (new MyMultiSig{salt: ...}(...) inside the deployers) — full creation bytecode, ordinary constructors, no proxies and no Initializable — so the deployed wallets are byte-identical to the classic-path ones.

| Classic (CREATE) | Deterministic (CREATE2) | Predict without deploying | | --- | --- | --- | | createMultiSig(name, owners, threshold) | createDeterministicMultiSig(..., salt) | predictMultiSigAddress(creator, ..., salt) | | createMyMultiSigExtended(...) | createDeterministicMyMultiSigExtended(..., salt) | predictMyMultiSigExtendedAddress(creator, ..., salt) | | createMyMultiSigAdvanced(...) | createDeterministicMyMultiSigAdvanced(..., salt) | predictMyMultiSigAdvancedAddress(creator, ..., salt) |

Address derivation:

  • The factory forwards computeSalt(msg.sender, salt) = keccak256(abi.encode(creator, salt)) to the deployer, so two creators using the same salt never race for one address.
  • Each deployer additionally namespaces the salt by its direct caller (keccak256(abi.encode(msg.sender, salt))) before the CREATE2. A third party calling a deployer directly therefore lands in its own address space and can never squat an address that a factory-mediated deploy resolves to. It also keeps Advanced deploys (routed through MyMultiSigAdvancedDeployerMyMultiSigExtendedDeployer) collision-free against Extended deploys that share a salt and constructor arguments.
  • The final wallet address is a pure function of (deployer address, creator, salt, constructor arguments). Deploy the factory + deployer set at the same addresses on several chains (see FACTORY_SALT / CANONICAL_CREATE2_DEPLOYER in constants/extended.ts) and the same creator + salt + arguments yield the same wallet address on every chain.

Re-using a salt with identical arguments reverts (the CREATE2 target address is occupied). ERC-4337 note: validateUserOp requires the wallet to be already deployed (userOp.sender == address(this)); the deterministic path gives counterfactual addresses, not initCode-based counterfactual deployment.

Migration story

Existing v0.4.0 wallets (deployed before this release) stay frozen at their original bytecode — they bind to the old 6-field EIP-712 typehash and the same EIP-712 domain separator format but with version() == '0.5.0' (matching the unified v0.5.0 versioning) and do not have the v0.5.0 operation-byte surface. To migrate, the owners redeploy MyMultiSigExtended with the new constructor (passing entryPoint) and move funds.

Cleanups bundled with v0.5.0

  • Single canonical version 0.5.0 across every contract. MyMultiSig.version(), MyMultiSigExtended.version(), MyMultiSigFactorable.version(), MyMultiSigFactory.version() (inherited), package.json, and the CONTRACT_VERSION TS constant all return '0.5.0'. The EIP-712 domain separator is therefore shared across the whole wallet family; only the typehash differs.
  • MyMultiSigExtendedDeployer.deployMyMultiSigExtended(...) and MyMultiSigAdvancedDeployer.deployMyMultiSigAdvanced(...) now take an entryPoint argument and forward it through.
  • MyMultiSigFactorable.createMyMultiSigExtended(...) and createMyMultiSigAdvanced(...) now take an entryPoint and forward it.
  • New custom errors: InvalidOperation(uint8), NotEntryPoint(), InvalidNonce(expected, got), RequiresOperationByte(). New events: TransactionExecutedOp(...), TxFailureOp(...), UserOpExecuted(bytes32 userOpHash, uint256 nonce).

🔧 Install Dependencies

To install all necessary dependencies, run the following command:

yarn

💻 Run Tests

To run tests, you have the option to use either Hardhat or Foundry.

🔨 Hardhat Tests

To run tests using Hardhat, use the following command:

yarn hardhat test

Additionally, you can run a coverage report using Hardhat with the following command:

yarn coverage

🔥 Foundry Tests

To run tests using Foundry, use the following command:

forge test

💥 Hardhat & Foundry Tests

To run tests using both Hardhat and Foundry, use the following command:

yarn test

🚀 Deploy Locally

To deploy the contract locally, you'll need to run a node with Hardhat.

In a first terminal, run the following command to start the Hardhat node:

npx hardhat node

In a second terminal, while the node is active, run the following command to deploy the contract:

yarn deploy-localhost

🙏 Acknowledgements

This project relies on the amazing work done by the Hardhat and Foundry teams. Thank you for your contributions to the Ethereum community!