Skip to content

About

multisig wallet that works with off-chain signatures, using ethers.js and foundry. transferring SPD tokens as an example using the SPD ERC-20 token: https://github.com/arshamhaq/ERC20-SPD

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

4 Commits

Folders and files

Repository files navigation

Off-chain Multisig Wallet

Off-chain and on-chain multisig flow

A 2-of-3 EVM wallet where owners sign an EIP-712 transaction off-chain and a relayer submits the signatures on-chain. The included example transfers 100 Spades (SPD) tokens from the wallet to Bob.

Educational code. It has not received a production security audit.

How it works

  1. Alice and Bob independently sign the same Transaction with ethers.js.
  2. The signed fields bind to, native value, calldata, nonce, and deadline.
  3. The EIP-712 domain binds the signature to MultiSigWallet, version 1, the current chain ID, and the deployed wallet address.
  4. A relayer calls executeTransaction; the wallet recovers distinct owner addresses with OpenZeppelin ECDSA and requires exactly two signatures.
  5. The wallet consumes the nonce and calls the target. In the example, it calls SPD.transfer(bob, 100 ether) and the TypeScript client verifies Bob's balance increase.

The deployer, Alice, and Bob are owners. The threshold is two, so the deployer does not sign the included Alice-and-Bob example.

Install

Requirements: Foundry, Git, Make, Node.js 20+, and npm.

git clone https://github.com/arshamhaq/multisig-wallet
cp .env.example .env
make install
make check

make install installs pinned OpenZeppelin/forge-std dependencies and the locked npm dependencies. The sample Anvil keys are public test keys; never use them on a public network.

Run locally

Start Anvil in one terminal:

make anvil

In another terminal, deploy SPD and the wallet:

make deploy-anvil

The script deploys local SPD, deploys the 2-of-3 wallet, and mints the configured SPD amount to Alice, Bob, and the wallet. Copy the two printed addresses into:

SPD_ADDRESS_ANVIL=<printed SPD address>
MULTISIG_ADDRESS_ANVIL=<printed wallet address>

Then sign, relay, and verify the transfer:

make run-anvil

The client prints the transaction hash and Bob's SPD balances before and after. It reads the current wallet nonce, uses a one-hour deadline, signs with Alice and Bob, and sends through RELAYER_PRIVATE_KEY_ANVIL.

Sepolia

Fill the _SEPOLIA values in .env, including funded deployer and relayer keys, Alice and Bob's owner addresses, signer keys, and RPC URL. Then run:

make deploy-sepolia
make run-sepolia

Sepolia uses the existing SPD contract at 0x805540aC3b8AE7dE3c991Df03999AD6fa36ca914. The deployer must already have its MINTER_ROLE and enough supply must remain under the token cap. After deployment, set MULTISIG_ADDRESS_SEPOLIA to the printed wallet address.

The deployment script only needs Alice and Bob's public *_ADDRESS_* values. Their private keys are used solely by the off-chain signer and should never be shared with the deployer in a real multisig setup.

Contracts and client

The wallet accepts plain ETH through receive(). It intentionally has no fallback function, so direct unknown calldata reverts. Signed execution can still call any target's receive, fallback, or function through executeTransaction.

Commands

Command Purpose
make help List the workflow commands.
make install Install Solidity and npm dependencies.
make check Check formatting, build both projects, and run tests.
make test-verbose Run Foundry tests with detailed traces.
make deploy-anvil / make deploy-sepolia Deploy for the selected chain.
make run-anvil / make run-sepolia Run the TypeScript signing flow.

The test suite covers EIP-712 parity, SPD and native-ETH execution, receive and fallback behavior, return/revert data, nonce replay, expiry, insufficient balance, duplicate/unauthorized signatures, constructor invariants, transaction tampering, and fuzzed value/calldata forwarding.

To inspect local SPD balances directly:

set -a; source .env; set +a
cast call "$SPD_ADDRESS_ANVIL" "balanceOf(address)(uint256)" \
  "$(cast wallet address --private-key "$BOB_PRIVATE_KEY_ANVIL")" \
  --rpc-url "$RPC_URL_ANVIL"
cast call "$SPD_ADDRESS_ANVIL" "balanceOf(address)(uint256)" \
  "$MULTISIG_ADDRESS_ANVIL" --rpc-url "$RPC_URL_ANVIL"

About

multisig wallet that works with off-chain signatures, using ethers.js and foundry. transferring SPD tokens as an example using the SPD ERC-20 token: https://github.com/arshamhaq/ERC20-SPD

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages