Skip to content

feat: support batch txs#24

Draft
g4titanx wants to merge 7 commits intomasterfrom
feat/support-batch-tx
Draft

feat: support batch txs#24
g4titanx wants to merge 7 commits intomasterfrom
feat/support-batch-tx

Conversation

@g4titanx
Copy link
Copy Markdown
Member

@g4titanx g4titanx commented May 6, 2026

Escrow Batch Implementation Summary

What This Implements

EscrowBatch is the batch version of the escrow contract. It lets a deployer define many expected transfers in one escrow, lets one or more nodes reserve different parts of that batch, and lets each node prove that the transfers they reserved were executed before they can collect.

The batch now supports mixed payment assets. A single batch can contain ERC20 transfers and native ETH transfers, for example USDC, USDT, and ETH in the same escrow.

The reward and bond token is still one ERC20 token. That token is set by tokenContract. The payment assets are defined per transfer row.

Transfer Model

Each row in the batch is represented by BatchTransfer:

struct BatchTransfer {
    AssetType assetType;
    address asset;
    address recipient;
    uint256 amount;
    uint256 rewardWeight;
}

assetType says whether the payment is ERC20 or native ETH.

asset is the ERC20 token address for ERC20 transfers. For native ETH, it must be address(0).

recipient and amount describe the transfer the node must execute.

rewardWeight is the common value used to split rewards and calculate bond requirements. This is needed because raw token amounts across USDC, USDT, and ETH cannot be compared directly. For example, 1 ether and 1 USDC are not the same value just because they are both represented as integers on-chain.

So the contract does not use amount to split rewards. It uses rewardWeight.

The contract does not calculate rewardWeight by itself. The deployer or frontend supplies it for each transfer row.

For a batch where every payment uses the same token, rewardWeight can simply match the transfer amount:

100 USDC -> rewardWeight = 100
50 USDC  -> rewardWeight = 50

For mixed-asset batches, rewardWeight should be normalized to a common value, usually USD value or reward-token value:

Transfer A: 100 USDC        -> rewardWeight = 100
Transfer B: 100 USDT        -> rewardWeight = 100
Transfer C: 0.05 ETH at $3k -> rewardWeight = 150

The total weight is:

100 + 100 + 150 = 350

If the total reward is 35 USDC, then the reward split is:

Transfer A reward = 10 USDC
Transfer B reward = 10 USDC
Transfer C reward = 15 USDC

If the deployer wants every transfer row to receive the same reward regardless of payment size, every row can use the same weight:

rewardWeight = 1

Funding

The deployer funds the batch with:

  • the reward token, using tokenContract;
  • all ERC20 payment assets used by the transfer rows;
  • native ETH through msg.value for rows where assetType == NATIVE.

Native funding must exactly equal the sum of all native ETH transfer amounts.

ERC20 payment assets are transferred from the deployer into the escrow contract.

The contract tracks payment balances per asset, so it knows how much USDC, USDT, ETH, or any other supported asset is still locked in the batch.

Funding is one-shot. Once the batch has been funded, it cannot be re-funded with stale state.

Multi-Node Reservations

Nodess do not have to execute the whole batch.

Each node can call bond() with the transfer indexes they want to execute. For example:

  • Node A reserves employees 0 to 9.
  • Node B reserves employees 10 to 19.
  • Node C reserves employees 20 to 29.

The contract prevents overlap. If a transfer index is already reserved or already completed, another node cannot reserve it.

Each reservation stores:

  • the executor address;
  • the reserved transfer indexes;
  • the bond amount;
  • the deadline;
  • the block where the reservation started;
  • the total reward weight reserved by that executor.

The minimum bond is calculated from the executor's share of the reward:

required bond = reward share for reserved rows / 2

The reward share is based on rewardWeight, not raw token amounts.

If a node fails to collect before the deadline, their reservation expires. The reserved transfer indexes become available again, and the forfeited bond is added to the reward pool.

Proof Model

Collection uses one interface:

function collect(BatchProof[] calldata proofs) external;

The array is important because one node may need to prove transfers that landed in different receipts, different transactions, or different blocks.

Each proof has a proofType:

enum AssetType {
    ERC20,
    NATIVE
}

For ERC20 transfers, the proof validates receipt logs.

For native ETH transfers, the proof validates both the transaction and the receipt.

ERC20 Proofs

An ERC20 proof can prove multiple transfer rows from one receipt.

For each ERC20 row, the node provides:

  • the receipt MPT proof;
  • the transfer indexes being proven;
  • the log indexes inside the receipt.

The contract checks that each log proves the exact expected transfer:

  • the token contract must match the row's asset;
  • the from address must be the collecting Nomad;
  • the to address must match the expected recipient;
  • the amount must match the expected amount.

This means a node cannot claim an old transfer, another sender's transfer, or a transfer from the wrong token contract.

Native ETH Proofs

Native ETH does not emit a standard ERC20 Transfer log, so it needs a different proof shape.

For native ETH rows, the contract validates:

  • the transaction MPT proof against the block's transaction root;
  • the receipt MPT proof against the block's receipt root;
  • the receipt status, so failed transactions cannot be claimed;
  • the transaction sender;
  • the transaction recipient;
  • the transaction value.

@ozwaldorf
Copy link
Copy Markdown
Member

failing ci

@g4titanx g4titanx marked this pull request as ready for review May 8, 2026 03:09
@g4titanx g4titanx marked this pull request as draft May 8, 2026 03:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants