Skip to content

taurushq-io/deposit-contract

Repository files navigation

Smart Contract Deposit

This project has not undergone an audit and is provided as-is without any warranties.

This project presents a proof of concept (PoC) to manage multiple on-chain wallets, each represented by a smart contract called a Deposit Contract, through a single contract called the MasterContract which serves as the main entry point.

The MasterContract manages multiple deposit addresses deployed as minimal proxy contracts (EIP-1167 clones) and can execute transactions on behalf of these addresses without requiring each deposit contract to hold ETH for gas fees.

This approach provides a form of gas sponsorship: a single operator pays gas once to move funds out of many deposit contracts.

The supported token standards are ERC-20, ERC-721, and ERC-1155, as well as native ETH.

Comparison with Other Gas Sponsorship Solutions

Several Ethereum standards address the problem of gas sponsorship, i.e. allowing a user (or a contract) to have transactions executed without paying gas themselves. This section compares the Deposit Contract approach with ERC-2612, ERC-2771, ERC-4337, and EIP-7702.

Summary Table

Feature Deposit Contract (this project) ERC-2612 (Permit) ERC-2771 (Meta-Tx) ERC-4337 (Account Abstraction) EIP-7702 (Set Code for EOAs)
Scope Multi-wallet management with centralized gas payment Gasless ERC-20 approvals only Generic meta-transactions via trusted forwarder Full account abstraction with smart contract wallets Temporary/persistent code delegation for EOAs
User custody Funds are controlled by the MasterContract operator User retains full custody User retains full custody User delegates custody to a smart wallet. Owner of the smart wallet controls the smart wallet User retains full custody
Gas payer MasterContract operator (EOA) Relayer calling permit Gas relay (paymaster) via on-chain trusted forwarder Bundler, optionally reimbursed by a Paymaster Transaction sender or a sponsor
Protocol changes required None None None None (higher-layer only) Yes (new transaction type 0x04)
Token support ERC-20, ERC-721, ERC-1155, native ETH ERC-20 only contract must inherit ERC2771Context and authorizes the forwarder used by the paymaster/gas relayer Any Any
Signature required from user No Yes (EIP-712 off-chain signature) Yes (off-chain signature verified by forwarder) Yes (UserOperation signature) Yes (authorization tuple signature)
On-chain infrastructure MasterContract + DepositContract clones permit() function on the ERC-20 token Trusted Forwarder contract EntryPoint singleton + Bundler network + optionally paymaster None (native EVM support)
Decentralization Centralized (operator-controlled) Decentralized (user-initiated) Semi-decentralized (depends on relayer) Decentralized (open bundler network). Some ERC-4337 wallet can share wallet ownership between end-user and platform provider (MPC wallet notably) Decentralized (user-initiated)
Complexity Low Low Medium High Medium

When to Use This Solution

The Deposit Contract model is best suited for centralized or semi-centralized custody scenarios where:

  • An operator manages many deposit addresses (e.g., exchange hot wallets, payment processors, treasury management).
  • Deposit addresses only need to receive funds from external parties and have their funds withdrawn by a trusted operator.
  • Simplicity and low deployment cost are priorities over decentralization and user self-custody.
  • No user interaction or signature is needed at withdrawal time.

For use cases requiring user self-custody, decentralization, or general-purpose gas abstraction, solutions like ERC-4337 (with Paymasters) or EIP-7702 are more appropriate.

Architecture

Overview

The system consists of two contracts:

  • MasterContract: The central control contract that creates deposit contracts, queues transfers, and executes them. It uses OpenZeppelin's AccessControl for role-based permissions.
  • DepositContract: A minimal proxy contract that holds funds and delegates execution authority exclusively to the MasterContract.

Deposit contracts are deployed as EIP-1167 minimal proxy clones, making creation gas-efficient. They can also be deployed deterministically using CREATE2 for predictable addresses.

Access Control Roles

