Skip to content

feat: tron batch payments contracts and tests#1724

Merged
LeoSlrRf merged 20 commits into
masterfrom
feat/batch-tron
May 28, 2026
Merged

feat: tron batch payments contracts and tests#1724
LeoSlrRf merged 20 commits into
masterfrom
feat/batch-tron

Conversation

@LeoSlrRf
Copy link
Copy Markdown
Contributor

@LeoSlrRf LeoSlrRf commented May 20, 2026

Description of the changes

Adds Tron-specific batch payment contracts and test infrastructure to the smart-contracts package.

This PR introduces ERC20BatchPayments, an ERC20-specific version of the BatchPayments contract.
This version offers better performance and increased security on Tron.
The fil itself is stored with the solidity contracts so it benefits from types generation at build time.

ERC20BatchPayments.test.js covers ERC20BatchPayments, including happy-path single-token and multi-token payments, zero-amount payments, zero-fee payments, BadTRC20 token handling, and error cases such as insufficient funds, missing allowance, and mismatched input arrays.

A shared helpers.js module provides utilities, including token deployment, approval helpers, balance diffing, batch fee computation, and revert/no-balance-change assertion helpers.

The root package.json workspace configuration is updated to prevent hoisting of @openzeppelin dependencies for the smart-contracts package, ensuring the Tron build resolves its own copy of those contracts.

The Trondeployments scripts have been updated to support the deployment of ERC20BatchPayments

Contracts Scope

This is the same contract as the BatchPayments contract from EVM without th following features:

  • The dedicated batch fees
  • The EthFeeProxy methods
  • The ownership

It also includes several minor improvements.

Security Consideration

The ERC20BatchPayment contract no longer has an owner compared to the original version, reducing the attack surface.

Additionally, the following updates have been made compared to the original BatchPayments contract:

  • paymentErc20FeeProxy can't be initialized with the zero address.
  • paymentErc20FeeProxy is now immutable.
  • tokens passed down to each method can't have the zero address.
  • enforce recipients / fee recipient not to be the zero address.tra

Gas Consideration

ERC20BatchPayment has a lower gas footprint as BatchPayments as it does not include batch-fee-related logic.

Additionally, the following updates have been made compared to the original BatchPayments contract:

  • Use unchecked integer within loops for better performance (gas)
  • Approval done only once per token

Deloyment Information

Nile: https://nile.tronscan.org/#/contract/TBAtFt46T7LUW5Sya6PNjw7MQrKkzKEFMx
Tron: https://tronscan.org/#/address/TRZbXXuLd3HW5utzVysA3rpLgU7sVBrd1D

Both contracts are verified ✅

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@LeoSlrRf LeoSlrRf marked this pull request as ready for review May 20, 2026 11:22
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 20, 2026

Greptile Summary

This PR introduces ERC20BatchPayments, a Tron-specific batch payment contract that routes payments through the existing ERC20FeeProxy. Compared to the EVM BatchPayments contract it is derived from, it removes ETH-path and ownership logic, adds immutability to the proxy reference, enforces zero-address guards on all inputs, and performs a single pull-and-approve per token upfront for improved gas efficiency.

  • New Solidity contract (ERC20BatchPayments.sol) with two entry points: single-token and multi-token batching. The pull-all-then-dispatch design guarantees zero residual balance on the contract after every successful call, and the _proxyApproved mapping eliminates repeated max-allowance approvals for tokens used more than once.
  • Full test suite (ERC20BatchPayments.test.js + helpers.js) covering happy paths, zero-amount entries, BadTRC20 handling, and all revert conditions; uses Tron-aware balance-diffing helpers that account for Tron's silent-revert semantics.
  • Artifact registration (src/lib/artifacts/ERC20BatchPayments/) with ABI and deployment coordinates (both Nile and mainnet) correctly matching the on-chain deployments; nohoist config added to package.json to ensure TronBox resolves its own @openzeppelin copy.

Confidence Score: 4/5

The contract and test suite are safe to merge; the remaining risk is in the mainnet deployment script, which can crash on a truly fresh environment where mainnet.json does not yet exist.

The core contract logic, ABI, and artifact coordinates are correct and well-tested. Issues flagged in earlier review rounds (wrong artifact name, stale creation block, missing threw declaration, internal function in ABI) have all been resolved in this revision. The only outstanding concern is in deploy-mainnet.js, where accessing existingDeployment.contracts on a null value (when no prior deployment file exists) would crash the script before writing the deployment JSON — a risk for anyone running a completely fresh mainnet deployment.

packages/smart-contracts/scripts/tron/deploy-mainnet.js — null-guard on the existingDeployment value before accessing .contracts

Important Files Changed

Filename Overview
packages/smart-contracts/src/contracts/ERC20BatchPayments.sol New Tron-only batch payments contract. Pull-all-tokens-upfront design is correct: single-token and multi-token paths both transfer the exact aggregate before iterating proxy calls, leaving zero residual balance. Zero-address guards, immutable proxy reference, and one-time max-approval pattern are sound.
packages/smart-contracts/test/tron/ERC20BatchPayments.test.js Comprehensive test suite covering single-token and multi-token happy paths, zero-amount entries, zero-fee paths, BadTRC20 handling, and all revert conditions. Artifact name now correctly matches the compiled contract.
packages/smart-contracts/test/tron/helpers.js Shared test utilities for Tron; assertBalancesUnchanged pattern is intentionally Tron-specific. One minor label inaccuracy on TRON_ZERO_ADDRESS (hex value, not base58). expectNonOwnerReverts is exported but unused by ERC20BatchPayments tests since the contract has no owner.
packages/smart-contracts/scripts/tron/deploy-mainnet.js Refactored to support deploying either ERC20FeeProxy or ERC20BatchPayments independently. Merges new contracts into the existing mainnet.json. The existingDeployment null-dereference edge cases noted in earlier review rounds remain in the script.
packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/index.ts New artifact registration. Both nile and mainnet addresses and creation block numbers now match the corresponding JSON deployment files (67830042 for nile, 83104290 for mainnet).
packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/0.1.0.json ABI accurately reflects the deployed contract. Internal functions are correctly excluded.
package.json Adds nohoist config to prevent Yarn hoisting @OpenZeppelin dependencies out of smart-contracts.
.github/workflows/tron-smart-contracts.yml Adds ERC20BatchPayments to the compiled-artifact verification list and ABI function checks.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Payer calls batch function] --> B{Arrays same length?}
    B -->|fail| Z[Revert]
    B -->|pass| C{Which function?}
    C -->|Single-token batch| D[Sum all amounts plus fees]
    D --> E{total greater than zero?}
    E -->|yes| F[Pull total from payer via transferFrom]
    F --> G{proxy already approved for token?}
    G -->|no| H[Approve proxy with max allowance and cache flag]
    H --> J
    G -->|yes| J[Payment loop]
    E -->|no| J
    J --> K{paymentSum == 0?}
    K -->|yes| L[skip entry]
    K -->|no| M[Validate recipient and feeAddr]
    M --> N[Invoke ERC20FeeProxy per payment]
    C -->|Multi-token batch| P[Deduplicate tokens via inner loop per entry]
    P --> Q[Pull each unique token total from payer and approve proxy once]
    Q --> R[Payment loop identical to single-token path]
    R --> N
Loading

Reviews (16): Last reviewed commit: "update global files" | Re-trigger Greptile

Comment thread packages/smart-contracts/test/tron/ERC20BatchPayments.test.js Outdated
Comment thread packages/smart-contracts/test/tron/ERC20BatchPayments.test.js Outdated
Comment thread packages/smart-contracts/test/tron/ERC20BatchPayments.test.js Outdated
Comment thread packages/smart-contracts/test/tron/helpers.js Outdated
Comment thread packages/smart-contracts/test/tron/helpers.js Outdated
Copy link
Copy Markdown
Contributor

MantisClone commented May 21, 2026

@LeoSlrRf is this still true? If not, please update the PR description.

> Currently, both contracts provide methods for managing the underlying proxies.

@LeoSlrRf
Copy link
Copy Markdown
Contributor Author

@MantisClone It wasn't the case. I just pushed the ownership removal and updated the PR description

Comment thread packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol Outdated
Copy link
Copy Markdown
Contributor

@MantisClone MantisClone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! 💯

@MantisClone
Copy link
Copy Markdown
Contributor

Post-approval optional hardening note:

  • Guard constructor against a zero proxy address.
  • Make paymentErc20FeeProxy immutable.
  • Make approvePaymentProxyToSpend() non-public since it is only used internally.

Copy link
Copy Markdown
Contributor Author

Post-approval optional hardening note:

  • Guard constructor against a zero proxy address.
  • Make paymentErc20FeeProxy immutable.
  • Make approvePaymentProxyToSpend() non-public since it is only used internally.

​Improvements implemented.
Contract deployed and verified

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 27, 2026

Want your agent to iterate on Greptile's feedback? Try greploops.

Comment thread packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/0.1.0.json Outdated
Comment thread packages/smart-contracts/scripts/tron/deploy-mainnet.js
@rodrigopavezi
Copy link
Copy Markdown
Contributor

Security Review — ERC20BatchPayments.sol

Medium — Zero-address collision in multi-token deduplication

batchERC20PaymentsMultiTokensWithReference uses address(0) as the empty-slot sentinel in uniqueTokens. If _tokenAddresses[i] == address(0), the inner loop matches it against an uninitialized slot (both are address(0)), accumulates amount there, then eventually calls _transferToContractAndApproveProxy(IERC20(address(0)), n) which reverts with a confusing error.

Fix: add require(_tokenAddresses[i] != address(0)) at the top of the function.


Low-Medium — Zero-amount payments skip pre-approval

The empty-slot insertion guard (_amounts[i] + _feeAmounts[i]) > 0 means a token that appears only with amount=0, fee=0 is never stored in uniqueTokens, never pulled, and never approved for the proxy. The dispatch loop still calls the proxy for that token. Standard ERC20s tolerate a zero-amount transferFrom with no allowance, but strict implementations will revert, breaking an otherwise valid batch.

Fix: skip dispatch for zero-sum entries, or record every token address unconditionally and skip only the pull step.


Low — type(uint256).max approval persists

After the first batch for a given token, the proxy holds unlimited approval permanently. Safe in practice (contract holds no tokens between txs, proxy is immutable), but any tokens accidentally forwarded to the contract would be instantly drainable via the proxy. Consider approving only amountAndFee per batch.


Low — No zero-address guard on recipients / feeAddress

Passing address(0) as a recipient or fee address silently burns tokens with no revert. Add explicit checks.


What looks good

  • Immutable proxy address eliminates the proxy-upgrade attack vector
  • Pull-then-push model makes reentrancy safe (reentrant caller would need fresh allowances as msg.sender)
  • Array length checks, pre-flight balance/allowance validation, and no-owner design all reduce attack surface

@rodrigopavezi
Copy link
Copy Markdown
Contributor

Gas Improvements

1. Remove redundant pre-flight checksallowance and balanceOf before safeTransferFrom are redundant; transferFrom reverts on both anyway. Saves ~2 external calls per token per batch.

2. Cache proxy approval in storage — the allowance(this, proxy) check is an external call on every batch. Replace with a mapping(address => bool) _proxyApproved flag: one cold SSTORE on first use, one warm SLOAD forever after.

3. Require sorted token input to drop O(n²) dedupbatchERC20PaymentsMultiTokensWithReference uses nested loops to deduplicate tokens. If callers pass payments sorted by token address, deduplication becomes a single O(n) pass. Largest saving for mixed-token batches.

4. unchecked { ++i } on loop counters — all loop bounds are array lengths, overflow is impossible. Free saving on every iteration.

Priority: #2 pays on every batch, #3 scales with batch size — those two are most worthwhile.

@rodrigopavezi
Copy link
Copy Markdown
Contributor

rodrigopavezi commented May 27, 2026

Other Smart Contract / Infra Findings

EthereumFeeProxy.sol symlink — remove it

tron/contracts/interfaces/EthereumFeeProxy.sol is added as a symlink but never imported by ERC20BatchPayments.sol. Dead file that misleads readers of the contract's dependency tree.

index.ts@ts-ignore hides a missing Tron type

The // @ts-ignore Cannot find module on the Tron type import silently removes type safety for all callers of erc20BatchPaymentsArtifact. Either generate the type or make the generic explicit.

nile.json — placeholder metadata + missing block numbers

timestamp: "2024-01-01" and deployer: "TO_BE_FILLED_ON_DEPLOYMENT" are placeholders. Also, the nile deploy script doesn't capture creationBlockNumber (unlike the mainnet script), so any future redeployment will produce an incomplete JSON.

Test helpers — dead code from BatchPayments

computeBatchFee, the setBatchFee branch in deployBaseSetup, and getApprovalAmount's batchFee param are all unreachable since ERC20BatchPayments has no batch fees. Carried over from the original helpers.

Test helpers — expectRevertOrNoBalanceChange swallows all errors

The helper catches and discards every exception, so a test passes as long as balances are unchanged — even if the tx failed for an unrelated reason. On Tron, where some errors don't throw JS exceptions, this can mask real bugs.

No events on the batch contract — worth a NatSpec note

ERC20BatchPayments emits no events; all TransferWithReferenceAndFee events come from ERC20FeeProxy. Integrators indexing batch payments need to filter proxy events by msg.sender == batchContract, not listen to the batch contract directly. A NatSpec line on the contract would prevent confusion.

Comment thread packages/smart-contracts/scripts/tron/deploy-mainnet.js
Copy link
Copy Markdown
Contributor Author

LeoSlrRf commented May 28, 2026

@rodrigopavezi I applied most changes from you comment. Here are the only one I skipped.

Security review

Low — type(uint256).max approval persists

This one goes against the other change you suggested later on - Cache proxy approval in storage. I went with approval caching instead.
The batch contract can never hold funds because everything he retrieves from the payer is spent by the proxy. For the contract to hold funds, it would mean someone made a direct transfer to the contract.

Gas Improvements

Require sorted token input to drop O(n²) dedup

I want to push back regarding this one. This contract is supposed to transpose the EVM batch feature to Tron. Currently, the only changes we have made are either the removal of unused EVM features, security improvements or gas optimization.

However, this one introduces a breaking change to how users can make batch payments, adding per-network constraints on how transactions can be built.

I think this improvement is a valid point, but it should be part of a broader batch redesign where the EVM contract is also redeployed, keeping the behavior on both networks aligned.

Copy link
Copy Markdown
Contributor

Sounds great! Thanks

@github-actions
Copy link
Copy Markdown

❌ Echidna Fuzzing Results

Mode: ( test sequences)
Status: Property Violations Found

Property Test Results

Status Count
✅ Passed 0
❌ Failed 0
Total 0
Pass Rate 0%

📄 Full report and corpus available in workflow artifacts.

ℹ️ About Echidna Fuzzing

Echidna is a property-based fuzzer that generates random sequences of transactions
to test invariants (properties that should always hold true).

Properties tested:

  • Fee calculation bounds
  • Access control enforcement
  • Amount constraints
  • No duplicate payments
  • Zero address validation
  • Integer overflow protection

@github-actions
Copy link
Copy Markdown

⚠️ Slither Security Analysis

Status: Issues Found

Findings Summary

Severity Count Status
✅ High 0 Pass
✅ Medium 0 Pass
🔵 Low 0 Info
ℹ️ Informational 0 Info

📄 Full report available in workflow artifacts.
🔍 View detailed findings in the Security tab.

@github-actions
Copy link
Copy Markdown

⚠️ Slither Security Analysis

Status: Issues Found

Findings Summary

Severity Count Status
✅ High 0 Pass
✅ Medium 0 Pass
🔵 Low 0 Info
ℹ️ Informational 0 Info

📄 Full report available in workflow artifacts.
🔍 View detailed findings in the Security tab.

@github-actions
Copy link
Copy Markdown

❌ Echidna Fuzzing Results

Mode: ( test sequences)
Status: Property Violations Found

Property Test Results

Status Count
✅ Passed 0
❌ Failed 0
Total 0
Pass Rate 0%

📄 Full report and corpus available in workflow artifacts.

ℹ️ About Echidna Fuzzing

Echidna is a property-based fuzzer that generates random sequences of transactions
to test invariants (properties that should always hold true).

Properties tested:

  • Fee calculation bounds
  • Access control enforcement
  • Amount constraints
  • No duplicate payments
  • Zero address validation
  • Integer overflow protection

@github-actions
Copy link
Copy Markdown

✅ Slither Security Analysis

Status: Passed

Findings Summary

Severity Count Status
✅ High 0 Pass
🟡 Medium 2 Review Recommended
🔵 Low 0 Info
ℹ️ Informational 1 Info

⚠️ Please review the findings in the Security tab or download the artifacts.

📄 Full report available in workflow artifacts.
🔍 View detailed findings in the Security tab.

@github-actions
Copy link
Copy Markdown

✅ Echidna Fuzzing Results

Mode: ci (50000 test sequences)
Status: All Properties Passed

Property Test Results

Status Count
✅ Passed 16
❌ Failed 0
Total 16
Pass Rate 100.0%

📄 Full report and corpus available in workflow artifacts.

ℹ️ About Echidna Fuzzing

Echidna is a property-based fuzzer that generates random sequences of transactions
to test invariants (properties that should always hold true).

Properties tested:

  • Fee calculation bounds
  • Access control enforcement
  • Amount constraints
  • No duplicate payments
  • Zero address validation
  • Integer overflow protection

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 28, 2026

Greptile encountered an error while reviewing this PR. Please reach out to support@greptile.com for assistance.

Copy link
Copy Markdown
Contributor Author

LeoSlrRf commented May 28, 2026

Merge activity

  • May 28, 5:41 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • May 28, 5:41 PM UTC: @LeoSlrRf merged this pull request with Graphite.

@LeoSlrRf LeoSlrRf merged commit 053cae9 into master May 28, 2026
15 checks passed
@LeoSlrRf LeoSlrRf deleted the feat/batch-tron branch May 28, 2026 17:41
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.

3 participants