B2BinPay smart contracts for managing deposits and withdrawals of ETH and ERC20 tokens using CREATE2 for deterministic address generation.
$ npm install$ npm run compileQuick start:
$ npm run deployTo deploy on the NETWORK copy .env.blank to .env.[BLOCKCHAIN].[NETWORK] and populate values.
.env.eth.sepolia.env.bsc.testnet.env.base.sepolia.env.arbitrum.sepolia.env.optimism.sepolia.env.avalanche.fuji.env.polygon.amoy
NOTE: For non-local deployment, enable the optimizer in hardhat.config.js.
# Deploy on the `Ethereum Sepolia`
$ npm run deploy:eth:sepolia
# Deploy on the `Binance Testnet`
$ npm run deploy:bsc:testnetTo deploy on the Mainnet copy .env.blank to .env.[BLOCKCHAIN].mainnet and populate values.
# Deploy on the `Ethereum Mainnet`
$ npm run deploy:eth:mainnet
# Deploy on the `Binance Mainnet`
$ npm run deploy:bsc:mainnetQuick start:
$ npm run demo
$ npm run demo:a:eth:sepolia # Demo for Factory A on Ethereum Sepolia
$ npm run demo:a:bsc:testnet # Demo for Factory A on Binance Testnet
$ npm run demo:b:eth:sepolia # Demo for Factory B on Ethereum Sepolia
$ npm run demo:b:bsc:testnet # Demo for Factory B on Binance Testnet
$ npm run demo:multisig:eth:sepolia # Demo for MultiSigWallet on Ethereum Sepolia
$ npm run demo:multisig:bsc:testnet # Demo for MultiSigWallet on Binance Testnet
$ npm run demo:multisig:arbitrum:sepolia # Demo for MultiSigWallet on Arbitrum Sepolia
$ npm run demo:multisig:base:sepolia # Demo for MultiSigWallet on Base Sepolia
$ npm run demo:multisig:optimism:sepolia # Demo for MultiSigWallet on Optimism Sepolia
$ npm run demo:multisig:polygon:amoy # Demo for MultiSigWallet on Polygon Amoy
$ npm run demo:multisig:avalanche:fuji # Demo for MultiSigWallet on Avalanche Fuji
$ npm run demo:short:multisig:eth:mainnet # Demo for MultiSigWallet on Ethereum Mainnet
$ npm run demo:short:multisig:bsc:mainnet # Demo for MultiSigWallet on Binance Mainnet
$ npm run demo:short:multisig:arbitrum:sepolia # Demo for MultiSigWallet on Arbitrum Sepolia
$ npm run demo:short:multisig:base:sepolia # Demo for MultiSigWallet on Base Sepolia
$ npm run demo:short:multisig:optimism:sepolia # Demo for MultiSigWallet on Optimism Sepolia
$ npm run demo:short:multisig:polygon:amoy # Demo for MultiSigWallet on Polygon Amoy
$ npm run demo:short:multisig:avalanche:fuji # Demo for MultiSigWallet on Avalanche Fuji
NOTE: To obtain the same contract addresses, run from the same deployer account for one factory on both blockchains. Set MULTICHAIN_CHECK=true to only check CREATE2 and obtain identical precomputed DepositAccount addresses.
# Run tests
$ npm test
# Watch tests
$ npm test:watch
# Coverage
$ npm run coverage# Report gas
$ npm run gas:report
# Contract sizes
$ npm run size
# Solidity version
$ npm run solc
# EVM version
$ npm run evm
# Optimizer config
$ npm run optimizerMinimal contract deployed by Factory[X] and MultiSigWallet to handle deposits.
Key Features:
- Self-destructs after withdrawal
- Handles both ETH and ERC20 tokens
- No storage variables
- Minimal gas usage
- Full test coverage
Main contract that manages the creation and handling of deposit accounts.
Adapted for ERC-1167 minimal proxy clones.
Key Features:
- Creates deterministic deposit addresses using
CREATE2 - Handles both ETH and ERC20 token withdrawals
- Supports batch operations
- Owner-controlled access
- Full test coverage
// Triggered when the contract has been initialized.
event Initialized();
event Claim(
address indexed recipient,
address deposit,
address token,
uint256 tokenAmount,
uint256 ethAmount
);error InvalidInitialization();
error UnauthorizedAccount(address account);
error InvalidRecipient(address recipient);
error FailedDeployment();// Returns contract version
function version() public pure returns (string)
// Returns owner address
function owner() public view returns (address)
// Returns recipient address
function recipient() public view returns (address)
// Returns current token address
function token() public view returns (address)
// Gets multiple deposit addresses
function getAccounts(bytes32[] accountId) public view returns (bytes32[], address[] accounts)// This method can only be called once and only when owner is not set (for proxy clones).
function initialize(address owner_) external
// Transfer funds from deposit to owner
function claim(address token, bytes32[] accountId) external
// Transfer funds to specific recipient
function claimTo(address to, address token, bytes32[] accountId) externalMain contract that manages the creation and handling of deposit accounts.
Note: For ERC-1167 FactoryB doesn't require initialization, use empty initializer.
Key Features:
- Recipient-specific deposits
- No owner restrictions (anyone can initiate withdrawals)
- Creates deterministic deposit addresses using
CREATE2 - Handles both ETH and ERC20 token withdrawals
- Supports batch operations
- Configurable recipient address
- Full test coverage
event Claim(
address indexed recipient,
address deposit,
address token,
uint256 tokenAmount,
uint256 ethAmount
);error InvalidRecipient(address recipient);
error FailedDeployment();
error InvalidArrayLength(uint256 recipientsLength, uint256 accountIdsLength);// Returns contract version
function version() public pure returns (string memory)
// Returns recipient address
function recipient() public view returns (address)
// Returns current token address
function token() public view returns (address)
// Gets deposit address by recipient address and account ID
function getAccount(address recipient, uint256 accountId) public view returns (bytes bytecode, bytes32 hash, address account);
// Gets multiple deposit addresses
function getAccounts(address[] recipients, uint256[] accountIds) public view returns (bytes32[] hashes, address[] accounts)// Transfer funds from deposit to recipient address
function claim(address token, address recipient, bytes32[] accountIds) externalMulti-signature contract that manages the creation and handling of deposit accounts with advanced security features.
Important: When deployed directly, MultiSigWallet should only be used as an implementation contract (master copy). In general, you should use ProxyFactory to create new wallet instances via ERC-1167 minimal proxy clones, which significantly reduces deployment gas costs. The ProxyFactory creates lightweight proxy contracts that delegate all calls to the singleton implementation.
Key Features:
- Multi-signature wallet functionality (M-of-N)
- EIP1271 standards for signature verification
- EIP712 compliant signature verification
- Creates deterministic deposit addresses using
CREATE2 - Claim access restricted to owners and whitelisted addresses
- Whitelist managed via multisig-authorized
setWhitelist - Multisig for payout operations (only owners can initiate payouts)
- Handles both ETH and ERC20 token withdrawals and payouts
- Supports multicall operations
- Configurable owner threshold
event ExecuteSuccess(uint256 nonce, bytes32 digiest, bytes32 id);
event SetConfig(address sender, uint256 ownerCount, uint256 threshold);
event SetWhitelist(address indexed account, bool status);
event Claim(
address indexed recipient,
address deposit,
address token,
uint256 tokenAmount,
uint256 ethAmount
);error InvalidOwner(address owner);
error DuplicateOwner(address owner);
error InvalidSignature(address owner);
error InvalidSignatureOrder(address owner);
error InvalidSignatureData();
error InvalidSignatureLength(uint256 length);
error InsufficientSignatures(uint256 signatures, uint256 threshold);
error UnauthorizedAccount(address account);
error InvalidRecipient(address recipient);
error FailedDeployment();
error InvalidRequirement(uint256 ownerCount, uint256 threshold);
error InvalidWhitelistUpdate(uint256 accounts, uint256 statuses);
error InvalidWhitelistAddress(address account);// Returns contract version
function version() external pure returns (string)
// Returns recipient address
function recipient() external view returns (address)
// Returns current token address
function token() external view returns (address)
// Returns list of current owners
function owners() external view returns (address[])
// Returns current required signatures threshold
function threshold() external view returns (uint256)
// Returns current transaction nonce
function nonce() external view returns (uint256)
// Checks if address is an owner
function isOwner(address account) external view returns (bool)
// Returns list of whitelisted addresses
function whitelist() external view returns (address[])
// Checks if address is whitelisted
function isWhitelisted(address account) external view returns (bool)
// Gets deposit address for an account ID
function getAccount(bytes32 accountId) external view returns (bytes, address)
// Gets multiple deposit addresses
function getAccounts(bytes32[] accountIds) external view returns (bytes32[], address[])
// ERC-1271 standard signature validation method for contracts
function isValidSignature(bytes32 hash, bytes signatures) public view returns (bytes4 magicValue)
// Checks whether the signature provided is valid for the provided data hash.
function checkSignatures(bytes32 hash, bytes calldata signatures) external view// Sets an initial owners. This method can only be called once.
function setup(address[] calldata owners_, uint256 threshold_) external
// Updates `owners` with `threshold` (requires signature)
function setConfig(address[] calldata owners_, uint threshold_) external
// Updates claim whitelist (requires signature). true = add, false = remove.
function setWhitelist(address[] calldata accounts, bool[] calldata statuses) external
// Batch transfer from multiple deposits to factory (owners and whitelisted only)
function claim(address token, bytes32[] calldata accountIds) external
// Batch transfer from multiple deposits to recipient (call via `execute`)
function claimTo(address erc20, address to, bytes32[] calldata accountIds) external onlyFactory
// Execute multi-sig transaction
execute(Operation[] operations) external returns (bytes[][] results)struct Call {
address to;
uint256 value;
bytes data;
}
struct Operation {
Call[] calls;
bytes signatures;
bytes32 id;
}Factory contract that creates new instances of Factory[X] and MultiSigWallet contracts using CREATE2 for deterministic address generation.
The ProxyFactory deploys ERC-1167 minimal proxy contracts (clones) that delegate all calls to a singleton implementation contract. The proxy contract stores the implementation address at a fixed storage slot, making it lightweight and inexpensive to deploy. This approach significantly reduces deployment gas costs compared to deploying the full contract bytecode, while the actual logic is handled by the singleton implementation contract.
For more information on how the proxy is deployed and the factors that determine the address of the proxy, see the computeAddress function.
Key Features:
- Deterministic contract deployment using
CREATE2 - Single-transaction deployment and initialization
- Predictable address computation
- Deployment validation
- ERC-1167 minimal proxy pattern for gas-efficient deployments
event CreateSuccess(address indexed factory);error FailedDeployment();// Returns the address where a contract will be stored if deployed via {createContract}
function computeAddress(
address implementation, // The address of the implementation contract to clone.
bytes initializer, // Payload for a message call to be sent to a new contract.
bytes32 saltNonce // Nonce that will be used to generate the salt to calculate the address of the new contract.
) external view returns (
bytes32 salt, // CREATE2 salt
address wallet // Expected deployment address
)// Creates new contract instance with optional initialization via {setup}
function createInstance(
address implementation, // The address of the implementation contract to clone.
bytes initializer, // Payload for a message call to be sent to a new contract.
bytes32 saltNonce // Nonce that will be used to generate the salt to calculate the address of the new contract.
) external returns (address wallet)// Get deterministic deposit address
[ids, accounts] = await factory.getAccounts([id]);// Deposit ETH
await sender.sendTransaction({ to: accounts[0], value: amount });
// Deposit ERC20
await token.transfer(accounts[0], amount);// Transfer both ETH and ERC20
await factory.claim(token /* ZERO_ADDRESS for ETH only */, [id]);
// Transfer both ETH and ERC20 to specific address
await factory.claimTo(recipient, token, [id]);
// Batch transfers
await factory.claim(token, [id1, id2, id3]);// Get deterministic deposit address
[bytecode, salt, account] = await factory.getAccount(recipient, accountId);// Deposit ETH
await sender.sendTransaction({ to: account, value: amount });
// Deposit ERC20
await token.transfer(account, amount);// Single transfer
await factory.claim(token /* ZERO_ADDRESS for ETH only */, [recipient], [accountId]);
// Batch transfer
await factory.claim(token, [recipient1, recipient2], [accountId1, accountId2]);// Get deterministic deposit address
[bytecode, account] = await factory.getAccount(recipient, accountId);// Deposit ETH
await sender.sendTransaction({ to: account, value: amount });
// Deposit ERC20
await token.transfer(account, amount);// Single transfer
await factory.claim(token /* ZERO_ADDRESS for ETH only */, [accountId]);
// Batch transfer
await factory.claim(token, [accountId1, accountId2]);Signatures use a packed binary format that supports both EOA and smart contract (ERC-1271) owners:
[signer:20 bytes][sigLen:2 bytes (uint16 BE)][sig:sigLen bytes] x N
- Signers must be sorted in strictly ascending address order (no duplicates)
- EOA owners:
sigLenmust be exactly 65 bytes (standard ECDSAr,s,v) - Contract owners (ERC-1271):
sigLenis variable, validated viaisValidSignature(bytes32,bytes)
// [viem.js]
// Generate payout data and signatures (single owner)
// ETH
const callETH: Call = { to: RECIPIENT, value: AMOUNT, data: "0x" };
// ERC20
const abi = parseAbi(["function transfer(address to, uint256 value) external returns (bool)"]);
const encodedData: Hex = encodeFunctionData({
abi,
functionName: "transfer",
args: [RECIPIENT, AMOUNT],
});
const callERC20: Call = { to: ERC20, value: 0n, data: encodedData };
// EIP-712 signature of the typed data
const signature = await signTypedData(owner, factory, [callETH, callERC20]);
// Pack signature: sort by signer address, encode as [signer:20][sigLen:2][sig:sigLen]
const packed = await packSignatures(digest, [signature]);
const operations = [
{ calls: [callETH, callERC20], signatures: packed, id: zeroHash },
];
// Execute payout
await factory.write.execute([operations]);For contract owners (ERC-1271), use packSignedData directly with the contract address as signer:
const packed = packSignedData([
{ signer: contractOwnerAddress, signature: sig },
]);// [viem.js]
// Generate initialization data
const owners = [owner1.address, owner2.address];
const threshold = 2n;
const saltNonce = '0x000000000000000000000000000000000000000000000000001ffffffffffffe';
const abi = parseAbi([
'function setup(address[] owners, uint256 threshold)'
]);
const initializer = encodeFunctionData({
abi,
functionName: 'setup',
args: [owners, threshold]
});
// Get artifact bytecode (creation bytecode)
const bytecode = (await hre.artifacts.readArtifact("MultiSigWallet")).bytecode;
// Compute expected address
const [salt, address] = await proxy.computeAddress(
bytecode,
initializer,
saltNonce
);// [viem.js]
// Generate initialization data
const owners = [owner1.address, owner2.address];
const threshold = 2n;
const saltNonce = '0x000000000000000000000000000000000000000000000000001ffffffffffffe';
const abi = parseAbi([
'function setup(address[] owners, uint256 threshold)'
]);
const initializer = encodeFunctionData({
abi,
functionName: 'setup',
args: [owners, threshold]
});
// Get artifact bytecode (creation bytecode)
const bytecode = (await hre.artifacts.readArtifact("MultiSigWallet")).bytecode;
// Deploy and initialize in single transaction
await proxy.write.createContract([ bytecode, initializer, saltNonce ]),