Role Description
DEFAULT_ADMIN_ROLE Full admin, inherits all other roles
PAUSER_ROLE Can pause/unpause the contract
CREATE_DEPOSIT_CONTRACT_ROLE Can create new deposit contracts
TRANSFER_REQUEST_CREATOR_ROLE Can queue and remove transfers
TRANSFER_REQUEST_EXECUTOR_ROLE Can execute queued transfers

Workflow

1. Deposit Contract Creation

Deposit contracts are created by calling createDepositContracts (non-deterministic) or createDeterministicDepositContracts (deterministic, using CREATE2 with a salt).

2. Receiving Funds

External users send funds (ETH, ERC-20, ERC-721, ERC-1155) directly to a deposit contract address. The deposit contract can receive ETH via its receive() function and tokens via standard transfer mechanisms (ERC721Holder, ERC1155Holder).

3. Queuing Transfers

The operator calls queueTransfers with an array of transfer requests. Each request specifies:

  • The source deposit contract
  • The recipient address
  • The token address and amounts/token IDs
  • The operation type (NATIVE, ERC20, ERC721, ERC1155)

4. Executing Transfers

The operator executes transfers by calling:

  • executeNextTransfer to process one transfer at a time (FIFO order)
  • flushPendingTransfers to execute all pending transfers in a single transaction

The MasterContract calls executeTransaction on each deposit contract, which performs the actual token transfer. The operator pays gas once for batch operations across many deposit contracts.

Directory

.
├── doc
│   ├── audit             # Audit reports
│   ├── compilation       # Flattened contracts for verification
│   ├── coverage          # Code coverage reports
│   ├── ERC               # Reference ERC/EIP specifications
│   ├── script            # Surya documentation scripts
│   └── surya             # Surya-generated diagrams
├── lib
│   └── openzeppelin-contracts
├── script                # Deployment scripts
├── src
│   ├── DepositContract.sol
│   ├── MasterContract.sol
│   ├── invariant         # Interfaces, errors, events, enums
│   ├── mocks             # Mock contracts for testing
│   └── modules           # AccessControlModule
└── test                  # Foundry tests

Schema

Deployment

depositContract-Page-creation.drawio

Execution

depositContract-Page-1.drawio

Master Contract

Inheritance

surya_inheritance_MasterContract.sol

Graph

surya_graph_AccessControlModule.sol

surya_graph_MasterContract.sol

Deposit Contract

Inheritance

surya_inheritance_DepositContract.sol

Graph

surya_graph_DepositContract.sol

Surya Description Report

Legend

Symbol Meaning
🛑 Function can modify state
💵 Function is payable

DepositContract

Contract Type Bases
Function Name Visibility Mutability Modifiers
DepositContract Implementation ReentrancyGuard, ERC1155Holder, ERC721Holder, DepositContractInvariant, DepositContractInterface
Public ❗️ 🛑 NO❗️
executeTransaction External ❗️ 🛑 nonReentrant onlyMaster
External ❗️ 💵 NO❗️

Master Contract

Contract Type Bases
Function Name Visibility Mutability Modifiers
MasterContract Implementation ReentrancyGuard, AccessControlModule, Pausable, MasterContractInvariant
Public ❗️ 🛑 AccessControlModule
pendingCurrentTransfers Public ❗️ NO❗️
pendingTransferCount Public ❗️ NO❗️
allDepositContracts Public ❗️ NO❗️
predictDeterministicAddress Public ❗️ NO❗️
predictDeterministicAddresses Public ❗️ NO❗️
checkSaltValidity Public ❗️ NO❗️
pause Public ❗️ 🛑 onlyRole
unpause Public ❗️ 🛑 onlyRole
createDeterministicDepositContracts Public ❗️ 🛑 onlyRole whenNotPaused
createDepositContracts Public ❗️ 🛑 onlyRole whenNotPaused
queueTransfers Public ❗️ 🛑 onlyRole whenNotPaused
executeNextTransfer Public ❗️ 🛑 nonReentrant onlyRole whenNotPaused
removeTransfer Public ❗️ 🛑 onlyRole
removeTransferBatch Public ❗️ 🛑 onlyRole
flushPendingTransfers Public ❗️ 🛑 nonReentrant onlyRole whenNotPaused
_commonCheckERC20Native Internal 🔒
_commonCheckERC721_1155 Internal 🔒
_executeNextTransfer Internal 🔒 🛑
_removeTransfer Internal 🔒 🛑
_pushDepositContracts Internal 🔒 🛑

