Skip to content

b2binpay/smart-contracts

Repository files navigation

Overview

B2BinPay smart contracts for managing deposits and withdrawals of ETH and ERC20 tokens using CREATE2 for deterministic address generation.

Installation

$ npm install

Compile

$ npm run compile

Deploy

Quick start:

$ npm run deploy

To 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:testnet

To 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:mainnet

Demo

Quick 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.

Tests

# Run tests
$ npm test

# Watch tests
$ npm test:watch

# Coverage
$ npm run coverage

Utils

# 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 optimizer

Contracts Documentation

Table of contents


DepositAccount

Minimal 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

FactoryA

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
Events
// Triggered when the contract has been initialized.
event Initialized();

event Claim(
    address indexed recipient,
    address deposit,
    address token,
    uint256 tokenAmount,
    uint256 ethAmount
);
Custom Errors
error InvalidInitialization();
error UnauthorizedAccount(address account);
error InvalidRecipient(address recipient);
error FailedDeployment();
View Functions
// 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)
Write Functions
//  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) external

FactoryB

Main 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
Events
event Claim(
    address indexed recipient,
    address deposit,
    address token,
    uint256 tokenAmount,
    uint256 ethAmount
);
Custom Errors
error InvalidRecipient(address recipient);
error FailedDeployment();
error InvalidArrayLength(uint256 recipientsLength, uint256 accountIdsLength);
View Functions
// 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)
Write Functions
// Transfer funds from deposit to recipient address
function claim(address token, address recipient, bytes32[] accountIds) external

MultiSigWallet

Multi-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
Events
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
);
Custom Errors
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);
View Functions
// 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
Write Functions
// 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;
}

ProxyFactory

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
Events
event CreateSuccess(address indexed factory);
Custom Errors
error FailedDeployment();
View Functions
// 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
)
Write Functions
// 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)

Usage

FactoryA

Creating Deposit Address
// Get deterministic deposit address
[ids, accounts] = await factory.getAccounts([id]);
Depositing Funds
// Deposit ETH
await sender.sendTransaction({ to: accounts[0], value: amount });

// Deposit ERC20
await token.transfer(accounts[0], amount);
Withdrawing Funds
// 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]);

FactoryB

Creating Deposit Address
// Get deterministic deposit address
[bytecode, salt, account] = await factory.getAccount(recipient, accountId);
Depositing Funds
// Deposit ETH
await sender.sendTransaction({ to: account, value: amount });

// Deposit ERC20
await token.transfer(account, amount);
Withdrawing Funds
// Single transfer
await factory.claim(token /* ZERO_ADDRESS for ETH only */, [recipient], [accountId]);

// Batch transfer
await factory.claim(token, [recipient1, recipient2], [accountId1, accountId2]);

MultiSigWallet

Creating Deposit Address
// Get deterministic deposit address
[bytecode, account] = await factory.getAccount(recipient, accountId);
Depositing Funds
// Deposit ETH
await sender.sendTransaction({ to: account, value: amount });

// Deposit ERC20
await token.transfer(account, amount);
Withdrawing Funds
// Single transfer
await factory.claim(token /* ZERO_ADDRESS for ETH only */, [accountId]);

// Batch transfer
await factory.claim(token, [accountId1, accountId2]);
Signature Format (v1.1)

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: sigLen must be exactly 65 bytes (standard ECDSA r,s,v)
  • Contract owners (ERC-1271): sigLen is variable, validated via isValidSignature(bytes32,bytes)
Payout Funds
// [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 },
]);

ProxyFactory

Computing Deployment Address

// [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
);
Creating New Factory
// [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 ]),

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors