A traditional multisig vault implementation using Foundry, where ETH is stored directly in the contract and requires signatures from two owners to execute transfers.
The code inplements a 2/2 multi signature scheme in which two signatures are required to unlock the funds managed by the smart contract.
- Two-Owner Multisig: Requires both owners to authorize transfers
- Off-chain Signing: Owner2 signs messages off-chain, reducing gas costs
- ERC-1271 Compatible: Uses OpenZeppelin's SignatureChecker for signature verification
- Smart Contract Support: Supports both EOA and smart contract signers
- Direct ETH Storage: Contract acts as a vault for ETH
- Comprehensive Testing: Unit tests, integration tests, and JavaScript tests
- Owner1 initiates transfers
- Owner2 reviews and signs transfer approval off-chain
- Owner1 completes the transfer with Owner2's signature
- Contract verifies signature using ERC-1271 scheme (see previous commits for simpler ecrecover() based version)
# Install Foundry dependencies (including OpenZeppelin)
forge install
# Install Node.js dependencies
npm installNote: This project uses OpenZeppelin Contracts which are installed via Forge and gitignored. After cloning, make sure to run forge install to get the required dependencies.
anvilnpm run deployThis will:
- Generate two new wallets for Owner1 and Owner2
- Save private keys to
.envfile - Deploy the MultisigVault contract
- Fund the vault with 10 ETH
- Save deployment info to
deployment.json
npm run user1:initiate <recipient_address> <amount_in_eth>
# Example:
npm run user1:initiate 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 1.5This creates a pending transfer and saves details to transfer-{nonce}.json.
npm run user2:sign <nonce>
# Example:
npm run user2:sign 0Owner2 can review the transfer details before signing. The signature is saved to signature-{nonce}.json.
npm run user1:complete <nonce>
# Example:
npm run user1:complete 0Owner1 submits the signature to complete the transfer.
The project includes a unified test suite that runs both Solidity and JavaScript tests automatically:
# Run complete test suite (Foundry + JavaScript)
npm test
# or
npm run test:suiteThis will automatically:
- Build the contracts
- Start Anvil blockchain
- Run Foundry tests (22 Solidity tests)
- Run JavaScript integration test
- Stop Anvil and cleanup
# Foundry tests only (Solidity)
npm run test:foundry
forge test -vvv
# JavaScript tests only (requires Anvil running)
npm run test:js
# Specific Foundry test
forge test --match-test testCompleteTransferWithValidSignature
# Gas report
forge test --gas-report
# Watch mode for development
npm run test:watchinitiateTransfer(address to, uint256 amount): Start a new transfer (Owner1 only)completeTransfer(uint256 nonce, uint8 v, bytes32 r, bytes32 s): Complete transfer with signature (Owner1 only)deposit(): Deposit ETH into the vaultreceive(): Fallback function to receive ETH
getTransferDetails(uint256 nonce): Get details of a pending/completed transfergetMessageToSign(uint256 nonce): Get the message hash for Owner2 to signgetBalance(): Get vault's ETH balanceowner1(): Get Owner1's addressowner2(): Get Owner2's addresstransferNonce(): Get current transfer nonce
- Private Key Management: Store private keys securely, never commit
.envto version control - Signature Verification: All signatures are verified on-chain using OpenZeppelin's battle-tested SignatureChecker
- ERC-1271 Support: Supports both EOA signatures and smart contract signatures via ERC-1271 standard
- Access Control: Only Owner1 can initiate and complete transfers
- Replay Protection: Each transfer has a unique nonce
- Balance Checks: Contract verifies sufficient balance before transfers
multisig-vault/
├── src/
│ └── MultisigVault.sol # Main contract
├── test/
│ ├── MultisigVault.t.sol # Unit tests
│ └── integration/
│ └── MultisigVault.integration.t.sol # Integration tests
├── scripts/
│ ├── deploy.js # Deployment script
│ ├── user1-initiate.js # Initiate transfer
│ ├── user2-sign.js # Sign transfer
│ └── user1-complete.js # Complete transfer
├── test-js/
│ └── integration.test.js # JavaScript tests
├── foundry.toml # Foundry configuration
├── package.json # Node.js configuration
├── vitest.config.js # Vitest configuration
└── .env.example # Environment variables template
.env: Private keys and addresses (created during deployment)deployment.json: Contract address and deployment infotransfer-{nonce}.json: Transfer details for each initiated transfersignature-{nonce}.json: Signatures from Owner2
The contract is optimized for gas efficiency:
- Off-chain signing reduces transaction costs for Owner2
- Efficient storage layout using mappings
- Minimal state changes per transaction
- Try to add a third user / account that can sign the transaction
- Change the scheme of the multisig to be 2/3 (two users required in a 3 users multisig setup)
MIT
Say Hi on Twitter