Library

  • Configuration (foundry.toml)

Security and Audits

This project is not audited.

An audit was performed by ABDK on a previous version of this project. Several improvements have been made since, notably additional validation checks in the queueTransfers function.

Audit Tools

Slither

slither . --checklist --filter-paths "openzeppelin-contracts|test|forge-std|mocks" > slither-report.md

See crytic/slither

Mythril

myth analyze src/DepositContract.sol --solc-json solc_setting.json > myth_depositContract_report.md
myth analyze src/MasterContract.sol --solc-json solc_setting.json > myth_masterContract_report.md

See Consensys/mythril

Cyfrin Aderyn

aderyn --output aderyn-report.md -x src/mocks

See Cyfrin/aderyn

Improvements

This section presents a list of potential improvement to this project suggested by Claude Code.

# Improvement Description Drawback
1 Revert in executeNextTransfer when all queued transfers are removed When all remaining entries have STATUS.REMOVED, the function silently succeeds without executing anything, desynchronizing off-chain accounting. Minor behavior change for callers that treat a successful transaction as a no-op signal.
2 Validate depositContract addresses in queueTransfers Any address can be passed as depositContract without verification it was created by this MasterContract, allowing a compromised creator role to target arbitrary contracts. One extra SLOAD per queued transfer; mapping must stay in sync with clone creation.
3 Skip failing transfers in flushPendingTransfers A single reverting transfer causes the entire batch to revert with no skip mechanism, blocking all subsequent transfers in the queue. Per-transfer failures are silent; partially executed batches require off-chain reconciliation.
4 Prevent double removal in removeTransfer Calling removeTransfer twice on the same ID emits a duplicate TransferRemoved event, confusing off-chain indexers tracking transfer lifecycle. One additional SLOAD per removal call.

1. Revert in executeNextTransfer when all queued transfers are removed

Description: When all remaining entries have STATUS.REMOVED, the function silently advances nextTransferIndex to the end without executing anything and without reverting. Off-chain executors observing a successful transaction may incorrectly assume a transfer occurred, desynchronizing accounting systems.

Drawback: Minor behavior change for callers that currently treat a successful transaction as a no-op signal.

Details

function executeNextTransfer() public nonReentrant onlyRole(TRANSFER_REQUEST_EXECUTOR_ROLE) whenNotPaused {
    uint256 nextTransferIndexLocal = nextTransferIndex;
    if (nextTransferIndexLocal == pendingTransfers.length) {
        revert MasterContract_NoPendingTransfers();
    }
    bool executed = false;
    uint256 pendingTransferLengthLocal = pendingTransfers.length;
    for (; nextTransferIndexLocal < pendingTransferLengthLocal; ++nextTransferIndexLocal) {
        Transfer memory transfer = pendingTransfers[nextTransferIndexLocal];
        if (transfer.status != STATUS.REMOVED) {
            ++nextTransferIndexLocal;
            _executeNextTransfer(transfer);
            executed = true;
            break;
        }
    }
    // All remaining entries were REMOVED — nothing executed
    if (!executed) revert MasterContract_NoPendingTransfers();
    nextTransferIndex = nextTransferIndexLocal;
}

2. Validate depositContract addresses in queueTransfers

Description: The queueTransfers function accepts any address in TransferInput.depositContract without verifying it was created by this MasterContract. A compromised TRANSFER_REQUEST_CREATOR_ROLE holder could queue transfers targeting arbitrary contracts, causing unpredictable behavior during execution.

Drawback: One extra SLOAD per queued transfer; the mapping must be populated on every clone creation.

Details

// New state variable
mapping(address => bool) public isDepositContract;

// Populate in _pushDepositContracts
function _pushDepositContracts(DepositContract clone, DepositContract[] memory newContracts, uint256 i)
    internal returns (DepositContract[] memory)
{
    isDepositContract[address(clone)] = true; // <-- add this
    newContracts[i] = clone;
    depositContracts.push(clone);
    emit DepositContractCreated(clone);
    return newContracts;
}

// Guard in queueTransfers
if (!isDepositContract[address(transfer.depositContract)]) {
    revert MasterContract_UnknownDepositContract();
}

3. Skip failing transfers in flushPendingTransfers instead of reverting

Description: A single reverting transfer (e.g., insufficient token balance, non-payable recipient) causes the entire batch to revert. There is no skip mechanism, meaning one problematic entry can permanently block the flush path until manually removed.

Drawback: Silent per-transfer failures are harder to debug; partially executed batches require careful off-chain reconciliation.

Details

// New event in MasterContractInvariant
event TransferFailed(uint256 indexed id, bytes reason);

// In flushPendingTransfers, replace the direct _executeNextTransfer call:
for (; nextTransferIndexLocal < pendingTransfersLengthLocal; ++nextTransferIndexLocal) {
    Transfer memory transfer = pendingTransfers[nextTransferIndexLocal];
    if (transfer.status != STATUS.REMOVED) {
        try DepositContract(payable(transfer.transferInput.depositContract))
            .executeTransaction(
                transfer.transferInput.to,
                transfer.transferInput.amounts,
                transfer.transferInput.token,
                transfer.transferInput.tokenIds,
                transfer.transferInput.operation
            )
        {
            emit TransferExecuted(transfer.id);
        } catch (bytes memory reason) {
            emit TransferFailed(transfer.id, reason);
        }
    }
}

4. Prevent double removal in removeTransfer

Description: _removeTransfer does not check whether a transfer already has STATUS.REMOVED. Calling it twice on the same ID emits a duplicate TransferRemoved event, which can confuse off-chain indexers and monitoring systems tracking transfer lifecycle.

Drawback: One additional SLOAD per removal call.

Details

// New error in MasterContractInvariant
error MasterContract_TransferAlreadyRemoved();

// In _removeTransfer, add before setting status:
function _removeTransfer(uint256 transferId_) internal {
    if (transferId_ < nextTransferIndex) {
        revert MasterContract_TransferAlreadyExecuted();
    }
    if (transferId_ >= pendingTransfers.length) {
        revert MasterContract_TransferIdInvalid();
    }
    if (pendingTransfers[transferId_].status == STATUS.REMOVED) {
        revert MasterContract_TransferAlreadyRemoved(); // <-- add this
    }
    pendingTransfers[transferId_].status = STATUS.REMOVED;
    emit TransferRemoved(transferId_);
}

Tools

Prettier

npx prettier --write --plugin=prettier-plugin-solidity 'src/**/*.sol'
npx prettier --write --plugin=prettier-plugin-solidity 'test/**/*.sol'

Surya

See ./doc/script

Foundry

Foundry is a fast, portable and modular toolkit for Ethereum application development written in Rust.

Foundry consists of:

  • Forge: Ethereum testing framework (like Truffle, Hardhat and DappTools).
  • Cast: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
  • Anvil: Local Ethereum node, akin to Ganache, Hardhat Network.
  • Chisel: Fast, utilitarian, and verbose Solidity REPL.

Documentation

https://book.getfoundry.sh/

Usage

Gas Report
forge test --gas-report
Build
forge build
Test
forge test
Format
forge fmt
Gas Snapshots
forge snapshot
Anvil
anvil
Deploy
forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
Cast
cast <subcommand>
Help
forge --help
anvil --help
cast --help

Coverage

coverage_1

coverage_2

  • Perform a code coverage
forge coverage
  • Generate LCOV report
forge coverage --report lcov
  • Generate index.html
forge coverage --ffi --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage

About

PoC to manage multiple deposit addresses through an unique MasterContract

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Contributors